diff options
252 files changed, 12034 insertions, 6159 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ca62abb7..04c319340 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,5 +7,5 @@ jobs: steps: - checkout - run: /python3.6/bin/pip install -U pip setuptools - - run: /python3.6/bin/pip install -U .[test,websupport] + - run: /python3.6/bin/pip install -U .[test] - run: make test PYTHON=/python3.6/bin/python diff --git a/.codecov.yml b/.codecov.yml index 22f35d710..652520158 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,3 +4,6 @@ coverage: project: default: enabled: no + patch: + default: + enabled: no diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 073a57795..6f4fe672f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,20 @@ Subject: <short purpose of this pull request> +<!-- + Before posting a pull request, please choose a appropriate branch: + + - Breaking changes: master + - Critical or severe bugs: X.Y.Z + - Others: X.Y + + For more details, see https://www.sphinx-doc.org/en/master/devguide.html#branch-model +--> + ### Feature or Bugfix <!-- please choose --> - Feature - Bugfix +- Refactoring ### Purpose - <long purpose of this pull request> diff --git a/.travis.yml b/.travis.yml index 9e90bc77d..3ceb2e3f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ -language: python -sudo: false +os: linux dist: xenial +language: python cache: pip env: @@ -9,7 +9,7 @@ env: - SKIP_LATEX_BUILD=1 - IS_PYTHON=true -matrix: +jobs: include: - python: '3.5' env: @@ -23,9 +23,7 @@ matrix: - python: '3.8' env: - TOXENV=du15 - # Disable codecov temporarily - # refs: https://github.com/sphinx-doc/sphinx/pull/7286#issuecomment-596617853 - # - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg" + - PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg" - python: 'nightly' env: - TOXENV=du16 @@ -39,8 +37,7 @@ matrix: env: TOXENV=flake8 - language: node_js - node_js: - - 10.7 + node_js: '10.7' env: IS_PYTHON=false services: xvfb @@ -1,3 +1,157 @@ +Release 3.0.0 (in development) +============================== + +Dependencies +------------ + +* LaTeX: drop dependency on :program:`extractbb` for image inclusion in + Japanese documents as ``.xbb`` files are unneeded by :program:`dvipdfmx` + since TeXLive2015 (refs: #6189) +* babel-2.0 or above is available (Unpinned) + +Incompatible changes +-------------------- + +* Drop features and APIs deprecated in 1.8.x +* #247: autosummary: stub files are overwritten automatically by default. see + :confval:`autosummary_generate_overwrite` to change the behavior +* #5923: autodoc: the members of ``object`` class are not documented by default + when ``:inherited-members:`` and ``:special-members:`` are given. +* #6830: py domain: ``meta`` fields in info-field-list becomes reserved. They + are not displayed on output document now +* #6417: py domain: doctree of desc_parameterlist has been changed. The + argument names, annotations and default values are wrapped with inline node +* The structure of ``sphinx.events.EventManager.listeners`` has changed +* Due to the scoping changes for :rst:dir:`productionlist` some uses of + :rst:role:`token` must be modified to include the scope which was previously + ignored. +* #6903: Internal data structure of Python, reST and standard domains have + changed. The node_id is added to the index of objects and modules. Now they + contains a pair of docname and node_id for cross reference. +* #7276: C++ domain: Non intended behavior is removed such as ``say_hello_`` + links to ``.. cpp:function:: say_hello()`` +* #7210: js domain: Non intended behavior is removed such as ``parseInt_`` links + to ``.. js:function:: parseInt`` +* #7229: rst domain: Non intended behavior is removed such as ``numref_`` links + to ``.. rst:role:: numref`` +* #6903: py domain: Non intended behavior is removed such as ``say_hello_`` + links to ``.. py:function:: say_hello()`` +* #7246: py domain: Drop special cross reference helper for exceptions, + functions and methods +* The C domain has been rewritten, with additional directives and roles. + The existing ones are now more strict, resulting in new warnings. +* The attribute ``sphinx_cpp_tagname`` in the ``desc_signature_line`` node + has been renamed to ``sphinx_line_type``. +* #6462: double backslashes in domain directives are no longer replaced by + single backslashes as default. A new configuration value + :confval:`strip_signature_backslash` can be used by users to reenable it. + +Deprecated +---------- + +* ``desc_signature['first']`` +* ``sphinx.directives.DescDirective`` +* ``sphinx.domains.std.StandardDomain.add_object()`` +* ``sphinx.domains.python.PyDecoratorMixin`` +* ``sphinx.ext.autodoc.get_documenters()`` +* ``sphinx.ext.autosummary.process_autosummary_toc()`` +* ``sphinx.parsers.Parser.app`` +* ``sphinx.testing.path.Path.text()`` +* ``sphinx.testing.path.Path.bytes()`` +* ``sphinx.util.inspect.getargspec()`` +* ``sphinx.writers.latex.LaTeXWriter.format_docclass()`` + +Features added +-------------- + +* #247: autosummary: Add :confval:`autosummary_generate_overwrite` to overwrite + old stub file +* #5923: autodoc: ``:inherited-members:`` option takes a name of anchestor class + not to document inherited members of the class and uppers +* #6830: autodoc: consider a member private if docstring contains + ``:meta private:`` in info-field-list +* #7165: autodoc: Support Annotated type (PEP-593) +* #2815: autodoc: Support singledispatch functions and methods +* #7079: autodoc: :confval:`autodoc_typehints` accepts ``"description"`` + configuration. It shows typehints as object description +* #7314: apidoc: Propagate ``--maxdepth`` option through package documents +* #6558: glossary: emit a warning for duplicated glossary entry +* #3106: domain: Register hyperlink target for index page automatically +* #6558: std domain: emit a warning for duplicated generic objects +* #6830: py domain: Add new event: :event:`object-description-transform` +* #6895: py domain: Do not emit nitpicky warnings for built-in types +* py domain: Support lambda functions in function signature +* #6417: py domain: Allow to make a style for arguments of functions and methods +* #7238, #7239: py domain: Emit a warning on describing a python object if the + entry is already added as the same name +* #7341: py domain: type annotations in singature are converted to cross refs +* Support priority of event handlers. For more detail, see + :py:meth:`.Sphinx.connect()` +* #3077: Implement the scoping for :rst:dir:`productionlist` as indicated + in the documentation. +* #1027: Support backslash line continuation in :rst:dir:`productionlist`. +* #7108: config: Allow to show an error message from conf.py via ``ConfigError`` +* #7032: html: :confval:`html_scaled_image_link` will be disabled for images having + ``no-scaled-link`` class +* #7144: Add CSS class indicating its domain for each desc node +* #7211: latex: Use babel for Chinese document when using XeLaTeX +* #6672: LaTeX: Support LaTeX Theming (experimental) +* #7005: LaTeX: Add LaTeX styling macro for :rst:role:`kbd` role +* #7220: genindex: Show "main" index entries at first +* #7103: linkcheck: writes all links to ``output.json`` +* #7025: html search: full text search can be disabled for individual document + using ``:nosearch:`` file-wide metadata +* #7293: html search: Allow to override JavaScript splitter via + ``SearchLanguage.js_splitter_code`` +* #7142: html theme: Add a theme option: ``pygments_dark_style`` to switch the + style of code-blocks in dark mode +* The C domain has been rewritten adding for example: + + - Cross-referencing respecting the current scope. + - Possible to document anonymous entities. + - More specific directives and roles for each type of entitiy, + e.g., handling scoping of enumerators. + - New role :rst:role:`c:expr` for rendering expressions and types + in text. + +* Added ``SphinxDirective.get_source_info()`` + and ``SphinxRole.get_source_info()``. +* #7324: sphinx-build: Emit a warning if multiple files having different file + extensions for same document found + +Bugs fixed +---------- + +* C++, fix cross reference lookup in certain cases involving + function overloads. +* #5078: C++, fix cross reference lookup when a directive contains multiple + declarations. +* C++, suppress warnings for directly dependent typenames in cross references + generated automatically in signatures. +* #5637: autodoc: Incorrect handling of nested class names on show-inheritance +* #7267: autodoc: error message for invalid directive options has wrong location +* #7329: autodoc: info-field-list is wrongly generated from type hints into the + class description even if ``autoclass_content='class'`` set +* #7331: autodoc: a cython-function is not recognized as a function +* #5637: inheritance_diagram: Incorrect handling of nested class names +* #7139: ``code-block:: guess`` does not work +* #7325: html: source_suffix containing dot leads to wrong source link +* #7357: html: Resizing SVG image fails with ValueError +* #7278: html search: Fix use of ``html_file_suffix`` instead of + ``html_link_suffix`` in search results +* #7297: html theme: ``bizstyle`` does not support ``sidebarwidth`` +* #3842: singlehtml: Path to images broken when master doc is not in source root +* #7179: std domain: Fix whitespaces are suppressed on referring GenericObject +* #7289: console: use bright colors instead of bold +* #1539: C, parse array types. +* #2377: C, parse function pointers even in complex types. +* #7345: sphinx-build: Sphinx crashes if output directory exists as a file +* #7290: sphinx-build: Ignore bdb.BdbQuit when handling exceptions +* #6240: napoleon: Attributes and Methods sections ignore :noindex: option + +Testing +-------- + Release 2.4.5 (in development) ============================== @@ -2057,7 +2211,7 @@ Features removed * ``termsep`` node * defindex.html template -* LDML format support in `today`, `today_fmt` and `html_last_updated_fmt` +* LDML format support in ``today``, ``today_fmt`` and ``html_last_updated_fmt`` * ``:inline:`` option for the directives of sphinx.ext.graphviz extension * sphinx.ext.pngmath extension * ``sphinx.util.compat.make_admonition()`` @@ -4861,7 +5015,7 @@ Features added - Added a "nitpicky" mode that emits warnings for all missing references. It is activated by the :option:`sphinx-build -n` command-line - switch or the `nitpicky` config value. + switch or the :confval:`nitpicky` config value. - Added ``latexpdf`` target in quickstart Makefile. * Markup: diff --git a/CODE_OF_CONDUCT b/CODE_OF_CONDUCT new file mode 100644 index 000000000..f820bd131 --- /dev/null +++ b/CODE_OF_CONDUCT @@ -0,0 +1,76 @@ +Like the technical community as a whole, the Sphinx team and community is made +up of volunteers from all over the world. +Diversity is a strength, but it can also lead to communication issues and +unhappiness. To that end, we have a few ground rules that we ask people to +adhere to. + +* **Be friendly and patient.** + +* **Be welcoming.** + We strive to be a community that welcomes and supports people of all + backgrounds and identities. This includes, but is not limited to members of + any race, ethnicity, culture, national origin, colour, immigration status, + social and economic class, educational level, sex, sexual orientation, gender + identity and expression, age, size, family status, political belief, religion, + and mental and physical ability. + +* **Be considerate.** + Your work will be used by other people, and you in turn will depend on the + work of others. Any decision you take will affect users and colleagues, and + you should take those consequences into account when making decisions. + Remember that we're a world-wide community, so you might not be communicating + in someone else's primary language. + +* **Be respectful.** + Not all of us will agree all the time, but disagreement is no excuse for poor + behavior and poor manners. We might all experience some frustration now and + then, but we cannot allow that frustration to turn into a personal attack. + It’s important to remember that a community where people feel uncomfortable or + threatened is not a productive one. Members of the Sphinx community should be + respectful when dealing with other members as well as with people outside the + Sphinx community. + +* **Be careful in the words that you choose.** + We are a community of professionals, and we conduct ourselves professionally. + Be kind to others. Do not insult or put down other participants. Harassment + and other exclusionary behavior aren't acceptable. This includes, but is not + limited to: + + * Violent threats or language directed against another person. + + * Discriminatory jokes and language. + + * Posting sexually explicit or violent material. + + * Posting (or threatening to post) other people's personally identifying + information ("doxing"). + + * Personal insults, especially those using racist or sexist terms. + + * Unwelcome sexual attention. + + * Advocating for, or encouraging, any of the above behavior. + + * Repeated harassment of others. In general, if someone asks you to stop, then + stop. + +* **When we disagree, try to understand why.** + Disagreements, both social and technical, happen all the time and Sphinx is no + exception. It is important that we resolve disagreements and differing views + constructively. Remember that we’re different. Different people have different + perspectives on issues. Being unable to understand why someone holds a + viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to + err and blaming each other doesn’t get us anywhere. Instead, focus on helping + to resolve issues and learning from mistakes. + +This isn’t an exhaustive list of things that you can’t do. +Rather, take it in the spirit in which it’s intended - a guide to make it easier +to enrich all of us and the technical communities in which we participate. +This code of conduct applies to all spaces of the Sphinx community. + +Attribution +----------- + +Original text courtesy of the Speak Up! project: +http://web.archive.org/web/20141109123859/http://speakup.io/coc.html. + diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6fd56fb9d..af12e4a5b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -387,18 +387,31 @@ Sphinx 2.x: * Sphinx 2.x will contain a backwards-compatible replica of the function which will raise a ``RemovedInSphinx40Warning``. + This is a subclass of :exc:`python:PendingDeprecationWarning`, i.e. it + will not get displayed by default. -* Sphinx 3.x will still contain the backwards-compatible replica. +* Sphinx 3.x will still contain the backwards-compatible replica, but + ``RemovedInSphinx40Warning`` will be a subclass of + :exc:`python:DeprecationWarning` then, and gets displayed by default. * Sphinx 4.0 will remove the feature outright. -The warnings are displayed by default. You can turn off display of these -warnings with: +Deprecation warnings +~~~~~~~~~~~~~~~~~~~~ + +Sphinx will enable its ``RemovedInNextVersionWarning`` warnings by default, +if :envvar:`python:PYTHONWARNINGS` is not set. +Therefore you can disable them using: * ``PYTHONWARNINGS= make html`` (Linux/Mac) * ``export PYTHONWARNINGS=`` and do ``make html`` (Linux/Mac) * ``set PYTHONWARNINGS=`` and do ``make html`` (Windows) +But you can also explicitly enable the pending ones using e.g. +``PYTHONWARNINGS=default`` (see the +:ref:`Python docs on configuring warnings <python:describing-warning-filters>`) +for more details. + Unit Testing ------------ @@ -52,7 +52,7 @@ Documentation using the classic theme * `Arb <http://arblib.org/>`__ * `Bazaar <http://doc.bazaar.canonical.com/>`__ (customized) * `Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ -* `Blender <https://docs.blender.org/api/current/>`__ +* `Blender API <https://docs.blender.org/api/current/>`__ * `Bugzilla <https://bugzilla.readthedocs.io/>`__ * `Buildbot <https://docs.buildbot.net/latest/>`__ * `CMake <https://cmake.org/documentation/>`__ (customized) @@ -114,6 +114,7 @@ Documentation using the sphinxdoc theme * `ABRT <https://abrt.readthedocs.io/>`__ * `cartopy <https://scitools.org.uk/cartopy/docs/latest/>`__ * `Jython <http://www.jython.org/docs/>`__ +* `LLVM <https://llvm.org/docs/>`__ * `Matplotlib <https://matplotlib.org/>`__ * `MDAnalysis Tutorial <https://www.mdanalysis.org/MDAnalysisTutorial/>`__ * `NetworkX <https://networkx.github.io/>`__ @@ -123,6 +124,7 @@ Documentation using the sphinxdoc theme * `Pysparse <http://pysparse.sourceforge.net/>`__ * `PyTango <https://www.esrf.eu/computing/cs/tango/tango_doc/kernel_doc/pytango/latest/>`__ * `Python Wild Magic <https://vmlaker.github.io/pythonwildmagic/>`__ (customized) +* `RDKit <https://www.rdkit.org/docs/>`__ * `Reteisi <http://www.reteisi.org/contents.html>`__ (customized) * `Sqlkit <http://sqlkit.argolinux.org/>`__ (customized) * `Turbulenz <http://docs.turbulenz.com/>`__ @@ -167,6 +169,7 @@ Documentation using sphinx_rtd_theme * `ASE <https://wiki.fysik.dtu.dk/ase/>`__ * `Autofac <http://docs.autofac.org/>`__ * `BigchainDB <https://docs.bigchaindb.com/>`__ +* `Blender Reference Manual <https://docs.blender.org/manual/>`__ * `Blocks <https://blocks.readthedocs.io/>`__ * `bootstrap-datepicker <https://bootstrap-datepicker.readthedocs.io/>`__ * `Certbot <https://letsencrypt.readthedocs.io/>`__ @@ -180,17 +183,20 @@ Documentation using sphinx_rtd_theme * `Databricks <https://docs.databricks.com/>`__ (customized) * `Dataiku DSS <https://doc.dataiku.com/>`__ * `DNF <https://dnf.readthedocs.io/>`__ +* `Django-cas-ng <https://djangocas.dev/docs/>`__ * `edX <https://docs.edx.org/>`__ * `Electrum <http://docs.electrum.org/>`__ * `Elemental <http://libelemental.org/documentation/dev/>`__ * `ESWP3 <https://eswp3.readthedocs.io/>`__ * `Ethereum Homestead <http://www.ethdocs.org/>`__ +* `Exhale <https://exhale.readthedocs.io/>`__ * `Faker <https://faker.readthedocs.io/>`__ * `Fidimag <https://fidimag.readthedocs.io/>`__ * `Flake8 <http://flake8.pycqa.org/>`__ * `Flatpak <http://docs.flatpak.org/>`__ * `FluidDyn <https://fluiddyn.readthedocs.io/>`__ * `Fluidsim <https://fluidsim.readthedocs.io/>`__ +* `Gallium <https://gallium.readthedocs.io/>`__ * `GeoNode <http://docs.geonode.org/>`__ * `Glances <https://glances.readthedocs.io/>`__ * `Godot <https://godot.readthedocs.io/>`__ @@ -206,11 +212,13 @@ Documentation using sphinx_rtd_theme * `Jupyter Notebook <https://jupyter-notebook.readthedocs.io/>`__ * `Lasagne <https://lasagne.readthedocs.io/>`__ * `latexindent.pl <https://latexindentpl.readthedocs.io/>`__ +* `Learning Apache Spark with Python <https://runawayhorse001.github.io/LearningApacheSpark>`__ * `Linguistica <https://linguistica-uchicago.github.io/lxa5/>`__ * `Linux kernel <https://www.kernel.org/doc/html/latest/index.html>`__ +* `Mailman <http://docs.list.org/>`__ * `MathJax <https://docs.mathjax.org/>`__ * `MDTraj <http://mdtraj.org/latest/>`__ (customized) -* `MICrobial Community Analysis (micca) <http://micca.org/docs/latest/>`__ +* `micca - MICrobial Community Analysis <https://micca.readthedocs.io/>`__ * `MicroPython <https://docs.micropython.org/>`__ * `Minds <https://www.minds.org/docs/>`__ (customized) * `Mink <http://mink.behat.org/>`__ @@ -252,6 +260,7 @@ Documentation using sphinx_rtd_theme * `Sphinx AutoAPI <https://sphinx-autoapi.readthedocs.io/>`__ * `sphinx-argparse <https://sphinx-argparse.readthedocs.io/>`__ * `Sphinx-Gallery <https://sphinx-gallery.readthedocs.io/>`__ (customized) +* `Sphinx with Github Webpages <https://runawayhorse001.github.io/SphinxGithub>`__ * `SpotBugs <https://spotbugs.readthedocs.io/>`__ * `StarUML <https://docs.staruml.io/>`__ * `Sublime Text Unofficial Documentation <http://docs.sublimetext.info/>`__ @@ -285,6 +294,7 @@ Documentation using sphinx_bootstrap_theme * `Hedge <https://documen.tician.de/hedge/>`__ * `ObsPy <https://docs.obspy.org/>`__ * `Open Dylan <https://opendylan.org/documentation/>`__ +* `OPNFV <https://docs.opnfv.org/>`__ * `Pootle <http://docs.translatehouse.org/projects/pootle/>`__ * `PyUblas <https://documen.tician.de/pyublas/>`__ * `seaborn <https://seaborn.pydata.org/>`__ @@ -321,6 +331,7 @@ Documentation using a custom theme or integrated in a website * `MongoDB <https://docs.mongodb.com/>`__ * `Music21 <https://web.mit.edu/music21/doc/>`__ * `MyHDL <http://docs.myhdl.org/>`__ +* `ndnSIM <https://ndnsim.net/current/>`__ * `nose <https://nose.readthedocs.io/>`__ * `ns-3 <https://www.nsnam.org/documentation/>`__ * `NumPy <https://docs.scipy.org/doc/numpy/reference/>`__ @@ -394,6 +405,7 @@ Books produced using Sphinx * `"Python Professional Programming" (in Japanese) <http://www.amazon.co.jp/dp/4798032948/>`__ * `"Python Professional Programming 2nd Edition" (in Japanese) <https://www.amazon.co.jp/dp/479804315X/>`__ * `"Python Professional Programming 3rd Edition" (in Japanese) <https://www.amazon.co.jp/dp/4798053821/>`__ +* `Python Course by Yuri Petrov (Russian) <https://www.yuripetrov.ru/edu/python>`__ * `"Real World HTTP -- Learning The Internet and Web Technology via its history and code (Japanese)" <https://www.oreilly.co.jp/books/9784873118048/>`__ * `"Redmine Primer 5th Edition (in Japanese)" <https://www.shuwasystem.co.jp/products/7980html/4825.html>`__ * `"The repoze.bfg Web Application Framework" <https://www.amazon.com/repoze-bfg-Web-Application-Framework-Version/dp/0615345379>`__ diff --git a/README.rst b/README.rst index 5a566c300..68065ac85 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,10 @@ :target: https://codecov.io/gh/sphinx-doc/sphinx :alt: Code Coverage Status (Codecov) +.. image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg + :target: https://opensource.org/licenses/BSD-3-Clause + :alt: BSD 3 Clause + Sphinx is a tool that makes it easy to create intelligent and beautiful documentation for Python projects (or other documents consisting of multiple reStructuredText sources), written by Georg Brandl. It was originally created @@ -90,6 +94,10 @@ Get in touch .. _on GitHub: https://github.com/sphinx-doc/sphinx .. _mailing list: https://groups.google.com/forum/#!forum/sphinx-users +Please adhere to our `code of conduct`__. + +__ http://www.sphinx-doc.org/en/master/code_of_conduct.html + Testing ======= diff --git a/doc/Makefile b/doc/Makefile index d90ae0881..84eafdbe1 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -5,7 +5,6 @@ PYTHON ?= python3 # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = $(PYTHON) ../sphinx/cmd/build.py -SPHINXPROJ = sphinx SOURCEDIR = . BUILDDIR = _build diff --git a/doc/_static/Makefile b/doc/_static/Makefile new file mode 100644 index 000000000..e8793874c --- /dev/null +++ b/doc/_static/Makefile @@ -0,0 +1,4 @@ +translation.svg: translation.puml + plantuml -tsvg $< +clean: + rm translation.svg diff --git a/doc/_static/translation.png b/doc/_static/translation.png Binary files differdeleted file mode 100644 index 11f3d02cd..000000000 --- a/doc/_static/translation.png +++ /dev/null diff --git a/doc/_static/translation.puml b/doc/_static/translation.puml new file mode 100644 index 000000000..5c3a7350b --- /dev/null +++ b/doc/_static/translation.puml @@ -0,0 +1,16 @@ +@startuml +file "SphinxProject" +file ".rst" +database ".pot" +database ".po" +database ".mo" +actor translator +file TranslatedBuild +translator -l-> .po +SphinxProject -r-> .rst +.rst -r-> .pot : sphinx-build gettext +.pot -r-> .po : Pootle +.po -d-> .mo : msgfmt +.mo -l-> TranslatedBuild +.rst -d-> TranslatedBuild : "sphinx-buid -Dlanguage=" +@enduml diff --git a/doc/_static/translation.svg b/doc/_static/translation.svg new file mode 100644 index 000000000..74b78a1e7 --- /dev/null +++ b/doc/_static/translation.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="224px" preserveAspectRatio="none" style="width:602px;height:224px;" version="1.1" viewBox="0 0 602 224" width="602px" zoomAndPan="magnify"><defs><filter height="300%" id="f7a11izs19byb" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--entity SphinxProject--><polygon fill="#FEFECE" filter="url(#f7a11izs19byb)" points="6,31.5,6,67.5986,112,67.5986,112,41.5,102,31.5,6,31.5" style="stroke: #000000; stroke-width: 1.5;"/><path d="M102,31.5 L102,41.5 L112,41.5 " fill="#FEFECE" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="86" x="16" y="54.6318">SphinxProject</text><!--entity .rst--><polygon fill="#FEFECE" filter="url(#f7a11izs19byb)" points="147,31.5,147,67.5986,187,67.5986,187,41.5,177,31.5,147,31.5" style="stroke: #000000; stroke-width: 1.5;"/><path d="M177,31.5 L177,41.5 L187,41.5 " fill="#FEFECE" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="20" x="157" y="54.6318">.rst</text><!--entity .pot--><path d="M337,37 C337,27 359,27 359,27 C359,27 381,27 381,37 L381,62.0986 C381,72.0986 359,72.0986 359,72.0986 C359,72.0986 337,72.0986 337,62.0986 L337,37 " fill="#FEFECE" filter="url(#f7a11izs19byb)" style="stroke: #000000; stroke-width: 1.5;"/><path d="M337,37 C337,47 359,47 359,47 C359,47 381,47 381,37 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="24" x="347" y="64.1318">.pot</text><!--entity .po--><path d="M455,37 C455,27 475,27 475,27 C475,27 495,27 495,37 L495,62.0986 C495,72.0986 475,72.0986 475,72.0986 C475,72.0986 455,72.0986 455,62.0986 L455,37 " fill="#FEFECE" filter="url(#f7a11izs19byb)" style="stroke: #000000; stroke-width: 1.5;"/><path d="M455,37 C455,47 475,47 475,47 C475,47 495,47 495,37 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="20" x="465" y="64.1318">.po</text><!--entity .mo--><path d="M373.5,177 C373.5,167 395,167 395,167 C395,167 416.5,167 416.5,177 L416.5,202.0986 C416.5,212.0986 395,212.0986 395,212.0986 C395,212.0986 373.5,212.0986 373.5,202.0986 L373.5,177 " fill="#FEFECE" filter="url(#f7a11izs19byb)" style="stroke: #000000; stroke-width: 1.5;"/><path d="M373.5,177 C373.5,187 395,187 395,187 C395,187 416.5,187 416.5,177 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="23" x="383.5" y="204.1318">.mo</text><!--entity translator--><ellipse cx="560" cy="18" fill="#FEFECE" filter="url(#f7a11izs19byb)" rx="8" ry="8" style="stroke: #A80036; stroke-width: 2.0;"/><path d="M560,26 L560,53 M547,34 L573,34 M560,53 L547,68 M560,53 L573,68 " fill="none" filter="url(#f7a11izs19byb)" style="stroke: #A80036; stroke-width: 2.0;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="60" x="530" y="88.1318">translator</text><!--entity TranslatedBuild--><polygon fill="#FEFECE" filter="url(#f7a11izs19byb)" points="202.5,171.5,202.5,207.5986,321.5,207.5986,321.5,181.5,311.5,171.5,202.5,171.5" style="stroke: #000000; stroke-width: 1.5;"/><path d="M311.5,171.5 L311.5,181.5 L321.5,181.5 " fill="#FEFECE" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="212.5" y="194.6318">TranslatedBuild</text><!--link .po to translator--><path d="M500.3972,49.5 C510.2325,49.5 520.0678,49.5 529.9032,49.5 " fill="none" id=".po-translator" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="495.2539,49.5,504.2539,53.5,500.2539,49.5,504.2539,45.5,495.2539,49.5" style="stroke: #A80036; stroke-width: 1.0;"/><!--link SphinxProject to .rst--><path d="M112.1563,49.5 C122.0076,49.5 131.859,49.5 141.7104,49.5 " fill="none" id="SphinxProject-.rst" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="146.8621,49.5,137.8621,45.5,141.8621,49.5,137.8621,53.5,146.8621,49.5" style="stroke: #A80036; stroke-width: 1.0;"/><!--link .rst to .pot--><path d="M187.1845,49.5 C221.8302,49.5 292.6458,49.5 331.676,49.5 " fill="none" id=".rst-.pot" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="336.7347,49.5,327.7347,45.5,331.7347,49.5,327.7347,53.5,336.7347,49.5" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="113" x="205.5" y="43.6938">sphinx-build gettext</text><!--link .pot to .po--><path d="M381.0918,49.5 C400.726,49.5 429.4114,49.5 449.9055,49.5 " fill="none" id=".pot-.po" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="454.9288,49.5,445.9288,45.5,449.9288,49.5,445.9288,53.5,454.9288,49.5" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="37" x="399.5" y="43.6938">Pootle</text><!--link .po to .mo--><path d="M461.9686,72.305 C447.9056,96.9151 425.5182,136.0932 410.5467,162.2933 " fill="none" id=".po-.mo" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="408.0259,166.7048,415.9642,160.8753,410.5066,162.3636,409.0183,156.9061,408.0259,166.7048" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="43" x="434" y="134.1938">msgfmt</text><!--link TranslatedBuild to .mo--><path d="M326.9772,189.5 C342.4024,189.5 357.8276,189.5 373.2527,189.5 " fill="none" id="TranslatedBuild-.mo" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="321.7461,189.5,330.7461,193.5,326.7461,189.5,330.7461,185.5,321.7461,189.5" style="stroke: #A80036; stroke-width: 1.0;"/><!--link .rst to TranslatedBuild--><path d="M179.2251,67.5159 C196.4449,92.8925 227.8341,139.1503 246.6178,166.8315 " fill="none" id=".rst-TranslatedBuild" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="249.5202,171.1086,247.7765,161.4153,246.7127,166.9712,241.1568,165.9074,249.5202,171.1086" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="143" x="227" y="134.1938">sphinx-buid -Dlanguage=</text><!-- +@startuml
+file "SphinxProject"
+file ".rst"
+database ".pot"
+database ".po"
+database ".mo"
+actor translator
+file TranslatedBuild
+translator -l-> .po
+SphinxProject -r-> .rst
+.rst -r-> .pot : sphinx-build gettext
+.pot -r-> .po : Pootle
+.po -d-> .mo : msgfmt
+.mo -l-> TranslatedBuild
+.rst -d-> TranslatedBuild : "sphinx-buid -Dlanguage="
+@enduml
+ +PlantUML version 1.2018.13(Mon Nov 26 18:11:51 CET 2018) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 11.0.6+10-post-Ubuntu-1ubuntu119.10.1 +Operating System: Linux +OS Version: 5.3.0-40-generic +Default Encoding: UTF-8 +Language: en +Country: US +--></g></svg>
\ No newline at end of file diff --git a/doc/_templates/index.html b/doc/_templates/index.html index be174317d..1fc4ee51c 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -97,6 +97,8 @@ <p>{%trans%}A Japanese book about Sphinx has been published by O'Reilly: <a href="https://www.oreilly.co.jp/books/9784873116488/">Sphinxをはじめよう / Learning Sphinx</a>.{%endtrans%}</p> + <p>{%trans%}In 2019 the second edition of a German book about Sphinx was published: + <a href="https://literatur.hasecke.com/post/software-dokumentation-mit-sphinx/">Software-Dokumentation mit Sphinx</a>.{%endtrans%}</p> <!-- <p><img src="{{ pathto("_static/bookcover.png", 1) }}"/></p> --> @@ -118,4 +120,8 @@ <li>{%trans path=pathto("authors")%}<a href="{{ path }}">Sphinx Authors</a></li>{%endtrans%} </ul> + <h2>{%trans%}Code of Conduct{%endtrans%}</h2> + + {%trans path=pathto("code_of_conduct")%}Please adhere to our <a href="{{ path }}">Code of Conduct</a>.{%endtrans%} + {% endblock %} diff --git a/doc/code_of_conduct.rst b/doc/code_of_conduct.rst new file mode 100644 index 000000000..c1af92ddc --- /dev/null +++ b/doc/code_of_conduct.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _code_of_conduct: + +Sphinx Code of Conduct +====================== + +.. include:: ../CODE_OF_CONDUCT diff --git a/doc/conf.py b/doc/conf.py index e9ceb638e..77d2a577a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -7,6 +7,7 @@ import sphinx extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.extlinks', + 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx.ext.inheritance_diagram'] master_doc = 'contents' @@ -146,6 +147,9 @@ def setup(app): app.add_object_type('confval', 'confval', objname='configuration value', indextemplate='pair: %s; configuration value') + app.add_object_type('setuptools-confval', 'setuptools-confval', + objname='setuptools configuration value', + indextemplate='pair: %s; setuptools configuration value') fdesc = GroupedField('parameter', label='Parameters', names=['param'], can_collapse=True) app.add_object_type('event', 'event', 'pair: %s; event', parse_event, diff --git a/doc/contents.rst b/doc/contents.rst index 91ce55917..7e87cfffa 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -34,6 +34,7 @@ Sphinx documentation contents changes examples authors + code_of_conduct diff --git a/doc/development/tutorials/examples/todo.py b/doc/development/tutorials/examples/todo.py index 6ba39944f..7eee534d0 100644 --- a/doc/development/tutorials/examples/todo.py +++ b/doc/development/tutorials/examples/todo.py @@ -70,6 +70,9 @@ def process_todo_nodes(app, doctree, fromdocname): # Augment each todo with a backlink to the original location. env = app.builder.env + if not hasattr(env, 'todo_all_todos'): + env.todo_all_todos = [] + for node in doctree.traverse(todolist): if not app.config.todo_include_todos: node.replace_self([]) diff --git a/doc/development/tutorials/recipe.rst b/doc/development/tutorials/recipe.rst index 2a3aa6408..dcfe42c1a 100644 --- a/doc/development/tutorials/recipe.rst +++ b/doc/development/tutorials/recipe.rst @@ -75,7 +75,7 @@ The first thing to examine is the ``RecipeDirective`` directive: .. literalinclude:: examples/recipe.py :language: python :linenos: - :lines: 17-37 + :pyobject: RecipeDirective Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from :class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method. @@ -103,7 +103,12 @@ reStructuredText in the body. .. literalinclude:: examples/recipe.py :language: python :linenos: - :lines: 40-102 + :pyobject: IngredientIndex + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :pyobject: RecipeIndex Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`. They implement custom logic to generate a tuple of values that define the @@ -117,6 +122,10 @@ all it really is is a list of tuples like ``('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup',...)``. Refer to the :doc:`domain API guide </extdev/domainapi>` for more information on this API. +These index pages can be referred by combination of domain name and its +``name`` using :rst:role:`ref` role. For example, ``RecipeIndex`` can be +referred by ``:ref:`recipe-recipe```. + .. rubric:: The domain A Sphinx domain is a specialized container that ties together roles, @@ -126,7 +135,7 @@ creating here. .. literalinclude:: examples/recipe.py :language: python :linenos: - :lines: 105-155 + :pyobject: RecipeDomain There are some interesting things to note about this ``recipe`` domain and domains in general. Firstly, we actually register our directives, roles and indices @@ -164,7 +173,7 @@ hook the various parts of our extension into Sphinx. Let's look at the .. literalinclude:: examples/recipe.py :language: python :linenos: - :lines: 158- + :pyobject: setup 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 diff --git a/doc/development/tutorials/todo.rst b/doc/development/tutorials/todo.rst index f4ac85ac9..e27528b5a 100644 --- a/doc/development/tutorials/todo.rst +++ b/doc/development/tutorials/todo.rst @@ -107,6 +107,20 @@ is just a "general" node. <http://docutils.sourceforge.net/docs/ref/doctree.html>`__ and :ref:`Sphinx <nodes>`. +.. attention:: + + It is important to know that while you can extend Sphinx without + leaving your ``conf.py``, if you declare an inherited node right + there, you'll hit an unobvious :py:class:`PickleError`. So if + something goes wrong, please make sure that you put inherited nodes + into a separate Python module. + + For more details, see: + + - https://github.com/sphinx-doc/sphinx/issues/6751 + - https://github.com/sphinx-doc/sphinx/issues/1493 + - https://github.com/sphinx-doc/sphinx/issues/1424 + .. rubric:: The directive classes A directive class is a class deriving usually from diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index e89da7ce9..961558600 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -45,7 +45,6 @@ package. .. automethod:: Sphinx.add_enumerable_node(node, figtype, title_getter=None, \*\*kwds) -.. method:: Sphinx.add_directive(name, func, content, arguments, \*\*options) .. automethod:: Sphinx.add_directive(name, directiveclass) .. automethod:: Sphinx.add_role(name, role) @@ -54,9 +53,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) .. automethod:: Sphinx.add_role_to_domain(domain, name, role) @@ -109,6 +105,7 @@ Emitting events --------------- .. class:: Sphinx + :noindex: .. automethod:: emit(event, \*arguments) @@ -270,11 +267,6 @@ connect handlers to the events. Example: environment from the main process. *docnames* is a set of document names that have been read in the subprocess. - For a sample of how to deal with this event, look at the standard - ``sphinx.ext.todo`` extension. The implementation is often similar to that - of :event:`env-purge-doc`, only that information is not removed, but added to - the main environment from the other environment. - .. versionadded:: 1.3 .. event:: env-updated (app, env) diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index d009454ea..9551f8613 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -26,6 +26,61 @@ The following is a list of deprecated interfaces. - (will be) Removed - Alternatives + * - ``desc_signature['first']`` + - + - 3.0 + - N/A + + * - ``sphinx.directives.DescDirective`` + - 3.0 + - 5.0 + - ``sphinx.directives.ObjectDescription`` + + * - ``sphinx.domains.std.StandardDomain.add_object()`` + - 3.0 + - 5.0 + - ``sphinx.domains.std.StandardDomain.note_object()`` + + * - ``sphinx.domains.python.PyDecoratorMixin`` + - 3.0 + - 5.0 + - N/A + + * - ``sphinx.ext.autodoc.get_documenters()`` + - 3.0 + - 5.0 + - ``sphinx.registry.documenters`` + + * - ``sphinx.ext.autosummary.process_autosummary_toc()`` + - 3.0 + - 5.0 + - N/A + + * - ``sphinx.parsers.Parser.app`` + - 3.0 + - 5.0 + - N/A + + * - ``sphinx.testing.path.Path.text()`` + - 3.0 + - 5.0 + - ``sphinx.testing.path.Path.read_text()`` + + * - ``sphinx.testing.path.Path.bytes()`` + - 3.0 + - 5.0 + - ``sphinx.testing.path.Path.read_bytes()`` + + * - ``sphinx.util.inspect.getargspec()`` + - 3.0 + - 5.0 + - ``inspect.getargspec()`` + + * - ``sphinx.writers.latex.LaTeXWriter.format_docclass()`` + - 3.0 + - 5.0 + - LaTeX Themes + * - ``decode`` argument of ``sphinx.pycode.ModuleAnalyzer()`` - 2.4 - 4.0 diff --git a/doc/faq.rst b/doc/faq.rst index 741901dad..28d14d79e 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -48,12 +48,10 @@ Using Sphinx with... -------------------- Read the Docs - https://readthedocs.org is a documentation hosting service based around - Sphinx. They will host sphinx documentation, along with supporting a number - of other features including version support, PDF generation, and more. The - `Getting Started - <https://read-the-docs.readthedocs.io/en/latest/getting_started.html>`_ - guide is a good place to start. + `Read the Docs <https://readthedocs.org>`_ is a documentation hosting + service based around Sphinx. They will host sphinx documentation, along + with supporting a number of other features including version support, PDF + generation, and more. The `Getting Started`_ guide is a good place to start. Epydoc There's a third-party extension providing an `api role`_ which refers to @@ -145,6 +143,7 @@ Google Search 3. Add ``searchbox.html`` to the :confval:`html_sidebars` configuration value. +.. _Getting Started: https://docs.readthedocs.io/en/stable/intro/getting-started-with-sphinx.html .. _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/latex.rst b/doc/latex.rst index dd5fd1679..59a785c1e 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -817,6 +817,8 @@ Macros multiple paragraphs in header cells of tables. .. versionadded:: 1.6.3 ``\sphinxstylecodecontinued`` and ``\sphinxstylecodecontinues``. + .. versionadded:: 3.0 + ``\sphinxkeyboard`` - ``\sphinxtableofcontents``: it is a wrapper (defined differently in :file:`sphinxhowto.cls` and in :file:`sphinxmanual.cls`) of standard ``\tableofcontents``. The macro diff --git a/doc/make.bat b/doc/make.bat index 1e6dc991e..4bc6ddb9a 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -7,7 +7,6 @@ if "%SPHINXBUILD%" == "" ( )
set SOURCEDIR=.
set BUILDDIR=_build
-set SPHINXPROJ=sphinx-doc
if "%1" == "" goto help
diff --git a/doc/man/sphinx-apidoc.rst b/doc/man/sphinx-apidoc.rst index 30bfde0bb..725d2f169 100644 --- a/doc/man/sphinx-apidoc.rst +++ b/doc/man/sphinx-apidoc.rst @@ -5,7 +5,7 @@ Synopsis -------- **sphinx-apidoc** [*OPTIONS*] -o <*OUTPUT_PATH*> <*MODULE_PATH*> -[*EXCLUDE_PATTERN*, ...] +[*EXCLUDE_PATTERN* ...] Description ----------- diff --git a/doc/man/sphinx-autogen.rst b/doc/man/sphinx-autogen.rst index ef84afcb9..18ae8d1e9 100644 --- a/doc/man/sphinx-autogen.rst +++ b/doc/man/sphinx-autogen.rst @@ -73,7 +73,7 @@ If you run the following: .. code-block:: bash - $ PYTHONPATH=. sphinx-autodoc doc/index.rst + $ PYTHONPATH=. sphinx-autogen docs/index.rst then the following stub files will be created in ``docs``:: diff --git a/doc/templating.rst b/doc/templating.rst index fd0f6b637..1f4bef9c8 100644 --- a/doc/templating.rst +++ b/doc/templating.rst @@ -227,6 +227,7 @@ them to generate links or output multiply used elements. documents. .. function:: pathto(file, 1) + :noindex: Return the path to a *file* which is a filename relative to the root of the generated output. Use this to refer to static files. @@ -413,10 +414,6 @@ are in HTML form), these variables are also available: nonempty if the :confval:`html_copy_source` value is ``True``. This has empty value on creating automatically-generated files. -.. data:: title - - The page title. - .. data:: toc The local table of contents for the current page, rendered as HTML bullet diff --git a/doc/theming.rst b/doc/theming.rst index e2ef1fcf2..6a154affd 100644 --- a/doc/theming.rst +++ b/doc/theming.rst @@ -63,6 +63,11 @@ Python :mod:`ConfigParser` module) and has the following structure: highlighting. This can be overridden by the user in the :confval:`pygments_style` config value. +* The **pygments_dark_style** setting gives the name of a Pygments style to use + for highlighting when the CSS media query ``(prefers-color-scheme: dark)`` + evaluates to true. It is injected into the page using + :meth:`~Sphinx.add_css_file()`. + * The **sidebars** setting gives the comma separated list of sidebar templates for constructing sidebars. This can be overridden by the user in the :confval:`html_sidebars` config value. diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst index 0174078eb..431c0904d 100644 --- a/doc/usage/advanced/intl.rst +++ b/doc/usage/advanced/intl.rst @@ -9,11 +9,11 @@ Complementary to translations provided for Sphinx-generated messages such as navigation bars, Sphinx provides mechanisms facilitating *document* translations in itself. See the :ref:`intl-options` for details on configuration. -.. figure:: /_static/translation.png +.. figure:: /_static/translation.svg :width: 100% - Workflow visualization of translations in Sphinx. (The stick-figure is taken - from an `XKCD comic <https://xkcd.com/779/>`_.) + Workflow visualization of translations in Sphinx. (The figure is created by + `plantuml <http://plantuml.com>`_.) .. contents:: :local: diff --git a/doc/usage/advanced/setuptools.rst b/doc/usage/advanced/setuptools.rst index 10cc6a77d..f4dfb7f66 100644 --- a/doc/usage/advanced/setuptools.rst +++ b/doc/usage/advanced/setuptools.rst @@ -57,7 +57,7 @@ Once configured, call this by calling the relevant command on ``setup.py``:: Options for setuptools integration ---------------------------------- -.. confval:: fresh-env +.. setuptools-confval:: fresh-env A boolean that determines whether the saved environment should be discarded on build. Default is false. @@ -68,7 +68,7 @@ Options for setuptools integration $ python setup.py build_sphinx -E -.. confval:: all-files +.. setuptools-confval:: all-files A boolean that determines whether all files should be built from scratch. Default is false. @@ -79,7 +79,7 @@ Options for setuptools integration $ python setup.py build_sphinx -a -.. confval:: source-dir +.. setuptools-confval:: source-dir The target source directory. This can be relative to the ``setup.py`` or ``setup.cfg`` file, or it can be absolute. It defaults to ``./doc`` or @@ -92,12 +92,12 @@ Options for setuptools integration $ python setup.py build_sphinx -s $SOURCE_DIR -.. confval:: build-dir +.. setuptools-confval:: build-dir The target build directory. This can be relative to the ``setup.py`` or ``setup.cfg`` file, or it can be absolute. Default is ``./build/sphinx``. -.. confval:: config-dir +.. setuptools-confval:: config-dir Location of the configuration directory. This can be relative to the ``setup.py`` or ``setup.cfg`` file, or it can be absolute. Default is to use @@ -111,7 +111,7 @@ Options for setuptools integration .. versionadded:: 1.0 -.. confval:: builder +.. setuptools-confval:: builder The builder or list of builders to use. Default is ``html``. @@ -124,7 +124,7 @@ Options for setuptools integration .. versionchanged:: 1.6 This can now be a comma- or space-separated list of builders -.. confval:: warning-is-error +.. setuptools-confval:: warning-is-error A boolean that ensures Sphinx warnings will result in a failed build. Default is false. @@ -137,32 +137,32 @@ Options for setuptools integration .. versionadded:: 1.5 -.. confval:: project +.. setuptools-confval:: project The documented project's name. Default is ``''``. .. versionadded:: 1.0 -.. confval:: version +.. setuptools-confval:: version The short X.Y version. Default is ``''``. .. versionadded:: 1.0 -.. confval:: release +.. setuptools-confval:: release The full version, including alpha/beta/rc tags. Default is ``''``. .. versionadded:: 1.0 -.. confval:: today +.. setuptools-confval:: today How to format the current date, used as the replacement for ``|today|``. Default is ``''``. .. versionadded:: 1.0 -.. confval:: link-index +.. setuptools-confval:: link-index A boolean that ensures index.html will be linked to the master doc. Default is false. @@ -175,13 +175,13 @@ Options for setuptools integration .. versionadded:: 1.0 -.. confval:: copyright +.. setuptools-confval:: copyright The copyright string. Default is ``''``. .. versionadded:: 1.3 -.. confval:: nitpicky +.. setuptools-confval:: nitpicky Run in nit-picky mode. Currently, this generates warnings for all missing references. See the config value :confval:`nitpick_ignore` for a way to @@ -189,7 +189,7 @@ Options for setuptools integration .. versionadded:: 1.8 -.. confval:: pdb +.. setuptools-confval:: pdb A boolean to configure ``pdb`` on exception. Default is false. diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 6a25ae9ec..42e517ea7 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -574,7 +574,7 @@ General configuration A dictionary of options that modify how the lexer specified by :confval:`highlight_language` generates highlighted source code. These are lexer-specific; for the options understood by each, see the - `Pygments documentation <http://pygments.org/docs/lexers/>`_. + `Pygments documentation <https://pygments.org/docs/lexers.html>`_. .. versionadded:: 1.3 @@ -634,6 +634,16 @@ General configuration .. versionchanged:: 1.1 Now also removes ``<BLANKLINE>``. +.. confval:: strip_signature_backslash + + Default is ``False``. + When backslash stripping is enabled then every occurrence of ``\\`` in a + domain directive will be changed to ``\``, even within string literals. + This was the behaviour before version 3.0, and setting this variable to + ``True`` will reinstate that behaviour. + + .. versionadded:: 3.0 + .. _intl-options: @@ -781,7 +791,7 @@ documentation on :ref:`intl` for details. i18n additionally. You can specify below names: :index: index terms - :literal-block: literal blocks: ``::`` and ``code-block``. + :literal-block: literal blocks (``::`` annotation and ``code-block`` directive) :doctest-block: doctest block :raw: raw content :image: image/figure uri and alt @@ -949,7 +959,7 @@ that use Sphinx's HTMLWriter class. Example:: - html_css_files = ['custom.css' + html_css_files = ['custom.css', 'https://example.com/css/custom.css', ('print.css', {'media': 'print'})] @@ -1357,8 +1367,21 @@ that use Sphinx's HTMLWriter class. 'target' option or scale related options: 'scale', 'width', 'height'. The default is ``True``. + Document authors can this feature manually with giving ``no-scaled-link`` + class to the image: + + .. code-block:: rst + + .. image:: sphinx.png + :scale: 50% + :class: no-scaled-link + .. versionadded:: 1.3 + .. versionchanged:: 2.4 + + It is disabled for images having ``no-scaled-link`` class + .. confval:: html_math_renderer The name of math_renderer extension for HTML output. The default is @@ -1891,7 +1914,7 @@ These options influence LaTeX output. This value determines how to group the document tree into LaTeX source files. It must be a list of tuples ``(startdocname, targetname, title, author, - documentclass, toctree_only)``, where the items are: + theme, toctree_only)``, where the items are: *startdocname* String that specifies the :term:`document name` of the LaTeX file's master @@ -1913,13 +1936,8 @@ These options influence LaTeX output. applies. Use ``\\and`` to separate multiple authors, as in: ``'John \\and Sarah'`` (backslashes must be Python-escaped to reach LaTeX). - *documentclass* - Normally, one of ``'manual'`` or ``'howto'`` (provided by Sphinx and based - on ``'report'``, resp. ``'article'``; Japanese documents use ``'jsbook'``, - resp. ``'jreport'``.) "howto" (non-Japanese) documents will not get - appendices. Also they have a simpler title page. Other document classes - can be given. Independently of the document class, the "sphinx" package is - always loaded in order to define Sphinx's custom LaTeX commands. + *theme* + LaTeX theme. See :confval:`latex_theme`. *toctree_only* Must be ``True`` or ``False``. If true, the *startdoc* document itself is @@ -2074,6 +2092,33 @@ These options influence LaTeX output. This overrides the files which is provided from Sphinx such as ``sphinx.sty``. +.. confval:: latex_theme + + The "theme" that the LaTeX output should use. It is a collection of settings + for LaTeX output (ex. document class, top level sectioning unit and so on). + + As a built-in LaTeX themes, ``manual`` and ``howto`` are bundled. + + ``manual`` + A LaTeX theme for writing a manual. It imports the ``report`` document + class (Japanese documents use ``jsbook``). + + ``howto`` + A LaTeX theme for writing an article. It imports the ``article`` document + class (Japanese documents use ``jreport`` rather). :confval:`latex_appendices` + is available only for this theme. + + It defaults to ``'manual'``. + + .. versionadded:: 3.0 + +.. confval:: latex_theme_path + + A list of paths that contain custom LaTeX themes as subdirectories. Relative + paths are taken as relative to the configuration directory. + + .. versionadded:: 3.0 + .. _text-options: diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index b56e42d4d..60cde1ac7 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -140,6 +140,20 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. versionadded:: 1.1 + * autodoc considers a member private if its docstring contains + ``:meta private:`` in its :ref:`info-field-lists`. + For example: + + .. code-block:: rst + + def my_function(my_arg, my_other_arg): + """blah blah blah + + :meta private: + """ + + .. versionadded:: 3.0 + * Python "special" members (that is, those named like ``__special__``) will be included if the ``special-members`` flag option is given:: @@ -157,7 +171,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, * For classes and exceptions, members inherited from base classes will be left out when documenting all members, unless you give the - ``inherited-members`` flag option, in addition to ``members``:: + ``inherited-members`` option, in addition to ``members``:: .. autoclass:: Noodle :members: @@ -166,11 +180,29 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, This can be combined with ``undoc-members`` to document *all* available members of the class or module. + It can take an ancestor class not to document inherited members from it. + By default, members of ``object`` class are not documented. To show them + all, give ``None`` to the option. + + For example; If your class ``Foo`` is derived from ``list`` class and + you don't want to document ``list.__len__()``, you should specify a + option ``:inherited-members: list`` to avoid special members of list + class. + + Another example; If your class Foo has ``__str__`` special method and + autodoc directive has both ``inherited-members`` and ``special-members``, + ``__str__`` will be documented as in the past, but other special method + that are not implemented in your class ``Foo``. + Note: this will lead to markup errors if the inherited members come from a module whose docstrings are not reST formatted. .. versionadded:: 0.3 + .. versionchanged:: 3.0 + + It takes an anchestor class name as an argument. + * It's possible to override the signature for explicitly documented callable objects (functions, methods, classes) with the regular syntax that will override the signature gained from introspection:: @@ -308,9 +340,6 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, a decorator replaces the decorated function with another, it must copy the original ``__doc__`` to the new function. - From Python 2.5, :func:`functools.wraps` can be used to create - well-behaved decorating functions. - Configuration ------------- @@ -437,9 +466,13 @@ There are also config values that you can set: following values: * ``'signature'`` -- Show typehints as its signature (default) + * ``'description'`` -- Show typehints as content of function or method * ``'none'`` -- Do not show typehints .. versionadded:: 2.1 + .. versionadded:: 3.0 + + New option ``'description'`` is added. .. confval:: autodoc_warningiserror @@ -567,24 +600,3 @@ member should be included in the documentation by using the following event: ``inherited_members``, ``undoc_members``, ``show_inheritance`` and ``noindex`` that are true if the flag option of same name was given to the auto directive - -Generating documents from type annotations ------------------------------------------- - -As an experimental feature, autodoc provides ``sphinx.ext.autodoc.typehints`` as -an additional extension. It extends autodoc itself to generate function document -from its type annotations. - -To enable the feature, please add ``sphinx.ext.autodoc.typehints`` to list of -extensions and set `'description'` to :confval:`autodoc_typehints`: - -.. code-block:: python - - extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autodoc.typehints'] - - autodoc_typehints = 'description' - -.. versionadded:: 2.4 - - Added as an experimental feature. This will be integrated into autodoc core - in Sphinx-3.0. diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index c5211639e..cedc8a42f 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -148,6 +148,13 @@ also use these config values: Emits :event:`autodoc-skip-member` event as :mod:`~sphinx.ext.autodoc` does. +.. confval:: autosummary_generate_overwrite + + If true, autosummary already overwrites stub files by generated contents. + Defaults to true (enabled). + + .. versionadded:: 3.0 + .. confval:: autosummary_mock_imports This value contains a list of modules to be mocked up. See @@ -266,6 +273,7 @@ Additionally, the following filters are available replaces the builtin Jinja `escape filter`_ that does html-escaping. .. function:: underline(s, line='=') + :noindex: Add a title underline to a piece of text. diff --git a/doc/usage/extensions/doctest.rst b/doc/usage/extensions/doctest.rst index 79a75536e..62d8577eb 100644 --- a/doc/usage/extensions/doctest.rst +++ b/doc/usage/extensions/doctest.rst @@ -11,11 +11,15 @@ pair: testing; snippets -This extension allows you to test snippets in the documentation in a natural -way. It works by collecting specially-marked up code blocks and running them as -doctest tests. +It is often helpful to include snippets of code in your documentation and +demonstrate the results of executing them. But it is important to ensure that +the documentation stays up-to-date with the code. -Within one document, test code is partitioned in *groups*, where each group +This extension allows you to test such code snippets in the documentation in +a natural way. If you mark the code blocks as shown here, the ``doctest`` +builder will collect them and run them as doctest tests. + +Within each document, you can assign each snippet to a *group*. Each group consists of: * zero or more *setup code* blocks (e.g. importing the module to test) diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index 0b2070400..619ec8c20 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -148,3 +148,13 @@ project. The following example prints the Intersphinx mapping of the Python 3 documentation:: $ python -msphinx.ext.intersphinx https://docs.python.org/3/objects.inv + +Using Intersphinx with inventory file under Basic Authorization +--------------------------------------------------------------- + +Intersphinx supports Basic Authorization like this:: + + intersphinx_mapping = {'python': ('https://user:password@docs.python.org/3', + None)} + +The user and password will be stripped from the URL when generating the links. diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index f51b3084e..ba93bf192 100644 --- a/doc/usage/installation.rst +++ b/doc/usage/installation.rst @@ -23,8 +23,7 @@ Linux Debian/Ubuntu ~~~~~~~~~~~~~ -Install either ``python3-sphinx`` (Python 3) or ``python-sphinx`` (Python 2) -using :command:`apt-get`: +Install either ``python3-sphinx`` using :command:`apt-get`: :: @@ -77,23 +76,22 @@ __ https://formulae.brew.sh/formula/sphinx-doc MacPorts ~~~~~~~~ -Install either ``python36-sphinx`` (Python 3) or ``python27-sphinx`` (Python 2) -using :command:`port`: +Install either ``python3x-sphinx`` using :command:`port`: :: - $ sudo port install py36-sphinx + $ sudo port install py38-sphinx To set up the executable paths, use the ``port select`` command: :: - $ sudo port select --set python python36 - $ sudo port select --set sphinx py36-sphinx + $ sudo port select --set python python38 + $ sudo port select --set sphinx py38-sphinx For more information, refer to the `package overview`__. -__ https://www.macports.org/ports.php?by=library&substr=py36-sphinx +__ https://www.macports.org/ports.php?by=library&substr=py38-sphinx Anaconda ~~~~~~~~ @@ -108,12 +106,13 @@ Windows .. todo:: Could we start packaging this? Most Windows users do not have Python installed by default, so we begin with -the installation of Python itself. If you are unsure, open the *Command -Prompt* (:kbd:`⊞Win-r` and type :command:`cmd`). Once the command prompt is -open, type :command:`python --version` and press Enter. If Python is -available, you will see the version of Python printed to the screen. If you do -not have Python installed, refer to the `Hitchhikers Guide to Python's`__ -Python on Windows installation guides. You must install `Python 3`__. +the installation of Python itself. To check if you already have Python +installed, open the *Command Prompt* (:kbd:`⊞Win-r` and type :command:`cmd`). +Once the command prompt is open, type :command:`python --version` and press +Enter. If Python is installed, you will see the version of Python printed to +the screen. If you do not have Python installed, refer to the `Hitchhikers +Guide to Python's`__ Python on Windows installation guides. You must install +`Python 3`__. Once Python is installed, you can install Sphinx using :command:`pip`. Refer to the :ref:`pip installation instructions <install-pypi>` below for more @@ -160,6 +159,37 @@ the ``--pre`` flag. $ pip install -U --pre sphinx +Docker +------ + +Docker images for Sphinx are published on the `Docker Hub <https://hub.docker.com/>`_. There are two kind of images: + +- `sphinxdoc/sphinx <https://hub.docker.com/repository/docker/sphinxdoc/sphinx>`_ +- `sphinxdoc/sphinx-latexpdf <https://hub.docker.com/repository/docker/sphinxdoc/sphinx-latexpdf>`_ + +Former one is used for standard usage of Sphinx, and latter one is mainly used for PDF builds using LaTeX. +Please choose one for your purpose. + +.. note:: + + sphinxdoc/sphinx-latexpdf contains TeXLive packages. So the image is very large (over 2GB!). + +.. hint:: + + When using docker images, please use ``docker run`` command to invoke sphinx commands. For example, + you can use following command to create a Sphinx project:: + + $ docker run --rm -v /path/to/document:/docs sphinxdoc/sphinx sphinx-quickstart + + And you can following command this to build HTML document:: + + $ docker run --rm -v /path/to/document:/docs sphinxdoc/sphinx make html + +For more details, please read `README file`__ of docker images. + +.. __: https://hub.docker.com/repository/docker/sphinxdoc/sphinx + + Installation from source ------------------------ diff --git a/doc/usage/quickstart.rst b/doc/usage/quickstart.rst index 5279d3f87..b5462a388 100644 --- a/doc/usage/quickstart.rst +++ b/doc/usage/quickstart.rst @@ -6,7 +6,7 @@ Once Sphinx is :doc:`installed </usage/installation>`, you can proceed with setting up your first Sphinx project. To ease the process of getting started, Sphinx provides a tool, :program:`sphinx-quickstart`, which will generate a documentation source directory and populate it with some defaults. We're going -to use the :program:`sphinx-quickstart` tool here, though it's use by no means +to use the :program:`sphinx-quickstart` tool here, though its use is by no means necessary. @@ -26,9 +26,6 @@ configuration values from a few questions it asks you. To use this, run: $ sphinx-quickstart -Answer each question asked. Be sure to say yes to the ``autodoc`` extension, as -we will use this later. - There is also an automatic "API documentation" generator called :program:`sphinx-apidoc`; see :doc:`/man/sphinx-apidoc` for details. @@ -37,12 +34,11 @@ Defining document structure --------------------------- Let's assume you've run :program:`sphinx-quickstart`. It created a source -directory with :file:`conf.py` and a master document, :file:`index.rst` (if you -accepted the defaults). The main function of the :term:`master document` is to -serve as a welcome page, and to contain the root of the "table of contents -tree" (or *toctree*). This is one of the main things that Sphinx adds to -reStructuredText, a way to connect multiple files to a single hierarchy of -documents. +directory with :file:`conf.py` and a master document, :file:`index.rst`. The +main function of the :term:`master document` is to serve as a welcome page, and +to contain the root of the "table of contents tree" (or *toctree*). This is one +of the main things that Sphinx adds to reStructuredText, a way to connect +multiple files to a single hierarchy of documents. .. sidebar:: reStructuredText directives @@ -103,7 +99,7 @@ In Sphinx source files, you can use most features of standard For example, you can add cross-file references in a portable way (which works for all output types) using the :rst:role:`ref` role. -For an example, if you are viewing the HTML version you can look at the source +For an example, if you are viewing the HTML version, you can look at the source for this document -- use the "Show Source" link in the sidebar. .. todo:: Update the below link when we add new guides on these. @@ -233,8 +229,7 @@ customize a config value that is not automatically added by Keep in mind that the file uses Python syntax for strings, numbers, lists and so on. The file is saved in UTF-8 by default, as indicated by the encoding -declaration in the first line. If you use non-ASCII characters in any string -value, you need to use Python Unicode strings (like ``project = u'Exposé'``). +declaration in the first line. |more| See :doc:`/usage/configuration` for documentation of all available config values. @@ -252,10 +247,12 @@ module that provides additional features for Sphinx projects) called *autodoc*. In order to use *autodoc*, you need to activate it in :file:`conf.py` by putting the string ``'sphinx.ext.autodoc'`` into the list assigned to the -:confval:`extensions` config value. Then, you have a few additional directives -at your disposal. +:confval:`extensions` config value:: + + extensions = ['sphinx.ext.autodoc'] -For example, to document the function ``io.open()``, reading its signature and +Then, you have a few additional directives at your disposal. For example, to +document the function ``io.open()``, reading its signature and docstring from the source file, you'd write this:: .. autofunction:: io.open diff --git a/doc/usage/restructuredtext/basics.rst b/doc/usage/restructuredtext/basics.rst index 1f36cdc40..dffaf297c 100644 --- a/doc/usage/restructuredtext/basics.rst +++ b/doc/usage/restructuredtext/basics.rst @@ -372,7 +372,8 @@ Docutils supports the following directives: * HTML specifics: - - :dudir:`meta` (generation of HTML ``<meta>`` tags) + - :dudir:`meta` + (generation of HTML ``<meta>`` tags, see also :ref:`html-meta` below) - :dudir:`title <metadata-document-title>` (override document title) * Influencing markup: @@ -538,6 +539,45 @@ You can indent text after a comment start to form multiline comments:: Still in the comment. +.. _html-meta: + +HTML Metadata +------------- + +The :rst:dir:`meta` directive (:dudir:`ref <meta>`) allows specifying the HTML +`metadata element`_ of a Sphinx documentation page. For example, the +directive:: + + .. meta:: + :description: The Sphinx documentation builder + :keywords: Sphinx, documentation, builder + +will generate the following HTML output: + +.. code:: html + + <meta name="description" content="The Sphinx documentation builder"> + <meta name="keywords" content="Sphinx, documentation, builder"> + +Also, Sphinx will add the keywords as specified in the meta directive to the +search index. Thereby, the ``lang`` attribute of the meta element is +considered. For example, the directive:: + + .. meta:: + :keywords: backup + :keywords lang=en: pleasefindthiskey pleasefindthiskeytoo + :keywords lang=de: bittediesenkeyfinden + +adds the following words to the search indices of builds with different language +configurations: + +* ``pleasefindthiskey``, ``pleasefindthiskeytoo`` to *English* builds; +* ``bittediesenkeyfinden`` to *German* builds; +* ``backup`` to builds in all languages. + +.. _metadata element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta + + Source encoding --------------- diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index 584fff472..7b9bd2f80 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -48,6 +48,12 @@ tables of contents. The ``toctree`` directive is the central element. to the source directory. A numeric ``maxdepth`` option may be given to indicate the depth of the tree; by default, all levels are included. [#]_ + The representation of "TOC tree" is changed in each output format. The + builders that output multiple files (ex. HTML) treat it as a collection of + hyperlinks. On the other hand, the builders that output a single file (ex. + LaTeX, man page, etc.) replace it with the content of the documents on the + TOC tree. + Consider this example (taken from the Python docs' library reference index):: .. toctree:: @@ -441,7 +447,7 @@ If highlighting with the selected language fails (i.e. Pygments emits an want to ensure consistent highlighting, you should fix your version of Pygments. -__ http://pygments.org/docs/lexers/ +__ http://pygments.org/docs/lexers .. rst:directive:: .. highlight:: language @@ -874,6 +880,19 @@ mainly contained in information units, such as the language reference. .. versionchanged:: 1.1 Added ``see`` and ``seealso`` types, as well as marking main entries. + .. rubric:: options + + .. rst:directive:option:: name: a label for hyperlink + :type: text + + Define implicit target name that can be referenced by using + :rst:role:`ref`. For example:: + + .. index:: Python + :name: py-index + + .. versionadded:: 3.0 + .. rst:role:: index While the :rst:dir:`index` directive is a block-level markup and links to the @@ -1139,7 +1158,7 @@ derived forms), but provides enough to allow context-free grammars to be displayed in a way that causes uses of a symbol to be rendered as hyperlinks to the definition of the symbol. There is this directive: -.. rst:directive:: .. productionlist:: [name] +.. rst:directive:: .. productionlist:: [productionGroup] This directive is used to enclose a group of productions. Each production is given on a single line and consists of a name, separated by a colon from @@ -1147,16 +1166,24 @@ the definition of the symbol. There is this directive: continuation line must begin with a colon placed at the same column as in the first line. - The argument to :rst:dir:`productionlist` serves to distinguish different - sets of production lists that belong to different grammars. + The *productionGroup* argument to :rst:dir:`productionlist` serves to + distinguish different sets of production lists that belong to different + grammars. Multiple production lists with the same *productionGroup* thus + define rules in the same scope. Blank lines are not allowed within ``productionlist`` directive arguments. The definition can contain token names which are marked as interpreted text - (e.g. ``sum ::= `integer` "+" `integer```) -- this generates + (e.g. "``sum ::= `integer` "+" `integer```") -- this generates cross-references to the productions of these tokens. Outside of the production list, you can reference to token productions using :rst:role:`token`. + However, if you have given a *productionGroup* argument you must prefix the + token name in the cross-reference with the group name and a colon, + e.g., "``myGroup:sum``" instead of just "``sum``". + If the group should not be shown in the title of the link either + an explicit title can be given (e.g., "``myTitle <myGroup:sum>``"), + or the target can be prefixed with a tilde (e.g., "``~myGroup:sum``"). Note that no further reST parsing is done in the production, so that you don't have to escape ``*`` or ``|`` characters. diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 870f1be63..7a987be70 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -378,6 +378,9 @@ Info field lists ~~~~~~~~~~~~~~~~ .. versionadded:: 0.4 +.. versionchanged:: 3.0 + + meta fields are added. Inside Python object description directives, reST field lists with these fields are recognized and formatted nicely: @@ -391,6 +394,10 @@ are recognized and formatted nicely: * ``vartype``: Type of a variable. Creates a link if possible. * ``returns``, ``return``: Description of the return value. * ``rtype``: Return type. Creates a link if possible. +* ``meta``: Add metadata to description of the python object. The metadata will + not be shown on output document. For example, ``:meta private:`` indicates + the python object is private member. It is used in + :py:mod:`sphinx.ext.autodoc` for filtering members. .. note:: @@ -545,47 +552,62 @@ The C Domain The C domain (name **c**) is suited for documentation of C API. +.. rst:directive:: .. c:member:: declaration + .. c:var:: declaration + + Describes a C struct member or variable. Example signature:: + + .. c:member:: PyObject *PyTypeObject.tp_bases + + The difference between the two directives is only cosmetic. + .. rst:directive:: .. c:function:: function prototype Describes a C function. The signature should be given as in C, e.g.:: - .. c:function:: PyObject* PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems) - - This is also used to describe function-like preprocessor macros. The names - of the arguments should be given so they may be used in the description. + .. c:function:: PyObject *PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems) Note that you don't have to backslash-escape asterisks in the signature, as it is not parsed by the reST inliner. -.. rst:directive:: .. c:member:: declaration +.. rst:directive:: .. c:macro:: name + .. c:macro:: name(arg list) - Describes a C struct member. Example signature:: + Describes a C macro, i.e., a C-language ``#define``, without the replacement + text. - .. c:member:: PyObject* PyTypeObject.tp_bases + .. versionadded:: 3.0 + The function style variant. - The text of the description should include the range of values allowed, how - the value should be interpreted, and whether the value can be changed. - References to structure members in text should use the ``member`` role. +.. rst:directive:: .. c:struct:: name -.. rst:directive:: .. c:macro:: name + Describes a C struct. + + .. versionadded:: 3.0 + +.. rst:directive:: .. c:union:: name + + Describes a C union. + + .. versionadded:: 3.0 - Describes a "simple" C macro. Simple macros are macros which are used for - code expansion, but which do not take arguments so cannot be described as - functions. This is a simple C-language ``#define``. Examples of its use in - the Python documentation include :c:macro:`PyObject_HEAD` and - :c:macro:`Py_BEGIN_ALLOW_THREADS`. +.. rst:directive:: .. c:enum:: name -.. rst:directive:: .. c:type:: name + Describes a C enum. - Describes a C type (whether defined by a typedef or struct). The signature - should just be the type name. + .. versionadded:: 3.0 -.. rst:directive:: .. c:var:: declaration +.. rst:directive:: .. c:enumerator:: name - Describes a global C variable. The signature should include the type, such - as:: + Describes a C enumerator. - .. c:var:: PyObject* PyClass_Type + .. versionadded:: 3.0 + +.. rst:directive:: .. c:type:: typedef-like declaration + .. c:type:: name + + Describes a C type, either as a typedef, or the alias for an unspecified + type. .. _c-roles: @@ -595,25 +617,93 @@ Cross-referencing C constructs The following roles create cross-references to C-language constructs if they are defined in the documentation: -.. rst:role:: c:func +.. rst:role:: c:member + c:data + c:var + c:func + c:macro + c:struct + c:union + c:enum + c:enumerator + c:type - Reference a C-language function. Should include trailing parentheses. + Reference a C declaration, as defined above. + Note that :rst:role:`c:member`, :rst:role:`c:data`, and + :rst:role:`c:var` are equivalent. -.. rst:role:: c:member + .. versionadded:: 3.0 + The var, struct, union, enum, and enumerator roles. + + +Anonymous Entities +~~~~~~~~~~~~~~~~~~ + +C supports anonymous structs, enums, and unions. +For the sake of documentation they must be given some name that starts with +``@``, e.g., ``@42`` or ``@data``. +These names can also be used in cross-references, +though nested symbols will be found even when omitted. +The ``@...`` name will always be rendered as **[anonymous]** (possibly as a +link). + +Example:: + + .. c:struct:: Data + + .. c:union:: @data + + .. c:var:: int a + + .. c:var:: double b + + Explicit ref: :c:var:`Data.@data.a`. Short-hand ref: :c:var:`Data.a`. + +This will be rendered as: + +.. c:struct:: Data + + .. c:union:: @data + + .. c:var:: int a + + .. c:var:: double b - Reference a C-language member of a struct. +Explicit ref: :c:var:`Data.@data.a`. Short-hand ref: :c:var:`Data.a`. -.. rst:role:: c:macro +.. versionadded:: 3.0 + + +Inline Expressions and Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rst:role:: c:expr + c:texpr + + Insert a C expression or type either as inline code (``cpp:expr``) + or inline text (``cpp:texpr``). For example:: + + .. c:var:: int a = 42 + + .. c:function:: int f(int i) + + An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`). + + A type: :c:expr:`const Data*` + (or as text :c:texpr:`const Data*`). + + will be rendered as follows: - Reference a "simple" C macro, as defined above. + .. c:var:: int a = 42 -.. rst:role:: c:type + .. c:function:: int f(int i) - Reference a C-language type. + An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`). -.. rst:role:: c:data + A type: :c:expr:`const Data*` + (or as text :c:texpr:`const Data*`). - Reference a C-language variable. + .. versionadded:: 3.0 .. _cpp-domain: diff --git a/doc/usage/restructuredtext/field-lists.rst b/doc/usage/restructuredtext/field-lists.rst index b84d238ba..28b3cfe1b 100644 --- a/doc/usage/restructuredtext/field-lists.rst +++ b/doc/usage/restructuredtext/field-lists.rst @@ -51,3 +51,12 @@ At the moment, these metadata fields are recognized: :orphan: .. versionadded:: 1.0 + +``nosearch`` + If set, full text search for this file is disabled. :: + + :nosearch: + + .. note:: object search is still available even if `nosearch` option is set. + + .. versionadded:: 3.0 @@ -59,6 +59,7 @@ markers = apidoc setup_command test_params +testpaths = tests [coverage:run] branch = True @@ -25,7 +25,7 @@ install_requires = [ 'Pygments>=2.0', 'docutils>=0.12', 'snowballstemmer>=1.1', - 'babel>=1.3,!=2.0', + 'babel>=1.3', 'alabaster>=0.7,<0.8', 'imagesize', 'requests>=2.5.0', @@ -41,15 +41,19 @@ extras_require = { 'docs': [ 'sphinxcontrib-websupport', ], - 'test': [ - 'pytest < 5.3.3', - 'pytest-cov', - 'html5lib', + 'lint': [ 'flake8>=3.5.0', 'flake8-import-order', 'mypy>=0.770', 'docutils-stubs', ], + 'test': [ + 'pytest', + 'pytest-cov', + 'html5lib', + 'typed_ast', # for py35-37 + 'cython', + ], } # Provide a "compile_catalog" command that also creates the translated diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 2b23bc412..4889f35d5 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.4.5+' -__released__ = '2.4.5' # 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.4.5' # used when Sphinx builds its own docs #: #: .. versionadded:: 1.2 #: Before version 1.2, check the string ``sphinx.__version__``. -version_info = (2, 4, 5, '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 5dac63867..847a6d715 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -12,9 +12,9 @@ import warnings from typing import Any, Dict, List, Sequence from docutils import nodes -from docutils.nodes import Node +from docutils.nodes import Element, Node -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning if False: # For type annotation @@ -119,7 +119,7 @@ class desc_signature_line(nodes.Part, nodes.Inline, nodes.FixedTextElement): It should only be used in a ``desc_signature`` with ``is_multiline`` set. Set ``add_permalink = True`` for the line that should get the permalink. """ - sphinx_cpp_tagname = '' + sphinx_line_type = '' # nodes to use within a desc_signature or desc_signature_line @@ -174,6 +174,31 @@ class desc_content(nodes.General, nodes.Element): """ +class desc_sig_element(nodes.inline): + """Common parent class of nodes for inline text of a signature.""" + classes = [] # type: List[str] + + def __init__(self, rawsource: str = '', text: str = '', + *children: Element, **attributes: Any) -> None: + super().__init__(rawsource, text, *children, **attributes) + self['classes'].extend(self.classes) + + +class desc_sig_name(desc_sig_element): + """Node for a name in a signature.""" + classes = ["n"] + + +class desc_sig_operator(desc_sig_element): + """Node for an operator in a signature.""" + classes = ["o"] + + +class desc_sig_punctuation(desc_sig_element): + """Node for a punctuation in a signature.""" + classes = ["p"] + + # new admonition-like constructs class versionmodified(nodes.Admonition, nodes.TextElement): @@ -199,59 +224,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): @@ -385,11 +357,13 @@ def setup(app: "Sphinx") -> Dict[str, Any]: app.add_node(desc_optional) app.add_node(desc_annotation) app.add_node(desc_content) + app.add_node(desc_sig_name) + app.add_node(desc_sig_operator) + app.add_node(desc_sig_punctuation) app.add_node(versionmodified) 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 152c8bb43..d2fd776ff 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -16,7 +16,6 @@ import platform import sys import warnings from collections import deque -from inspect import isclass from io import StringIO from os import path from typing import Any, Callable, Dict, IO, List, Tuple, Union @@ -30,9 +29,7 @@ from pygments.lexer import Lexer import sphinx from sphinx import package_dir, locale from sphinx.config import Config -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain, Index from sphinx.environment import BuildEnvironment from sphinx.environment.collectors import EnvironmentCollector @@ -46,11 +43,10 @@ from sphinx.registry import SphinxComponentRegistry from sphinx.roles import XRefRole from sphinx.theming import Theme 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 CatalogRepository from sphinx.util.logging import prefixed_warnings from sphinx.util.osutil import abspath, ensuredir, relpath @@ -105,7 +101,6 @@ builtin_extensions = ( '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 @@ -168,6 +163,10 @@ class Sphinx: raise ApplicationError(__('Cannot find source directory (%s)') % self.srcdir) + if path.exists(self.outdir) and not path.isdir(self.outdir): + raise ApplicationError(__('Output directory (%s) is not a directory') % + self.srcdir) + if self.srcdir == self.outdir: raise ApplicationError(__('Source directory and destination ' 'directory cannot be identical')) @@ -355,13 +354,15 @@ class Sphinx: else __('finished with problems')) if self._warncount: if self.warningiserror: - msg = __('build %s, %s warning (with warnings treated as errors).', - 'build %s, %s warnings (with warnings treated as errors).', - self._warncount) + if self._warncount == 1: + msg = __('build %s, %s warning (with warnings treated as errors).') + else: + msg = __('build %s, %s warnings (with warnings treated as errors).') else: - msg = __('build %s, %s warning.', - 'build %s, %s warnings.', - self._warncount) + if self._warncount == 1: + msg = __('build %s, %s warning.') + else: + msg = __('build %s, %s warnings.') logger.info(bold(msg % (status, self._warncount))) else: @@ -408,29 +409,26 @@ class Sphinx: if version > sphinx.__display_version__[:3]: raise VersionRequirementError(version) - def import_object(self, objname: str, source: str = None) -> 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: str, callback: Callable) -> int: + def connect(self, event: str, callback: Callable, priority: int = 500) -> int: """Register *callback* to be called when *event* is emitted. For details on available core events and the arguments of callback functions, please see :ref:`events`. + Registered callbacks will be invoked on event in the order of *priority* and + registration. The priority is ascending order. + The method returns a "listener ID" that can be used as an argument to :meth:`disconnect`. + + .. versionchanged:: 3.0 + + Support *priority* """ - listener_id = self.events.connect(event, callback) - logger.debug('[app] connecting event %r: %r [id=%s]', event, callback, listener_id) + listener_id = self.events.connect(event, callback, priority) + logger.debug('[app] connecting event %r (%d): %r [id=%s]', + event, priority, callback, listener_id) return listener_id def disconnect(self, listener_id: int) -> None: @@ -592,36 +590,13 @@ class Sphinx: self.registry.add_enumerable_node(node, figtype, title_getter, override=override) self.add_node(node, override=override, **kwargs) - @property - def enumerable_nodes(self) -> Dict["Type[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: str, obj: Any, content: bool = None, - arguments: Tuple[int, int, bool] = None, override: bool = False, - **options: Any) -> None: + def add_directive(self, name: str, cls: "Type[Directive]", override: bool = False) -> 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 - <http://docutils.sourceforge.net/docs/howto/rst-directives.html>`_ - 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 + <http://docutils.sourceforge.net/docs/howto/rst-directives.html>`_ . For example, the (already existing) :rst:dir:`literalinclude` directive would be added like this: @@ -652,17 +627,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: str, role: Any, override: bool = False) -> None: """Register a Docutils role. @@ -712,25 +682,8 @@ class Sphinx: """ self.registry.add_domain(domain, override=override) - def override_domain(self, domain: "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: str, name: str, obj: Any, - has_content: bool = None, argument_spec: Any = None, - override: bool = False, **option_spec: Any) -> None: + def add_directive_to_domain(self, domain: str, name: str, + cls: "Type[Directive]", override: bool = False) -> None: """Register a Docutils directive in a domain. Like :meth:`add_directive`, but the directive is added to the domain @@ -740,9 +693,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: str, name: str, role: Union[RoleFunction, XRefRole], override: bool = False) -> None: @@ -924,8 +875,10 @@ class Sphinx: Add *filename* to the list of JavaScript files that the default HTML template will include. The filename must be relative to the HTML - static path , or a full URI with scheme. The keyword arguments are - also accepted for attributes of ``<script>`` tag. + static path , or a full URI with scheme. If the keyword argument + ``body`` is given, its value will be added between the + ``<script>`` tags. Extra keyword arguments are included as + attributes of the ``<script>`` tag. Example:: @@ -935,6 +888,9 @@ class Sphinx: app.add_js_file('example.js', async="async") # => <script src="_static/example.js" async="async"></script> + app.add_js_file(None, body="var myVariable = 'foo';") + # => <script>var myVariable = 'foo';</script> + .. versionadded:: 0.5 .. versionchanged:: 1.8 @@ -1197,12 +1153,6 @@ class Sphinx: return True - @property - def _setting_up_extension(self) -> List[str]: - warnings.warn('app._setting_up_extension is deprecated.', - RemovedInSphinx30Warning) - return ['?'] - class TemplateBridge: """ @@ -1239,12 +1189,3 @@ class TemplateBridge: specified context (a Python dictionary). """ raise NotImplementedError('must be implemented in subclasses') - - -from sphinx.config import CONFIG_FILENAME # NOQA - -deprecated_alias('sphinx.application', - { - 'CONFIG_FILENAME': CONFIG_FILENAME, - }, - RemovedInSphinx30Warning) diff --git a/sphinx/builders/html.py b/sphinx/builders/html/__init__.py index b3d2f1da2..6488146d5 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html/__init__.py @@ -28,7 +28,7 @@ from sphinx import package_dir, __display_version__ from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.config import Config -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain, Index, IndexEntry from sphinx.environment.adapters.asset import ImageAdapter from sphinx.environment.adapters.indexentries import IndexEntries @@ -53,7 +53,7 @@ if False: from typing import Type # for python3.5.1 -# HTML5 Writer is avialable or not +# HTML5 Writer is available or not if is_html5_writer_available(): from sphinx.writers.html5 import HTML5Translator html5_ready = True @@ -103,35 +103,6 @@ class Stylesheet(str): return self -class JSContainer(list): - """The container for JavaScript scripts.""" - def insert(self, index: int, obj: str) -> None: - warnings.warn('To modify script_files in the theme is deprecated. ' - 'Please insert a <script> tag directly in your theme instead.', - RemovedInSphinx30Warning, stacklevel=3) - super().insert(index, obj) - - def extend(self, other: List[str]) -> None: # type: ignore - warnings.warn('To modify script_files in the theme is deprecated. ' - 'Please insert a <script> tag directly in your theme instead.', - RemovedInSphinx30Warning, stacklevel=3) - for item in other: - self.append(item) - - def __iadd__(self, other: List[str]) -> "JSContainer": # type: ignore - warnings.warn('To modify script_files in the theme is deprecated. ' - 'Please insert a <script> tag directly in your theme instead.', - RemovedInSphinx30Warning, stacklevel=3) - for item in other: - self.append(item) - return self - - def __add__(self, other: List[str]) -> "JSContainer": - ret = JSContainer(self) - ret += other - return ret - - class JavaScript(str): """A metadata of javascript file. @@ -234,7 +205,7 @@ class StandaloneHTMLBuilder(Builder): self.css_files = [] # type: List[Dict[str, str]] # JS files - self.script_files = JSContainer() # type: List[JavaScript] + self.script_files = [] # type: List[JavaScript] def init(self) -> None: self.build_info = self.create_build_info() @@ -300,6 +271,19 @@ class StandaloneHTMLBuilder(Builder): style = 'sphinx' self.highlighter = PygmentsBridge('html', style) + if self.theme: + dark_style = self.theme.get_config('theme', 'pygments_dark_style', None) + else: + dark_style = None + + if dark_style is not None: + self.dark_highlighter = PygmentsBridge('html', dark_style) + self.add_css_file('pygments_dark.css', + media='(prefers-color-scheme: dark)', + id='pygments_dark_css') + else: + self.dark_highlighter = None + def init_css_files(self) -> None: for filename, attrs in self.app.registry.css_files: self.add_css_file(filename, **attrs) @@ -502,6 +486,7 @@ class StandaloneHTMLBuilder(Builder): 'show_source': self.config.html_show_sourcelink, 'sourcelink_suffix': self.config.html_sourcelink_suffix, 'file_suffix': self.out_suffix, + 'link_suffix': self.link_suffix, 'script_files': self.script_files, 'language': self.config.language, 'css_files': self.css_files, @@ -512,7 +497,7 @@ class StandaloneHTMLBuilder(Builder): 'parents': [], 'logo': logo, 'favicon': favicon, - 'html5_doctype': html5_ready and not self.config.html4_writer + 'html5_doctype': html5_ready and not self.config.html4_writer, } if self.theme: self.globalcontext.update( @@ -567,7 +552,7 @@ class StandaloneHTMLBuilder(Builder): title = self.render_partial(title_node)['title'] if title_node else '' # Suffix for the document - source_suffix = path.splitext(self.env.doc2path(docname))[1] + source_suffix = self.env.doc2path(docname, False)[len(docname):] # the name for the copied source if self.config.html_copy_source: @@ -747,6 +732,10 @@ class StandaloneHTMLBuilder(Builder): with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f: f.write(self.highlighter.get_stylesheet()) + if self.dark_highlighter: + with open(path.join(self.outdir, '_static', 'pygments_dark.css'), 'w') as f: + f.write(self.dark_highlighter.get_stylesheet()) + def copy_translation_js(self) -> None: """Copy a JavaScript file for translations.""" if self.config.language is not None: @@ -836,13 +825,17 @@ class StandaloneHTMLBuilder(Builder): if self.config.html_scaled_image_link and self.html_scaled_image_link: for node in doctree.traverse(nodes.image): - scale_keys = ('scale', 'width', 'height') - if not any((key in node) for key in scale_keys) or \ - isinstance(node.parent, nodes.reference): - # docutils does unfortunately not preserve the - # ``target`` attribute on images, so we need to check - # the parent node here. + if not any((key in node) for key in ['scale', 'width', 'height']): + # resizing options are not given. scaled image link is available + # only for resized images. + continue + elif isinstance(node.parent, nodes.reference): + # A image having hyperlink target + continue + elif 'no-scaled-link' in node['classes']: + # scaled image link is disabled for this node continue + uri = node['uri'] reference = nodes.reference('', '', internal=True) if uri in self.images: @@ -876,7 +869,11 @@ class StandaloneHTMLBuilder(Builder): if self.indexer is not None and title: filename = self.env.doc2path(pagename, base=None) try: - self.indexer.feed(pagename, filename, title, doctree) + metadata = self.env.metadata.get(pagename, {}) + if 'nosearch' in metadata: + self.indexer.feed(pagename, filename, '', new_document('')) + else: + self.indexer.feed(pagename, filename, title, doctree) except TypeError: # fallback for old search-adapters self.indexer.feed(pagename, title, doctree) # type: ignore @@ -1000,15 +997,6 @@ class StandaloneHTMLBuilder(Builder): return False ctx['hasdoc'] = hasdoc - def warn(*args: Any, **kwargs: Any) -> str: - """Simple warn() wrapper for themes.""" - warnings.warn('The template function warn() was deprecated. ' - 'Use warning() instead.', - RemovedInSphinx30Warning, stacklevel=2) - logger.warning(*args, **kwargs) - return '' # return empty string - ctx['warn'] = warn - ctx['toctree'] = lambda **kwargs: self._get_local_toctree(pagename, **kwargs) self.add_sidebars(pagename, ctx) ctx.update(addctx) diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 6712ffc25..1348f0e12 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -20,7 +20,8 @@ import sphinx.builders.latex.nodes # NOQA # Workaround: import this before wri from sphinx import package_dir, addnodes, highlighting from sphinx.application import Sphinx from sphinx.builders import Builder -from sphinx.builders.latex.constants import ADDITIONAL_SETTINGS, DEFAULT_SETTINGS +from sphinx.builders.latex.constants import ADDITIONAL_SETTINGS, DEFAULT_SETTINGS, SHORTHANDOFF +from sphinx.builders.latex.theming import Theme, ThemeFactory from sphinx.builders.latex.util import ExtBabel from sphinx.config import Config, ENUM from sphinx.deprecation import RemovedInSphinx40Warning @@ -126,11 +127,13 @@ class LaTeXBuilder(Builder): self.context = {} # type: Dict[str, Any] self.docnames = [] # type: Iterable[str] self.document_data = [] # type: List[Tuple[str, str, str, str, str, bool]] + self.themes = ThemeFactory(self.app) self.usepackages = self.app.registry.latex_packages texescape.init() self.init_context() self.init_babel() + self.init_multilingual() def get_outdated_docs(self) -> Union[str, List[str]]: return 'all documents' # for now @@ -206,6 +209,41 @@ class LaTeXBuilder(Builder): logger.warning(__('no Babel option known for language %r'), self.config.language) + def init_multilingual(self) -> None: + if self.context['latex_engine'] == 'pdflatex': + if not self.babel.uses_cyrillic(): + if 'X2' in self.context['fontenc']: + self.context['substitutefont'] = '\\usepackage{substitutefont}' + self.context['textcyrillic'] = '\\usepackage[Xtwo]{sphinxcyrillic}' + elif 'T2A' in self.context['fontenc']: + self.context['substitutefont'] = '\\usepackage{substitutefont}' + self.context['textcyrillic'] = '\\usepackage[TtwoA]{sphinxcyrillic}' + if 'LGR' in self.context['fontenc']: + self.context['substitutefont'] = '\\usepackage{substitutefont}' + else: + self.context['textgreek'] = '' + + # 'babel' key is public and user setting must be obeyed + if self.context['babel']: + self.context['classoptions'] += ',' + self.babel.get_language() + # this branch is not taken for xelatex/lualatex if default settings + self.context['multilingual'] = self.context['babel'] + if self.config.language: + self.context['shorthandoff'] = SHORTHANDOFF + + # Times fonts don't work with Cyrillic languages + if self.babel.uses_cyrillic() and 'fontpkg' not in self.config.latex_elements: + self.context['fontpkg'] = '' + elif self.context['polyglossia']: + self.context['classoptions'] += ',' + self.babel.get_language() + options = self.babel.get_mainlanguage_options() + if options: + language = r'\setmainlanguage[%s]{%s}' % (options, self.babel.get_language()) + else: + language = r'\setmainlanguage{%s}' % self.babel.get_language() + + self.context['multilingual'] = '%s\n%s' % (self.context['polyglossia'], language) + def write_stylesheet(self) -> None: highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style) stylesheet = path.join(self.outdir, 'sphinxhighlight.sty') @@ -227,7 +265,8 @@ class LaTeXBuilder(Builder): self.write_stylesheet() for entry in self.document_data: - docname, targetname, title, author, docclass = entry[:5] + docname, targetname, title, author, themename = entry[:5] + theme = self.themes.get(themename) toctree_only = False if len(entry) > 5: toctree_only = entry[5] @@ -243,21 +282,22 @@ class LaTeXBuilder(Builder): doctree = self.assemble_doctree( docname, toctree_only, - appendices=(self.config.latex_appendices if docclass != 'howto' else [])) - doctree['docclass'] = docclass + appendices=(self.config.latex_appendices if theme.name != 'howto' else [])) + doctree['docclass'] = theme.docclass doctree['contentsname'] = self.get_contentsname(docname) doctree['tocdepth'] = tocdepth self.post_process_images(doctree) - self.update_doc_context(title, author) + self.update_doc_context(title, author, theme) with progress_message(__("writing")): docsettings._author = author docsettings._title = title docsettings._contentsname = doctree['contentsname'] docsettings._docname = docname - docsettings._docclass = docclass + docsettings._docclass = theme.name doctree.settings = docsettings + docwriter.theme = theme docwriter.write(doctree, destination) def get_contentsname(self, indexfile: str) -> str: @@ -270,9 +310,11 @@ class LaTeXBuilder(Builder): return contentsname - def update_doc_context(self, title: str, author: str) -> None: + def update_doc_context(self, title: str, author: str, theme: Theme) -> None: self.context['title'] = title self.context['author'] = author + self.context['docclass'] = theme.docclass + self.context['wrapperclass'] = theme.wrapperclass def assemble_doctree(self, indexfile: str, toctree_only: bool, appendices: List[str]) -> nodes.document: # NOQA self.docnames = set([indexfile] + appendices) @@ -408,31 +450,31 @@ def patch_settings(settings: Any) -> Any: class Values(type(settings)): # type: ignore @property - def author(self): + def author(self) -> str: warnings.warn('settings.author is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self._author @property - def title(self): + def title(self) -> str: warnings.warn('settings.title is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self._title @property - def contentsname(self): + def contentsname(self) -> str: warnings.warn('settings.contentsname is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self._contentsname @property - def docname(self): + def docname(self) -> str: warnings.warn('settings.docname is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self._docname @property - def docclass(self): + def docclass(self) -> str: warnings.warn('settings.docclass is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self._docclass @@ -444,7 +486,7 @@ def patch_settings(settings: Any) -> Any: def validate_config_values(app: Sphinx, config: Config) -> None: for key in list(config.latex_elements): if key not in DEFAULT_SETTINGS: - msg = __("Unknown configure key: latex_elements[%r]. ignored.") + msg = __("Unknown configure key: latex_elements[%r], ignored.") logger.warning(msg % (key,)) config.latex_elements.pop(key) @@ -487,7 +529,7 @@ def default_latex_documents(config: Config) -> List[Tuple[str, str, str, str, st make_filename_from_project(config.project) + '.tex', texescape.escape_abbr(project), texescape.escape_abbr(author), - 'manual')] + config.latex_theme)] def setup(app: Sphinx) -> Dict[str, Any]: @@ -510,6 +552,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('latex_show_pagerefs', False, None) app.add_config_value('latex_elements', {}, None) app.add_config_value('latex_additional_files', [], None) + app.add_config_value('latex_theme', 'manual', None, [str]) + app.add_config_value('latex_theme_path', [], None, [list]) app.add_config_value('latex_docclass', default_latex_docclass, None) diff --git a/sphinx/builders/latex/constants.py b/sphinx/builders/latex/constants.py index 39fbe0195..9a89036bf 100644 --- a/sphinx/builders/latex/constants.py +++ b/sphinx/builders/latex/constants.py @@ -184,9 +184,19 @@ ADDITIONAL_SETTINGS = { 'babel': '\\usepackage{babel}', }, ('xelatex', 'zh'): { + 'polyglossia': '', + 'babel': '\\usepackage{babel}', 'fontenc': '\\usepackage{xeCJK}', }, ('xelatex', 'el'): { 'fontpkg': XELATEX_GREEK_DEFAULT_FONTPKG, }, } # type: Dict[Any, Dict[str, Any]] + + +SHORTHANDOFF = r''' +\ifdefined\shorthandoff + \ifnum\catcode`\=\string=\active\shorthandoff{=}\fi + \ifnum\catcode`\"=\active\shorthandoff{"}\fi +\fi +''' diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py new file mode 100644 index 000000000..56f2735f0 --- /dev/null +++ b/sphinx/builders/latex/theming.py @@ -0,0 +1,118 @@ +""" + sphinx.builders.latex.theming + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Theming support for LaTeX builder. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import configparser +from os import path +from typing import Dict + +from sphinx.application import Sphinx +from sphinx.config import Config +from sphinx.errors import ThemeError +from sphinx.locale import __ +from sphinx.util import logging + +logger = logging.getLogger(__name__) + + +class Theme: + """A set of LaTeX configurations.""" + + def __init__(self, name: str) -> None: + self.name = name + self.docclass = name + self.wrapperclass = name + self.toplevel_sectioning = 'chapter' + + +class BuiltInTheme(Theme): + """A built-in LaTeX theme.""" + + def __init__(self, name: str, config: Config) -> None: + # Note: Don't call supermethod here. + self.name = name + self.latex_docclass = config.latex_docclass # type: Dict[str, str] + + @property + def docclass(self) -> str: # type: ignore + if self.name == 'howto': + return self.latex_docclass.get('howto', 'article') + else: + return self.latex_docclass.get('manual', 'report') + + @property + def wrapperclass(self) -> str: # type: ignore + if self.name in ('manual', 'howto'): + return 'sphinx' + self.name + else: + return self.name + + @property + def toplevel_sectioning(self) -> str: # type: ignore + # we assume LaTeX class provides \chapter command except in case + # of non-Japanese 'howto' case + if self.name == 'howto' and not self.docclass.startswith('j'): + return 'section' + else: + return 'chapter' + + +class UserTheme(Theme): + """A user defined LaTeX theme.""" + + def __init__(self, name: str, filename: str) -> None: + self.name = name + self.config = configparser.RawConfigParser() + self.config.read(path.join(filename)) + + try: + self.docclass = self.config.get('theme', 'docclass') + self.wrapperclass = self.config.get('theme', 'wrapperclass') + self.toplevel_sectioning = self.config.get('theme', 'toplevel_sectioning') + except configparser.NoSectionError: + raise ThemeError(__('%r doesn\'t have "theme" setting') % filename) + except configparser.NoOptionError as exc: + raise ThemeError(__('%r doesn\'t have "%s" setting') % (filename, exc.args[0])) + + +class ThemeFactory: + """A factory class for LaTeX Themes.""" + + def __init__(self, app: Sphinx) -> None: + self.themes = {} # type: Dict[str, Theme] + self.theme_paths = [path.join(app.srcdir, p) for p in app.config.latex_theme_path] + self.load_builtin_themes(app.config) + + def load_builtin_themes(self, config: Config) -> None: + """Load built-in themes.""" + self.themes['manual'] = BuiltInTheme('manual', config) + self.themes['howto'] = BuiltInTheme('howto', config) + + def get(self, name: str) -> Theme: + """Get a theme for given *name*.""" + if name in self.themes: + return self.themes[name] + else: + theme = self.find_user_theme(name) + if theme: + return theme + else: + return Theme(name) + + def find_user_theme(self, name: str) -> Theme: + """Find a theme named as *name* from latex_theme_path.""" + for theme_path in self.theme_paths: + config_path = path.join(theme_path, name, 'theme.conf') + if path.isfile(config_path): + try: + return UserTheme(name, config_path) + except ThemeError as exc: + logger.warning(exc) + + return None diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 3ef0ff5d9..28841ad77 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -591,7 +591,7 @@ class IndexInSectionTitleTransform(SphinxTransform): """ default_priority = 400 - def apply(self): + def apply(self, **kwargs: Any) -> None: for node in self.document.traverse(nodes.title): if isinstance(node.parent, nodes.section): for i, index in enumerate(node.traverse(addnodes.index)): diff --git a/sphinx/builders/latex/util.py b/sphinx/builders/latex/util.py index 8155d1fd7..b7d79121c 100644 --- a/sphinx/builders/latex/util.py +++ b/sphinx/builders/latex/util.py @@ -8,12 +8,8 @@ :license: BSD, see LICENSE for details. """ -import warnings - from docutils.writers.latex2e import Babel -from sphinx.deprecation import RemovedInSphinx30Warning - class ExtBabel(Babel): cyrillic_languages = ('bulgarian', 'kazakh', 'mongolian', 'russian', 'ukrainian') @@ -24,12 +20,6 @@ class ExtBabel(Babel): self.supported = True super().__init__(language_code or '') - def get_shorthandoff(self) -> str: - warnings.warn('ExtBabel.get_shorthandoff() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - from sphinx.writers.latex import SHORTHANDOFF - return SHORTHANDOFF - def uses_cyrillic(self) -> bool: return self.language in self.cyrillic_languages diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 1b0dd4011..9fe689ec9 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -8,6 +8,7 @@ :license: BSD, see LICENSE for details. """ +import json import queue import re import socket @@ -90,6 +91,8 @@ class CheckExternalLinksBuilder(Builder): socket.setdefaulttimeout(5.0) # create output file open(path.join(self.outdir, 'output.txt'), 'w').close() + # create JSON output file + open(path.join(self.outdir, 'output.json'), 'w').close() # create queues and worker threads self.wqueue = queue.Queue() # type: queue.Queue @@ -225,9 +228,16 @@ class CheckExternalLinksBuilder(Builder): def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: uri, docname, lineno, status, info, code = result + + filename = self.env.doc2path(docname, None) + linkstat = dict(filename=filename, lineno=lineno, + status=status, code=code, uri=uri, + info=info) if status == 'unchecked': + self.write_linkstat(linkstat) return if status == 'working' and info == 'old': + self.write_linkstat(linkstat) return if lineno: logger.info('(line %4d) ', lineno, nonl=True) @@ -236,18 +246,22 @@ class CheckExternalLinksBuilder(Builder): logger.info(darkgray('-ignored- ') + uri + ': ' + info) else: logger.info(darkgray('-ignored- ') + uri) + self.write_linkstat(linkstat) elif status == 'local': logger.info(darkgray('-local- ') + uri) - self.write_entry('local', docname, lineno, uri) + self.write_entry('local', docname, filename, lineno, uri) + self.write_linkstat(linkstat) elif status == 'working': logger.info(darkgreen('ok ') + uri + info) + self.write_linkstat(linkstat) elif status == 'broken': - self.write_entry('broken', docname, lineno, uri + ': ' + info) if self.app.quiet or self.app.warningiserror: logger.warning(__('broken link: %s (%s)'), uri, info, - location=(self.env.doc2path(docname), lineno)) + location=(filename, lineno)) else: logger.info(red('broken ') + uri + red(' - ' + info)) + self.write_entry('broken', docname, filename, lineno, uri + ': ' + info) + self.write_linkstat(linkstat) elif status == 'redirected': try: text, color = { @@ -259,9 +273,11 @@ class CheckExternalLinksBuilder(Builder): }[code] except KeyError: text, color = ('with unknown code', purple) - self.write_entry('redirected ' + text, docname, lineno, - uri + ' to ' + info) + linkstat['text'] = text logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info)) + self.write_entry('redirected ' + text, docname, filename, + lineno, uri + ' to ' + info) + self.write_linkstat(linkstat) def get_target_uri(self, docname: str, typ: str = None) -> str: return '' @@ -301,10 +317,15 @@ class CheckExternalLinksBuilder(Builder): if self.broken: self.app.statuscode = 1 - def write_entry(self, what: str, docname: str, line: int, uri: str) -> None: - with open(path.join(self.outdir, 'output.txt'), 'a', encoding='utf-8') as output: - output.write("%s:%s: [%s] %s\n" % (self.env.doc2path(docname, None), - line, what, uri)) + def write_entry(self, what: str, docname: str, filename: str, line: int, + uri: str) -> None: + with open(path.join(self.outdir, 'output.txt'), 'a') as output: + output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) + + def write_linkstat(self, data: dict) -> None: + with open(path.join(self.outdir, 'output.json'), 'a') as output: + output.write(json.dumps(data)) + output.write('\n') def finish(self) -> None: for worker in self.workers: diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index bcf2a20c3..cf50f1730 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -9,9 +9,11 @@ """ import argparse +import bdb import locale import multiprocessing import os +import pdb import sys import traceback from typing import Any, IO, List @@ -29,8 +31,10 @@ from sphinx.util.docutils import docutils_namespace, patch_docutils def handle_exception(app: Sphinx, args: Any, exception: BaseException, stderr: IO = sys.stderr) -> None: # NOQA + if isinstance(exception, bdb.BdbQuit): + return + if args.pdb: - import pdb print(red(__('Exception occurred while building, starting debugger:')), file=stderr) traceback.print_exc() @@ -180,7 +184,7 @@ files can be built by specifying individual filenames. group.add_argument('-W', action='store_true', dest='warningiserror', help=__('turn warnings into errors')) group.add_argument('--keep-going', action='store_true', dest='keep_going', - help=__("With -W, keep going when getting warnings")) + help=__("with -W, keep going when getting warnings")) group.add_argument('-T', action='store_true', dest='traceback', help=__('show full traceback on exception')) group.add_argument('-P', action='store_true', dest='pdb', diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index eb7e9eb02..8f8ae58a1 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -54,10 +54,8 @@ EXTENSIONS = OrderedDict([ ('imgmath', __('include math, rendered as PNG or SVG images')), ('mathjax', __('include math, rendered in the browser by MathJax')), ('ifconfig', __('conditional inclusion of content based on config values')), - ('viewcode', - __('include links to the source code of documented Python objects')), - ('githubpages', - __('create .nojekyll file to publish the document on GitHub pages')), + ('viewcode', __('include links to the source code of documented Python objects')), + ('githubpages', __('create .nojekyll file to publish the document on GitHub pages')), ]) DEFAULTS = { @@ -129,8 +127,7 @@ def boolean(x: str) -> bool: def suffix(x: str) -> str: if not (x[0:1] == '.' and len(x) > 1): - raise ValidationError(__("Please enter a file suffix, " - "e.g. '.rst' or '.txt'.")) + raise ValidationError(__("Please enter a file suffix, e.g. '.rst' or '.txt'.")) return x @@ -228,16 +225,16 @@ def ask_user(d: Dict) -> None: """ print(bold(__('Welcome to the Sphinx %s quickstart utility.')) % __display_version__) - print(__(''' -Please enter values for the following settings (just press Enter to -accept a default value, if one is given in brackets).''')) + print() + print(__('Please enter values for the following settings (just press Enter to\n' + 'accept a default value, if one is given in brackets).')) if 'path' in d: - print(bold(__(''' -Selected root path: %s''') % d['path'])) + print() + print(bold(__('Selected root path: %s')) % d['path']) else: - print(__(''' -Enter the root path for documentation.''')) + print() + print(__('Enter the root path for documentation.')) d['path'] = do_prompt(__('Root path for the documentation'), '.', is_path) while path.isfile(path.join(d['path'], 'conf.py')) or \ @@ -247,70 +244,68 @@ Enter the root path for documentation.''')) 'selected root path.'))) print(__('sphinx-quickstart will not overwrite existing Sphinx projects.')) print() - d['path'] = do_prompt(__('Please enter a new root path (or just Enter ' - 'to exit)'), '', is_path) + d['path'] = do_prompt(__('Please enter a new root path (or just Enter to exit)'), + '', is_path) if not d['path']: sys.exit(1) if 'sep' not in d: - print(__(''' -You have two options for placing the build directory for Sphinx output. -Either, you use a directory "_build" within the root path, or you separate -"source" and "build" directories within the root path.''')) - d['sep'] = do_prompt(__('Separate source and build directories (y/n)'), - 'n', boolean) + print() + print(__('You have two options for placing the build directory for Sphinx output.\n' + 'Either, you use a directory "_build" within the root path, or you separate\n' + '"source" and "build" directories within the root path.')) + d['sep'] = do_prompt(__('Separate source and build directories (y/n)'), 'n', boolean) if 'dot' not in d: - print(__(''' -Inside the root directory, two more directories will be created; "_templates" -for custom HTML templates and "_static" for custom stylesheets and other static -files. You can enter another prefix (such as ".") to replace the underscore.''')) + print() + print(__('Inside the root directory, two more directories will be created; "_templates"\n' # NOQA + 'for custom HTML templates and "_static" for custom stylesheets and other static\n' # NOQA + 'files. You can enter another prefix (such as ".") to replace the underscore.')) # NOQA d['dot'] = do_prompt(__('Name prefix for templates and static dir'), '_', ok) if 'project' not in d: - print(__(''' -The project name will occur in several places in the built documentation.''')) + print() + print(__('The project name will occur in several places in the built documentation.')) d['project'] = do_prompt(__('Project name')) if 'author' not in d: d['author'] = do_prompt(__('Author name(s)')) if 'version' not in d: - print(__(''' -Sphinx has the notion of a "version" and a "release" for the -software. Each version can have multiple releases. For example, for -Python the version is something like 2.5 or 3.0, while the release is -something like 2.5.1 or 3.0a1. If you don't need this dual structure, -just set both to the same value.''')) + print() + print(__('Sphinx has the notion of a "version" and a "release" for the\n' + 'software. Each version can have multiple releases. For example, for\n' + 'Python the version is something like 2.5 or 3.0, while the release is\n' + 'something like 2.5.1 or 3.0a1. If you don\'t need this dual structure,\n' + 'just set both to the same value.')) d['version'] = do_prompt(__('Project version'), '', allow_empty) if 'release' not in d: d['release'] = do_prompt(__('Project release'), d['version'], allow_empty) if 'language' not in d: - print(__(''' -If the documents are to be written in a language other than English, -you can select a language here by its language code. Sphinx will then -translate text that it generates into that language. - -For a list of supported codes, see -https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.''')) + print() + print(__('If the documents are to be written in a language other than English,\n' + 'you can select a language here by its language code. Sphinx will then\n' + 'translate text that it generates into that language.\n' + '\n' + 'For a list of supported codes, see\n' + 'https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.')) # NOQA d['language'] = do_prompt(__('Project language'), 'en') if d['language'] == 'en': d['language'] = None if 'suffix' not in d: - print(__(''' -The file name suffix for source files. Commonly, this is either ".txt" -or ".rst". Only files with this suffix are considered documents.''')) + print() + print(__('The file name suffix for source files. Commonly, this is either ".txt"\n' + 'or ".rst". Only files with this suffix are considered documents.')) d['suffix'] = do_prompt(__('Source file suffix'), '.rst', suffix) if 'master' not in d: - print(__(''' -One document is special in that it is considered the top node of the -"contents tree", that is, it is the root of the hierarchical structure -of the documents. Normally, this is "index", but if your "index" -document is a custom template, you can also set this to another filename.''')) - d['master'] = do_prompt(__('Name of your master document (without suffix)'), - 'index') + print() + print(__('One document is special in that it is considered the top node of the\n' + '"contents tree", that is, it is the root of the hierarchical structure\n' + 'of the documents. Normally, this is "index", but if your "index"\n' + 'document is a custom template, you can also set this to another filename.')) + d['master'] = do_prompt(__('Name of your master document (without suffix)'), 'index') while path.isfile(path.join(d['path'], d['master'] + d['suffix'])) or \ path.isfile(path.join(d['path'], 'source', d['master'] + d['suffix'])): @@ -323,8 +318,7 @@ document is a custom template, you can also set this to another filename.''')) 'existing file and press Enter'), d['master']) if 'extensions' not in d: - print(__('Indicate which of the following Sphinx extensions should be ' - 'enabled:')) + print(__('Indicate which of the following Sphinx extensions should be enabled:')) d['extensions'] = [] for name, description in EXTENSIONS.items(): if do_prompt('%s: %s (y/n)' % (name, description), 'n', boolean): @@ -332,20 +326,19 @@ document is a custom template, you can also set this to another filename.''')) # Handle conflicting options if {'sphinx.ext.imgmath', 'sphinx.ext.mathjax'}.issubset(d['extensions']): - print(__('Note: imgmath and mathjax cannot be enabled at the same ' - 'time. imgmath has been deselected.')) + print(__('Note: imgmath and mathjax cannot be enabled at the same time. ' + 'imgmath has been deselected.')) d['extensions'].remove('sphinx.ext.imgmath') if 'makefile' not in d: - print(__(''' -A Makefile and a Windows command file can be generated for you so that you -only have to run e.g. `make html' instead of invoking sphinx-build -directly.''')) + print() + print(__('A Makefile and a Windows command file can be generated for you so that you\n' + 'only have to run e.g. `make html\' instead of invoking sphinx-build\n' + 'directly.')) d['makefile'] = do_prompt(__('Create Makefile? (y/n)'), 'y', boolean) if 'batchfile' not in d: - d['batchfile'] = do_prompt(__('Create Windows command file? (y/n)'), - 'y', boolean) + d['batchfile'] = do_prompt(__('Create Windows command file? (y/n)'), 'y', boolean) print() @@ -428,17 +421,18 @@ def generate(d: Dict, overwrite: bool = True, silent: bool = False, templatedir: return print() print(bold(__('Finished: An initial directory structure has been created.'))) - print(__(''' -You should now populate your master file %s and create other documentation -source files. ''') % masterfile + ((d['makefile'] or d['batchfile']) and __('''\ -Use the Makefile to build the docs, like so: - make builder -''') or __('''\ -Use the sphinx-build command to build the docs, like so: - sphinx-build -b builder %s %s -''') % (srcdir, builddir)) + __('''\ -where "builder" is one of the supported builders, e.g. html, latex or linkcheck. -''')) + print() + print(__('You should now populate your master file %s and create other documentation\n' + 'source files. ') % masterfile, end='') + if d['makefile'] or d['batchfile']: + print(__('Use the Makefile to build the docs, like so:\n' + ' make builder')) + else: + print(__('Use the sphinx-build command to build the docs, like so:\n' + ' sphinx-build -b builder %s %s') % (srcdir, builddir)) + print(__('where "builder" is one of the supported builders, ' + 'e.g. html, latex or linkcheck.')) + print() def valid_dir(d: Dict) -> bool: @@ -471,16 +465,18 @@ def valid_dir(d: Dict) -> bool: def get_parser() -> argparse.ArgumentParser: + description = __( + "\n" + "Generate required files for a Sphinx project.\n" + "\n" + "sphinx-quickstart is an interactive tool that asks some questions about your\n" + "project and then generates a complete documentation directory and sample\n" + "Makefile to be used with sphinx-build.\n" + ) parser = argparse.ArgumentParser( usage='%(prog)s [OPTIONS] <PROJECT_DIR>', epilog=__("For more information, visit <http://sphinx-doc.org/>."), - description=__(""" -Generate required files for a Sphinx project. - -sphinx-quickstart is an interactive tool that asks some questions about your -project and then generates a complete documentation directory and sample -Makefile to be used with sphinx-build. -""")) + description=description) parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', default=None, @@ -579,8 +575,8 @@ def main(argv: List[str] = sys.argv[1:]) -> int: try: if 'quiet' in d: if not {'project', 'author'}.issubset(d): - print(__('''"quiet" is specified, but any of "project" or \ -"author" is not specified.''')) + print(__('"quiet" is specified, but any of "project" or ' + '"author" is not specified.')) return 1 if {'quiet', 'project', 'author'}.issubset(d): diff --git a/sphinx/cmdline.py b/sphinx/cmdline.py deleted file mode 100644 index f938f8234..000000000 --- a/sphinx/cmdline.py +++ /dev/null @@ -1,43 +0,0 @@ -""" - sphinx.cmdline - ~~~~~~~~~~~~~~ - - sphinx-build command-line handling. - - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import argparse -import sys -import warnings -from typing import Any, IO, List, Union - -from sphinx.application import Sphinx -from sphinx.cmd import build -from sphinx.deprecation import RemovedInSphinx30Warning - - -def handle_exception(app: Sphinx, args: Any, exception: Union[Exception, KeyboardInterrupt], - stderr: IO = sys.stderr) -> None: - warnings.warn('sphinx.cmdline module is deprecated. Use sphinx.cmd.build instead.', - RemovedInSphinx30Warning, stacklevel=2) - build.handle_exception(app, args, exception, stderr) - - -def jobs_argument(value: str) -> int: - warnings.warn('sphinx.cmdline module is deprecated. Use sphinx.cmd.build instead.', - RemovedInSphinx30Warning, stacklevel=2) - return build.jobs_argument(value) - - -def get_parser() -> argparse.ArgumentParser: - warnings.warn('sphinx.cmdline module is deprecated. Use sphinx.cmd.build instead.', - RemovedInSphinx30Warning, stacklevel=2) - return build.get_parser() - - -def main(argv: List[str] = sys.argv[1:]) -> int: - warnings.warn('sphinx.cmdline module is deprecated. Use sphinx.cmd.build instead.', - RemovedInSphinx30Warning, stacklevel=2) - return build.main(argv) diff --git a/sphinx/config.py b/sphinx/config.py index bba994389..87007c33d 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -18,7 +18,7 @@ from typing import ( Any, Callable, Dict, Generator, Iterator, List, NamedTuple, Set, Tuple, Union ) -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ConfigError, ExtensionError from sphinx.locale import _, __ from sphinx.util import logging @@ -154,26 +154,7 @@ class Config: 'env', []), } # type: Dict[str, Tuple] - def __init__(self, *args: Any) -> None: - if len(args) == 4: - # old style arguments: (dirname, filename, overrides, tags) - warnings.warn('The argument of Config() class has been changed. ' - 'Use Config.read() to read configuration from conf.py.', - RemovedInSphinx30Warning, stacklevel=2) - dirname, filename, overrides, tags = args - if dirname is None: - config = {} # type: Dict[str, Any] - else: - config = eval_config_file(path.join(dirname, filename), tags) - else: - # new style arguments: (config={}, overrides={}) - if len(args) == 0: - config, overrides = {}, {} - elif len(args) == 1: - config, overrides = args[0], {} - else: - config, overrides = args[:2] - + def __init__(self, config: Dict[str, Any] = {}, overrides: Dict[str, Any] = {}) -> None: self.overrides = dict(overrides) self.values = Config.config_values.copy() self._raw_config = config @@ -193,16 +174,6 @@ class Config: namespace = eval_config_file(filename, tags) return cls(namespace, overrides or {}) - def check_types(self) -> None: - warnings.warn('Config.check_types() is deprecated. Use check_confval_types() instead.', - RemovedInSphinx30Warning, stacklevel=2) - check_confval_types(None, self) - - def check_unicode(self) -> None: - warnings.warn('Config.check_unicode() is deprecated. Use check_unicode() instead.', - RemovedInSphinx30Warning, stacklevel=2) - check_unicode(self) - def convert_overrides(self, name: str, value: Any) -> Any: if not isinstance(value, str): return value @@ -353,6 +324,9 @@ def eval_config_file(filename: str, tags: Tags) -> Dict[str, Any]: msg = __("The configuration file (or one of the modules it imports) " "called sys.exit()") raise ConfigError(msg) + except ConfigError: + # pass through ConfigError from conf.py as is. It will be shown in console. + raise except Exception: msg = __("There is a programmable error in your configuration file:\n\n%s") raise ConfigError(msg % traceback.format_exc()) diff --git a/sphinx/deprecation.py b/sphinx/deprecation.py index dec5efa85..5e5e673d2 100644 --- a/sphinx/deprecation.py +++ b/sphinx/deprecation.py @@ -15,15 +15,15 @@ from typing import Any, Dict from typing import Type # for python3.5.1 -class RemovedInSphinx30Warning(DeprecationWarning): +class RemovedInSphinx40Warning(DeprecationWarning): pass -class RemovedInSphinx40Warning(PendingDeprecationWarning): +class RemovedInSphinx50Warning(PendingDeprecationWarning): pass -RemovedInNextVersionWarning = RemovedInSphinx30Warning +RemovedInNextVersionWarning = RemovedInSphinx40Warning def deprecated_alias(modname: str, objects: Dict, warning: Type[Warning]) -> None: diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 9a2fb4412..6b5df39b8 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -18,7 +18,9 @@ from docutils.parsers.rst import directives, roles from sphinx import addnodes from sphinx.addnodes import desc_signature -from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias +from sphinx.deprecation import ( + RemovedInSphinx40Warning, RemovedInSphinx50Warning, deprecated_alias +) from sphinx.util import docutils from sphinx.util.docfields import DocFieldTransformer, Field, TypedField from sphinx.util.docutils import SphinxDirective @@ -34,7 +36,7 @@ nl_escape_re = re.compile(r'\\\n') strip_backslash_re = re.compile(r'\\(.)') -def optional_int(argument): +def optional_int(argument: str) -> int: """ Check for an integer argument or None value; raise ``ValueError`` if not. """ @@ -89,12 +91,13 @@ class ObjectDescription(SphinxDirective): """ Retrieve the signatures to document from the directive arguments. By default, signatures are given as arguments, one per line. - - Backslash-escaping of newlines is supported. """ lines = nl_escape_re.sub('', self.arguments[0]).split('\n') - # remove backslashes to support (dummy) escapes; helps Vim highlighting - return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines] + if self.config.strip_signature_backslash: + # remove backslashes to support (dummy) escapes; helps Vim highlighting + return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines] + else: + return [line.strip() for line in lines] def handle_signature(self, sig: str, signode: desc_signature) -> Any: """ @@ -160,6 +163,8 @@ class ObjectDescription(SphinxDirective): # 'desctype' is a backwards compatible attribute node['objtype'] = node['desctype'] = self.objtype node['noindex'] = noindex = ('noindex' in self.options) + if self.domain: + node['classes'].append(self.domain) self.names = [] # type: List[Any] signatures = self.get_signatures() @@ -167,7 +172,7 @@ class ObjectDescription(SphinxDirective): # add a signature node for each signature in the current unit # and add a reference target for it signode = addnodes.desc_signature(sig, '') - signode['first'] = False + self.set_source_info(signode) node.append(signode) try: # name can also be a tuple, e.g. (classname, objname); @@ -285,12 +290,15 @@ deprecated_alias('sphinx.directives', }, RemovedInSphinx40Warning) - -# backwards compatible old name (will be marked deprecated in 3.0) -DescDirective = ObjectDescription +deprecated_alias('sphinx.directives', + { + 'DescDirective': ObjectDescription, + }, + RemovedInSphinx50Warning) def setup(app: "Sphinx") -> Dict[str, Any]: + app.add_config_value("strip_signature_backslash", False, 'env') directives.register_directive('default-role', DefaultRole) directives.register_directive('default-domain', DefaultDomain) directives.register_directive('describe', ObjectDescription) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 87d769b41..e4fcc0f5c 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -85,14 +85,14 @@ class TocTree(SphinxDirective): ret.append(wrappernode) return ret - def parse_content(self, toctree): + def parse_content(self, toctree: addnodes.toctree) -> List[Node]: suffixes = self.config.source_suffix # glob target documents all_docnames = self.env.found_docs.copy() all_docnames.remove(self.env.docname) # remove current document - ret = [] + ret = [] # type: List[Node] excluded = Matcher(self.config.exclude_patterns) for entry in self.content: if not entry: diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index b521cd025..11b3a4604 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -11,6 +11,7 @@ import copy from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, Union +from typing import cast from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -70,6 +71,9 @@ class Index: a domain, subclass Index, overriding the three name attributes: * `name` is an identifier used for generating file names. + It is also used for a hyperlink target for the index. Therefore, users can + refer the index page using ``ref`` role and a string which is combined + domain name and ``name`` attribute (ex. ``:ref:`py-modindex```). * `localname` is the section title for the index. * `shortname` is a short name for the index, for use in the relation bar in HTML output. Can be empty to disable entries in the relation bar. @@ -77,6 +81,11 @@ class Index: and providing a :meth:`generate()` method. Then, add the index class to your domain's `indices` list. Extensions can add indices to existing domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. + + .. versionchanged:: 3.0 + + Index pages can be referred by domain name and index name via + :rst:role:`ref` role. """ name = None # type: str @@ -219,6 +228,17 @@ class Domain: self.objtypes_for_role = self._role2type.get # type: Callable[[str], List[str]] self.role_for_objtype = self._type2role.get # type: Callable[[str], str] + def setup(self) -> None: + """Set up domain object.""" + from sphinx.domains.std import StandardDomain + + # Add special hyperlink target for index pages (ex. py-modindex) + std = cast(StandardDomain, self.env.get_domain('std')) + for index in self.indices: + if index.name and index.localname: + docname = "%s-%s" % (self.name, index.name) + std.note_hyperlink_target(docname, docname, '', index.localname) + def add_object_type(self, name: str, objtype: ObjType) -> None: """Add an object type.""" self.object_types[name] = objtype diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index cc24df47e..8f071a9aa 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -9,53 +9,2930 @@ """ import re -import string -from typing import Any, Dict, Iterator, List, Tuple +from typing import ( + Any, Callable, Dict, Iterator, List, Type, Tuple, Union +) from typing import cast from docutils import nodes -from docutils.nodes import Element +from docutils.nodes import Element, Node, TextElement, system_message from sphinx import addnodes -from sphinx.addnodes import pending_xref, desc_signature +from sphinx.addnodes import pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ -from sphinx.roles import XRefRole +from sphinx.roles import SphinxRole, XRefRole from sphinx.util import logging +from sphinx.util.cfamily import ( + NoOldIdError, ASTBaseBase, verify_description_mode, StringifyTransform, + BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, + identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, + hex_literal_re, binary_literal_re, float_literal_re, + char_literal_re +) from sphinx.util.docfields import Field, TypedField from sphinx.util.nodes import make_refnode - logger = logging.getLogger(__name__) -# RE to split at word boundaries -wsplit_re = re.compile(r'(\W+)') - -# REs for C signatures -c_sig_re = re.compile( - r'''^([^(]*?) # return type - ([\w:.]+) \s* # thing name (colon allowed for C++) - (?: \((.*)\) )? # optionally arguments - (\s+const)? $ # const specifier - ''', re.VERBOSE) -c_funcptr_sig_re = re.compile( - r'''^([^(]+?) # return type - (\( [^()]+ \)) \s* # name in parentheses - \( (.*) \) # arguments - (\s+const)? $ # const specifier - ''', re.VERBOSE) -c_funcptr_arg_sig_re = re.compile( - r'''^\s*([^(,]+?) # return type - \( ([^()]+) \) \s* # name in parentheses - \( (.*) \) # arguments - (\s+const)? # const specifier - \s*(?=$|,) # end with comma or end of string - ''', re.VERBOSE) -c_funcptr_name_re = re.compile(r'^\(\s*\*\s*(.*?)\s*\)$') +# https://en.cppreference.com/w/c/keyword +_keywords = [ + 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', + 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'inline', 'int', 'long', + 'register', 'restrict', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', + 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', + '_Alignas', 'alignas', '_Alignof', 'alignof', '_Atomic', '_Bool', 'bool', + '_Complex', 'complex', '_Generic', '_Imaginary', 'imaginary', + '_Noreturn', 'noreturn', '_Static_assert', 'static_assert', + '_Thread_local', 'thread_local', +] + +# these are ordered by preceedence +_expression_bin_ops = [ + ['||'], + ['&&'], + ['|'], + ['^'], + ['&'], + ['==', '!='], + ['<=', '>=', '<', '>'], + ['<<', '>>'], + ['+', '-'], + ['*', '/', '%'], + ['.*', '->*'] +] +_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "~"] +_expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", + ">>=", "<<=", "&=", "^=", "|="] + +_max_id = 1 +_id_prefix = [None, 'c.', 'Cv2.'] +# Ids are used in lookup keys which are used across pickled files, +# so when _max_id changes, make sure to update the ENV_VERSION. + +_string_re = re.compile(r"[LuU8]?('([^'\\]*(?:\\.[^'\\]*)*)'" + r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) + + +class _DuplicateSymbolError(Exception): + def __init__(self, symbol: "Symbol", declaration: "ASTDeclaration") -> None: + assert symbol + assert declaration + self.symbol = symbol + self.declaration = declaration + + def __str__(self) -> str: + return "Internal C duplicate symbol error:\n%s" % self.symbol.dump(0) + + +class ASTBase(ASTBaseBase): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + raise NotImplementedError(repr(self)) + + +# Names +################################################################################ + +class ASTIdentifier(ASTBaseBase): + def __init__(self, identifier: str) -> None: + assert identifier is not None + assert len(identifier) != 0 + self.identifier = identifier + + def is_anon(self) -> bool: + return self.identifier[0] == '@' + + # and this is where we finally make a difference between __str__ and the display string + + def __str__(self) -> str: + return self.identifier + + def get_display_string(self) -> str: + return "[anonymous]" if self.is_anon() else self.identifier + + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", + prefix: str, symbol: "Symbol") -> None: + # note: slightly different signature of describe_signature due to the prefix + verify_description_mode(mode) + if mode == 'markType': + targetText = prefix + self.identifier + pnode = addnodes.pending_xref('', refdomain='c', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + # key = symbol.get_lookup_key() + # pnode['c:parent_key'] = key + if self.is_anon(): + pnode += nodes.strong(text="[anonymous]") + else: + pnode += nodes.Text(self.identifier) + signode += pnode + elif mode == 'lastIsName': + if self.is_anon(): + signode += nodes.strong(text="[anonymous]") + else: + signode += addnodes.desc_name(self.identifier, self.identifier) + elif mode == 'noneIsName': + if self.is_anon(): + signode += nodes.strong(text="[anonymous]") + else: + signode += nodes.Text(self.identifier) + else: + raise Exception('Unknown description mode: %s' % mode) + + +class ASTNestedName(ASTBase): + def __init__(self, names: List[ASTIdentifier], rooted: bool) -> None: + assert len(names) > 0 + self.names = names + self.rooted = rooted + + @property + def name(self) -> "ASTNestedName": + return self + + def get_id(self, version: int) -> str: + return '.'.join(str(n) for n in self.names) + + def _stringify(self, transform: StringifyTransform) -> str: + res = '.'.join(transform(n) for n in self.names) + if self.rooted: + return '.' + res + else: + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + # just print the name part, with template args, not template params + if mode == 'noneIsName': + signode += nodes.Text(str(self)) + elif mode == 'param': + name = str(self) + signode += nodes.emphasis(name, name) + elif mode == 'markType' or mode == 'lastIsName' or mode == 'markName': + # Each element should be a pending xref targeting the complete + # prefix. + prefix = '' + first = True + names = self.names[:-1] if mode == 'lastIsName' else self.names + # If lastIsName, then wrap all of the prefix in a desc_addname, + # else append directly to signode. + # TODO: also for C? + # NOTE: Breathe relies on the prefix being in the desc_addname node, + # so it can remove it in inner declarations. + dest = signode + if mode == 'lastIsName': + dest = addnodes.desc_addname() + for i in range(len(names)): + ident = names[i] + if not first: + dest += nodes.Text('.') + prefix += '.' + first = False + txt_ident = str(ident) + if txt_ident != '': + ident.describe_signature(dest, 'markType', env, prefix, symbol) + prefix += txt_ident + if mode == 'lastIsName': + if len(self.names) > 1: + dest += addnodes.desc_addname('.', '.') + signode += dest + self.names[-1].describe_signature(signode, mode, env, '', symbol) + else: + raise Exception('Unknown description mode: %s' % mode) + + +################################################################################ +# Expressions +################################################################################ + +class ASTExpression(ASTBase): + pass + + +# Primary expressions +################################################################################ + +class ASTLiteral(ASTExpression): + pass + + +class ASTBooleanLiteral(ASTLiteral): + def __init__(self, value: bool) -> None: + self.value = value + + def _stringify(self, transform: StringifyTransform) -> str: + if self.value: + return 'true' + else: + return 'false' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text(str(self))) + + +class ASTNumberLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + txt = str(self) + signode.append(nodes.Text(txt, txt)) + + +class ASTCharLiteral(ASTLiteral): + def __init__(self, prefix: str, data: str) -> None: + self.prefix = prefix # may be None when no prefix + self.data = data + decoded = data.encode().decode('unicode-escape') + if len(decoded) == 1: + self.value = ord(decoded) + else: + raise UnsupportedMultiCharacterCharLiteral(decoded) + + def _stringify(self, transform: StringifyTransform) -> str: + if self.prefix is None: + return "'" + self.data + "'" + else: + return self.prefix + "'" + self.data + "'" + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + txt = str(self) + signode.append(nodes.Text(txt, txt)) + + +class ASTStringLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + txt = str(self) + signode.append(nodes.Text(txt, txt)) + + +class ASTIdExpression(ASTExpression): + def __init__(self, name: ASTNestedName): + # note: this class is basically to cast a nested name as an expression + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def get_id(self, version: int) -> str: + return self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.name.describe_signature(signode, mode, env, symbol) + + +class ASTParenExpr(ASTExpression): + def __init__(self, expr): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '(' + transform(self.expr) + ')' + + def get_id(self, version: int) -> str: + return self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('(', '(')) + self.expr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')', ')')) + + +# Postfix expressions +################################################################################ + +class ASTPostfixOp(ASTBase): + pass + + +class ASTPostfixCallExpr(ASTPostfixOp): + def __init__(self, lst: Union["ASTParenExprList", "ASTBracedInitList"]) -> None: + self.lst = lst + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.lst) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.lst.describe_signature(signode, mode, env, symbol) + + +class ASTPostfixArray(ASTPostfixOp): + def __init__(self, expr): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '[' + transform(self.expr) + ']' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('[')) + self.expr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(']')) + + +class ASTPostfixInc(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '++' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('++')) + + +class ASTPostfixDec(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '--' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('--')) + + +class ASTPostfixMember(ASTPostfixOp): + def __init__(self, name): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '.' + transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('.')) + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixMemberOfPointer(ASTPostfixOp): + def __init__(self, name): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '->' + transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('->')) + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixExpr(ASTExpression): + def __init__(self, prefix: ASTExpression, postFixes: List[ASTPostfixOp]): + self.prefix = prefix + self.postFixes = postFixes + + def _stringify(self, transform: StringifyTransform) -> str: + res = [transform(self.prefix)] + for p in self.postFixes: + res.append(transform(p)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.prefix.describe_signature(signode, mode, env, symbol) + for p in self.postFixes: + p.describe_signature(signode, mode, env, symbol) + + +# Unary expressions +################################################################################ + +class ASTUnaryOpExpr(ASTExpression): + def __init__(self, op: str, expr: ASTExpression): + self.op = op + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.op) + transform(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text(self.op)) + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTSizeofType(ASTExpression): + def __init__(self, typ): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof(" + transform(self.typ) + ")" + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('sizeof(')) + self.typ.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) + + +class ASTSizeofExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof " + transform(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('sizeof ')) + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTAlignofExpr(ASTExpression): + def __init__(self, typ: "ASTType"): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "alignof(" + transform(self.typ) + ")" + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('alignof(')) + self.typ.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) + + +# Other expressions +################################################################################ + +class ASTCastExpr(ASTExpression): + def __init__(self, typ: "ASTType", expr: ASTExpression): + self.typ = typ + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.typ)) + res.append(')') + res.append(transform(self.expr)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('(')) + self.typ.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTBinOpExpr(ASTBase): + def __init__(self, exprs: List[ASTExpression], ops: List[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode.append(nodes.Text(' ')) + signode.append(nodes.Text(self.ops[i - 1])) + signode.append(nodes.Text(' ')) + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTAssignmentExpr(ASTExpression): + def __init__(self, exprs: List[ASTExpression], ops: List[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode.append(nodes.Text(' ')) + signode.append(nodes.Text(self.ops[i - 1])) + signode.append(nodes.Text(' ')) + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTFallbackExpr(ASTExpression): + def __init__(self, expr: str): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return self.expr + + def get_id(self, version: int) -> str: + return str(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode += nodes.Text(self.expr) + + +################################################################################ +# Types +################################################################################ + +class ASTTrailingTypeSpec(ASTBase): + pass + + +class ASTTrailingTypeSpecFundamental(ASTTrailingTypeSpec): + def __init__(self, name: str) -> None: + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return self.name + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode += nodes.Text(str(self.name)) + + +class ASTTrailingTypeSpecName(ASTTrailingTypeSpec): + def __init__(self, prefix: str, nestedName: ASTNestedName) -> None: + self.prefix = prefix + self.nestedName = nestedName + + @property + def name(self) -> ASTNestedName: + return self.nestedName + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.prefix: + res.append(self.prefix) + res.append(' ') + res.append(transform(self.nestedName)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + if self.prefix: + signode += addnodes.desc_annotation(self.prefix, self.prefix) + signode += nodes.Text(' ') + self.nestedName.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTFunctionParameter(ASTBase): + def __init__(self, arg: "ASTTypeWithInit", ellipsis: bool = False) -> None: + self.arg = arg + self.ellipsis = ellipsis + + def _stringify(self, transform: StringifyTransform) -> str: + if self.ellipsis: + return '...' + else: + return transform(self.arg) + + def describe_signature(self, signode: Any, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.ellipsis: + signode += nodes.Text('...') + else: + self.arg.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTParameters(ASTBase): + def __init__(self, args: List[ASTFunctionParameter]) -> None: + self.args = args + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.args + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append('(') + first = True + for a in self.args: + if not first: + res.append(', ') + first = False + res.append(str(a)) + res.append(')') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + paramlist = addnodes.desc_parameterlist() + for arg in self.args: + param = addnodes.desc_parameter('', '', noemph=True) + if mode == 'lastIsName': # i.e., outer-function params + arg.describe_signature(param, 'param', env, symbol=symbol) + else: + arg.describe_signature(param, 'markType', env, symbol=symbol) + paramlist += param + signode += paramlist + + +class ASTDeclSpecsSimple(ASTBaseBase): + def __init__(self, storage: str, threadLocal: str, inline: bool, + restrict: bool, volatile: bool, const: bool, attrs: List[Any]) -> None: + self.storage = storage + self.threadLocal = threadLocal + self.inline = inline + self.restrict = restrict + self.volatile = volatile + self.const = const + self.attrs = attrs + + def mergeWith(self, other: "ASTDeclSpecsSimple") -> "ASTDeclSpecsSimple": + if not other: + return self + return ASTDeclSpecsSimple(self.storage or other.storage, + self.threadLocal or other.threadLocal, + self.inline or other.inline, + self.volatile or other.volatile, + self.const or other.const, + self.restrict or other.restrict, + self.attrs + other.attrs) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] # type: List[str] + res.extend(transform(attr) for attr in self.attrs) + if self.storage: + res.append(self.storage) + if self.threadLocal: + res.append(self.threadLocal) + if self.inline: + res.append('inline') + if self.restrict: + res.append('restrict') + if self.volatile: + res.append('volatile') + if self.const: + res.append('const') + return ' '.join(res) + + def describe_signature(self, modifiers: List[Node]) -> None: + def _add(modifiers: List[Node], text: str) -> None: + if len(modifiers) > 0: + modifiers.append(nodes.Text(' ')) + modifiers.append(addnodes.desc_annotation(text, text)) + + for attr in self.attrs: + if len(modifiers) > 0: + modifiers.append(nodes.Text(' ')) + modifiers.append(attr.describe_signature(modifiers)) + if self.storage: + _add(modifiers, self.storage) + if self.threadLocal: + _add(modifiers, self.threadLocal) + if self.inline: + _add(modifiers, 'inline') + if self.restrict: + _add(modifiers, 'restrict') + if self.volatile: + _add(modifiers, 'volatile') + if self.const: + _add(modifiers, 'const') + + +class ASTDeclSpecs(ASTBase): + def __init__(self, outer: str, + leftSpecs: ASTDeclSpecsSimple, + rightSpecs: ASTDeclSpecsSimple, + trailing: ASTTrailingTypeSpec) -> None: + # leftSpecs and rightSpecs are used for output + # allSpecs are used for id generation TODO: remove? + self.outer = outer + self.leftSpecs = leftSpecs + self.rightSpecs = rightSpecs + self.allSpecs = self.leftSpecs.mergeWith(self.rightSpecs) + self.trailingTypeSpec = trailing + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] # type: List[str] + l = transform(self.leftSpecs) + if len(l) > 0: + res.append(l) + if self.trailingTypeSpec: + if len(res) > 0: + res.append(" ") + res.append(transform(self.trailingTypeSpec)) + r = str(self.rightSpecs) + if len(r) > 0: + if len(res) > 0: + res.append(" ") + res.append(r) + return "".join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + modifiers = [] # type: List[Node] + + def _add(modifiers: List[Node], text: str) -> None: + if len(modifiers) > 0: + modifiers.append(nodes.Text(' ')) + modifiers.append(addnodes.desc_annotation(text, text)) + + self.leftSpecs.describe_signature(modifiers) + + for m in modifiers: + signode += m + if self.trailingTypeSpec: + if len(modifiers) > 0: + signode += nodes.Text(' ') + self.trailingTypeSpec.describe_signature(signode, mode, env, + symbol=symbol) + modifiers = [] + self.rightSpecs.describe_signature(modifiers) + if len(modifiers) > 0: + signode += nodes.Text(' ') + for m in modifiers: + signode += m + + +# Declarator +################################################################################ + +class ASTArray(ASTBase): + def __init__(self, size: ASTExpression): + self.size = size + + def _stringify(self, transform: StringifyTransform) -> str: + if self.size: + return '[' + transform(self.size) + ']' + else: + return '[]' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode.append(nodes.Text("[")) + if self.size: + self.size.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text("]")) + + +class ASTDeclarator(ASTBase): + @property + def name(self) -> ASTNestedName: + raise NotImplementedError(repr(self)) + + @property + def function_params(self) -> List[ASTFunctionParameter]: + raise NotImplementedError(repr(self)) + + def require_space_after_declSpecs(self) -> bool: + raise NotImplementedError(repr(self)) + + +class ASTDeclaratorNameParam(ASTDeclarator): + def __init__(self, declId: ASTNestedName, + arrayOps: List[ASTArray], param: ASTParameters) -> None: + self.declId = declId + self.arrayOps = arrayOps + self.param = param + + @property + def name(self) -> ASTNestedName: + return self.declId + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.param.function_params + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + for op in self.arrayOps: + res.append(transform(op)) + if self.param: + res.append(transform(self.param)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + for op in self.arrayOps: + op.describe_signature(signode, mode, env, symbol) + if self.param: + self.param.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorNameBitField(ASTDeclarator): + def __init__(self, declId: ASTNestedName, size: ASTExpression): + self.declId = declId + self.size = size + + @property + def name(self) -> ASTNestedName: + return self.declId + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + res.append(" : ") + res.append(transform(self.size)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + signode += nodes.Text(' : ', ' : ') + self.size.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorPtr(ASTDeclarator): + def __init__(self, next: ASTDeclarator, restrict: bool, volatile: bool, const: bool, + attrs: Any) -> None: + assert next + self.next = next + self.restrict = restrict + self.volatile = volatile + self.const = const + self.attrs = attrs + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.next.function_params + + def require_space_after_declSpecs(self) -> bool: + return self.const or self.volatile or self.restrict or \ + len(self.attrs) > 0 or \ + self.next.require_space_after_declSpecs() + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['*'] + for a in self.attrs: + res.append(transform(a)) + if len(self.attrs) > 0 and (self.restrict or self.volatile or self.const): + res.append(' ') + if self.restrict: + res.append('restrict') + if self.volatile: + if self.restrict: + res.append(' ') + res.append('volatile') + if self.const: + if self.restrict or self.volatile: + res.append(' ') + res.append('const') + if self.const or self.volatile or self.restrict or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + res.append(' ') + res.append(transform(self.next)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode += nodes.Text("*") + for a in self.attrs: + a.describe_signature(signode) + if len(self.attrs) > 0 and (self.restrict or self.volatile or self.const): + signode += nodes.Text(' ') + + def _add_anno(signode: TextElement, text: str) -> None: + signode += addnodes.desc_annotation(text, text) + + if self.restrict: + _add_anno(signode, 'restrict') + if self.volatile: + if self.restrict: + signode += nodes.Text(' ') + _add_anno(signode, 'volatile') + if self.const: + if self.restrict or self.volatile: + signode += nodes.Text(' ') + _add_anno(signode, 'const') + if self.const or self.volatile or self.restrict or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + signode += nodes.Text(' ') + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorParen(ASTDeclarator): + def __init__(self, inner: ASTDeclarator, next: ASTDeclarator) -> None: + assert inner + assert next + self.inner = inner + self.next = next + # TODO: we assume the name and params are in inner + + @property + def name(self) -> ASTNestedName: + return self.inner.name + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.inner.function_params + + def require_space_after_declSpecs(self) -> bool: + return True + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.inner)) + res.append(')') + res.append(transform(self.next)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode += nodes.Text('(') + self.inner.describe_signature(signode, mode, env, symbol) + signode += nodes.Text(')') + self.next.describe_signature(signode, "noneIsName", env, symbol) + + +# Initializer +################################################################################ + +class ASTParenExprList(ASTBase): + def __init__(self, exprs: List[ASTExpression]) -> None: + self.exprs = exprs + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = [transform(e) for e in self.exprs] + return '(%s)' % ', '.join(exprs) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode.append(nodes.Text('(')) + first = True + for e in self.exprs: + if not first: + signode.append(nodes.Text(', ')) + else: + first = False + e.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) + + +class ASTBracedInitList(ASTBase): + def __init__(self, exprs: List[ASTExpression], trailingComma: bool) -> None: + self.exprs = exprs + self.trailingComma = trailingComma + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = [transform(e) for e in self.exprs] + trailingComma = ',' if self.trailingComma else '' + return '{%s%s}' % (', '.join(exprs), trailingComma) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode.append(nodes.Text('{')) + first = True + for e in self.exprs: + if not first: + signode.append(nodes.Text(', ')) + else: + first = False + e.describe_signature(signode, mode, env, symbol) + if self.trailingComma: + signode.append(nodes.Text(',')) + signode.append(nodes.Text('}')) + + +class ASTInitializer(ASTBase): + def __init__(self, value: Union[ASTBracedInitList, ASTExpression], + hasAssign: bool = True) -> None: + self.value = value + self.hasAssign = hasAssign + + def _stringify(self, transform: StringifyTransform) -> str: + val = transform(self.value) + if self.hasAssign: + return ' = ' + val + else: + return val + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.hasAssign: + signode.append(nodes.Text(' = ')) + self.value.describe_signature(signode, 'markType', env, symbol) + + +class ASTType(ASTBase): + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: + assert declSpecs + assert decl + self.declSpecs = declSpecs + self.decl = decl + + @property + def name(self) -> ASTNestedName: + return self.decl.name + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.decl.function_params + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + declSpecs = transform(self.declSpecs) + res.append(declSpecs) + if self.decl.require_space_after_declSpecs() and len(declSpecs) > 0: + res.append(' ') + res.append(transform(self.decl)) + return ''.join(res) + + def get_type_declaration_prefix(self) -> str: + if self.declSpecs.trailingTypeSpec: + return 'typedef' + else: + return 'type' + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.declSpecs.describe_signature(signode, 'markType', env, symbol) + if (self.decl.require_space_after_declSpecs() and + len(str(self.declSpecs)) > 0): + signode += nodes.Text(' ') + # for parameters that don't really declare new names we get 'markType', + # this should not be propagated, but be 'noneIsName'. + if mode == 'markType': + mode = 'noneIsName' + self.decl.describe_signature(signode, mode, env, symbol) + + +class ASTTypeWithInit(ASTBase): + def __init__(self, type: ASTType, init: ASTInitializer) -> None: + self.type = type + self.init = init + + @property + def name(self) -> ASTNestedName: + return self.type.name + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.type)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.type.describe_signature(signode, mode, env, symbol) + if self.init: + self.init.describe_signature(signode, mode, env, symbol) + + +class ASTMacroParameter(ASTBase): + def __init__(self, arg: ASTNestedName, ellipsis: bool = False) -> None: + self.arg = arg + self.ellipsis = ellipsis + + def _stringify(self, transform: StringifyTransform) -> str: + if self.ellipsis: + return '...' + else: + return transform(self.arg) + + def describe_signature(self, signode: Any, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.ellipsis: + signode += nodes.Text('...') + else: + self.arg.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTMacro(ASTBase): + def __init__(self, ident: ASTNestedName, args: List[ASTMacroParameter]) -> None: + self.ident = ident + self.args = args + + @property + def name(self) -> ASTNestedName: + return self.ident + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.ident)) + if self.args is not None: + res.append('(') + first = True + for arg in self.args: + if not first: + res.append(', ') + first = False + res.append(transform(arg)) + res.append(')') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.ident.describe_signature(signode, mode, env, symbol) + if self.args is None: + return + paramlist = addnodes.desc_parameterlist() + for arg in self.args: + param = addnodes.desc_parameter('', '', noemph=True) + arg.describe_signature(param, 'param', env, symbol=symbol) + paramlist += param + signode += paramlist + + +class ASTStruct(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTUnion(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTEnum(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTEnumerator(ASTBase): + def __init__(self, name: ASTNestedName, init: ASTInitializer) -> None: + self.name = name + self.init = init + + def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.name)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol) + if self.init: + self.init.describe_signature(signode, 'markType', env, symbol) + + +class ASTDeclaration(ASTBaseBase): + def __init__(self, objectType: str, directiveType: str, declaration: Any) -> None: + self.objectType = objectType + self.directiveType = directiveType + self.declaration = declaration + + self.symbol = None # type: Symbol + # set by CObject._add_enumerator_to_parent + self.enumeratorScopedSymbol = None # type: Symbol + + @property + def name(self) -> ASTNestedName: + return self.declaration.name + + @property + def function_params(self) -> List[ASTFunctionParameter]: + if self.objectType != 'function': + return None + return self.declaration.function_params + + def get_id(self, version: int, prefixed: bool = True) -> str: + if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: + return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed) + id_ = self.symbol.get_full_nested_name().get_id(version) + if prefixed: + return _id_prefix[version] + id_ + else: + return id_ + + def get_newest_id(self) -> str: + return self.get_id(_max_id, True) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.declaration) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", options: Dict) -> None: + verify_description_mode(mode) + assert self.symbol + # The caller of the domain added a desc_signature node. + # Always enable multiline: + signode['is_multiline'] = True + # Put each line in a desc_signature_line node. + mainDeclNode = addnodes.desc_signature_line() + mainDeclNode.sphinx_line_type = 'declarator' + mainDeclNode['add_permalink'] = not self.symbol.isRedeclaration + signode += mainDeclNode + + if self.objectType == 'member': + pass + elif self.objectType == 'function': + pass + elif self.objectType == 'macro': + pass + elif self.objectType == 'struct': + mainDeclNode += addnodes.desc_annotation('struct ', 'struct ') + elif self.objectType == 'union': + mainDeclNode += addnodes.desc_annotation('union ', 'union ') + elif self.objectType == 'enum': + mainDeclNode += addnodes.desc_annotation('enum ', 'enum ') + elif self.objectType == 'enumerator': + mainDeclNode += addnodes.desc_annotation('enumerator ', 'enumerator ') + elif self.objectType == 'type': + prefix = self.declaration.get_type_declaration_prefix() + prefix += ' ' + mainDeclNode += addnodes.desc_annotation(prefix, prefix) + else: + assert False + self.declaration.describe_signature(mainDeclNode, mode, env, self.symbol) + + +class SymbolLookupResult: + def __init__(self, symbols: Iterator["Symbol"], parentSymbol: "Symbol", + ident: ASTIdentifier) -> None: + self.symbols = symbols + self.parentSymbol = parentSymbol + self.ident = ident + + +class LookupKey: + def __init__(self, data: List[Tuple[ASTIdentifier, str]]) -> None: + self.data = data + + def __str__(self): + return '[{}]'.format(', '.join("({}, {})".format( + ident, id_) for ident, id_ in self.data)) + + +class Symbol: + debug_indent = 0 + debug_indent_string = " " + debug_lookup = False + debug_show_tree = False + + @staticmethod + def debug_print(*args: Any) -> None: + print(Symbol.debug_indent_string * Symbol.debug_indent, end="") + print(*args) + + def _assert_invariants(self) -> None: + if not self.parent: + # parent == None means global scope, so declaration means a parent + assert not self.declaration + assert not self.docname + else: + if self.declaration: + assert self.docname + + def __setattr__(self, key, value): + if key == "children": + assert False + else: + return super().__setattr__(key, value) + + def __init__(self, parent: "Symbol", ident: ASTIdentifier, + declaration: ASTDeclaration, docname: str) -> None: + self.parent = parent + # declarations in a single directive are linked together + self.siblingAbove = None # type: Symbol + self.siblingBelow = None # type: Symbol + self.ident = ident + self.declaration = declaration + self.docname = docname + self.isRedeclaration = False + self._assert_invariants() + + # Remember to modify Symbol.remove if modifications to the parent change. + self._children = [] # type: List[Symbol] + self._anonChildren = [] # type: List[Symbol] + # note: _children includes _anonChildren + if self.parent: + self.parent._children.append(self) + if self.declaration: + self.declaration.symbol = self + + # Do symbol addition after self._children has been initialised. + self._add_function_params() + + def _fill_empty(self, declaration: ASTDeclaration, docname: str) -> None: + self._assert_invariants() + assert not self.declaration + assert not self.docname + assert declaration + assert docname + self.declaration = declaration + self.declaration.symbol = self + self.docname = docname + self._assert_invariants() + # and symbol addition should be done as well + self._add_function_params() + + def _add_function_params(self) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_function_params:") + # Note: we may be called from _fill_empty, so the symbols we want + # to add may actually already be present (as empty symbols). + + # add symbols for function parameters, if any + if self.declaration is not None and self.declaration.function_params is not None: + for p in self.declaration.function_params: + if p.arg is None: + continue + nn = p.arg.name + if nn is None: + continue + # (comparing to the template params: we have checked that we are a declaration) + decl = ASTDeclaration('functionParam', None, p) + assert not nn.rooted + assert len(nn.names) == 1 + self._add_symbols(nn, decl, self.docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + + def remove(self) -> None: + if self.parent is None: + return + assert self in self.parent._children + self.parent._children.remove(self) + self.parent = None + + def clear_doc(self, docname: str) -> None: + newChildren = [] # type: List[Symbol] + for sChild in self._children: + sChild.clear_doc(docname) + if sChild.declaration and sChild.docname == docname: + sChild.declaration = None + sChild.docname = None + if sChild.siblingAbove is not None: + sChild.siblingAbove.siblingBelow = sChild.siblingBelow + if sChild.siblingBelow is not None: + sChild.siblingBelow.siblingAbove = sChild.siblingAbove + sChild.siblingAbove = None + sChild.siblingBelow = None + newChildren.append(sChild) + self._children = newChildren + + def get_all_symbols(self) -> Iterator["Symbol"]: + yield self + for sChild in self._children: + for s in sChild.get_all_symbols(): + yield s + + @property + def children_recurse_anon(self) -> Iterator["Symbol"]: + for c in self._children: + yield c + if not c.ident.is_anon(): + continue + yield from c.children_recurse_anon + + def get_lookup_key(self) -> "LookupKey": + # The pickle files for the environment and for each document are distinct. + # The environment has all the symbols, but the documents has xrefs that + # must know their scope. A lookup key is essentially a specification of + # how to find a specific symbol. + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + key = [] + for s in symbols: + if s.declaration is not None: + # TODO: do we need the ID? + key.append((s.ident, s.declaration.get_newest_id())) + else: + key.append((s.ident, None)) + return LookupKey(key) + + def get_full_nested_name(self) -> ASTNestedName: + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + names = [] + for s in symbols: + names.append(s.ident) + return ASTNestedName(names, rooted=False) + + def _find_first_named_symbol(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool) -> "Symbol": + # TODO: further simplification from C++ to C + if Symbol.debug_lookup: + Symbol.debug_print("_find_first_named_symbol ->") + res = self._find_named_symbols(ident, matchSelf, recurseInAnon, + searchInSiblings=False) + try: + return next(res) + except StopIteration: + return None + + def _find_named_symbols(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool, + searchInSiblings: bool) -> Iterator["Symbol"]: + # TODO: further simplification from C++ to C + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_find_named_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("ident: ", ident) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + def candidates(): + s = self + if Symbol.debug_lookup: + Symbol.debug_print("searching in self:") + print(s.to_string(Symbol.debug_indent + 1), end="") + while True: + if matchSelf: + yield s + if recurseInAnon: + yield from s.children_recurse_anon + else: + yield from s._children + + if s.siblingAbove is None: + break + s = s.siblingAbove + if Symbol.debug_lookup: + Symbol.debug_print("searching in sibling:") + print(s.to_string(Symbol.debug_indent + 1), end="") + + for s in candidates(): + if Symbol.debug_lookup: + Symbol.debug_print("candidate:") + print(s.to_string(Symbol.debug_indent + 1), end="") + if s.ident == ident: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("matches") + Symbol.debug_indent -= 3 + yield s + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + + def _symbol_lookup(self, nestedName: ASTNestedName, + onMissingQualifiedSymbol: Callable[["Symbol", ASTIdentifier], "Symbol"], # NOQA + ancestorLookupType: str, matchSelf: bool, + recurseInAnon: bool, searchInSiblings: bool) -> SymbolLookupResult: + # TODO: further simplification from C++ to C + # ancestorLookupType: if not None, specifies the target type of the lookup + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_symbol_lookup:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("ancestorLookupType:", ancestorLookupType) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + names = nestedName.names + + # find the right starting point for lookup + parentSymbol = self + if nestedName.rooted: + while parentSymbol.parent: + parentSymbol = parentSymbol.parent + if ancestorLookupType is not None: + # walk up until we find the first identifier + firstName = names[0] + while parentSymbol.parent: + if parentSymbol.find_identifier(firstName, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings): + break + parentSymbol = parentSymbol.parent + + if Symbol.debug_lookup: + Symbol.debug_print("starting point:") + print(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # and now the actual lookup + for ident in names[:-1]: + symbol = parentSymbol._find_first_named_symbol( + ident, matchSelf=matchSelf, recurseInAnon=recurseInAnon) + if symbol is None: + symbol = onMissingQualifiedSymbol(parentSymbol, ident) + if symbol is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + # We have now matched part of a nested name, and need to match more + # so even if we should matchSelf before, we definitely shouldn't + # even more. (see also issue #2666) + matchSelf = False + parentSymbol = symbol + + if Symbol.debug_lookup: + Symbol.debug_print("handle last name from:") + print(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # handle the last name + ident = names[-1] + + symbols = parentSymbol._find_named_symbols( + ident, matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings) + if Symbol.debug_lookup: + symbols = list(symbols) # type: ignore + Symbol.debug_indent -= 2 + return SymbolLookupResult(symbols, parentSymbol, ident) + + def _add_symbols(self, nestedName: ASTNestedName, + declaration: ASTDeclaration, docname: str) -> "Symbol": + # TODO: further simplification from C++ to C + # Used for adding a whole path of symbols, where the last may or may not + # be an actual declaration. + + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print("doc: ", docname) + + def onMissingQualifiedSymbol(parentSymbol: "Symbol", ident: ASTIdentifier) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols, onMissingQualifiedSymbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", ident) + Symbol.debug_indent -= 2 + return Symbol(parent=parentSymbol, ident=ident, + declaration=None, docname=None) + + lookupResult = self._symbol_lookup(nestedName, + onMissingQualifiedSymbol, + ancestorLookupType=None, + matchSelf=False, + recurseInAnon=True, + searchInSiblings=False) + assert lookupResult is not None # we create symbols all the way, so that can't happen + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, no symbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", lookupResult.ident) + Symbol.debug_print("declaration: ", declaration) + Symbol.debug_print("docname: ", docname) + Symbol.debug_indent -= 1 + symbol = Symbol(parent=lookupResult.parentSymbol, + ident=lookupResult.ident, + declaration=declaration, + docname=docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return symbol + + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("number symbols:", len(symbols)) + Symbol.debug_indent -= 1 + + if not declaration: + if Symbol.debug_lookup: + Symbol.debug_print("no delcaration") + Symbol.debug_indent -= 2 + # good, just a scope creation + # TODO: what if we have more than one symbol? + return symbols[0] + + noDecl = [] + withDecl = [] + dupDecl = [] + for s in symbols: + if s.declaration is None: + noDecl.append(s) + elif s.isRedeclaration: + dupDecl.append(s) + else: + withDecl.append(s) + if Symbol.debug_lookup: + Symbol.debug_print("#noDecl: ", len(noDecl)) + Symbol.debug_print("#withDecl:", len(withDecl)) + Symbol.debug_print("#dupDecl: ", len(dupDecl)) + + # With partial builds we may start with a large symbol tree stripped of declarations. + # Essentially any combination of noDecl, withDecl, and dupDecls seems possible. + # TODO: make partial builds fully work. What should happen when the primary symbol gets + # deleted, and other duplicates exist? The full document should probably be rebuild. + + # First check if one of those with a declaration matches. + # If it's a function, we need to compare IDs, + # otherwise there should be only one symbol with a declaration. + def makeCandSymbol(): + if Symbol.debug_lookup: + Symbol.debug_print("begin: creating candidate symbol") + symbol = Symbol(parent=lookupResult.parentSymbol, + ident=lookupResult.ident, + declaration=declaration, + docname=docname) + if Symbol.debug_lookup: + Symbol.debug_print("end: creating candidate symbol") + return symbol + + if len(withDecl) == 0: + candSymbol = None + else: + candSymbol = makeCandSymbol() + + def handleDuplicateDeclaration(symbol, candSymbol): + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("redeclaration") + Symbol.debug_indent -= 1 + Symbol.debug_indent -= 2 + # Redeclaration of the same symbol. + # Let the new one be there, but raise an error to the client + # so it can use the real symbol as subscope. + # This will probably result in a duplicate id warning. + candSymbol.isRedeclaration = True + raise _DuplicateSymbolError(symbol, declaration) + + if declaration.objectType != "function": + assert len(withDecl) <= 1 + handleDuplicateDeclaration(withDecl[0], candSymbol) + # (not reachable) + + # a function, so compare IDs + candId = declaration.get_newest_id() + if Symbol.debug_lookup: + Symbol.debug_print("candId:", candId) + for symbol in withDecl: + oldId = symbol.declaration.get_newest_id() + if Symbol.debug_lookup: + Symbol.debug_print("oldId: ", oldId) + if candId == oldId: + handleDuplicateDeclaration(symbol, candSymbol) + # (not reachable) + # no candidate symbol found with matching ID + # if there is an empty symbol, fill that one + if len(noDecl) == 0: + if Symbol.debug_lookup: + Symbol.debug_print("no match, no empty, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_indent -= 2 + if candSymbol is not None: + return candSymbol + else: + return makeCandSymbol() + else: + if Symbol.debug_lookup: + Symbol.debug_print( + "no match, but fill an empty declaration, candSybmol is not None?:", + candSymbol is not None) # NOQA + Symbol.debug_indent -= 2 + if candSymbol is not None: + candSymbol.remove() + # assert len(noDecl) == 1 + # TODO: enable assertion when we at some point find out how to do cleanup + # for now, just take the first one, it should work fine ... right? + symbol = noDecl[0] + # If someone first opened the scope, and then later + # declares it, e.g, + # .. namespace:: Test + # .. namespace:: nullptr + # .. class:: Test + symbol._fill_empty(declaration, docname) + return symbol + + def merge_with(self, other: "Symbol", docnames: List[str], + env: "BuildEnvironment") -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("merge_with:") + assert other is not None + for otherChild in other._children: + ourChild = self._find_first_named_symbol( + ident=otherChild.ident, matchSelf=False, + recurseInAnon=False) + if ourChild is None: + # TODO: hmm, should we prune by docnames? + self._children.append(otherChild) + otherChild.parent = self + otherChild._assert_invariants() + continue + if otherChild.declaration and otherChild.docname in docnames: + if not ourChild.declaration: + ourChild._fill_empty(otherChild.declaration, otherChild.docname) + elif ourChild.docname != otherChild.docname: + name = str(ourChild.declaration) + msg = __("Duplicate declaration, also defined in '%s'.\n" + "Declaration is '%s'.") + msg = msg % (ourChild.docname, name) + logger.warning(msg, location=otherChild.docname) + else: + # Both have declarations, and in the same docname. + # This can apparently happen, it should be safe to + # just ignore it, right? + pass + ourChild.merge_with(otherChild, docnames, env) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + + def add_name(self, nestedName: ASTNestedName) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_name:") + res = self._add_symbols(nestedName, declaration=None, docname=None) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def add_declaration(self, declaration: ASTDeclaration, docname: str) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_declaration:") + assert declaration + assert docname + nestedName = declaration.name + res = self._add_symbols(nestedName, declaration, docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def find_identifier(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool, searchInSiblings: bool + ) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_identifier:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", ident) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings:", searchInSiblings) + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + current = self + while current is not None: + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + Symbol.debug_print("trying:") + print(current.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + if matchSelf and current.ident == ident: + return current + children = current.children_recurse_anon if recurseInAnon else current._children + for s in children: + if s.ident == ident: + return s + if not searchInSiblings: + break + current = current.siblingAbove + return None + + def direct_lookup(self, key: "LookupKey") -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("direct_lookup:") + Symbol.debug_indent += 1 + s = self + for name, id_ in key.data: + res = None + for cand in s._children: + if cand.ident == name: + res = cand + break + s = res + if Symbol.debug_lookup: + Symbol.debug_print("name: ", name) + Symbol.debug_print("id: ", id_) + if s is not None: + print(s.to_string(Symbol.debug_indent + 1), end="") + else: + Symbol.debug_print("not found") + if s is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return s + + def find_declaration(self, nestedName: ASTNestedName, typ: str, + matchSelf: bool, recurseInAnon: bool) -> "Symbol": + # templateShorthand: missing template parameter lists for templates is ok + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_declaration:") + + def onMissingQualifiedSymbol(parentSymbol: "Symbol", + ident: ASTIdentifier) -> "Symbol": + return None + + lookupResult = self._symbol_lookup(nestedName, + onMissingQualifiedSymbol, + ancestorLookupType=typ, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + if lookupResult is None: + return None + + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + return None + return symbols[0] + + def to_string(self, indent: int) -> str: + res = [Symbol.debug_indent_string * indent] + if not self.parent: + res.append('::') + else: + if self.ident: + res.append(str(self.ident)) + else: + res.append(str(self.declaration)) + if self.declaration: + res.append(": ") + if self.isRedeclaration: + res.append('!!duplicate!! ') + res.append(str(self.declaration)) + if self.docname: + res.append('\t(') + res.append(self.docname) + res.append(')') + res.append('\n') + return ''.join(res) + + def dump(self, indent: int) -> str: + res = [self.to_string(indent)] + for c in self._children: + res.append(c.dump(indent + 1)) + return ''.join(res) + + +class DefinitionParser(BaseParser): + # those without signedness and size modifiers + # see https://en.cppreference.com/w/cpp/language/types + _simple_fundamental_types = ( + 'void', '_Bool', 'bool', 'char', 'int', 'float', 'double', + '__int64', + ) + + _prefix_keys = ('struct', 'enum', 'union') + + def _parse_string(self) -> str: + if self.current_char != '"': + return None + startPos = self.pos + self.pos += 1 + escape = False + while True: + if self.eof: + self.fail("Unexpected end during inside string.") + elif self.current_char == '"' and not escape: + self.pos += 1 + break + elif self.current_char == '\\': + escape = True + else: + escape = False + self.pos += 1 + return self.definition[startPos:self.pos] + + def _parse_attribute(self) -> Any: + return None + # self.skip_ws() + # # try C++11 style + # startPos = self.pos + # if self.skip_string_and_ws('['): + # if not self.skip_string('['): + # self.pos = startPos + # else: + # # TODO: actually implement the correct grammar + # arg = self._parse_balanced_token_seq(end=[']']) + # if not self.skip_string_and_ws(']'): + # self.fail("Expected ']' in end of attribute.") + # if not self.skip_string_and_ws(']'): + # self.fail("Expected ']' in end of attribute after [[...]") + # return ASTCPPAttribute(arg) + # + # # try GNU style + # if self.skip_word_and_ws('__attribute__'): + # if not self.skip_string_and_ws('('): + # self.fail("Expected '(' after '__attribute__'.") + # if not self.skip_string_and_ws('('): + # self.fail("Expected '(' after '__attribute__('.") + # attrs = [] + # while 1: + # if self.match(identifier_re): + # name = self.matched_text + # self.skip_ws() + # if self.skip_string_and_ws('('): + # self.fail('Parameterized GNU style attribute not yet supported.') + # attrs.append(ASTGnuAttribute(name, None)) + # # TODO: parse arguments for the attribute + # if self.skip_string_and_ws(','): + # continue + # elif self.skip_string_and_ws(')'): + # break + # else: + # self.fail("Expected identifier, ')', or ',' in __attribute__.") + # if not self.skip_string_and_ws(')'): + # self.fail("Expected ')' after '__attribute__((...)'") + # return ASTGnuAttributeList(attrs) + # + # # try the simple id attributes defined by the user + # for id in self.config.cpp_id_attributes: + # if self.skip_word_and_ws(id): + # return ASTIdAttribute(id) + # + # # try the paren attributes defined by the user + # for id in self.config.cpp_paren_attributes: + # if not self.skip_string_and_ws(id): + # continue + # if not self.skip_string('('): + # self.fail("Expected '(' after user-defined paren-attribute.") + # arg = self._parse_balanced_token_seq(end=[')']) + # if not self.skip_string(')'): + # self.fail("Expected ')' to end user-defined paren-attribute.") + # return ASTParenAttribute(id, arg) + + return None + + def _parse_literal(self) -> ASTLiteral: + # -> integer-literal + # | character-literal + # | floating-literal + # | string-literal + # | boolean-literal -> "false" | "true" + self.skip_ws() + if self.skip_word('true'): + return ASTBooleanLiteral(True) + if self.skip_word('false'): + return ASTBooleanLiteral(False) + for regex in [float_literal_re, binary_literal_re, hex_literal_re, + integer_literal_re, octal_literal_re]: + pos = self.pos + if self.match(regex): + while self.current_char in 'uUlLfF': + self.pos += 1 + return ASTNumberLiteral(self.definition[pos:self.pos]) + + string = self._parse_string() + if string is not None: + return ASTStringLiteral(string) + + # character-literal + if self.match(char_literal_re): + prefix = self.last_match.group(1) # may be None when no prefix + data = self.last_match.group(2) + try: + return ASTCharLiteral(prefix, data) + except UnicodeDecodeError as e: + self.fail("Can not handle character literal. Internal error was: %s" % e) + except UnsupportedMultiCharacterCharLiteral: + self.fail("Can not handle character literal" + " resulting in multiple decoded characters.") + return None + + def _parse_paren_expression(self) -> ASTExpression: + # "(" expression ")" + if self.current_char != '(': + return None + self.pos += 1 + res = self._parse_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' in end of parenthesized expression.") + return ASTParenExpr(res) + + def _parse_primary_expression(self) -> ASTExpression: + # literal + # "(" expression ")" + # id-expression -> we parse this with _parse_nested_name + self.skip_ws() + res = self._parse_literal() # type: ASTExpression + if res is not None: + return res + res = self._parse_paren_expression() + if res is not None: + return res + nn = self._parse_nested_name() + if nn is not None: + return ASTIdExpression(nn) + return None + + def _parse_initializer_list(self, name: str, open: str, close: str + ) -> Tuple[List[ASTExpression], bool]: + # Parse open and close with the actual initializer-list inbetween + # -> initializer-clause '...'[opt] + # | initializer-list ',' initializer-clause '...'[opt] + # TODO: designators + self.skip_ws() + if not self.skip_string_and_ws(open): + return None, None + if self.skip_string(close): + return [], False + + exprs = [] + trailingComma = False + while True: + self.skip_ws() + expr = self._parse_expression() + self.skip_ws() + exprs.append(expr) + self.skip_ws() + if self.skip_string(close): + break + if not self.skip_string_and_ws(','): + self.fail("Error in %s, expected ',' or '%s'." % (name, close)) + if self.current_char == close and close == '}': + self.pos += 1 + trailingComma = True + break + return exprs, trailingComma + + def _parse_paren_expression_list(self) -> ASTParenExprList: + # -> '(' expression-list ')' + # though, we relax it to also allow empty parens + # as it's needed in some cases + # + # expression-list + # -> initializer-list + exprs, trailingComma = self._parse_initializer_list("parenthesized expression-list", + '(', ')') + if exprs is None: + return None + return ASTParenExprList(exprs) + + def _parse_braced_init_list(self) -> ASTBracedInitList: + # -> '{' initializer-list ','[opt] '}' + # | '{' '}' + exprs, trailingComma = self._parse_initializer_list("braced-init-list", '{', '}') + if exprs is None: + return None + return ASTBracedInitList(exprs, trailingComma) + + def _parse_postfix_expression(self) -> ASTPostfixExpr: + # -> primary + # | postfix "[" expression "]" + # | postfix "[" braced-init-list [opt] "]" + # | postfix "(" expression-list [opt] ")" + # | postfix "." id-expression + # | postfix "->" id-expression + # | postfix "++" + # | postfix "--" + + prefix = self._parse_primary_expression() + + # and now parse postfixes + postFixes = [] # type: List[ASTPostfixOp] + while True: + self.skip_ws() + if self.skip_string_and_ws('['): + expr = self._parse_expression() + self.skip_ws() + if not self.skip_string(']'): + self.fail("Expected ']' in end of postfix expression.") + postFixes.append(ASTPostfixArray(expr)) + continue + if self.skip_string('.'): + if self.skip_string('*'): + # don't steal the dot + self.pos -= 2 + elif self.skip_string('..'): + # don't steal the dot + self.pos -= 3 + else: + name = self._parse_nested_name() + postFixes.append(ASTPostfixMember(name)) + continue + if self.skip_string('->'): + if self.skip_string('*'): + # don't steal the arrow + self.pos -= 3 + else: + name = self._parse_nested_name() + postFixes.append(ASTPostfixMemberOfPointer(name)) + continue + if self.skip_string('++'): + postFixes.append(ASTPostfixInc()) + continue + if self.skip_string('--'): + postFixes.append(ASTPostfixDec()) + continue + lst = self._parse_paren_expression_list() + if lst is not None: + postFixes.append(ASTPostfixCallExpr(lst)) + continue + break + return ASTPostfixExpr(prefix, postFixes) + + def _parse_unary_expression(self) -> ASTExpression: + # -> postfix + # | "++" cast + # | "--" cast + # | unary-operator cast -> (* | & | + | - | ! | ~) cast + # The rest: + # | "sizeof" unary + # | "sizeof" "(" type-id ")" + # | "alignof" "(" type-id ")" + self.skip_ws() + for op in _expression_unary_ops: + # TODO: hmm, should we be able to backtrack here? + if self.skip_string(op): + expr = self._parse_cast_expression() + return ASTUnaryOpExpr(op, expr) + if self.skip_word_and_ws('sizeof'): + if self.skip_string_and_ws('('): + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'sizeof'.") + return ASTSizeofType(typ) + expr = self._parse_unary_expression() + return ASTSizeofExpr(expr) + if self.skip_word_and_ws('alignof'): + if not self.skip_string_and_ws('('): + self.fail("Expecting '(' after 'alignof'.") + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'alignof'.") + return ASTAlignofExpr(typ) + return self._parse_postfix_expression() + + def _parse_cast_expression(self) -> ASTExpression: + # -> unary | "(" type-id ")" cast + pos = self.pos + self.skip_ws() + if self.skip_string('('): + try: + typ = self._parse_type(False) + if not self.skip_string(')'): + self.fail("Expected ')' in cast expression.") + expr = self._parse_cast_expression() + return ASTCastExpr(typ, expr) + except DefinitionError as exCast: + self.pos = pos + try: + return self._parse_unary_expression() + except DefinitionError as exUnary: + errs = [] + errs.append((exCast, "If type cast expression")) + errs.append((exUnary, "If unary expression")) + raise self._make_multi_error(errs, "Error in cast expression.") + else: + return self._parse_unary_expression() + + def _parse_logical_or_expression(self) -> ASTExpression: + # logical-or = logical-and || + # logical-and = inclusive-or && + # inclusive-or = exclusive-or | + # exclusive-or = and ^ + # and = equality & + # equality = relational ==, != + # relational = shift <, >, <=, >= + # shift = additive <<, >> + # additive = multiplicative +, - + # multiplicative = pm *, /, % + # pm = cast .*, ->* + def _parse_bin_op_expr(self, opId): + if opId + 1 == len(_expression_bin_ops): + def parser() -> ASTExpression: + return self._parse_cast_expression() + else: + def parser() -> ASTExpression: + return _parse_bin_op_expr(self, opId + 1) + exprs = [] + ops = [] + exprs.append(parser()) + while True: + self.skip_ws() + pos = self.pos + oneMore = False + for op in _expression_bin_ops[opId]: + if not self.skip_string(op): + continue + if op == '&' and self.current_char == '&': + # don't split the && 'token' + self.pos -= 1 + # and btw. && has lower precedence, so we are done + break + try: + expr = parser() + exprs.append(expr) + ops.append(op) + oneMore = True + break + except DefinitionError: + self.pos = pos + if not oneMore: + break + return ASTBinOpExpr(exprs, ops) + return _parse_bin_op_expr(self, 0) + + def _parse_conditional_expression_tail(self, orExprHead): + # -> "?" expression ":" assignment-expression + return None + + def _parse_assignment_expression(self) -> ASTExpression: + # -> conditional-expression + # | logical-or-expression assignment-operator initializer-clause + # -> conditional-expression -> + # logical-or-expression + # | logical-or-expression "?" expression ":" assignment-expression + # | logical-or-expression assignment-operator initializer-clause + exprs = [] + ops = [] + orExpr = self._parse_logical_or_expression() + exprs.append(orExpr) + # TODO: handle ternary with _parse_conditional_expression_tail + while True: + oneMore = False + self.skip_ws() + for op in _expression_assignment_ops: + if not self.skip_string(op): + continue + expr = self._parse_logical_or_expression() + exprs.append(expr) + ops.append(op) + oneMore = True + if not oneMore: + break + return ASTAssignmentExpr(exprs, ops) + + def _parse_constant_expression(self) -> ASTExpression: + # -> conditional-expression + orExpr = self._parse_logical_or_expression() + # TODO: use _parse_conditional_expression_tail + return orExpr + + def _parse_expression(self) -> ASTExpression: + # -> assignment-expression + # | expression "," assignment-expresion + # TODO: actually parse the second production + return self._parse_assignment_expression() + + def _parse_expression_fallback( + self, end: List[str], + parser: Callable[[], ASTExpression], + allow: bool = True) -> ASTExpression: + # Stupidly "parse" an expression. + # 'end' should be a list of characters which ends the expression. + + # first try to use the provided parser + prevPos = self.pos + try: + return parser() + except DefinitionError as e: + # some places (e.g., template parameters) we really don't want to use fallback, + # and for testing we may want to globally disable it + if not allow or not self.allowFallbackExpressionParsing: + raise + self.warn("Parsing of expression failed. Using fallback parser." + " Error was:\n%s" % e) + self.pos = prevPos + # and then the fallback scanning + assert end is not None + self.skip_ws() + startPos = self.pos + if self.match(_string_re): + value = self.matched_text + else: + # TODO: add handling of more bracket-like things, and quote handling + brackets = {'(': ')', '{': '}', '[': ']'} + symbols = [] # type: List[str] + while not self.eof: + if (len(symbols) == 0 and self.current_char in end): + break + if self.current_char in brackets.keys(): + symbols.append(brackets[self.current_char]) + elif len(symbols) > 0 and self.current_char == symbols[-1]: + symbols.pop() + self.pos += 1 + if len(end) > 0 and self.eof: + self.fail("Could not find end of expression starting at %d." + % startPos) + value = self.definition[startPos:self.pos].strip() + return ASTFallbackExpr(value.strip()) + + def _parse_nested_name(self) -> ASTNestedName: + names = [] # type: List[Any] + + self.skip_ws() + rooted = False + if self.skip_string('.'): + rooted = True + while 1: + self.skip_ws() + if not self.match(identifier_re): + self.fail("Expected identifier in nested name.") + identifier = self.matched_text + # make sure there isn't a keyword + if identifier in _keywords: + self.fail("Expected identifier in nested name, " + "got keyword: %s" % identifier) + ident = ASTIdentifier(identifier) + names.append(ident) + + self.skip_ws() + if not self.skip_string('.'): + break + return ASTNestedName(names, rooted) + + def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: + # fundamental types + self.skip_ws() + for t in self._simple_fundamental_types: + if self.skip_word(t): + return ASTTrailingTypeSpecFundamental(t) + + # TODO: this could/should be more strict + elements = [] + if self.skip_word_and_ws('signed'): + elements.append('signed') + elif self.skip_word_and_ws('unsigned'): + elements.append('unsigned') + while 1: + if self.skip_word_and_ws('short'): + elements.append('short') + elif self.skip_word_and_ws('long'): + elements.append('long') + else: + break + if self.skip_word_and_ws('char'): + elements.append('char') + elif self.skip_word_and_ws('int'): + elements.append('int') + elif self.skip_word_and_ws('double'): + elements.append('double') + elif self.skip_word_and_ws('__int64'): + elements.append('__int64') + if len(elements) > 0: + return ASTTrailingTypeSpecFundamental(' '.join(elements)) + + # prefixed + prefix = None + self.skip_ws() + for k in self._prefix_keys: + if self.skip_word_and_ws(k): + prefix = k + break + + nestedName = self._parse_nested_name() + return ASTTrailingTypeSpecName(prefix, nestedName) + + def _parse_parameters(self, paramMode: str) -> ASTParameters: + self.skip_ws() + if not self.skip_string('('): + if paramMode == 'function': + self.fail('Expecting "(" in parameters.') + else: + return None + + args = [] + self.skip_ws() + if not self.skip_string(')'): + while 1: + self.skip_ws() + if self.skip_string('...'): + args.append(ASTFunctionParameter(None, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in parameters.') + break + # note: it seems that function arguments can always be named, + # even in function pointers and similar. + arg = self._parse_type_with_init(outer=None, named='single') + # TODO: parse default parameters # TODO: didn't we just do that? + args.append(ASTFunctionParameter(arg)) + + self.skip_ws() + if self.skip_string(','): + continue + elif self.skip_string(')'): + break + else: + self.fail( + 'Expecting "," or ")" in parameters, ' + 'got "%s".' % self.current_char) + return ASTParameters(args) + + def _parse_decl_specs_simple(self, outer: str, typed: bool) -> ASTDeclSpecsSimple: + """Just parse the simple ones.""" + storage = None + threadLocal = None + inline = None + restrict = None + volatile = None + const = None + attrs = [] + while 1: # accept any permutation of a subset of some decl-specs + self.skip_ws() + if not storage: + if outer == 'member': + if self.skip_word('auto'): + storage = 'auto' + continue + if self.skip_word('register'): + storage = 'register' + continue + if outer in ('member', 'function'): + if self.skip_word('static'): + storage = 'static' + continue + if self.skip_word('extern'): + storage = 'extern' + continue + if outer == 'member' and not threadLocal: + if self.skip_word('thread_local'): + threadLocal = 'thread_local' + continue + if self.skip_word('_Thread_local'): + threadLocal = '_Thread_local' + continue + if outer == 'function' and not inline: + inline = self.skip_word('inline') + if inline: + continue + + if not restrict and typed: + restrict = self.skip_word('restrict') + if restrict: + continue + if not volatile and typed: + volatile = self.skip_word('volatile') + if volatile: + continue + if not const and typed: + const = self.skip_word('const') + if const: + continue + attr = self._parse_attribute() + if attr: + attrs.append(attr) + continue + break + return ASTDeclSpecsSimple(storage, threadLocal, inline, + restrict, volatile, const, attrs) + + def _parse_decl_specs(self, outer: str, typed: bool = True) -> ASTDeclSpecs: + if outer: + if outer not in ('type', 'member', 'function'): + raise Exception('Internal error, unknown outer "%s".' % outer) + leftSpecs = self._parse_decl_specs_simple(outer, typed) + rightSpecs = None + + if typed: + trailing = self._parse_trailing_type_spec() + rightSpecs = self._parse_decl_specs_simple(outer, typed) + else: + trailing = None + return ASTDeclSpecs(outer, leftSpecs, rightSpecs, trailing) + + def _parse_declarator_name_suffix( + self, named: Union[bool, str], paramMode: str, typed: bool + ) -> ASTDeclarator: + # now we should parse the name, and then suffixes + if named == 'maybe': + pos = self.pos + try: + declId = self._parse_nested_name() + except DefinitionError: + self.pos = pos + declId = None + elif named == 'single': + if self.match(identifier_re): + identifier = ASTIdentifier(self.matched_text) + declId = ASTNestedName([identifier], rooted=False) + else: + declId = None + elif named: + declId = self._parse_nested_name() + else: + declId = None + arrayOps = [] + while 1: + self.skip_ws() + if typed and self.skip_string('['): + self.skip_ws() + if self.skip_string(']'): + arrayOps.append(ASTArray(None)) + continue + + def parser(): + return self._parse_expression() + + value = self._parse_expression_fallback([']'], parser) + if not self.skip_string(']'): + self.fail("Expected ']' in end of array operator.") + arrayOps.append(ASTArray(value)) + continue + else: + break + param = self._parse_parameters(paramMode) + if param is None and len(arrayOps) == 0: + # perhaps a bit-field + if named and paramMode == 'type' and typed: + self.skip_ws() + if self.skip_string(':'): + size = self._parse_constant_expression() + return ASTDeclaratorNameBitField(declId=declId, size=size) + return ASTDeclaratorNameParam(declId=declId, arrayOps=arrayOps, + param=param) + + def _parse_declarator(self, named: Union[bool, str], paramMode: str, + typed: bool = True) -> ASTDeclarator: + # 'typed' here means 'parse return type stuff' + if paramMode not in ('type', 'function'): + raise Exception( + "Internal error, unknown paramMode '%s'." % paramMode) + prevErrors = [] + self.skip_ws() + if typed and self.skip_string('*'): + self.skip_ws() + restrict = False + volatile = False + const = False + attrs = [] + while 1: + if not restrict: + restrict = self.skip_word_and_ws('restrict') + if restrict: + continue + if not volatile: + volatile = self.skip_word_and_ws('volatile') + if volatile: + continue + if not const: + const = self.skip_word_and_ws('const') + if const: + continue + attr = self._parse_attribute() + if attr is not None: + attrs.append(attr) + continue + break + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorPtr(next=next, + restrict=restrict, volatile=volatile, const=const, + attrs=attrs) + if typed and self.current_char == '(': # note: peeking, not skipping + # maybe this is the beginning of params,try that first, + # otherwise assume it's noptr->declarator > ( ptr-declarator ) + pos = self.pos + try: + # assume this is params + res = self._parse_declarator_name_suffix(named, paramMode, + typed) + return res + except DefinitionError as exParamQual: + prevErrors.append((exParamQual, "If declId and parameters")) + self.pos = pos + try: + assert self.current_char == '(' + self.skip_string('(') + # TODO: hmm, if there is a name, it must be in inner, right? + # TODO: hmm, if there must be parameters, they must b + # inside, right? + inner = self._parse_declarator(named, paramMode, typed) + if not self.skip_string(')'): + self.fail("Expected ')' in \"( ptr-declarator )\"") + next = self._parse_declarator(named=False, + paramMode="type", + typed=typed) + return ASTDeclaratorParen(inner=inner, next=next) + except DefinitionError as exNoPtrParen: + self.pos = pos + prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator")) + header = "Error in declarator" + raise self._make_multi_error(prevErrors, header) + pos = self.pos + try: + return self._parse_declarator_name_suffix(named, paramMode, typed) + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If declarator-id")) + header = "Error in declarator or parameters" + raise self._make_multi_error(prevErrors, header) + + def _parse_initializer(self, outer: str = None, allowFallback: bool = True + ) -> ASTInitializer: + self.skip_ws() + if outer == 'member' and False: # TODO + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit, hasAssign=False) + + if not self.skip_string('='): + return None + + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit) + + if outer == 'member': + fallbackEnd = [] # type: List[str] + elif outer is None: # function parameter + fallbackEnd = [',', ')'] + else: + self.fail("Internal error, initializer for outer '%s' not " + "implemented." % outer) + + def parser(): + return self._parse_assignment_expression() + + value = self._parse_expression_fallback(fallbackEnd, parser, allow=allowFallback) + return ASTInitializer(value) + + def _parse_type(self, named: Union[bool, str], outer: str = None) -> ASTType: + """ + named=False|'maybe'|True: 'maybe' is e.g., for function objects which + doesn't need to name the arguments + """ + if outer: # always named + if outer not in ('type', 'member', 'function'): + raise Exception('Internal error, unknown outer "%s".' % outer) + assert named + + if outer == 'type': + # We allow type objects to just be a name. + prevErrors = [] + startPos = self.pos + # first try without the type + try: + declSpecs = self._parse_decl_specs(outer=outer, typed=False) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=False) + self.assert_end() + except DefinitionError as exUntyped: + desc = "If just a name" + prevErrors.append((exUntyped, desc)) + self.pos = startPos + try: + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=True, paramMode=outer) + except DefinitionError as exTyped: + self.pos = startPos + desc = "If typedef-like declaration" + prevErrors.append((exTyped, desc)) + # Retain the else branch for easier debugging. + # TODO: it would be nice to save the previous stacktrace + # and output it here. + if True: + header = "Type must be either just a name or a " + header += "typedef-like declaration." + raise self._make_multi_error(prevErrors, header) + else: + # For testing purposes. + # do it again to get the proper traceback (how do you + # reliably save a traceback when an exception is + # constructed?) + self.pos = startPos + typed = True + declSpecs = self._parse_decl_specs(outer=outer, typed=typed) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=typed) + elif outer == 'function': + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=True, paramMode=outer) + else: + paramMode = 'type' + if outer == 'member': # i.e., member + named = True + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=named, paramMode=paramMode) + return ASTType(declSpecs, decl) + + def _parse_type_with_init(self, named: Union[bool, str], outer: str) -> ASTTypeWithInit: + if outer: + assert outer in ('type', 'member', 'function') + type = self._parse_type(outer=outer, named=named) + init = self._parse_initializer(outer=outer) + return ASTTypeWithInit(type, init) + + def _parse_macro(self) -> ASTMacro: + self.skip_ws() + ident = self._parse_nested_name() + if ident is None: + self.fail("Expected identifier in macro definition.") + self.skip_ws() + if not self.skip_string_and_ws('('): + return ASTMacro(ident, None) + if self.skip_string(')'): + return ASTMacro(ident, []) + args = [] + while 1: + self.skip_ws() + if self.skip_string('...'): + args.append(ASTMacroParameter(None, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in macro parameters.') + break + if not self.match(identifier_re): + self.fail("Expected identifier in macro parameters.") + nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False) + arg = ASTMacroParameter(nn) + args.append(arg) + self.skip_ws() + if self.skip_string_and_ws(','): + continue + elif self.skip_string_and_ws(')'): + break + else: + self.fail("Expected identifier, ')', or ',' in macro parameter list.") + return ASTMacro(ident, args) + + def _parse_struct(self) -> ASTStruct: + name = self._parse_nested_name() + return ASTStruct(name) + + def _parse_union(self) -> ASTUnion: + name = self._parse_nested_name() + return ASTUnion(name) + + def _parse_enum(self) -> ASTEnum: + name = self._parse_nested_name() + return ASTEnum(name) + + def _parse_enumerator(self) -> ASTEnumerator: + name = self._parse_nested_name() + self.skip_ws() + init = None + if self.skip_string('='): + self.skip_ws() + + def parser(): + return self._parse_constant_expression() + + initVal = self._parse_expression_fallback([], parser) + init = ASTInitializer(initVal) + return ASTEnumerator(name, init) + + def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: + if objectType not in ('function', 'member', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + raise Exception('Internal error, unknown objectType "%s".' % objectType) + if directiveType not in ('function', 'member', 'var', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + raise Exception('Internal error, unknown directiveType "%s".' % directiveType) + + declaration = None # type: Any + if objectType == 'member': + declaration = self._parse_type_with_init(named=True, outer='member') + elif objectType == 'function': + declaration = self._parse_type(named=True, outer='function') + elif objectType == 'macro': + declaration = self._parse_macro() + elif objectType == 'struct': + declaration = self._parse_struct() + elif objectType == 'union': + declaration = self._parse_union() + elif objectType == 'enum': + declaration = self._parse_enum() + elif objectType == 'enumerator': + declaration = self._parse_enumerator() + elif objectType == 'type': + declaration = self._parse_type(named=True, outer='type') + else: + assert False + return ASTDeclaration(objectType, directiveType, declaration) + + def parse_xref_object(self) -> ASTNestedName: + name = self._parse_nested_name() + # if there are '()' left, just skip them + self.skip_ws() + self.skip_string('()') + self.assert_end() + return name + + def parse_expression(self) -> Union[ASTExpression, ASTType]: + pos = self.pos + res = None # type: Union[ASTExpression, ASTType] + try: + res = self._parse_expression() + self.skip_ws() + self.assert_end() + except DefinitionError as exExpr: + self.pos = pos + try: + res = self._parse_type(False) + self.skip_ws() + self.assert_end() + except DefinitionError as exType: + header = "Error when parsing (type) expression." + errs = [] + errs.append((exExpr, "If expression")) + errs.append((exType, "If type")) + raise self._make_multi_error(errs, header) + return res + + +def _make_phony_error_name() -> ASTNestedName: + return ASTNestedName([ASTIdentifier("PhonyNameDueToError")], rooted=False) class CObject(ObjectDescription): @@ -73,162 +2950,217 @@ class CObject(ObjectDescription): names=('rtype',)), ] - # These C types aren't described anywhere, so don't try to create - # a cross-reference to them - stopwords = { - 'const', 'void', 'char', 'wchar_t', 'int', 'short', - 'long', 'float', 'double', 'unsigned', 'signed', 'FILE', - 'clock_t', 'time_t', 'ptrdiff_t', 'size_t', 'ssize_t', - 'struct', '_Bool', - } + def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: + assert ast.objectType == 'enumerator' + # find the parent, if it exists && is an enum + # then add the name to the parent scope + symbol = ast.symbol + assert symbol + assert symbol.ident is not None + parentSymbol = symbol.parent + assert parentSymbol + if parentSymbol.parent is None: + # TODO: we could warn, but it is somewhat equivalent to + # enumeratorss, without the enum + return # no parent + parentDecl = parentSymbol.declaration + if parentDecl is None: + # the parent is not explicitly declared + # TODO: we could warn, but? + return + if parentDecl.objectType != 'enum': + # TODO: maybe issue a warning, enumerators in non-enums is weird, + # but it is somewhat equivalent to enumeratorss, without the enum + return + if parentDecl.directiveType != 'enum': + return - def _parse_type(self, node: Element, ctype: str) -> None: - # add cross-ref nodes for all words - for part in [_f for _f in wsplit_re.split(ctype) if _f]: - tnode = nodes.Text(part, part) - if part[0] in string.ascii_letters + '_' and \ - part not in self.stopwords: - pnode = pending_xref('', refdomain='c', reftype='type', reftarget=part, - modname=None, classname=None) - pnode += tnode - node += pnode - else: - node += tnode + targetSymbol = parentSymbol.parent + s = targetSymbol.find_identifier(symbol.ident, matchSelf=False, recurseInAnon=True, + searchInSiblings=False) + if s is not None: + # something is already declared with that name + return + declClone = symbol.declaration.clone() + declClone.enumeratorScopedSymbol = symbol + Symbol(parent=targetSymbol, ident=symbol.ident, + declaration=declClone, + docname=self.env.docname) - def _parse_arglist(self, arglist: str) -> Iterator[str]: - while True: - m = c_funcptr_arg_sig_re.match(arglist) - if m: - yield m.group() - arglist = c_funcptr_arg_sig_re.sub('', arglist) - if ',' in arglist: - _, arglist = arglist.split(',', 1) - else: - break - else: - if ',' in arglist: - arg, arglist = arglist.split(',', 1) - yield arg - else: - yield arglist - break + def add_target_and_index(self, ast: ASTDeclaration, sig: str, + signode: TextElement) -> None: + ids = [] + for i in range(1, _max_id + 1): + try: + id = ast.get_id(version=i) + ids.append(id) + except NoOldIdError: + assert i < _max_id + # let's keep the newest first + ids = list(reversed(ids)) + newestId = ids[0] + assert newestId # shouldn't be None - def handle_signature(self, sig: str, signode: desc_signature) -> str: - """Transform a C signature into RST nodes.""" - # first try the function pointer signature regex, it's more specific - m = c_funcptr_sig_re.match(sig) - if m is None: - m = c_sig_re.match(sig) - if m is None: - raise ValueError('no match') - rettype, name, arglist, const = m.groups() - - desc_type = addnodes.desc_type('', '') - signode += desc_type - self._parse_type(desc_type, rettype) - try: - classname, funcname = name.split('::', 1) - classname += '::' - signode += addnodes.desc_addname(classname, classname) - signode += addnodes.desc_name(funcname, funcname) - # name (the full name) is still both parts - except ValueError: - signode += addnodes.desc_name(name, name) - # clean up parentheses from canonical name - m = c_funcptr_name_re.match(name) - if m: - name = m.group(1) - - typename = self.env.ref_context.get('c:type') - if self.name == 'c:member' and typename: - fullname = typename + '.' + name - else: - fullname = name + name = ast.symbol.get_full_nested_name().get_display_string().lstrip('.') + if newestId not in self.state.document.ids: + # always add the newest id + assert newestId + signode['ids'].append(newestId) + # only add compatibility ids when there are no conflicts + for id in ids[1:]: + if not id: # is None when the element didn't exist in that version + continue + if id not in self.state.document.ids: + signode['ids'].append(id) - if not arglist: - if self.objtype == 'function' or \ - self.objtype == 'macro' and sig.rstrip().endswith('()'): - # for functions, add an empty parameter list - signode += addnodes.desc_parameterlist() - if const: - signode += addnodes.desc_addname(const, const) - return fullname + self.state.document.note_explicit_target(signode) - paramlist = addnodes.desc_parameterlist() - arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup - # this messes up function pointer types, but not too badly ;) - for arg in self._parse_arglist(arglist): - arg = arg.strip() - param = addnodes.desc_parameter('', '', noemph=True) - try: - m = c_funcptr_arg_sig_re.match(arg) - if m: - self._parse_type(param, m.group(1) + '(') - param += nodes.emphasis(m.group(2), m.group(2)) - self._parse_type(param, ')(' + m.group(3) + ')') - if m.group(4): - param += addnodes.desc_addname(m.group(4), m.group(4)) - else: - ctype, argname = arg.rsplit(' ', 1) - self._parse_type(param, ctype) - # separate by non-breaking space in the output - param += nodes.emphasis(' ' + argname, '\xa0' + argname) - except ValueError: - # no argument name given, only the type - self._parse_type(param, arg) - paramlist += param - signode += paramlist - if const: - signode += addnodes.desc_addname(const, const) - return fullname + domain = cast(CDomain, self.env.get_domain('c')) + domain.note_object(name, self.objtype, newestId) + + indexText = self.get_index_text(name) + self.indexnode['entries'].append(('single', indexText, newestId, '', None)) + + @property + def object_type(self) -> str: + raise NotImplementedError() + + @property + def display_object_type(self) -> str: + return self.object_type def get_index_text(self, name: str) -> str: - if self.objtype == 'function': - return _('%s (C function)') % name - elif self.objtype == 'member': - return _('%s (C member)') % name - elif self.objtype == 'macro': - return _('%s (C macro)') % name - elif self.objtype == 'type': - return _('%s (C type)') % name - elif self.objtype == 'var': - return _('%s (C variable)') % name - else: - return '' - - def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: - # for C API items we add a prefix since names are usually not qualified - # by a module name and so easily clash with e.g. section titles - targetname = 'c.' + name - if targetname not in self.state.document.ids: - signode['names'].append(targetname) - signode['ids'].append(targetname) - signode['first'] = (not self.names) - self.state.document.note_explicit_target(signode) + return _('%s (C %s)') % (name, self.display_object_type) - domain = cast(CDomain, self.env.get_domain('c')) - domain.note_object(name, self.objtype) + def parse_definition(self, parser: DefinitionParser) -> ASTDeclaration: + return parser.parse_declaration(self.object_type, self.objtype) + + def describe_signature(self, signode: TextElement, ast: Any, options: Dict) -> None: + ast.describe_signature(signode, 'lastIsName', self.env, options) + + def run(self) -> List[Node]: + env = self.state.document.settings.env # from ObjectDescription.run + if 'c:parent_symbol' not in env.temp_data: + root = env.domaindata['c']['root_symbol'] + env.temp_data['c:parent_symbol'] = root + env.ref_context['c:parent_key'] = root.get_lookup_key() + + # When multiple declarations are made in the same directive + # they need to know about each other to provide symbol lookup for function parameters. + # We use last_symbol to store the latest added declaration in a directive. + env.temp_data['c:last_symbol'] = None + return super().run() + + def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: + parentSymbol = self.env.temp_data['c:parent_symbol'] # type: Symbol + + parser = DefinitionParser(sig, location=signode) + try: + ast = self.parse_definition(parser) + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=signode) + # It is easier to assume some phony name than handling the error in + # the possibly inner declarations. + name = _make_phony_error_name() + symbol = parentSymbol.add_name(name) + self.env.temp_data['c:last_symbol'] = symbol + raise ValueError - indextext = self.get_index_text(name) - if indextext: - self.indexnode['entries'].append(('single', indextext, - targetname, '', None)) + try: + symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) + # append the new declaration to the sibling list + assert symbol.siblingAbove is None + assert symbol.siblingBelow is None + symbol.siblingAbove = self.env.temp_data['c:last_symbol'] + if symbol.siblingAbove is not None: + assert symbol.siblingAbove.siblingBelow is None + symbol.siblingAbove.siblingBelow = symbol + self.env.temp_data['c:last_symbol'] = symbol + except _DuplicateSymbolError as e: + # Assume we are actually in the old symbol, + # instead of the newly created duplicate. + self.env.temp_data['c:last_symbol'] = e.symbol + logger.warning("Duplicate declaration, %s", sig, location=signode) + + if ast.objectType == 'enumerator': + self._add_enumerator_to_parent(ast) + + # note: handle_signature may be called multiple time per directive, + # if it has multiple signatures, so don't mess with the original options. + options = dict(self.options) + self.describe_signature(signode, ast, options) + return ast def before_content(self) -> None: - self.typename_set = False - if self.name == 'c:type': - if self.names: - self.env.ref_context['c:type'] = self.names[0] - self.typename_set = True + lastSymbol = self.env.temp_data['c:last_symbol'] # type: Symbol + assert lastSymbol + self.oldParentSymbol = self.env.temp_data['c:parent_symbol'] + self.oldParentKey = self.env.ref_context['c:parent_key'] # type: LookupKey + self.env.temp_data['c:parent_symbol'] = lastSymbol + self.env.ref_context['c:parent_key'] = lastSymbol.get_lookup_key() def after_content(self) -> None: - if self.typename_set: - self.env.ref_context.pop('c:type', None) + self.env.temp_data['c:parent_symbol'] = self.oldParentSymbol + self.env.ref_context['c:parent_key'] = self.oldParentKey + + def make_old_id(self, name: str) -> str: + """Generate old styled node_id for C objects. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return 'c.' + name + + +class CMemberObject(CObject): + object_type = 'member' + + @property + def display_object_type(self) -> str: + # the distinction between var and member is only cosmetic + assert self.objtype in ('member', 'var') + return self.objtype + + +class CFunctionObject(CObject): + object_type = 'function' + + +class CMacroObject(CObject): + object_type = 'macro' + + +class CStructObject(CObject): + object_type = 'struct' + + +class CUnionObject(CObject): + object_type = 'union' + + +class CEnumObject(CObject): + object_type = 'enum' + + +class CEnumeratorObject(CObject): + object_type = 'enumerator' + + +class CTypeObject(CObject): + object_type = 'type' class CXRefRole(XRefRole): def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: + refnode.attributes.update(env.ref_context) + + if not has_explicit_title: + # major hax: replace anon names via simple string manipulation. + # Can this actually fail? + title = anon_identifier_re.sub("[anonymous]", str(title)) + if not has_explicit_title: target = target.lstrip('~') # only has a meaning for the title # if the first character is a tilde, don't display the module/class @@ -241,88 +3173,203 @@ class CXRefRole(XRefRole): return title, target +class CExprRole(SphinxRole): + def __init__(self, asCode: bool) -> None: + super().__init__() + if asCode: + # render the expression as inline code + self.class_type = 'c-expr' + self.node_type = nodes.literal # type: Type[TextElement] + else: + # render the expression as inline text + self.class_type = 'c-texpr' + self.node_type = nodes.inline + + def run(self) -> Tuple[List[Node], List[system_message]]: + text = self.text.replace('\n', ' ') + parser = DefinitionParser(text, location=self.get_source_info()) + # attempt to mimic XRefRole classes, except that... + classes = ['xref', 'c', self.class_type] + try: + ast = parser.parse_expression() + except DefinitionError as ex: + logger.warning('Unparseable C expression: %r\n%s', text, ex, + location=self.get_source_info()) + # see below + return [self.node_type(text, text, classes=classes)], [] + parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) + if parentSymbol is None: + parentSymbol = self.env.domaindata['c']['root_symbol'] + # ...most if not all of these classes should really apply to the individual references, + # not the container node + signode = self.node_type(classes=classes) + ast.describe_signature(signode, 'markType', self.env, parentSymbol) + return [signode], [] + + class CDomain(Domain): """C language domain.""" name = 'c' label = 'C' object_types = { 'function': ObjType(_('function'), 'func'), - 'member': ObjType(_('member'), 'member'), - 'macro': ObjType(_('macro'), 'macro'), - 'type': ObjType(_('type'), 'type'), - 'var': ObjType(_('variable'), 'data'), + 'member': ObjType(_('member'), 'member'), + 'macro': ObjType(_('macro'), 'macro'), + 'type': ObjType(_('type'), 'type'), + 'var': ObjType(_('variable'), 'data'), } directives = { - 'function': CObject, - 'member': CObject, - 'macro': CObject, - 'type': CObject, - 'var': CObject, + 'member': CMemberObject, + 'var': CMemberObject, + 'function': CFunctionObject, + 'macro': CMacroObject, + 'struct': CStructObject, + 'union': CUnionObject, + 'enum': CEnumObject, + 'enumerator': CEnumeratorObject, + 'type': CTypeObject, } roles = { - 'func': CXRefRole(fix_parens=True), 'member': CXRefRole(), - 'macro': CXRefRole(), - 'data': CXRefRole(), - 'type': CXRefRole(), + 'data': CXRefRole(), + 'var': CXRefRole(), + 'func': CXRefRole(fix_parens=True), + 'macro': CXRefRole(), + 'struct': CXRefRole(), + 'union': CXRefRole(), + 'enum': CXRefRole(), + 'enumerator': CXRefRole(), + 'type': CXRefRole(), + 'expr': CExprRole(asCode=True), + 'texpr': CExprRole(asCode=False) } initial_data = { - 'objects': {}, # fullname -> docname, objtype - } # type: Dict[str, Dict[str, Tuple[str, Any]]] + 'root_symbol': Symbol(None, None, None, None), + 'objects': {}, # fullname -> docname, node_id, objtype + } # type: Dict[str, Union[Symbol, Dict[str, Tuple[str, str, str]]]] @property - def objects(self) -> Dict[str, Tuple[str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, objtype + def objects(self) -> Dict[str, Tuple[str, str, str]]: + return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def note_object(self, name: str, objtype: str, location: Any = None) -> None: + def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: if name in self.objects: docname = self.objects[name][0] - logger.warning(__('duplicate C object description of %s, ' + logger.warning(__('Duplicate C object description of %s, ' 'other instance in %s, use :noindex: for one of them'), name, docname, location=location) - self.objects[name] = (self.env.docname, objtype) + self.objects[name] = (self.env.docname, node_id, objtype) def clear_doc(self, docname: str) -> None: - for fullname, (fn, _l) in list(self.objects.items()): + if Symbol.debug_show_tree: + print("clear_doc:", docname) + print("\tbefore:") + print(self.data['root_symbol'].dump(1)) + print("\tbefore end") + + rootSymbol = self.data['root_symbol'] + rootSymbol.clear_doc(docname) + + if Symbol.debug_show_tree: + print("\tafter:") + print(self.data['root_symbol'].dump(1)) + print("\tafter end") + print("clear_doc end:", docname) + for fullname, (fn, _id, _l) in list(self.objects.items()): if fn == docname: del self.objects[fullname] + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + if Symbol.debug_show_tree: + print("process_doc:", docname) + print(self.data['root_symbol'].dump(0)) + print("process_doc end:", docname) + + def process_field_xref(self, pnode: pending_xref) -> None: + pnode.attributes.update(self.env.ref_context) + def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: - # XXX check duplicates - for fullname, (fn, objtype) in otherdata['objects'].items(): + if Symbol.debug_show_tree: + print("merge_domaindata:") + print("\tself:") + print(self.data['root_symbol'].dump(1)) + print("\tself end") + print("\tother:") + print(otherdata['root_symbol'].dump(1)) + print("\tother end") + print("merge_domaindata end") + + self.data['root_symbol'].merge_with(otherdata['root_symbol'], + docnames, self.env) + ourObjects = self.data['objects'] + for fullname, (fn, id_, objtype) in otherdata['objects'].items(): if fn in docnames: - self.data['objects'][fullname] = (fn, objtype) + if fullname in ourObjects: + msg = __("Duplicate declaration, also defined in '%s'.\n" + "Name of declaration is '%s'.") + msg = msg % (ourObjects[fullname], fullname) + logger.warning(msg, location=fn) + else: + ourObjects[fullname] = (fn, id_, objtype) + + def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, + contnode: Element) -> Tuple[Element, str]: + parser = DefinitionParser(target, location=node) + try: + name = parser.parse_xref_object() + except DefinitionError as e: + logger.warning('Unparseable C cross-reference: %r\n%s', target, e, + location=node) + return None, None + parentKey = node.get("c:parent_key", None) # type: LookupKey + rootSymbol = self.data['root_symbol'] + if parentKey: + parentSymbol = rootSymbol.direct_lookup(parentKey) # type: Symbol + if not parentSymbol: + print("Target: ", target) + print("ParentKey: ", parentKey) + print(rootSymbol.dump(1)) + assert parentSymbol # should be there + else: + parentSymbol = rootSymbol + s = parentSymbol.find_declaration(name, typ, + matchSelf=True, recurseInAnon=True) + if s is None or s.declaration is None: + return None, None + + # TODO: check role type vs. object type + + declaration = s.declaration + displayName = name.get_display_string() + docname = s.docname + assert docname + + return make_refnode(builder, fromdocname, docname, + declaration.get_newest_id(), contnode, displayName + ), declaration.objectType def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: - # strip pointer asterisk - target = target.rstrip(' *') - # becase TypedField can generate xrefs - if target in CObject.stopwords: - return contnode - if target not in self.objects: - return None - obj = self.objects[target] - return make_refnode(builder, fromdocname, obj[0], 'c.' + target, - contnode, target) + typ: str, target: str, node: pending_xref, + contnode: Element) -> Element: + return self._resolve_xref_inner(env, fromdocname, builder, typ, + target, node, contnode)[0] def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element ) -> List[Tuple[str, Element]]: - # strip pointer asterisk - target = target.rstrip(' *') - if target not in self.objects: - return [] - obj = self.objects[target] - return [('c:' + self.role_for_objtype(obj[1]), - make_refnode(builder, fromdocname, obj[0], 'c.' + target, - contnode, target))] + with logging.suppress_logging(): + retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, + 'any', target, node, contnode) + if retnode: + return [('c:' + self.role_for_objtype(objtype), retnode)] + return [] def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: - for refname, (docname, type) in list(self.objects.items()): - yield (refname, refname, type, docname, 'c.' + refname, 1) + for refname, (docname, node_id, objtype) in list(self.objects.items()): + yield (refname, refname, objtype, docname, node_id, 1) def setup(app: Sphinx) -> Dict[str, Any]: @@ -330,7 +3377,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py index f3f1d8aef..a07944db8 100644 --- a/sphinx/domains/changeset.py +++ b/sphinx/domains/changeset.py @@ -16,8 +16,6 @@ from docutils import nodes from docutils.nodes import Node from sphinx import addnodes -from sphinx import locale -from sphinx.deprecation import DeprecatedDict, RemovedInSphinx30Warning from sphinx.domains import Domain from sphinx.locale import _ from sphinx.util.docutils import SphinxDirective @@ -41,13 +39,6 @@ versionlabel_classes = { 'deprecated': 'deprecated', } -locale.versionlabels = DeprecatedDict( - versionlabels, - 'sphinx.locale.versionlabels is deprecated. ' - 'Please use sphinx.domains.changeset.versionlabels instead.', - RemovedInSphinx30Warning -) - # TODO: move to typing.NamedTuple after dropping py35 support (see #5958) ChangeSet = namedtuple('ChangeSet', diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 53c958601..7f7558cb3 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -9,12 +9,12 @@ """ import re -import warnings -from copy import deepcopy -from typing import Any, Callable, Dict, Iterator, List, Match, Pattern, Tuple, Type, Union +from typing import ( + Any, Callable, Dict, Iterator, List, Tuple, Type, TypeVar, Union +) -from docutils import nodes, utils -from docutils.nodes import Element, Node, TextElement +from docutils import nodes +from docutils.nodes import Element, Node, TextElement, system_message from docutils.parsers.rst import directives from sphinx import addnodes @@ -22,23 +22,29 @@ from sphinx.addnodes import desc_signature, pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.config import Config -from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.environment import BuildEnvironment from sphinx.errors import NoUri from sphinx.locale import _, __ -from sphinx.roles import XRefRole +from sphinx.roles import SphinxRole, XRefRole from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging +from sphinx.util.cfamily import ( + NoOldIdError, ASTBaseBase, verify_description_mode, StringifyTransform, + BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, + identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, + hex_literal_re, binary_literal_re, float_literal_re, + char_literal_re +) from sphinx.util.docfields import Field, GroupedField from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode logger = logging.getLogger(__name__) -StringifyTransform = Callable[[Any], str] +T = TypeVar('T') """ Important note on ids @@ -61,7 +67,7 @@ StringifyTransform = Callable[[Any], str] Each signature is in a desc_signature node, where all children are desc_signature_line nodes. Each of these lines will have the attribute - 'sphinx_cpp_tagname' set to one of the following (prioritized): + 'sphinx_line_type' set to one of the following (prioritized): - 'declarator', if the line contains the name of the declared object. - 'templateParams', if the line starts a template parameter list, - 'templateParams', if the line has template parameters @@ -290,47 +296,6 @@ StringifyTransform = Callable[[Any], str] nested-name """ -_integer_literal_re = re.compile(r'[1-9][0-9]*') -_octal_literal_re = re.compile(r'0[0-7]*') -_hex_literal_re = re.compile(r'0[xX][0-9a-fA-F][0-9a-fA-F]*') -_binary_literal_re = re.compile(r'0[bB][01][01]*') -_integer_suffix_re = re.compile(r'') -_float_literal_re = re.compile(r'''(?x) - [+-]?( - # decimal - ([0-9]+[eE][+-]?[0-9]+) - | ([0-9]*\.[0-9]+([eE][+-]?[0-9]+)?) - | ([0-9]+\.([eE][+-]?[0-9]+)?) - # hex - | (0[xX][0-9a-fA-F]+[pP][+-]?[0-9a-fA-F]+) - | (0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+([pP][+-]?[0-9a-fA-F]+)?) - | (0[xX][0-9a-fA-F]+\.([pP][+-]?[0-9a-fA-F]+)?) - ) -''') -_char_literal_re = re.compile(r'''(?x) - ((?:u8)|u|U|L)? - '( - (?:[^\\']) - | (\\( - (?:['"?\\abfnrtv]) - | (?:[0-7]{1,3}) - | (?:x[0-9a-fA-F]{2}) - | (?:u[0-9a-fA-F]{4}) - | (?:U[0-9a-fA-F]{8}) - )) - )' -''') - -_anon_identifier_re = re.compile(r'(@[a-zA-Z0-9_])[a-zA-Z0-9_]*\b') -_identifier_re = re.compile(r'''(?x) - ( # This 'extends' _anon_identifier_re with the ordinary identifiers, - # make sure they are in sync. - (~?\b[a-zA-Z_]) # ordinary identifiers - | (@[a-zA-Z0-9_]) # our extension for names of anonymous entities - ) - [a-zA-Z0-9_]*\b -''') -_whitespace_re = re.compile(r'(?u)\s+') _string_re = re.compile(r"[LuU8]?('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) _visibility_re = re.compile(r'\b(public|private|protected)\b') @@ -367,6 +332,8 @@ _keywords = [ _max_id = 4 _id_prefix = [None, '', '_CPPv2', '_CPPv3', '_CPPv4'] +# Ids are used in lookup keys which are used across pickled files, +# so when _max_id changes, make sure to update the ENV_VERSION. # ------------------------------------------------------------------------------ # Id v1 constants @@ -576,27 +543,8 @@ _id_explicit_cast = { } -class NoOldIdError(Exception): - # Used to avoid implementing unneeded id generation for old id schemes. - @property - def description(self) -> str: - warnings.warn('%s.description is deprecated. ' - 'Coerce the instance to a string instead.' % self.__class__.__name__, - RemovedInSphinx40Warning, stacklevel=2) - return str(self) - - -class DefinitionError(Exception): - @property - def description(self) -> str: - warnings.warn('%s.description is deprecated. ' - 'Coerce the instance to a string instead.' % self.__class__.__name__, - RemovedInSphinx40Warning, stacklevel=2) - return str(self) - - class _DuplicateSymbolError(Exception): - def __init__(self, symbol: "Symbol", declaration: Any) -> None: + def __init__(self, symbol: "Symbol", declaration: "ASTDeclaration") -> None: assert symbol assert declaration self.symbol = symbol @@ -606,54 +554,237 @@ class _DuplicateSymbolError(Exception): return "Internal C++ duplicate symbol error:\n%s" % self.symbol.dump(0) -class ASTBase: - def __eq__(self, other: Any) -> bool: - if type(self) is not type(other): - return False - try: - for key, value in self.__dict__.items(): - if value != getattr(other, key): - return False - except AttributeError: - return False - return True +class ASTBase(ASTBaseBase): + pass - __hash__ = None # type: Callable[[], int] - def clone(self) -> Any: - """Clone a definition expression node.""" - return deepcopy(self) +# Names +################################################################################ - def _stringify(self, transform: StringifyTransform) -> str: - raise NotImplementedError(repr(self)) +class ASTIdentifier(ASTBase): + def __init__(self, identifier: str) -> None: + assert identifier is not None + assert len(identifier) != 0 + self.identifier = identifier + + def is_anon(self) -> bool: + return self.identifier[0] == '@' + + def get_id(self, version: int) -> str: + if self.is_anon() and version < 3: + raise NoOldIdError() + if version == 1: + if self.identifier == 'size_t': + return 's' + else: + return self.identifier + if self.identifier == "std": + return 'St' + elif self.identifier[0] == "~": + # a destructor, just use an arbitrary version of dtors + return 'D0' + else: + if self.is_anon(): + return 'Ut%d_%s' % (len(self.identifier) - 1, self.identifier[1:]) + else: + return str(len(self.identifier)) + self.identifier + + # and this is where we finally make a difference between __str__ and the display string def __str__(self) -> str: - return self._stringify(lambda ast: str(ast)) + return self.identifier def get_display_string(self) -> str: - return self._stringify(lambda ast: ast.get_display_string()) + return "[anonymous]" if self.is_anon() else self.identifier + + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", + prefix: str, templateArgs: str, symbol: "Symbol") -> None: + verify_description_mode(mode) + if mode == 'markType': + targetText = prefix + self.identifier + templateArgs + pnode = addnodes.pending_xref('', refdomain='cpp', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + key = symbol.get_lookup_key() + pnode['cpp:parent_key'] = key + if self.is_anon(): + pnode += nodes.strong(text="[anonymous]") + else: + pnode += nodes.Text(self.identifier) + signode += pnode + elif mode == 'lastIsName': + if self.is_anon(): + signode += nodes.strong(text="[anonymous]") + else: + signode += addnodes.desc_name(self.identifier, self.identifier) + elif mode == 'noneIsName': + if self.is_anon(): + signode += nodes.strong(text="[anonymous]") + else: + signode += nodes.Text(self.identifier) + else: + raise Exception('Unknown description mode: %s' % mode) + + +class ASTNestedNameElement(ASTBase): + def __init__(self, identOrOp: Union[ASTIdentifier, "ASTOperator"], + templateArgs: "ASTTemplateArgs") -> None: + self.identOrOp = identOrOp + self.templateArgs = templateArgs + + def is_operator(self) -> bool: + return False + + def get_id(self, version: int) -> str: + res = self.identOrOp.get_id(version) + if self.templateArgs: + res += self.templateArgs.get_id(version) + return res + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.identOrOp) + if self.templateArgs: + res += transform(self.templateArgs) + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", prefix: str, symbol: "Symbol") -> None: + tArgs = str(self.templateArgs) if self.templateArgs is not None else '' + self.identOrOp.describe_signature(signode, mode, env, prefix, tArgs, symbol) + if self.templateArgs is not None: + self.templateArgs.describe_signature(signode, mode, env, symbol) + + +class ASTNestedName(ASTBase): + def __init__(self, names: List[ASTNestedNameElement], + templates: List[bool], rooted: bool) -> None: + assert len(names) > 0 + self.names = names + self.templates = templates + assert len(self.names) == len(self.templates) + self.rooted = rooted + + @property + def name(self) -> "ASTNestedName": + return self + + def num_templates(self) -> int: + count = 0 + for n in self.names: + if n.is_operator(): + continue + if n.templateArgs: + count += 1 + return count + + def get_id(self, version: int, modifiers: str = '') -> str: + if version == 1: + tt = str(self) + if tt in _id_shorthands_v1: + return _id_shorthands_v1[tt] + else: + return '::'.join(n.get_id(version) for n in self.names) - def __repr__(self) -> str: - return '<%s>' % self.__class__.__name__ + res = [] + if len(self.names) > 1 or len(modifiers) > 0: + res.append('N') + res.append(modifiers) + for n in self.names: + res.append(n.get_id(version)) + if len(self.names) > 1 or len(modifiers) > 0: + res.append('E') + return ''.join(res) + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.rooted: + res.append('') + for i in range(len(self.names)): + n = self.names[i] + t = self.templates[i] + if t: + res.append("template " + transform(n)) + else: + res.append(transform(n)) + return '::'.join(res) -def _verify_description_mode(mode: str) -> None: - if mode not in ('lastIsName', 'noneIsName', 'markType', 'markName', 'param'): - raise Exception("Description mode '%s' is invalid." % mode) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + # just print the name part, with template args, not template params + if mode == 'noneIsName': + signode += nodes.Text(str(self)) + elif mode == 'param': + name = str(self) + signode += nodes.emphasis(name, name) + elif mode == 'markType' or mode == 'lastIsName' or mode == 'markName': + # Each element should be a pending xref targeting the complete + # prefix. however, only the identifier part should be a link, such + # that template args can be a link as well. + # For 'lastIsName' we should also prepend template parameter lists. + templateParams = [] # type: List[Any] + if mode == 'lastIsName': + assert symbol is not None + if symbol.declaration.templatePrefix is not None: + templateParams = symbol.declaration.templatePrefix.templates + iTemplateParams = 0 + templateParamsPrefix = '' + prefix = '' + first = True + names = self.names[:-1] if mode == 'lastIsName' else self.names + # If lastIsName, then wrap all of the prefix in a desc_addname, + # else append directly to signode. + # NOTE: Breathe relies on the prefix being in the desc_addname node, + # so it can remove it in inner declarations. + dest = signode + if mode == 'lastIsName': + dest = addnodes.desc_addname() + for i in range(len(names)): + nne = names[i] + template = self.templates[i] + if not first: + dest += nodes.Text('::') + prefix += '::' + if template: + dest += nodes.Text("template ") + first = False + txt_nne = str(nne) + if txt_nne != '': + if nne.templateArgs and iTemplateParams < len(templateParams): + templateParamsPrefix += str(templateParams[iTemplateParams]) + iTemplateParams += 1 + nne.describe_signature(dest, 'markType', + env, templateParamsPrefix + prefix, symbol) + prefix += txt_nne + if mode == 'lastIsName': + if len(self.names) > 1: + dest += addnodes.desc_addname('::', '::') + signode += dest + if self.templates[-1]: + signode += nodes.Text("template ") + self.names[-1].describe_signature(signode, mode, env, '', symbol) + else: + raise Exception('Unknown description mode: %s' % mode) ################################################################################ # Attributes ################################################################################ -class ASTCPPAttribute(ASTBase): +class ASTAttribute(ASTBase): + def describe_signature(self, signode: TextElement) -> None: + raise NotImplementedError(repr(self)) + + +class ASTCPPAttribute(ASTAttribute): def __init__(self, arg: str) -> None: self.arg = arg - def _stringify(self, transform): + def _stringify(self, transform: StringifyTransform) -> str: return "[[" + self.arg + "]]" - def describe_signature(self, signode: desc_signature) -> None: + def describe_signature(self, signode: TextElement) -> None: txt = str(self) signode.append(nodes.Text(txt, txt)) @@ -672,8 +803,8 @@ class ASTGnuAttribute(ASTBase): return ''.join(res) -class ASTGnuAttributeList(ASTBase): - def __init__(self, attrs: List[Any]) -> None: +class ASTGnuAttributeList(ASTAttribute): + def __init__(self, attrs: List[ASTGnuAttribute]) -> None: self.attrs = attrs def _stringify(self, transform: StringifyTransform) -> str: @@ -687,12 +818,12 @@ class ASTGnuAttributeList(ASTBase): res.append('))') return ''.join(res) - def describe_signature(self, signode: desc_signature) -> None: + def describe_signature(self, signode: TextElement) -> None: txt = str(self) signode.append(nodes.Text(txt, txt)) -class ASTIdAttribute(ASTBase): +class ASTIdAttribute(ASTAttribute): """For simple attributes defined by the user.""" def __init__(self, id: str) -> None: @@ -701,11 +832,11 @@ class ASTIdAttribute(ASTBase): def _stringify(self, transform: StringifyTransform) -> str: return self.id - def describe_signature(self, signode: desc_signature) -> None: + def describe_signature(self, signode: TextElement) -> None: signode.append(nodes.Text(self.id, self.id)) -class ASTParenAttribute(ASTBase): +class ASTParenAttribute(ASTAttribute): """For paren attributes defined by the user.""" def __init__(self, id: str, arg: str) -> None: @@ -715,28 +846,45 @@ class ASTParenAttribute(ASTBase): def _stringify(self, transform: StringifyTransform) -> str: return self.id + '(' + self.arg + ')' - def describe_signature(self, signode: desc_signature) -> None: + def describe_signature(self, signode: TextElement) -> None: txt = str(self) signode.append(nodes.Text(txt, txt)) ################################################################################ -# Expressions and Literals +# Expressions +################################################################################ + +class ASTExpression(ASTBase): + def get_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + raise NotImplementedError(repr(self)) + + +# Primary expressions ################################################################################ -class ASTPointerLiteral(ASTBase): +class ASTLiteral(ASTExpression): + pass + + +class ASTPointerLiteral(ASTLiteral): def _stringify(self, transform: StringifyTransform) -> str: return 'nullptr' def get_id(self, version: int) -> str: return 'LDnE' - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('nullptr')) -class ASTBooleanLiteral(ASTBase): - def __init__(self, value): +class ASTBooleanLiteral(ASTLiteral): + def __init__(self, value: bool) -> None: self.value = value def _stringify(self, transform: StringifyTransform) -> str: @@ -751,11 +899,12 @@ class ASTBooleanLiteral(ASTBase): else: return 'L0E' - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text(str(self))) -class ASTNumberLiteral(ASTBase): +class ASTNumberLiteral(ASTLiteral): def __init__(self, data: str) -> None: self.data = data @@ -765,21 +914,30 @@ class ASTNumberLiteral(ASTBase): def get_id(self, version: int) -> str: return "L%sE" % self.data - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: txt = str(self) signode.append(nodes.Text(txt, txt)) -class UnsupportedMultiCharacterCharLiteral(Exception): - @property - def decoded(self) -> str: - warnings.warn('%s.decoded is deprecated. ' - 'Coerce the instance to a string instead.' % self.__class__.__name__, - RemovedInSphinx40Warning, stacklevel=2) - return str(self) +class ASTStringLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def get_id(self, version: int) -> str: + # note: the length is not really correct with escaping + return "LA%d_KcE" % (len(self.data) - 2) -class ASTCharLiteral(ASTBase): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + txt = str(self) + signode.append(nodes.Text(txt, txt)) + + +class ASTCharLiteral(ASTLiteral): def __init__(self, prefix: str, data: str) -> None: self.prefix = prefix # may be None when no prefix self.data = data @@ -800,56 +958,27 @@ class ASTCharLiteral(ASTBase): def get_id(self, version: int) -> str: return self.type + str(self.value) - def describe_signature(self, signode, mode, env, symbol): - txt = str(self) - signode.append(nodes.Text(txt, txt)) - - -class ASTStringLiteral(ASTBase): - def __init__(self, data: str) -> None: - self.data = data - - def _stringify(self, transform: StringifyTransform) -> str: - return self.data - - def get_id(self, version: int) -> str: - # note: the length is not really correct with escaping - return "LA%d_KcE" % (len(self.data) - 2) - - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: txt = str(self) signode.append(nodes.Text(txt, txt)) -class ASTThisLiteral(ASTBase): +class ASTThisLiteral(ASTExpression): def _stringify(self, transform: StringifyTransform) -> str: return "this" def get_id(self, version: int) -> str: return "fpT" - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text("this")) -class ASTParenExpr(ASTBase): - def __init__(self, expr): - self.expr = expr - - def _stringify(self, transform: StringifyTransform) -> str: - return '(' + transform(self.expr) + ')' - - def get_id(self, version: int) -> str: - return self.expr.get_id(version) - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('(', '(')) - self.expr.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text(')', ')')) - - -class ASTFoldExpr(ASTBase): - def __init__(self, leftExpr: Any, op: str, rightExpr: Any) -> None: +class ASTFoldExpr(ASTExpression): + def __init__(self, leftExpr: ASTExpression, + op: str, rightExpr: ASTExpression) -> None: assert leftExpr is not None or rightExpr is not None self.leftExpr = leftExpr self.op = op @@ -892,7 +1021,8 @@ class ASTFoldExpr(ASTBase): res.append(self.rightExpr.get_id(version)) return ''.join(res) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('(')) if self.leftExpr: self.leftExpr.describe_signature(signode, mode, env, symbol) @@ -908,99 +1038,224 @@ class ASTFoldExpr(ASTBase): signode.append(nodes.Text(')')) -class ASTBinOpExpr(ASTBase): - def __init__(self, exprs, ops): - assert len(exprs) > 0 - assert len(exprs) == len(ops) + 1 - self.exprs = exprs - self.ops = ops +class ASTParenExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - res = [] - res.append(transform(self.exprs[0])) - for i in range(1, len(self.exprs)): - res.append(' ') - res.append(self.ops[i - 1]) - res.append(' ') - res.append(transform(self.exprs[i])) - return ''.join(res) + return '(' + transform(self.expr) + ')' def get_id(self, version: int) -> str: - assert version >= 2 - res = [] - for i in range(len(self.ops)): - res.append(_id_operator_v2[self.ops[i]]) - res.append(self.exprs[i].get_id(version)) - res.append(self.exprs[-1].get_id(version)) + return self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('(', '(')) + self.expr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')', ')')) + + +class ASTIdExpression(ASTExpression): + def __init__(self, name: ASTNestedName): + # note: this class is basically to cast a nested name as an expression + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def get_id(self, version: int) -> str: + return self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.name.describe_signature(signode, mode, env, symbol) + + +# Postfix expressions +################################################################################ + +class ASTPostfixOp(ASTBase): + def get_id(self, idPrefix: str, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + raise NotImplementedError(repr(self)) + + +class ASTPostfixArray(ASTPostfixOp): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '[' + transform(self.expr) + ']' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'ix' + idPrefix + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('[')) + self.expr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(']')) + + +class ASTPostfixMember(ASTPostfixOp): + def __init__(self, name: ASTNestedName): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '.' + transform(self.name) + + def get_id(self, idPrefix: str, version: int) -> str: + return 'dt' + idPrefix + self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('.')) + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixMemberOfPointer(ASTPostfixOp): + def __init__(self, name: ASTNestedName): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '->' + transform(self.name) + + def get_id(self, idPrefix: str, version: int) -> str: + return 'pt' + idPrefix + self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('->')) + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixInc(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '++' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'pp' + idPrefix + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('++')) + + +class ASTPostfixDec(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '--' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'mm' + idPrefix + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('--')) + + +class ASTPostfixCallExpr(ASTPostfixOp): + def __init__(self, lst: Union["ASTParenExprList", "ASTBracedInitList"]) -> None: + self.lst = lst + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.lst) + + def get_id(self, idPrefix: str, version: int) -> str: + res = ['cl', idPrefix] + for e in self.lst.exprs: + res.append(e.get_id(version)) + res.append('E') return ''.join(res) - def describe_signature(self, signode, mode, env, symbol): - self.exprs[0].describe_signature(signode, mode, env, symbol) - for i in range(1, len(self.exprs)): - signode.append(nodes.Text(' ')) - signode.append(nodes.Text(self.ops[i - 1])) - signode.append(nodes.Text(' ')) - self.exprs[i].describe_signature(signode, mode, env, symbol) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.lst.describe_signature(signode, mode, env, symbol) -class ASTAssignmentExpr(ASTBase): - def __init__(self, exprs, ops): - assert len(exprs) > 0 - assert len(exprs) == len(ops) + 1 - self.exprs = exprs - self.ops = ops +class ASTPostfixExpr(ASTExpression): + def __init__(self, prefix: "ASTType", postFixes: List[ASTPostfixOp]): + self.prefix = prefix + self.postFixes = postFixes def _stringify(self, transform: StringifyTransform) -> str: - res = [] - res.append(transform(self.exprs[0])) - for i in range(1, len(self.exprs)): - res.append(' ') - res.append(self.ops[i - 1]) - res.append(' ') - res.append(transform(self.exprs[i])) + res = [transform(self.prefix)] + for p in self.postFixes: + res.append(transform(p)) return ''.join(res) def get_id(self, version: int) -> str: - res = [] - for i in range(len(self.ops)): - res.append(_id_operator_v2[self.ops[i]]) - res.append(self.exprs[i].get_id(version)) - res.append(self.exprs[-1].get_id(version)) - return ''.join(res) + id = self.prefix.get_id(version) + for p in self.postFixes: + id = p.get_id(id, version) + return id - def describe_signature(self, signode, mode, env, symbol): - self.exprs[0].describe_signature(signode, mode, env, symbol) - for i in range(1, len(self.exprs)): - signode.append(nodes.Text(' ')) - signode.append(nodes.Text(self.ops[i - 1])) - signode.append(nodes.Text(' ')) - self.exprs[i].describe_signature(signode, mode, env, symbol) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.prefix.describe_signature(signode, mode, env, symbol) + for p in self.postFixes: + p.describe_signature(signode, mode, env, symbol) -class ASTCastExpr(ASTBase): - def __init__(self, typ, expr): +class ASTExplicitCast(ASTExpression): + def __init__(self, cast: str, typ: "ASTType", expr: ASTExpression): + assert cast in _id_explicit_cast + self.cast = cast self.typ = typ self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - res = ['('] + res = [self.cast] + res.append('<') res.append(transform(self.typ)) - res.append(')') + res.append('>(') res.append(transform(self.expr)) + res.append(')') return ''.join(res) def get_id(self, version: int) -> str: - return 'cv' + self.typ.get_id(version) + self.expr.get_id(version) + return (_id_explicit_cast[self.cast] + + self.typ.get_id(version) + + self.expr.get_id(version)) - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('(')) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text(self.cast)) + signode.append(nodes.Text('<')) self.typ.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text(')')) + signode.append(nodes.Text('>')) + signode.append(nodes.Text('(')) self.expr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) -class ASTUnaryOpExpr(ASTBase): - def __init__(self, op, expr): +class ASTTypeId(ASTExpression): + def __init__(self, typeOrExpr: Union["ASTType", ASTExpression], isType: bool): + self.typeOrExpr = typeOrExpr + self.isType = isType + + def _stringify(self, transform: StringifyTransform) -> str: + return 'typeid(' + transform(self.typeOrExpr) + ')' + + def get_id(self, version: int) -> str: + prefix = 'ti' if self.isType else 'te' + return prefix + self.typeOrExpr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode.append(nodes.Text('typeid')) + signode.append(nodes.Text('(')) + self.typeOrExpr.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(')')) + + +# Unary expressions +################################################################################ + +class ASTUnaryOpExpr(ASTExpression): + def __init__(self, op: str, expr: ASTExpression): self.op = op self.expr = expr @@ -1010,13 +1265,14 @@ class ASTUnaryOpExpr(ASTBase): def get_id(self, version: int) -> str: return _id_operator_unary_v2[self.op] + self.expr.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text(self.op)) self.expr.describe_signature(signode, mode, env, symbol) -class ASTSizeofParamPack(ASTBase): - def __init__(self, identifier): +class ASTSizeofParamPack(ASTExpression): + def __init__(self, identifier: ASTIdentifier): self.identifier = identifier def _stringify(self, transform: StringifyTransform) -> str: @@ -1025,15 +1281,16 @@ class ASTSizeofParamPack(ASTBase): def get_id(self, version: int) -> str: return 'sZ' + self.identifier.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('sizeof...(')) self.identifier.describe_signature(signode, mode, env, symbol=symbol, prefix="", templateArgs="") signode.append(nodes.Text(')')) -class ASTSizeofType(ASTBase): - def __init__(self, typ): +class ASTSizeofType(ASTExpression): + def __init__(self, typ: "ASTType"): self.typ = typ def _stringify(self, transform: StringifyTransform) -> str: @@ -1042,14 +1299,15 @@ class ASTSizeofType(ASTBase): def get_id(self, version: int) -> str: return 'st' + self.typ.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('sizeof(')) self.typ.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text(')')) -class ASTSizeofExpr(ASTBase): - def __init__(self, expr): +class ASTSizeofExpr(ASTExpression): + def __init__(self, expr: ASTExpression): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: @@ -1058,13 +1316,14 @@ class ASTSizeofExpr(ASTBase): def get_id(self, version: int) -> str: return 'sz' + self.expr.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('sizeof ')) self.expr.describe_signature(signode, mode, env, symbol) -class ASTAlignofExpr(ASTBase): - def __init__(self, typ): +class ASTAlignofExpr(ASTExpression): + def __init__(self, typ: "ASTType"): self.typ = typ def _stringify(self, transform: StringifyTransform) -> str: @@ -1073,14 +1332,15 @@ class ASTAlignofExpr(ASTBase): def get_id(self, version: int) -> str: return 'at' + self.typ.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('alignof(')) self.typ.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text(')')) -class ASTNoexceptExpr(ASTBase): - def __init__(self, expr): +class ASTNoexceptExpr(ASTExpression): + def __init__(self, expr: ASTExpression): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: @@ -1089,14 +1349,16 @@ class ASTNoexceptExpr(ASTBase): def get_id(self, version: int) -> str: return 'nx' + self.expr.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('noexcept(')) self.expr.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text(')')) -class ASTNewExpr(ASTBase): - def __init__(self, rooted: bool, isNewTypeId: bool, typ: "ASTType", initList: Any) -> None: +class ASTNewExpr(ASTExpression): + def __init__(self, rooted: bool, isNewTypeId: bool, typ: "ASTType", + initList: Union["ASTParenExprList", "ASTBracedInitList"]) -> None: self.rooted = rooted self.isNewTypeId = isNewTypeId self.typ = typ @@ -1128,7 +1390,8 @@ class ASTNewExpr(ASTBase): res.append('E') return ''.join(res) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: if self.rooted: signode.append(nodes.Text('::')) signode.append(nodes.Text('new ')) @@ -1141,8 +1404,8 @@ class ASTNewExpr(ASTBase): self.initList.describe_signature(signode, mode, env, symbol) -class ASTDeleteExpr(ASTBase): - def __init__(self, rooted, array, expr): +class ASTDeleteExpr(ASTExpression): + def __init__(self, rooted: bool, array: bool, expr: ASTExpression): self.rooted = rooted self.array = array self.expr = expr @@ -1164,7 +1427,8 @@ class ASTDeleteExpr(ASTBase): id = "dl" return id + self.expr.get_id(version) - def describe_signature(self, signode, mode, env, symbol): + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: if self.rooted: signode.append(nodes.Text('::')) signode.append(nodes.Text('delete ')) @@ -1173,653 +1437,127 @@ class ASTDeleteExpr(ASTBase): self.expr.describe_signature(signode, mode, env, symbol) -class ASTExplicitCast(ASTBase): - def __init__(self, cast, typ, expr): - assert cast in _id_explicit_cast - self.cast = cast +# Other expressions +################################################################################ + +class ASTCastExpr(ASTExpression): + def __init__(self, typ: "ASTType", expr: ASTExpression): self.typ = typ self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - res = [self.cast] - res.append('<') + res = ['('] res.append(transform(self.typ)) - res.append('>(') - res.append(transform(self.expr)) res.append(')') + res.append(transform(self.expr)) return ''.join(res) def get_id(self, version: int) -> str: - return (_id_explicit_cast[self.cast] + - self.typ.get_id(version) + - self.expr.get_id(version)) - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text(self.cast)) - signode.append(nodes.Text('<')) - self.typ.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text('>')) - signode.append(nodes.Text('(')) - self.expr.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text(')')) - - -class ASTTypeId(ASTBase): - def __init__(self, typeOrExpr, isType): - self.typeOrExpr = typeOrExpr - self.isType = isType - - def _stringify(self, transform: StringifyTransform) -> str: - return 'typeid(' + transform(self.typeOrExpr) + ')' - - def get_id(self, version: int) -> str: - prefix = 'ti' if self.isType else 'te' - return prefix + self.typeOrExpr.get_id(version) + return 'cv' + self.typ.get_id(version) + self.expr.get_id(version) - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('typeid')) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('(')) - self.typeOrExpr.describe_signature(signode, mode, env, symbol) + self.typ.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text(')')) - - -class ASTPostfixCallExpr(ASTBase): - def __init__(self, lst: Union["ASTParenExprList", "ASTBracedInitList"]) -> None: - self.lst = lst - - def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.lst) - - def get_id(self, idPrefix: str, version: int) -> str: - res = ['cl', idPrefix] - for e in self.lst.exprs: - res.append(e.get_id(version)) - res.append('E') - return ''.join(res) - - def describe_signature(self, signode, mode, env, symbol): - self.lst.describe_signature(signode, mode, env, symbol) - - -class ASTPostfixArray(ASTBase): - def __init__(self, expr): - self.expr = expr - - def _stringify(self, transform: StringifyTransform) -> str: - return '[' + transform(self.expr) + ']' - - def get_id(self, idPrefix: str, version: int) -> str: - return 'ix' + idPrefix + self.expr.get_id(version) - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('[')) self.expr.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text(']')) - - -class ASTPostfixInc(ASTBase): - def _stringify(self, transform: StringifyTransform) -> str: - return '++' - - def get_id(self, idPrefix: str, version: int) -> str: - return 'pp' + idPrefix - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('++')) - -class ASTPostfixDec(ASTBase): - def _stringify(self, transform: StringifyTransform) -> str: - return '--' - - def get_id(self, idPrefix: str, version: int) -> str: - return 'mm' + idPrefix - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('--')) - - -class ASTPostfixMember(ASTBase): - def __init__(self, name): - self.name = name - - def _stringify(self, transform: StringifyTransform) -> str: - return '.' + transform(self.name) - - def get_id(self, idPrefix: str, version: int) -> str: - return 'dt' + idPrefix + self.name.get_id(version) - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('.')) - self.name.describe_signature(signode, 'noneIsName', env, symbol) - - -class ASTPostfixMemberOfPointer(ASTBase): - def __init__(self, name): - self.name = name - - def _stringify(self, transform: StringifyTransform) -> str: - return '->' + transform(self.name) - - def get_id(self, idPrefix: str, version: int) -> str: - return 'pt' + idPrefix + self.name.get_id(version) - - def describe_signature(self, signode, mode, env, symbol): - signode.append(nodes.Text('->')) - self.name.describe_signature(signode, 'noneIsName', env, symbol) - - -class ASTPostfixExpr(ASTBase): - def __init__(self, prefix, postFixes): - assert len(postFixes) > 0 - self.prefix = prefix - self.postFixes = postFixes +class ASTBinOpExpr(ASTExpression): + def __init__(self, exprs: List[ASTExpression], ops: List[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops def _stringify(self, transform: StringifyTransform) -> str: - res = [transform(self.prefix)] - for p in self.postFixes: - res.append(transform(p)) + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) return ''.join(res) def get_id(self, version: int) -> str: - id = self.prefix.get_id(version) - for p in self.postFixes: - id = p.get_id(id, version) - return id - - def describe_signature(self, signode, mode, env, symbol): - self.prefix.describe_signature(signode, mode, env, symbol) - for p in self.postFixes: - p.describe_signature(signode, mode, env, symbol) - - -class ASTPackExpansionExpr(ASTBase): - def __init__(self, expr): - self.expr = expr - - def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.expr) + '...' - - def get_id(self, version: int) -> str: - id = self.expr.get_id(version) - return 'sp' + id - - def describe_signature(self, signode, mode, env, symbol): - self.expr.describe_signature(signode, mode, env, symbol) - signode += nodes.Text('...') - - -class ASTFallbackExpr(ASTBase): - def __init__(self, expr): - self.expr = expr - - def _stringify(self, transform: StringifyTransform) -> str: - return self.expr - - def get_id(self, version: int) -> str: - return str(self.expr) - - def describe_signature(self, signode, mode, env, symbol): - signode += nodes.Text(self.expr) - - -################################################################################ -# The Rest -################################################################################ - -class ASTIdentifier(ASTBase): - def __init__(self, identifier: str) -> None: - assert identifier is not None - assert len(identifier) != 0 - self.identifier = identifier - - def is_anon(self) -> bool: - return self.identifier[0] == '@' - - def get_id(self, version: int) -> str: - if self.is_anon() and version < 3: - raise NoOldIdError() - if version == 1: - if self.identifier == 'size_t': - return 's' - else: - return self.identifier - if self.identifier == "std": - return 'St' - elif self.identifier[0] == "~": - # a destructor, just use an arbitrary version of dtors - return 'D0' - else: - if self.is_anon(): - return 'Ut%d_%s' % (len(self.identifier) - 1, self.identifier[1:]) - else: - return str(len(self.identifier)) + self.identifier - - # and this is where we finally make a difference between __str__ and the display string - - def __str__(self) -> str: - return self.identifier - - def get_display_string(self) -> str: - return "[anonymous]" if self.is_anon() else self.identifier - - def describe_signature(self, signode: Any, mode: str, env: "BuildEnvironment", - prefix: str, templateArgs: str, symbol: "Symbol") -> None: - _verify_description_mode(mode) - if mode == 'markType': - targetText = prefix + self.identifier + templateArgs - pnode = addnodes.pending_xref('', refdomain='cpp', - reftype='identifier', - reftarget=targetText, modname=None, - classname=None) - key = symbol.get_lookup_key() - pnode['cpp:parent_key'] = key - if self.is_anon(): - pnode += nodes.strong(text="[anonymous]") - else: - pnode += nodes.Text(self.identifier) - signode += pnode - elif mode == 'lastIsName': - if self.is_anon(): - signode += nodes.strong(text="[anonymous]") - else: - signode += addnodes.desc_name(self.identifier, self.identifier) - elif mode == 'noneIsName': - if self.is_anon(): - signode += nodes.strong(text="[anonymous]") - else: - signode += nodes.Text(self.identifier) - else: - raise Exception('Unknown description mode: %s' % mode) - - -class ASTTemplateKeyParamPackIdDefault(ASTBase): - def __init__(self, key: str, identifier: ASTIdentifier, - parameterPack: bool, default: "ASTType") -> None: - assert key - if parameterPack: - assert default is None - self.key = key - self.identifier = identifier - self.parameterPack = parameterPack - self.default = default - - def get_identifier(self) -> ASTIdentifier: - return self.identifier - - def get_id(self, version: int) -> str: assert version >= 2 - # this is not part of the normal name mangling in C++ res = [] - if self.parameterPack: - res.append('Dp') - else: - res.append('0') # we need to put something - return ''.join(res) - - def _stringify(self, transform: StringifyTransform) -> str: - res = [self.key] - if self.parameterPack: - if self.identifier: - res.append(' ') - res.append('...') - if self.identifier: - if not self.parameterPack: - res.append(' ') - res.append(transform(self.identifier)) - if self.default: - res.append(' = ') - res.append(transform(self.default)) + for i in range(len(self.ops)): + res.append(_id_operator_v2[self.ops[i]]) + res.append(self.exprs[i].get_id(version)) + res.append(self.exprs[-1].get_id(version)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - signode += nodes.Text(self.key) - if self.parameterPack: - if self.identifier: - signode += nodes.Text(' ') - signode += nodes.Text('...') - if self.identifier: - if not self.parameterPack: - signode += nodes.Text(' ') - self.identifier.describe_signature(signode, mode, env, '', '', symbol) - if self.default: - signode += nodes.Text(' = ') - self.default.describe_signature(signode, 'markType', env, symbol) - - -class ASTTemplateParamType(ASTBase): - def __init__(self, data: ASTTemplateKeyParamPackIdDefault) -> None: - assert data - self.data = data - - @property - def name(self) -> "ASTNestedName": - id = self.get_identifier() - return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) - - @property - def isPack(self) -> bool: - return self.data.parameterPack - - def get_identifier(self) -> ASTIdentifier: - return self.data.get_identifier() - - def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: - # this is not part of the normal name mangling in C++ - assert version >= 2 - if symbol: - # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=False) - else: - return self.data.get_id(version) - - def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.data) - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - self.data.describe_signature(signode, mode, env, symbol) - - -class ASTTemplateParamConstrainedTypeWithInit(ASTBase): - def __init__(self, type: Any, init: Any) -> None: - assert type - self.type = type - self.init = init - - @property - def name(self) -> "ASTNestedName": - return self.type.name - - @property - def isPack(self) -> bool: - return self.type.isPack - - def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: - # this is not part of the normal name mangling in C++ - assert version >= 2 - if symbol: - # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=False) - else: - return self.type.get_id(version) - - def _stringify(self, transform: StringifyTransform) -> str: - res = transform(self.type) - if self.init: - res += " = " - res += transform(self.init) - return res - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - self.type.describe_signature(signode, mode, env, symbol) - if self.init: - signode += nodes.Text(" = ") - self.init.describe_signature(signode, mode, env, symbol) - - -class ASTTemplateParamTemplateType(ASTBase): - def __init__(self, nestedParams: Any, data: ASTTemplateKeyParamPackIdDefault) -> None: - assert nestedParams - assert data - self.nestedParams = nestedParams - self.data = data - - @property - def name(self) -> "ASTNestedName": - id = self.get_identifier() - return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) - - @property - def isPack(self) -> bool: - return self.data.parameterPack - - def get_identifier(self) -> ASTIdentifier: - return self.data.get_identifier() - - def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: - assert version >= 2 - # this is not part of the normal name mangling in C++ - if symbol: - # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=None) - else: - return self.nestedParams.get_id(version) + self.data.get_id(version) - - def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.nestedParams) + transform(self.data) - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - self.nestedParams.describe_signature(signode, 'noneIsName', env, symbol) - signode += nodes.Text(' ') - self.data.describe_signature(signode, mode, env, symbol) - - -class ASTTemplateParamNonType(ASTBase): - def __init__(self, param: Any) -> None: - assert param - self.param = param - - @property - def name(self) -> "ASTNestedName": - id = self.get_identifier() - return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) - - @property - def isPack(self) -> bool: - return self.param.isPack - - def get_identifier(self) -> ASTIdentifier: - name = self.param.name - if name: - assert len(name.names) == 1 - assert name.names[0].identOrOp - assert not name.names[0].templateArgs - return name.names[0].identOrOp - else: - return None - - def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: - assert version >= 2 - # this is not part of the normal name mangling in C++ - if symbol: - # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=None) - else: - return '_' + self.param.get_id(version) - - def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.param) - - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - self.param.describe_signature(signode, mode, env, symbol) - + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode.append(nodes.Text(' ')) + signode.append(nodes.Text(self.ops[i - 1])) + signode.append(nodes.Text(' ')) + self.exprs[i].describe_signature(signode, mode, env, symbol) -class ASTTemplateParams(ASTBase): - def __init__(self, params: Any) -> None: - assert params is not None - self.params = params - self.isNested = False # whether it's a template template param - def get_id(self, version: int) -> str: - assert version >= 2 - res = [] - res.append("I") - for param in self.params: - res.append(param.get_id(version)) - res.append("E") - return ''.join(res) +class ASTAssignmentExpr(ASTExpression): + def __init__(self, exprs: List[ASTExpression], ops: List[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops def _stringify(self, transform: StringifyTransform) -> str: res = [] - res.append("template<") - res.append(", ".join(transform(a) for a in self.params)) - res.append("> ") + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) return ''.join(res) - def describe_signature(self, parentNode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol", lineSpec: bool = None - ) -> None: - # 'lineSpec' is defaulted becuase of template template parameters - def makeLine(parentNode=parentNode): - signode = addnodes.desc_signature_line() - parentNode += signode - signode.sphinx_cpp_tagname = 'templateParams' - return signode - if self.isNested: - lineNode = parentNode - else: - lineNode = makeLine() - lineNode += nodes.Text("template<") - first = True - for param in self.params: - if not first: - lineNode += nodes.Text(", ") - first = False - if lineSpec: - lineNode = makeLine() - param.describe_signature(lineNode, mode, env, symbol) - if lineSpec and not first: - lineNode = makeLine() - lineNode += nodes.Text(">") - - -class ASTTemplateIntroductionParameter(ASTBase): - def __init__(self, identifier: ASTIdentifier, parameterPack: bool) -> None: - self.identifier = identifier - self.parameterPack = parameterPack - - @property - def name(self) -> "ASTNestedName": - id = self.get_identifier() - return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) - - @property - def isPack(self) -> bool: - return self.parameterPack - - def get_identifier(self) -> ASTIdentifier: - return self.identifier - - def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: - assert version >= 2 - # this is not part of the normal name mangling in C++ - if symbol: - # the anchor will be our parent - return symbol.parent.declaration.get_id(version, prefixed=None) - else: - if self.parameterPack: - return 'Dp' - else: - return '0' # we need to put something - - def get_id_as_arg(self, version: int) -> str: - assert version >= 2 - # used for the implicit requires clause - res = self.identifier.get_id(version) - if self.parameterPack: - return 'sp' + res - else: - return res - - def _stringify(self, transform: StringifyTransform) -> str: + def get_id(self, version: int) -> str: res = [] - if self.parameterPack: - res.append('...') - res.append(transform(self.identifier)) + for i in range(len(self.ops)): + res.append(_id_operator_v2[self.ops[i]]) + res.append(self.exprs[i].get_id(version)) + res.append(self.exprs[-1].get_id(version)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - if self.parameterPack: - signode += nodes.Text('...') - self.identifier.describe_signature(signode, mode, env, '', '', symbol) - + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode.append(nodes.Text(' ')) + signode.append(nodes.Text(self.ops[i - 1])) + signode.append(nodes.Text(' ')) + self.exprs[i].describe_signature(signode, mode, env, symbol) -class ASTTemplateIntroduction(ASTBase): - def __init__(self, concept: Any, params: List[Any]) -> None: - assert len(params) > 0 - self.concept = concept - self.params = params - def get_id(self, version: int) -> str: - assert version >= 2 - # first do the same as a normal template parameter list - res = [] - res.append("I") - for param in self.params: - res.append(param.get_id(version)) - res.append("E") - # let's use X expr E, which is otherwise for constant template args - res.append("X") - res.append(self.concept.get_id(version)) - res.append("I") - for param in self.params: - res.append(param.get_id_as_arg(version)) - res.append("E") - res.append("E") - return ''.join(res) +class ASTFallbackExpr(ASTExpression): + def __init__(self, expr: str): + self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - res = [] - res.append(transform(self.concept)) - res.append('{') - res.append(', '.join(transform(param) for param in self.params)) - res.append('} ') - return ''.join(res) - - def describe_signature(self, parentNode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol", lineSpec: bool) -> None: - # Note: 'lineSpec' has no effect on template introductions. - signode = addnodes.desc_signature_line() - parentNode += signode - signode.sphinx_cpp_tagname = 'templateIntroduction' - self.concept.describe_signature(signode, 'markType', env, symbol) - signode += nodes.Text('{') - first = True - for param in self.params: - if not first: - signode += nodes.Text(', ') - first = False - param.describe_signature(signode, mode, env, symbol) - signode += nodes.Text('}') - - -class ASTTemplateDeclarationPrefix(ASTBase): - def __init__(self, templates: List[Any]) -> None: - # templates is None means it's an explicit instantiation of a variable - self.templates = templates + return self.expr def get_id(self, version: int) -> str: - assert version >= 2 - # this is not part of a normal name mangling system - res = [] - for t in self.templates: - res.append(t.get_id(version)) - return ''.join(res) - - def _stringify(self, transform: StringifyTransform) -> str: - res = [] - for t in self.templates: - res.append(transform(t)) - return ''.join(res) + return str(self.expr) - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol", lineSpec: bool) -> None: - _verify_description_mode(mode) - for t in self.templates: - t.describe_signature(signode, 'lastIsName', env, symbol, lineSpec) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode += nodes.Text(self.expr) -############################################################################################## +################################################################################ +# Types +################################################################################ +# Things for ASTNestedName +################################################################################ class ASTOperator(ASTBase): - def is_anon(self): + def is_anon(self) -> bool: return False def is_operator(self) -> bool: @@ -1828,10 +1566,10 @@ class ASTOperator(ASTBase): def get_id(self, version: int) -> str: raise NotImplementedError() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", prefix: str, templateArgs: str, symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) identifier = str(self) if mode == 'lastIsName': signode += addnodes.desc_name(identifier, identifier) @@ -1860,42 +1598,39 @@ class ASTOperatorBuildIn(ASTOperator): return 'operator' + self.op -class ASTOperatorType(ASTOperator): - def __init__(self, type: Any) -> None: - self.type = type +class ASTOperatorLiteral(ASTOperator): + def __init__(self, identifier: ASTIdentifier) -> None: + self.identifier = identifier def get_id(self, version: int) -> str: if version == 1: - return 'castto-%s-operator' % self.type.get_id(version) + raise NoOldIdError() else: - return 'cv' + self.type.get_id(version) + return 'li' + self.identifier.get_id(version) def _stringify(self, transform: StringifyTransform) -> str: - return ''.join(['operator ', transform(self.type)]) - - def get_name_no_template(self) -> str: - return str(self) + return 'operator""' + transform(self.identifier) -class ASTOperatorLiteral(ASTOperator): - def __init__(self, identifier: Any) -> None: - self.identifier = identifier +class ASTOperatorType(ASTOperator): + def __init__(self, type: "ASTType") -> None: + self.type = type def get_id(self, version: int) -> str: if version == 1: - raise NoOldIdError() + return 'castto-%s-operator' % self.type.get_id(version) else: - return 'li' + self.identifier.get_id(version) + return 'cv' + self.type.get_id(version) def _stringify(self, transform: StringifyTransform) -> str: - return 'operator""' + transform(self.identifier) - + return ''.join(['operator ', transform(self.type)]) -############################################################################################## + def get_name_no_template(self) -> str: + return str(self) class ASTTemplateArgConstant(ASTBase): - def __init__(self, value: Any) -> None: + def __init__(self, value: ASTExpression) -> None: self.value = value def _stringify(self, transform: StringifyTransform) -> str: @@ -1908,14 +1643,14 @@ class ASTTemplateArgConstant(ASTBase): return 'X' + str(self) + 'E' return 'X' + self.value.get_id(version) + 'E' - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.value.describe_signature(signode, mode, env, symbol) class ASTTemplateArgs(ASTBase): - def __init__(self, args: List[Any]) -> None: + def __init__(self, args: List[Union["ASTType", ASTTemplateArgConstant]]) -> None: assert args is not None self.args = args @@ -1938,9 +1673,9 @@ class ASTTemplateArgs(ASTBase): res = ', '.join(transform(a) for a in self.args) return '<' + res + '>' - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode += nodes.Text('<') first = True for a in self.args: @@ -1951,148 +1686,19 @@ class ASTTemplateArgs(ASTBase): signode += nodes.Text('>') -class ASTNestedNameElement(ASTBase): - def __init__(self, identOrOp: Union["ASTIdentifier", "ASTOperator"], - templateArgs: ASTTemplateArgs) -> None: - self.identOrOp = identOrOp - self.templateArgs = templateArgs - - def is_operator(self) -> bool: - return False +# Main part of declarations +################################################################################ +class ASTTrailingTypeSpec(ASTBase): def get_id(self, version: int) -> str: - res = self.identOrOp.get_id(version) - if self.templateArgs: - res += self.templateArgs.get_id(version) - return res - - def _stringify(self, transform: StringifyTransform) -> str: - res = transform(self.identOrOp) - if self.templateArgs: - res += transform(self.templateArgs) - return res - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", prefix: str, symbol: "Symbol") -> None: - tArgs = str(self.templateArgs) if self.templateArgs is not None else '' - self.identOrOp.describe_signature(signode, mode, env, prefix, tArgs, symbol) - if self.templateArgs is not None: - self.templateArgs.describe_signature(signode, mode, env, symbol) - - -class ASTNestedName(ASTBase): - def __init__(self, names: List[ASTNestedNameElement], - templates: List[bool], rooted: bool) -> None: - assert len(names) > 0 - self.names = names - self.templates = templates - assert len(self.names) == len(self.templates) - self.rooted = rooted - - @property - def name(self) -> "ASTNestedName": - return self - - def num_templates(self) -> int: - count = 0 - for n in self.names: - if n.is_operator(): - continue - if n.templateArgs: - count += 1 - return count - - def get_id(self, version: int, modifiers: str = '') -> str: - if version == 1: - tt = str(self) - if tt in _id_shorthands_v1: - return _id_shorthands_v1[tt] - else: - return '::'.join(n.get_id(version) for n in self.names) - - res = [] - if len(self.names) > 1 or len(modifiers) > 0: - res.append('N') - res.append(modifiers) - for n in self.names: - res.append(n.get_id(version)) - if len(self.names) > 1 or len(modifiers) > 0: - res.append('E') - return ''.join(res) - - def _stringify(self, transform: StringifyTransform) -> str: - res = [] - if self.rooted: - res.append('') - for i in range(len(self.names)): - n = self.names[i] - t = self.templates[i] - if t: - res.append("template " + transform(n)) - else: - res.append(transform(n)) - return '::'.join(res) + raise NotImplementedError(repr(self)) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) - # just print the name part, with template args, not template params - if mode == 'noneIsName': - signode += nodes.Text(str(self)) - elif mode == 'param': - name = str(self) - signode += nodes.emphasis(name, name) - elif mode == 'markType' or mode == 'lastIsName' or mode == 'markName': - # Each element should be a pending xref targeting the complete - # prefix. however, only the identifier part should be a link, such - # that template args can be a link as well. - # For 'lastIsName' we should also prepend template parameter lists. - templateParams = [] # type: List[Any] - if mode == 'lastIsName': - assert symbol is not None - if symbol.declaration.templatePrefix is not None: - templateParams = symbol.declaration.templatePrefix.templates - iTemplateParams = 0 - templateParamsPrefix = '' - prefix = '' - first = True - names = self.names[:-1] if mode == 'lastIsName' else self.names - # If lastIsName, then wrap all of the prefix in a desc_addname, - # else append directly to signode. - # NOTE: Breathe relies on the prefix being in the desc_addname node, - # so it can remove it in inner declarations. - dest = signode - if mode == 'lastIsName': - dest = addnodes.desc_addname() # type: ignore - for i in range(len(names)): - nne = names[i] - template = self.templates[i] - if not first: - dest += nodes.Text('::') - prefix += '::' - if template: - dest += nodes.Text("template ") - first = False - txt_nne = str(nne) - if txt_nne != '': - if nne.templateArgs and iTemplateParams < len(templateParams): - templateParamsPrefix += str(templateParams[iTemplateParams]) - iTemplateParams += 1 - nne.describe_signature(dest, 'markType', - env, templateParamsPrefix + prefix, symbol) - prefix += txt_nne - if mode == 'lastIsName': - if len(self.names) > 1: - dest += addnodes.desc_addname('::', '::') - signode += dest - if self.templates[-1]: - signode += nodes.Text("template ") - self.names[-1].describe_signature(signode, mode, env, '', symbol) - else: - raise Exception('Unknown description mode: %s' % mode) + raise NotImplementedError(repr(self)) -class ASTTrailingTypeSpecFundamental(ASTBase): +class ASTTrailingTypeSpecFundamental(ASTTrailingTypeSpec): def __init__(self, name: str) -> None: self.name = name @@ -2116,40 +1722,12 @@ class ASTTrailingTypeSpecFundamental(ASTBase): 'parser should have rejected it.' % self.name) return _id_fundamental_v2[self.name] - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode += nodes.Text(str(self.name)) -class ASTTrailingTypeSpecName(ASTBase): - def __init__(self, prefix: str, nestedName: Any) -> None: - self.prefix = prefix - self.nestedName = nestedName - - @property - def name(self) -> ASTNestedName: - return self.nestedName - - def get_id(self, version: int) -> str: - return self.nestedName.get_id(version) - - def _stringify(self, transform: StringifyTransform) -> str: - res = [] - if self.prefix: - res.append(self.prefix) - res.append(' ') - res.append(transform(self.nestedName)) - return ''.join(res) - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - if self.prefix: - signode += addnodes.desc_annotation(self.prefix, self.prefix) - signode += nodes.Text(' ') - self.nestedName.describe_signature(signode, mode, env, symbol=symbol) - - -class ASTTrailingTypeSpecDecltypeAuto(ASTBase): +class ASTTrailingTypeSpecDecltypeAuto(ASTTrailingTypeSpec): def _stringify(self, transform: StringifyTransform) -> str: return 'decltype(auto)' @@ -2158,13 +1736,13 @@ class ASTTrailingTypeSpecDecltypeAuto(ASTBase): raise NoOldIdError() return 'Dc' - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text(str(self))) -class ASTTrailingTypeSpecDecltype(ASTBase): - def __init__(self, expr): +class ASTTrailingTypeSpecDecltype(ASTTrailingTypeSpec): + def __init__(self, expr: ASTExpression): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: @@ -2175,15 +1753,45 @@ class ASTTrailingTypeSpecDecltype(ASTBase): raise NoOldIdError() return 'DT' + self.expr.get_id(version) + "E" - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text('decltype(')) self.expr.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text(')')) +class ASTTrailingTypeSpecName(ASTTrailingTypeSpec): + def __init__(self, prefix: str, nestedName: ASTNestedName) -> None: + self.prefix = prefix + self.nestedName = nestedName + + @property + def name(self) -> ASTNestedName: + return self.nestedName + + def get_id(self, version: int) -> str: + return self.nestedName.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.prefix: + res.append(self.prefix) + res.append(' ') + res.append(transform(self.nestedName)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + if self.prefix: + signode += addnodes.desc_annotation(self.prefix, self.prefix) + signode += nodes.Text(' ') + self.nestedName.describe_signature(signode, mode, env, symbol=symbol) + + class ASTFunctionParameter(ASTBase): - def __init__(self, arg: Any, ellipsis: bool = False) -> None: + def __init__(self, arg: Union["ASTTypeWithInit", + "ASTTemplateParamConstrainedTypeWithInit"], + ellipsis: bool = False) -> None: self.arg = arg self.ellipsis = ellipsis @@ -2204,9 +1812,9 @@ class ASTFunctionParameter(ASTBase): else: return transform(self.arg) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) if self.ellipsis: signode += nodes.Text('...') else: @@ -2214,7 +1822,8 @@ class ASTFunctionParameter(ASTBase): class ASTParametersQualifiers(ASTBase): - def __init__(self, args: List[Any], volatile: bool, const: bool, refQual: str, + def __init__(self, args: List[ASTFunctionParameter], + volatile: bool, const: bool, refQual: str, exceptionSpec: str, override: bool, final: bool, initializer: str) -> None: self.args = args self.volatile = volatile @@ -2226,7 +1835,7 @@ class ASTParametersQualifiers(ASTBase): self.initializer = initializer @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.args def get_modifiers_id(self, version: int) -> str: @@ -2284,9 +1893,9 @@ class ASTParametersQualifiers(ASTBase): res.append(self.initializer) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) paramlist = addnodes.desc_parameterlist() for arg in self.args: param = addnodes.desc_parameter('', '', noemph=True) @@ -2297,11 +1906,11 @@ class ASTParametersQualifiers(ASTBase): paramlist += param signode += paramlist - def _add_anno(signode, text): + def _add_anno(signode: TextElement, text: str) -> None: signode += nodes.Text(' ') signode += addnodes.desc_annotation(text, text) - def _add_text(signode, text): + def _add_text(signode: TextElement, text: str) -> None: signode += nodes.Text(' ' + text) if self.volatile: @@ -2323,7 +1932,7 @@ class ASTParametersQualifiers(ASTBase): class ASTDeclSpecsSimple(ASTBase): def __init__(self, storage: str, threadLocal: bool, inline: bool, virtual: bool, explicit: bool, constexpr: bool, volatile: bool, const: bool, - friend: bool, attrs: List[Any]) -> None: + friend: bool, attrs: List[ASTAttribute]) -> None: self.storage = storage self.threadLocal = threadLocal self.inline = inline @@ -2372,38 +1981,44 @@ class ASTDeclSpecsSimple(ASTBase): res.append('const') return ' '.join(res) - def describe_signature(self, modifiers: List[Node]) -> None: - def _add(modifiers, text): - if len(modifiers) > 0: - modifiers.append(nodes.Text(' ')) - modifiers.append(addnodes.desc_annotation(text, text)) + def describe_signature(self, signode: TextElement) -> None: + addSpace = False for attr in self.attrs: - if len(modifiers) > 0: - modifiers.append(nodes.Text(' ')) - modifiers.append(attr.describe_signature(modifiers)) + if addSpace: + signode += nodes.Text(' ') + addSpace = True + attr.describe_signature(signode) + + def _add(signode: TextElement, text: str) -> bool: + if addSpace: + signode += nodes.Text(' ') + signode += addnodes.desc_annotation(text, text) + return True + if self.storage: - _add(modifiers, self.storage) + addSpace = _add(signode, self.storage) if self.threadLocal: - _add(modifiers, 'thread_local') + addSpace = _add(signode, 'thread_local') if self.inline: - _add(modifiers, 'inline') + addSpace = _add(signode, 'inline') if self.friend: - _add(modifiers, 'friend') + addSpace = _add(signode, 'friend') if self.virtual: - _add(modifiers, 'virtual') + addSpace = _add(signode, 'virtual') if self.explicit: - _add(modifiers, 'explicit') + addSpace = _add(signode, 'explicit') if self.constexpr: - _add(modifiers, 'constexpr') + addSpace = _add(signode, 'constexpr') if self.volatile: - _add(modifiers, 'volatile') + addSpace = _add(signode, 'volatile') if self.const: - _add(modifiers, 'const') + addSpace = _add(signode, 'const') class ASTDeclSpecs(ASTBase): - def __init__(self, outer: Any, leftSpecs: ASTDeclSpecsSimple, - rightSpecs: ASTDeclSpecsSimple, trailing: Any) -> None: + def __init__(self, outer: str, + leftSpecs: ASTDeclSpecsSimple, rightSpecs: ASTDeclSpecsSimple, + trailing: ASTTrailingTypeSpec) -> None: # leftSpecs and rightSpecs are used for output # allSpecs are used for id generation self.outer = outer @@ -2412,10 +2027,6 @@ class ASTDeclSpecs(ASTBase): self.allSpecs = self.leftSpecs.mergeWith(self.rightSpecs) self.trailingTypeSpec = trailing - @property - def name(self) -> ASTNestedName: - return self.trailingTypeSpec.name - def get_id(self, version: int) -> str: if version == 1: res = [] @@ -2450,35 +2061,29 @@ class ASTDeclSpecs(ASTBase): res.append(r) return "".join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) - modifiers = [] # type: List[nodes.Node] - - def _add(modifiers, text): - if len(modifiers) > 0: - modifiers.append(nodes.Text(' ')) - modifiers.append(addnodes.desc_annotation(text, text)) + verify_description_mode(mode) + numChildren = len(signode) + self.leftSpecs.describe_signature(signode) + addSpace = len(signode) != numChildren - self.leftSpecs.describe_signature(modifiers) - - for m in modifiers: - signode += m if self.trailingTypeSpec: - if len(modifiers) > 0: + if addSpace: signode += nodes.Text(' ') self.trailingTypeSpec.describe_signature(signode, mode, env, symbol=symbol) - modifiers = [] - self.rightSpecs.describe_signature(modifiers) - if len(modifiers) > 0: + numChildren = len(signode) + self.rightSpecs.describe_signature(signode) + if len(signode) != numChildren: signode += nodes.Text(' ') - for m in modifiers: - signode += m +# Declarator +################################################################################ + class ASTArray(ASTBase): - def __init__(self, size): + def __init__(self, size: ASTExpression): self.size = size def _stringify(self, transform: StringifyTransform) -> str: @@ -2500,16 +2105,174 @@ class ASTArray(ASTBase): else: return 'A_' - def describe_signature(self, signode, mode, env, symbol): - _verify_description_mode(mode) + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) signode.append(nodes.Text("[")) if self.size: self.size.describe_signature(signode, mode, env, symbol) signode.append(nodes.Text("]")) -class ASTDeclaratorPtr(ASTBase): - def __init__(self, next: Any, volatile: bool, const: bool, attrs: Any) -> None: +class ASTDeclarator(ASTBase): + @property + def name(self) -> ASTNestedName: + raise NotImplementedError(repr(self)) + + @property + def isPack(self) -> bool: + raise NotImplementedError(repr(self)) + + @property + def function_params(self) -> List[ASTFunctionParameter]: + raise NotImplementedError(repr(self)) + + def require_space_after_declSpecs(self) -> bool: + raise NotImplementedError(repr(self)) + + def get_modifiers_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_param_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_ptr_suffix_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + raise NotImplementedError(repr(self)) + + def is_function_type(self) -> bool: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + raise NotImplementedError(repr(self)) + + +class ASTDeclaratorNameParamQual(ASTDeclarator): + def __init__(self, declId: ASTNestedName, + arrayOps: List[ASTArray], + paramQual: ASTParametersQualifiers) -> None: + self.declId = declId + self.arrayOps = arrayOps + self.paramQual = paramQual + + @property + def name(self) -> ASTNestedName: + return self.declId + + @property + def isPack(self) -> bool: + return False + + @property + def function_params(self) -> List[ASTFunctionParameter]: + return self.paramQual.function_params + + # only the modifiers for a function, e.g., + def get_modifiers_id(self, version: int) -> str: + # cv-qualifiers + if self.paramQual: + return self.paramQual.get_modifiers_id(version) + raise Exception("This should only be called on a function: %s" % self) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + if self.paramQual: + return self.paramQual.get_param_id(version) + else: + return '' + + def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers + return ''.join(a.get_id(version) for a in self.arrayOps) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + res = [] + # TOOD: can we actually have both array ops and paramQual? + res.append(self.get_ptr_suffix_id(version)) + if self.paramQual: + res.append(self.get_modifiers_id(version)) + res.append('F') + res.append(returnTypeId) + res.append(self.get_param_id(version)) + res.append('E') + else: + res.append(returnTypeId) + return ''.join(res) + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def is_function_type(self) -> bool: + return self.paramQual is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + for op in self.arrayOps: + res.append(transform(op)) + if self.paramQual: + res.append(transform(self.paramQual)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + for op in self.arrayOps: + op.describe_signature(signode, mode, env, symbol) + if self.paramQual: + self.paramQual.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorNameBitField(ASTDeclarator): + def __init__(self, declId: ASTNestedName, size: ASTExpression): + self.declId = declId + self.size = size + + @property + def name(self) -> ASTNestedName: + return self.declId + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + return '' + + def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers + return '' + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def is_function_type(self) -> bool: + return False + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + res.append(" : ") + res.append(transform(self.size)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + signode.append(nodes.Text(' : ', ' : ')) + self.size.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorPtr(ASTDeclarator): + def __init__(self, next: ASTDeclarator, volatile: bool, const: bool, + attrs: List[ASTAttribute]) -> None: assert next self.next = next self.volatile = volatile @@ -2521,12 +2284,11 @@ class ASTDeclaratorPtr(ASTBase): return self.next.name @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params def require_space_after_declSpecs(self) -> bool: - # TODO: if has paramPack, then False ? - return True + return self.next.require_space_after_declSpecs() def _stringify(self, transform: StringifyTransform) -> str: res = ['*'] @@ -2541,7 +2303,7 @@ class ASTDeclaratorPtr(ASTBase): res.append(' ') res.append('const') if self.const or self.volatile or len(self.attrs) > 0: - if self.next.require_space_after_declSpecs: + if self.next.require_space_after_declSpecs(): res.append(' ') res.append(transform(self.next)) return ''.join(res) @@ -2583,16 +2345,16 @@ class ASTDeclaratorPtr(ASTBase): def is_function_type(self) -> bool: return self.next.is_function_type() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode += nodes.Text("*") for a in self.attrs: a.describe_signature(signode) if len(self.attrs) > 0 and (self.volatile or self.const): signode += nodes.Text(' ') - def _add_anno(signode, text): + def _add_anno(signode: TextElement, text: str) -> None: signode += addnodes.desc_annotation(text, text) if self.volatile: _add_anno(signode, 'volatile') @@ -2601,13 +2363,13 @@ class ASTDeclaratorPtr(ASTBase): signode += nodes.Text(' ') _add_anno(signode, 'const') if self.const or self.volatile or len(self.attrs) > 0: - if self.next.require_space_after_declSpecs: + if self.next.require_space_after_declSpecs(): signode += nodes.Text(' ') self.next.describe_signature(signode, mode, env, symbol) -class ASTDeclaratorRef(ASTBase): - def __init__(self, next: Any, attrs: Any) -> None: +class ASTDeclaratorRef(ASTDeclarator): + def __init__(self, next: ASTDeclarator, attrs: List[ASTAttribute]) -> None: assert next self.next = next self.attrs = attrs @@ -2621,7 +2383,7 @@ class ASTDeclaratorRef(ASTBase): return True @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params def require_space_after_declSpecs(self) -> bool: @@ -2631,7 +2393,7 @@ class ASTDeclaratorRef(ASTBase): res = ['&'] for a in self.attrs: res.append(transform(a)) - if len(self.attrs) > 0 and self.next.require_space_after_declSpecs: + if len(self.attrs) > 0 and self.next.require_space_after_declSpecs(): res.append(' ') res.append(transform(self.next)) return ''.join(res) @@ -2656,19 +2418,19 @@ class ASTDeclaratorRef(ASTBase): def is_function_type(self) -> bool: return self.next.is_function_type() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode += nodes.Text("&") for a in self.attrs: a.describe_signature(signode) - if len(self.attrs) > 0 and self.next.require_space_after_declSpecs: + if len(self.attrs) > 0 and self.next.require_space_after_declSpecs(): signode += nodes.Text(' ') self.next.describe_signature(signode, mode, env, symbol) -class ASTDeclaratorParamPack(ASTBase): - def __init__(self, next: Any) -> None: +class ASTDeclaratorParamPack(ASTDeclarator): + def __init__(self, next: ASTDeclarator) -> None: assert next self.next = next @@ -2677,7 +2439,7 @@ class ASTDeclaratorParamPack(ASTBase): return self.next.name @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params def require_space_after_declSpecs(self) -> bool: @@ -2709,17 +2471,18 @@ class ASTDeclaratorParamPack(ASTBase): def is_function_type(self) -> bool: return self.next.is_function_type() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode += nodes.Text("...") if self.next.name: signode += nodes.Text(' ') self.next.describe_signature(signode, mode, env, symbol) -class ASTDeclaratorMemPtr(ASTBase): - def __init__(self, className: Any, const: bool, volatile: bool, next: Any) -> None: +class ASTDeclaratorMemPtr(ASTDeclarator): + def __init__(self, className: ASTNestedName, + const: bool, volatile: bool, next: ASTDeclarator) -> None: assert className assert next self.className = className @@ -2732,7 +2495,7 @@ class ASTDeclaratorMemPtr(ASTBase): return self.next.name @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params def require_space_after_declSpecs(self) -> bool: @@ -2743,9 +2506,11 @@ class ASTDeclaratorMemPtr(ASTBase): res.append(transform(self.className)) res.append('::*') if self.volatile: - res.append(' volatile') + res.append('volatile') if self.const: - res.append(' const') + if self.volatile: + res.append(' ') + res.append('const') if self.next.require_space_after_declSpecs(): res.append(' ') res.append(transform(self.next)) @@ -2786,13 +2551,13 @@ class ASTDeclaratorMemPtr(ASTBase): def is_function_type(self) -> bool: return self.next.is_function_type() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.className.describe_signature(signode, mode, env, symbol) signode += nodes.Text('::*') - def _add_anno(signode, text): + def _add_anno(signode: TextElement, text: str) -> None: signode += addnodes.desc_annotation(text, text) if self.volatile: _add_anno(signode, 'volatile') @@ -2801,13 +2566,12 @@ class ASTDeclaratorMemPtr(ASTBase): signode += nodes.Text(' ') _add_anno(signode, 'const') if self.next.require_space_after_declSpecs(): - if self.volatile or self.const: - signode += nodes.Text(' ') + signode += nodes.Text(' ') self.next.describe_signature(signode, mode, env, symbol) -class ASTDeclaratorParen(ASTBase): - def __init__(self, inner: Any, next: Any) -> None: +class ASTDeclaratorParen(ASTDeclarator): + def __init__(self, inner: ASTDeclarator, next: ASTDeclarator) -> None: assert inner assert next self.inner = inner @@ -2819,7 +2583,7 @@ class ASTDeclaratorParen(ASTBase): return self.inner.name @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.inner.function_params def require_space_after_declSpecs(self) -> bool: @@ -2856,135 +2620,37 @@ class ASTDeclaratorParen(ASTBase): def is_function_type(self) -> bool: return self.inner.is_function_type() - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode += nodes.Text('(') self.inner.describe_signature(signode, mode, env, symbol) signode += nodes.Text(')') self.next.describe_signature(signode, "noneIsName", env, symbol) -class ASTDeclaratorNameParamQual(ASTBase): - def __init__(self, declId: Any, arrayOps: List[Any], paramQual: Any) -> None: - self.declId = declId - self.arrayOps = arrayOps - self.paramQual = paramQual - - @property - def name(self) -> ASTNestedName: - return self.declId - - @property - def isPack(self) -> bool: - return False - - @property - def function_params(self) -> Any: - return self.paramQual.function_params - - # only the modifiers for a function, e.g., - def get_modifiers_id(self, version: int) -> str: - # cv-qualifiers - if self.paramQual: - return self.paramQual.get_modifiers_id(version) - raise Exception("This should only be called on a function: %s" % self) - - def get_param_id(self, version: int) -> str: # only the parameters (if any) - if self.paramQual: - return self.paramQual.get_param_id(version) - else: - return '' - - def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers - return ''.join(a.get_id(version) for a in self.arrayOps) - - def get_type_id(self, version: int, returnTypeId: str) -> str: - assert version >= 2 - res = [] - # TOOD: can we actually have both array ops and paramQual? - res.append(self.get_ptr_suffix_id(version)) - if self.paramQual: - res.append(self.get_modifiers_id(version)) - res.append('F') - res.append(returnTypeId) - res.append(self.get_param_id(version)) - res.append('E') - else: - res.append(returnTypeId) - return ''.join(res) - - # ------------------------------------------------------------------------ - - def require_space_after_declSpecs(self) -> bool: - return self.declId is not None +# Type and initializer stuff +############################################################################################## - def is_function_type(self) -> bool: - return self.paramQual is not None +class ASTPackExpansionExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - res = [] - if self.declId: - res.append(transform(self.declId)) - for op in self.arrayOps: - res.append(transform(op)) - if self.paramQual: - res.append(transform(self.paramQual)) - return ''.join(res) - - def describe_signature(self, signode: desc_signature, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) - if self.declId: - self.declId.describe_signature(signode, mode, env, symbol) - for op in self.arrayOps: - op.describe_signature(signode, mode, env, symbol) - if self.paramQual: - self.paramQual.describe_signature(signode, mode, env, symbol) - - -class ASTDeclaratorNameBitField(ASTBase): - def __init__(self, declId, size): - self.declId = declId - self.size = size - - @property - def name(self) -> ASTNestedName: - return self.declId - - def get_param_id(self, version: int) -> str: # only the parameters (if any) - return '' - - def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers - return '' - - # ------------------------------------------------------------------------ - - def require_space_after_declSpecs(self) -> bool: - return self.declId is not None - - def is_function_type(self) -> bool: - return False + return transform(self.expr) + '...' - def _stringify(self, transform: StringifyTransform) -> str: - res = [] - if self.declId: - res.append(transform(self.declId)) - res.append(" : ") - res.append(transform(self.size)) - return ''.join(res) + def get_id(self, version: int) -> str: + id = self.expr.get_id(version) + return 'sp' + id - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) - if self.declId: - self.declId.describe_signature(signode, mode, env, symbol) - signode.append(nodes.Text(' : ', ' : ')) - self.size.describe_signature(signode, mode, env, symbol) + self.expr.describe_signature(signode, mode, env, symbol) + signode += nodes.Text('...') class ASTParenExprList(ASTBase): - def __init__(self, exprs: List[Any]) -> None: + def __init__(self, exprs: List[ASTExpression]) -> None: self.exprs = exprs def get_id(self, version: int) -> str: @@ -2994,9 +2660,9 @@ class ASTParenExprList(ASTBase): exprs = [transform(e) for e in self.exprs] return '(%s)' % ', '.join(exprs) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode.append(nodes.Text('(')) first = True for e in self.exprs: @@ -3009,7 +2675,7 @@ class ASTParenExprList(ASTBase): class ASTBracedInitList(ASTBase): - def __init__(self, exprs: List[Any], trailingComma: bool) -> None: + def __init__(self, exprs: List[ASTExpression], trailingComma: bool) -> None: self.exprs = exprs self.trailingComma = trailingComma @@ -3021,9 +2687,9 @@ class ASTBracedInitList(ASTBase): trailingComma = ',' if self.trailingComma else '' return '{%s%s}' % (', '.join(exprs), trailingComma) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) signode.append(nodes.Text('{')) first = True for e in self.exprs: @@ -3038,7 +2704,8 @@ class ASTBracedInitList(ASTBase): class ASTInitializer(ASTBase): - def __init__(self, value: Any, hasAssign: bool = True) -> None: + def __init__(self, value: Union[ASTExpression, ASTBracedInitList], + hasAssign: bool = True) -> None: self.value = value self.hasAssign = hasAssign @@ -3049,16 +2716,16 @@ class ASTInitializer(ASTBase): else: return val - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) if self.hasAssign: signode.append(nodes.Text(' = ')) self.value.describe_signature(signode, 'markType', env, symbol) class ASTType(ASTBase): - def __init__(self, declSpecs: Any, decl: Any) -> None: + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: assert declSpecs assert decl self.declSpecs = declSpecs @@ -3073,7 +2740,7 @@ class ASTType(ASTBase): return self.decl.isPack @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: return self.decl.function_params def get_id(self, version: int, objectType: str = None, @@ -3144,9 +2811,9 @@ class ASTType(ASTBase): else: return 'type' - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.declSpecs.describe_signature(signode, 'markType', env, symbol) if (self.decl.require_space_after_declSpecs() and len(str(self.declSpecs)) > 0): @@ -3158,8 +2825,46 @@ class ASTType(ASTBase): self.decl.describe_signature(signode, mode, env, symbol) +class ASTTemplateParamConstrainedTypeWithInit(ASTBase): + def __init__(self, type: ASTType, init: ASTType) -> None: + assert type + self.type = type + self.init = init + + @property + def name(self) -> ASTNestedName: + return self.type.name + + @property + def isPack(self) -> bool: + return self.type.isPack + + def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: + # this is not part of the normal name mangling in C++ + assert version >= 2 + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + else: + return self.type.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.type) + if self.init: + res += " = " + res += transform(self.init) + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.type.describe_signature(signode, mode, env, symbol) + if self.init: + signode += nodes.Text(" = ") + self.init.describe_signature(signode, mode, env, symbol) + + class ASTTypeWithInit(ASTBase): - def __init__(self, type: Any, init: Any) -> None: + def __init__(self, type: ASTType, init: ASTInitializer) -> None: self.type = type self.init = init @@ -3187,16 +2892,16 @@ class ASTTypeWithInit(ASTBase): res.append(transform(self.init)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.type.describe_signature(signode, mode, env, symbol) if self.init: self.init.describe_signature(signode, mode, env, symbol) class ASTTypeUsing(ASTBase): - def __init__(self, name: Any, type: Any) -> None: + def __init__(self, name: ASTNestedName, type: ASTType) -> None: self.name = name self.type = type @@ -3217,17 +2922,20 @@ class ASTTypeUsing(ASTBase): def get_type_declaration_prefix(self) -> str: return 'using' - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.name.describe_signature(signode, mode, env, symbol=symbol) if self.type: signode += nodes.Text(' = ') self.type.describe_signature(signode, 'markType', env, symbol=symbol) +# Other declarations +############################################################################################## + class ASTConcept(ASTBase): - def __init__(self, nestedName: Any, initializer: Any) -> None: + def __init__(self, nestedName: ASTNestedName, initializer: ASTInitializer) -> None: self.nestedName = nestedName self.initializer = initializer @@ -3247,7 +2955,7 @@ class ASTConcept(ASTBase): res += transform(self.initializer) return res - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: self.nestedName.describe_signature(signode, mode, env, symbol) if self.initializer: @@ -3275,9 +2983,9 @@ class ASTBaseClass(ASTBase): res.append('...') return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) if self.visibility is not None: signode += addnodes.desc_annotation(self.visibility, self.visibility) @@ -3314,9 +3022,9 @@ class ASTClass(ASTBase): res.append(transform(b)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.name.describe_signature(signode, mode, env, symbol=symbol) if self.final: signode += nodes.Text(' ') @@ -3330,7 +3038,7 @@ class ASTClass(ASTBase): class ASTUnion(ASTBase): - def __init__(self, name: Any) -> None: + def __init__(self, name: ASTNestedName) -> None: self.name = name def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str: @@ -3341,14 +3049,15 @@ class ASTUnion(ASTBase): def _stringify(self, transform: StringifyTransform) -> str: return transform(self.name) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.name.describe_signature(signode, mode, env, symbol=symbol) class ASTEnum(ASTBase): - def __init__(self, name: Any, scoped: str, underlyingType: Any) -> None: + def __init__(self, name: ASTNestedName, scoped: str, + underlyingType: ASTType) -> None: self.name = name self.scoped = scoped self.underlyingType = underlyingType @@ -3369,9 +3078,9 @@ class ASTEnum(ASTBase): res.append(transform(self.underlyingType)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) # self.scoped has been done by the CPPEnumObject self.name.describe_signature(signode, mode, env, symbol=symbol) if self.underlyingType: @@ -3381,7 +3090,7 @@ class ASTEnum(ASTBase): class ASTEnumerator(ASTBase): - def __init__(self, name: Any, init: Any) -> None: + def __init__(self, name: ASTNestedName, init: ASTInitializer) -> None: self.name = name self.init = init @@ -3397,14 +3106,398 @@ class ASTEnumerator(ASTBase): res.append(transform(self.init)) return ''.join(res) - def describe_signature(self, signode: desc_signature, mode: str, + def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - _verify_description_mode(mode) + verify_description_mode(mode) self.name.describe_signature(signode, mode, env, symbol) if self.init: self.init.describe_signature(signode, 'markType', env, symbol) +################################################################################ +# Templates +################################################################################ + +# Parameters +################################################################################ + +class ASTTemplateParam(ASTBase): + def get_identifier(self) -> ASTIdentifier: + raise NotImplementedError(repr(self)) + + def get_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, parentNode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + raise NotImplementedError(repr(self)) + + +class ASTTemplateKeyParamPackIdDefault(ASTTemplateParam): + def __init__(self, key: str, identifier: ASTIdentifier, + parameterPack: bool, default: ASTType) -> None: + assert key + if parameterPack: + assert default is None + self.key = key + self.identifier = identifier + self.parameterPack = parameterPack + self.default = default + + def get_identifier(self) -> ASTIdentifier: + return self.identifier + + def get_id(self, version: int) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + res = [] + if self.parameterPack: + res.append('Dp') + else: + res.append('0') # we need to put something + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [self.key] + if self.parameterPack: + if self.identifier: + res.append(' ') + res.append('...') + if self.identifier: + if not self.parameterPack: + res.append(' ') + res.append(transform(self.identifier)) + if self.default: + res.append(' = ') + res.append(transform(self.default)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode += nodes.Text(self.key) + if self.parameterPack: + if self.identifier: + signode += nodes.Text(' ') + signode += nodes.Text('...') + if self.identifier: + if not self.parameterPack: + signode += nodes.Text(' ') + self.identifier.describe_signature(signode, mode, env, '', '', symbol) + if self.default: + signode += nodes.Text(' = ') + self.default.describe_signature(signode, 'markType', env, symbol) + + +class ASTTemplateParamType(ASTTemplateParam): + def __init__(self, data: ASTTemplateKeyParamPackIdDefault) -> None: + assert data + self.data = data + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.data.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.data.get_identifier() + + def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: + # this is not part of the normal name mangling in C++ + assert version >= 2 + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + else: + return self.data.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.data) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.data.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParamTemplateType(ASTTemplateParam): + def __init__(self, nestedParams: "ASTTemplateParams", + data: ASTTemplateKeyParamPackIdDefault) -> None: + assert nestedParams + assert data + self.nestedParams = nestedParams + self.data = data + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.data.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.data.get_identifier() + + def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + return self.nestedParams.get_id(version) + self.data.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.nestedParams) + transform(self.data) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.nestedParams.describe_signature(signode, 'noneIsName', env, symbol) + signode += nodes.Text(' ') + self.data.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParamNonType(ASTTemplateParam): + def __init__(self, + param: Union[ASTTypeWithInit, + ASTTemplateParamConstrainedTypeWithInit]) -> None: + assert param + self.param = param + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.param.isPack + + def get_identifier(self) -> ASTIdentifier: + name = self.param.name + if name: + assert len(name.names) == 1 + assert name.names[0].identOrOp + assert not name.names[0].templateArgs + res = name.names[0].identOrOp + assert isinstance(res, ASTIdentifier) + return res + else: + return None + + def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + return '_' + self.param.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.param) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.param.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParams(ASTBase): + def __init__(self, params: List[ASTTemplateParam]) -> None: + assert params is not None + self.params = params + + def get_id(self, version: int) -> str: + assert version >= 2 + res = [] + res.append("I") + for param in self.params: + res.append(param.get_id(version)) + res.append("E") + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append("template<") + res.append(", ".join(transform(a) for a in self.params)) + res.append("> ") + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + signode += nodes.Text("template<") + first = True + for param in self.params: + if not first: + signode += nodes.Text(", ") + first = False + param.describe_signature(signode, mode, env, symbol) + signode += nodes.Text(">") + + def describe_signature_as_introducer( + self, parentNode: desc_signature, mode: str, env: "BuildEnvironment", + symbol: "Symbol", lineSpec: bool) -> None: + def makeLine(parentNode: desc_signature) -> addnodes.desc_signature_line: + signode = addnodes.desc_signature_line() + parentNode += signode + signode.sphinx_line_type = 'templateParams' + return signode + lineNode = makeLine(parentNode) + lineNode += nodes.Text("template<") + first = True + for param in self.params: + if not first: + lineNode += nodes.Text(", ") + first = False + if lineSpec: + lineNode = makeLine(parentNode) + param.describe_signature(lineNode, mode, env, symbol) + if lineSpec and not first: + lineNode = makeLine(parentNode) + lineNode += nodes.Text(">") + + +# Template introducers +################################################################################ + +class ASTTemplateIntroductionParameter(ASTBase): + def __init__(self, identifier: ASTIdentifier, parameterPack: bool) -> None: + self.identifier = identifier + self.parameterPack = parameterPack + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.identifier + + def get_id(self, version: int, objectType: str = None, symbol: "Symbol" = None) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + if self.parameterPack: + return 'Dp' + else: + return '0' # we need to put something + + def get_id_as_arg(self, version: int) -> str: + assert version >= 2 + # used for the implicit requires clause + res = self.identifier.get_id(version) + if self.parameterPack: + return 'sp' + res + else: + return res + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.parameterPack: + res.append('...') + res.append(transform(self.identifier)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + if self.parameterPack: + signode += nodes.Text('...') + self.identifier.describe_signature(signode, mode, env, '', '', symbol) + + +class ASTTemplateIntroduction(ASTBase): + def __init__(self, concept: ASTNestedName, + params: List[ASTTemplateIntroductionParameter]) -> None: + assert len(params) > 0 + self.concept = concept + self.params = params + + def get_id(self, version: int) -> str: + assert version >= 2 + # first do the same as a normal template parameter list + res = [] + res.append("I") + for param in self.params: + res.append(param.get_id(version)) + res.append("E") + # let's use X expr E, which is otherwise for constant template args + res.append("X") + res.append(self.concept.get_id(version)) + res.append("I") + for param in self.params: + res.append(param.get_id_as_arg(version)) + res.append("E") + res.append("E") + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.concept)) + res.append('{') + res.append(', '.join(transform(param) for param in self.params)) + res.append('} ') + return ''.join(res) + + def describe_signature_as_introducer( + self, parentNode: desc_signature, mode: str, + env: "BuildEnvironment", symbol: "Symbol", lineSpec: bool) -> None: + # Note: 'lineSpec' has no effect on template introductions. + signode = addnodes.desc_signature_line() + parentNode += signode + signode.sphinx_line_type = 'templateIntroduction' + self.concept.describe_signature(signode, 'markType', env, symbol) + signode += nodes.Text('{') + first = True + for param in self.params: + if not first: + signode += nodes.Text(', ') + first = False + param.describe_signature(signode, mode, env, symbol) + signode += nodes.Text('}') + + +class ASTTemplateDeclarationPrefix(ASTBase): + def __init__(self, + templates: List[Union[ASTTemplateParams, + ASTTemplateIntroduction]]) -> None: + # templates is None means it's an explicit instantiation of a variable + self.templates = templates + + def get_id(self, version: int) -> str: + assert version >= 2 + # this is not part of a normal name mangling system + res = [] + for t in self.templates: + res.append(t.get_id(version)) + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + for t in self.templates: + res.append(transform(t)) + return ''.join(res) + + def describe_signature(self, signode: desc_signature, mode: str, + env: "BuildEnvironment", symbol: "Symbol", lineSpec: bool) -> None: + verify_description_mode(mode) + for t in self.templates: + t.describe_signature_as_introducer(signode, 'lastIsName', env, symbol, lineSpec) + + +################################################################################ +################################################################################ + class ASTDeclaration(ASTBase): def __init__(self, objectType: str, directiveType: str, visibility: str, templatePrefix: ASTTemplateDeclarationPrefix, declaration: Any) -> None: @@ -3432,7 +3525,7 @@ class ASTDeclaration(ASTBase): return self.declaration.name @property - def function_params(self) -> Any: + def function_params(self) -> List[ASTFunctionParameter]: if self.objectType != 'function': return None return self.declaration.function_params @@ -3471,14 +3564,14 @@ class ASTDeclaration(ASTBase): def describe_signature(self, signode: desc_signature, mode: str, env: "BuildEnvironment", options: Dict) -> None: - _verify_description_mode(mode) + verify_description_mode(mode) assert self.symbol # The caller of the domain added a desc_signature node. # Always enable multiline: signode['is_multiline'] = True # Put each line in a desc_signature_line node. mainDeclNode = addnodes.desc_signature_line() - mainDeclNode.sphinx_cpp_tagname = 'declarator' + mainDeclNode.sphinx_line_type = 'declarator' mainDeclNode['add_permalink'] = not self.symbol.isRedeclaration if self.templatePrefix: @@ -3547,10 +3640,25 @@ class SymbolLookupResult: self.templateArgs = templateArgs +class LookupKey: + def __init__(self, data: List[Tuple[ASTNestedNameElement, + Union[ASTTemplateParams, + ASTTemplateIntroduction], + str]]) -> None: + self.data = data + + class Symbol: + debug_indent = 0 + debug_indent_string = " " debug_lookup = False debug_show_tree = False + @staticmethod + def debug_print(*args: Any) -> None: + print(Symbol.debug_indent_string * Symbol.debug_indent, end="") + print(*args) + def _assert_invariants(self) -> None: if not self.parent: # parent == None means global scope, so declaration means a parent @@ -3570,9 +3678,12 @@ class Symbol: return super().__setattr__(key, value) def __init__(self, parent: "Symbol", identOrOp: Union[ASTIdentifier, ASTOperator], - templateParams: Any, templateArgs: Any, declaration: ASTDeclaration, - docname: str) -> None: + templateParams: Union[ASTTemplateParams, ASTTemplateIntroduction], + templateArgs: Any, declaration: ASTDeclaration, docname: str) -> None: self.parent = parent + # declarations in a single directive are linked together + self.siblingAbove = None # type: Symbol + self.siblingBelow = None # type: Symbol self.identOrOp = identOrOp self.templateParams = templateParams # template<templateParams> self.templateArgs = templateArgs # identifier<templateArgs> @@ -3606,38 +3717,43 @@ class Symbol: # and symbol addition should be done as well self._add_template_and_function_params() - def _add_template_and_function_params(self): + def _add_template_and_function_params(self) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_template_and_function_params:") # Note: we may be called from _fill_empty, so the symbols we want # to add may actually already be present (as empty symbols). # add symbols for the template params if self.templateParams: - for p in self.templateParams.params: - if not p.get_identifier(): + for tp in self.templateParams.params: + if not tp.get_identifier(): continue # only add a declaration if we our self are from a declaration if self.declaration: - decl = ASTDeclaration('templateParam', None, None, None, p) + decl = ASTDeclaration('templateParam', None, None, None, tp) else: decl = None - nne = ASTNestedNameElement(p.get_identifier(), None) + nne = ASTNestedNameElement(tp.get_identifier(), None) nn = ASTNestedName([nne], [False], rooted=False) self._add_symbols(nn, [], decl, self.docname) # add symbols for function parameters, if any if self.declaration is not None and self.declaration.function_params is not None: - for p in self.declaration.function_params: - if p.arg is None: + for fp in self.declaration.function_params: + if fp.arg is None: continue - nn = p.arg.name + nn = fp.arg.name if nn is None: continue # (comparing to the template params: we have checked that we are a declaration) - decl = ASTDeclaration('functionParam', None, None, None, p) + decl = ASTDeclaration('functionParam', None, None, None, fp) assert not nn.rooted assert len(nn.names) == 1 self._add_symbols(nn, [], decl, self.docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 - def remove(self): + def remove(self) -> None: if self.parent is None: return assert self in self.parent._children @@ -3645,12 +3761,18 @@ class Symbol: self.parent = None def clear_doc(self, docname: str) -> None: - newChildren = [] + newChildren = [] # type: List[Symbol] for sChild in self._children: sChild.clear_doc(docname) if sChild.declaration and sChild.docname == docname: sChild.declaration = None sChild.docname = None + if sChild.siblingAbove is not None: + sChild.siblingAbove.siblingBelow = sChild.siblingBelow + if sChild.siblingBelow is not None: + sChild.siblingBelow.siblingAbove = sChild.siblingAbove + sChild.siblingAbove = None + sChild.siblingBelow = None newChildren.append(sChild) self._children = newChildren @@ -3669,7 +3791,11 @@ class Symbol: yield from c.children_recurse_anon - def get_lookup_key(self) -> List[Tuple[ASTNestedNameElement, Any]]: + def get_lookup_key(self) -> "LookupKey": + # The pickle files for the environment and for each document are distinct. + # The environment has all the symbols, but the documents has xrefs that + # must know their scope. A lookup key is essentially a specification of + # how to find a specific symbol. symbols = [] s = self while s.parent: @@ -3679,14 +3805,23 @@ class Symbol: key = [] for s in symbols: nne = ASTNestedNameElement(s.identOrOp, s.templateArgs) - key.append((nne, s.templateParams)) - return key + if s.declaration is not None: + key.append((nne, s.templateParams, s.declaration.get_newest_id())) + else: + key.append((nne, s.templateParams, None)) + return LookupKey(key) def get_full_nested_name(self) -> ASTNestedName: + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() names = [] templates = [] - for nne, templateParams in self.get_lookup_key(): - names.append(nne) + for s in symbols: + names.append(ASTNestedNameElement(s.identOrOp, s.templateArgs)) templates.append(False) return ASTNestedName(names, templates, rooted=False) @@ -3695,9 +3830,12 @@ class Symbol: templateShorthand: bool, matchSelf: bool, recurseInAnon: bool, correctPrimaryTemplateArgs: bool ) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_print("_find_first_named_symbol ->") res = self._find_named_symbols(identOrOp, templateParams, templateArgs, templateShorthand, matchSelf, recurseInAnon, - correctPrimaryTemplateArgs) + correctPrimaryTemplateArgs, + searchInSiblings=False) try: return next(res) except StopIteration: @@ -3706,10 +3844,24 @@ class Symbol: def _find_named_symbols(self, identOrOp: Union[ASTIdentifier, ASTOperator], templateParams: Any, templateArgs: ASTTemplateArgs, templateShorthand: bool, matchSelf: bool, - recurseInAnon: bool, correctPrimaryTemplateArgs: bool - ) -> Iterator["Symbol"]: - - def isSpecialization(): + recurseInAnon: bool, correctPrimaryTemplateArgs: bool, + searchInSiblings: bool) -> Iterator["Symbol"]: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_find_named_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("templateParams: ", templateParams) + Symbol.debug_print("templateArgs: ", templateArgs) + Symbol.debug_print("templateShorthand: ", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("correctPrimaryTemplateAargs:", correctPrimaryTemplateArgs) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + def isSpecialization() -> bool: # the names of the template parameters must be given exactly as args # and params that are packs must in the args be the name expanded if len(templateParams.params) != len(templateArgs.args): @@ -3759,20 +3911,64 @@ class Symbol: if str(s.templateArgs) != str(templateArgs): return False return True - if matchSelf and matches(self): - yield self - children = self.children_recurse_anon if recurseInAnon else self._children - for s in children: + + def candidates(): + s = self + if Symbol.debug_lookup: + Symbol.debug_print("searching in self:") + print(s.to_string(Symbol.debug_indent + 1), end="") + while True: + if matchSelf: + yield s + if recurseInAnon: + yield from s.children_recurse_anon + else: + yield from s._children + + if s.siblingAbove is None: + break + s = s.siblingAbove + if Symbol.debug_lookup: + Symbol.debug_print("searching in sibling:") + print(s.to_string(Symbol.debug_indent + 1), end="") + + for s in candidates(): + if Symbol.debug_lookup: + Symbol.debug_print("candidate:") + print(s.to_string(Symbol.debug_indent + 1), end="") if matches(s): + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("matches") + Symbol.debug_indent -= 3 yield s + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 def _symbol_lookup(self, nestedName: ASTNestedName, templateDecls: List[Any], onMissingQualifiedSymbol: Callable[["Symbol", Union[ASTIdentifier, ASTOperator], Any, ASTTemplateArgs], "Symbol"], # NOQA strictTemplateParamArgLists: bool, ancestorLookupType: str, templateShorthand: bool, matchSelf: bool, - recurseInAnon: bool, correctPrimaryTemplateArgs: bool - ) -> SymbolLookupResult: + recurseInAnon: bool, correctPrimaryTemplateArgs: bool, + searchInSiblings: bool) -> SymbolLookupResult: # ancestorLookupType: if not None, specifies the target type of the lookup + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_symbol_lookup:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("templateDecls: ", templateDecls) + Symbol.debug_print("strictTemplateParamArgLists:", strictTemplateParamArgLists) + Symbol.debug_print("ancestorLookupType:", ancestorLookupType) + Symbol.debug_print("templateShorthand: ", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("correctPrimaryTemplateArgs: ", correctPrimaryTemplateArgs) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) if strictTemplateParamArgLists: # Each template argument list must have a template parameter list. @@ -3796,7 +3992,8 @@ class Symbol: while parentSymbol.parent: if parentSymbol.find_identifier(firstName.identOrOp, matchSelf=matchSelf, - recurseInAnon=recurseInAnon): + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings): # if we are in the scope of a constructor but wants to # reference the class we need to walk one extra up if (len(names) == 1 and ancestorLookupType == 'class' and matchSelf and @@ -3807,6 +4004,10 @@ class Symbol: break parentSymbol = parentSymbol.parent + if Symbol.debug_lookup: + Symbol.debug_print("starting point:") + print(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + # and now the actual lookup iTemplateDecl = 0 for name in names[:-1]: @@ -3840,6 +4041,8 @@ class Symbol: symbol = onMissingQualifiedSymbol(parentSymbol, identOrOp, templateParams, templateArgs) if symbol is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 return None # We have now matched part of a nested name, and need to match more # so even if we should matchSelf before, we definitely shouldn't @@ -3847,6 +4050,10 @@ class Symbol: matchSelf = False parentSymbol = symbol + if Symbol.debug_lookup: + Symbol.debug_print("handle last name from:") + print(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + # handle the last name name = names[-1] identOrOp = name.identOrOp @@ -3861,7 +4068,11 @@ class Symbol: symbols = parentSymbol._find_named_symbols( identOrOp, templateParams, templateArgs, templateShorthand=templateShorthand, matchSelf=matchSelf, - recurseInAnon=recurseInAnon, correctPrimaryTemplateArgs=False) + recurseInAnon=recurseInAnon, correctPrimaryTemplateArgs=False, + searchInSiblings=searchInSiblings) + if Symbol.debug_lookup: + symbols = list(symbols) # type: ignore + Symbol.debug_indent -= 2 return SymbolLookupResult(symbols, parentSymbol, identOrOp, templateParams, templateArgs) @@ -3871,21 +4082,26 @@ class Symbol: # be an actual declaration. if Symbol.debug_lookup: - print("_add_symbols:") - print(" tdecls:", templateDecls) - print(" nn: ", nestedName) - print(" decl: ", declaration) - print(" doc: ", docname) + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("tdecls:", templateDecls) + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print("doc: ", docname) def onMissingQualifiedSymbol(parentSymbol: "Symbol", identOrOp: Union[ASTIdentifier, ASTOperator], templateParams: Any, templateArgs: ASTTemplateArgs ) -> "Symbol": if Symbol.debug_lookup: - print(" _add_symbols, onMissingQualifiedSymbol:") - print(" templateParams:", templateParams) - print(" identOrOp: ", identOrOp) - print(" templateARgs: ", templateArgs) + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols, onMissingQualifiedSymbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("templateParams:", templateParams) + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("templateARgs: ", templateArgs) + Symbol.debug_indent -= 2 return Symbol(parent=parentSymbol, identOrOp=identOrOp, templateParams=templateParams, templateArgs=templateArgs, declaration=None, @@ -3898,32 +4114,40 @@ class Symbol: templateShorthand=False, matchSelf=False, recurseInAnon=True, - correctPrimaryTemplateArgs=True) + correctPrimaryTemplateArgs=True, + searchInSiblings=False) assert lookupResult is not None # we create symbols all the way, so that can't happen symbols = list(lookupResult.symbols) if len(symbols) == 0: if Symbol.debug_lookup: - print(" _add_symbols, result, no symbol:") - print(" templateParams:", lookupResult.templateParams) - print(" identOrOp: ", lookupResult.identOrOp) - print(" templateArgs: ", lookupResult.templateArgs) - print(" declaration: ", declaration) - print(" docname: ", docname) + Symbol.debug_print("_add_symbols, result, no symbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("templateParams:", lookupResult.templateParams) + Symbol.debug_print("identOrOp: ", lookupResult.identOrOp) + Symbol.debug_print("templateArgs: ", lookupResult.templateArgs) + Symbol.debug_print("declaration: ", declaration) + Symbol.debug_print("docname: ", docname) + Symbol.debug_indent -= 1 symbol = Symbol(parent=lookupResult.parentSymbol, identOrOp=lookupResult.identOrOp, templateParams=lookupResult.templateParams, templateArgs=lookupResult.templateArgs, declaration=declaration, docname=docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 return symbol if Symbol.debug_lookup: - print(" _add_symbols, result, symbols:") - print(" number symbols:", len(symbols)) + Symbol.debug_print("_add_symbols, result, symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("number symbols:", len(symbols)) + Symbol.debug_indent -= 1 if not declaration: if Symbol.debug_lookup: - print(" no delcaration") + Symbol.debug_print("no delcaration") + Symbol.debug_indent -= 2 # good, just a scope creation # TODO: what if we have more than one symbol? return symbols[0] @@ -3939,9 +4163,9 @@ class Symbol: else: withDecl.append(s) if Symbol.debug_lookup: - print(" #noDecl: ", len(noDecl)) - print(" #withDecl:", len(withDecl)) - print(" #dupDecl: ", len(dupDecl)) + Symbol.debug_print("#noDecl: ", len(noDecl)) + Symbol.debug_print("#withDecl:", len(withDecl)) + Symbol.debug_print("#dupDecl: ", len(dupDecl)) # With partial builds we may start with a large symbol tree stripped of declarations. # Essentially any combination of noDecl, withDecl, and dupDecls seems possible. # TODO: make partial builds fully work. What should happen when the primary symbol gets @@ -3952,7 +4176,7 @@ class Symbol: # otherwise there should be only one symbol with a declaration. def makeCandSymbol(): if Symbol.debug_lookup: - print(" begin: creating candidate symbol") + Symbol.debug_print("begin: creating candidate symbol") symbol = Symbol(parent=lookupResult.parentSymbol, identOrOp=lookupResult.identOrOp, templateParams=lookupResult.templateParams, @@ -3960,7 +4184,7 @@ class Symbol: declaration=declaration, docname=docname) if Symbol.debug_lookup: - print(" end: creating candidate symbol") + Symbol.debug_print("end: creating candidate symbol") return symbol if len(withDecl) == 0: candSymbol = None @@ -3969,7 +4193,10 @@ class Symbol: def handleDuplicateDeclaration(symbol, candSymbol): if Symbol.debug_lookup: - print(" redeclaration") + Symbol.debug_indent += 1 + Symbol.debug_print("redeclaration") + Symbol.debug_indent -= 1 + Symbol.debug_indent -= 2 # Redeclaration of the same symbol. # Let the new one be there, but raise an error to the client # so it can use the real symbol as subscope. @@ -3985,11 +4212,11 @@ class Symbol: # a function, so compare IDs candId = declaration.get_newest_id() if Symbol.debug_lookup: - print(" candId:", candId) + Symbol.debug_print("candId:", candId) for symbol in withDecl: oldId = symbol.declaration.get_newest_id() if Symbol.debug_lookup: - print(" oldId: ", oldId) + Symbol.debug_print("oldId: ", oldId) if candId == oldId: handleDuplicateDeclaration(symbol, candSymbol) # (not reachable) @@ -3997,14 +4224,16 @@ class Symbol: # if there is an empty symbol, fill that one if len(noDecl) == 0: if Symbol.debug_lookup: - print(" no match, no empty, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_print("no match, no empty, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_indent -= 2 if candSymbol is not None: return candSymbol else: return makeCandSymbol() else: if Symbol.debug_lookup: - print(" no match, but fill an empty declaration, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_print("no match, but fill an empty declaration, candSybmol is not None?:", candSymbol is not None) # NOQA + Symbol.debug_indent -= 2 if candSymbol is not None: candSymbol.remove() # assert len(noDecl) == 1 @@ -4021,6 +4250,9 @@ class Symbol: def merge_with(self, other: "Symbol", docnames: List[str], env: "BuildEnvironment") -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("merge_with:") assert other is not None for otherChild in other._children: ourChild = self._find_first_named_symbol( @@ -4050,17 +4282,28 @@ class Symbol: # just ignore it, right? pass ourChild.merge_with(otherChild, docnames, env) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 def add_name(self, nestedName: ASTNestedName, templatePrefix: ASTTemplateDeclarationPrefix = None) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_name:") if templatePrefix: templateDecls = templatePrefix.templates else: templateDecls = [] - return self._add_symbols(nestedName, templateDecls, - declaration=None, docname=None) + res = self._add_symbols(nestedName, templateDecls, + declaration=None, docname=None) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res def add_declaration(self, declaration: ASTDeclaration, docname: str) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_declaration:") assert declaration assert docname nestedName = declaration.name @@ -4068,37 +4311,105 @@ class Symbol: templateDecls = declaration.templatePrefix.templates else: templateDecls = [] - return self._add_symbols(nestedName, templateDecls, declaration, docname) + res = self._add_symbols(nestedName, templateDecls, declaration, docname) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res def find_identifier(self, identOrOp: Union[ASTIdentifier, ASTOperator], - matchSelf: bool, recurseInAnon: bool) -> "Symbol": - if matchSelf and self.identOrOp == identOrOp: - return self - children = self.children_recurse_anon if recurseInAnon else self._children - for s in children: - if s.identOrOp == identOrOp: - return s + matchSelf: bool, recurseInAnon: bool, searchInSiblings: bool + ) -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_identifier:") + Symbol.debug_indent += 1 + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings:", searchInSiblings) + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + current = self + while current is not None: + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + Symbol.debug_print("trying:") + print(current.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + if matchSelf and current.identOrOp == identOrOp: + return current + children = current.children_recurse_anon if recurseInAnon else current._children + for s in children: + if s.identOrOp == identOrOp: + return s + if not searchInSiblings: + break + current = current.siblingAbove return None - def direct_lookup(self, key: List[Tuple[ASTNestedNameElement, Any]]) -> "Symbol": + def direct_lookup(self, key: "LookupKey") -> "Symbol": + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("direct_lookup:") + Symbol.debug_indent += 1 s = self - for name, templateParams in key: - identOrOp = name.identOrOp - templateArgs = name.templateArgs - s = s._find_first_named_symbol(identOrOp, - templateParams, templateArgs, - templateShorthand=False, - matchSelf=False, - recurseInAnon=False, - correctPrimaryTemplateArgs=False) - if not s: + for name, templateParams, id_ in key.data: + if id_ is not None: + res = None + for cand in s._children: + if cand.declaration is None: + continue + if cand.declaration.get_newest_id() == id_: + res = cand + break + s = res + else: + identOrOp = name.identOrOp + templateArgs = name.templateArgs + s = s._find_first_named_symbol(identOrOp, + templateParams, templateArgs, + templateShorthand=False, + matchSelf=False, + recurseInAnon=False, + correctPrimaryTemplateArgs=False) + if Symbol.debug_lookup: + Symbol.debug_print("name: ", name) + Symbol.debug_print("templateParams:", templateParams) + Symbol.debug_print("id: ", id_) + if s is not None: + print(s.to_string(Symbol.debug_indent + 1), end="") + else: + Symbol.debug_print("not found") + if s is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 return None + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 return s def find_name(self, nestedName: ASTNestedName, templateDecls: List[Any], typ: str, templateShorthand: bool, matchSelf: bool, - recurseInAnon: bool) -> List["Symbol"]: + recurseInAnon: bool, searchInSiblings: bool) -> Tuple[List["Symbol"], str]: # templateShorthand: missing template parameter lists for templates is ok + # If the first component is None, + # then the second component _may_ be a string explaining why. + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_name:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + print(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("templateDecls: ", templateDecls) + Symbol.debug_print("typ: ", typ) + Symbol.debug_print("templateShorthand:", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + class QualifiedSymbolIsTemplateParam(Exception): + pass def onMissingQualifiedSymbol(parentSymbol: "Symbol", identOrOp: Union[ASTIdentifier, ASTOperator], @@ -4108,37 +4419,58 @@ class Symbol: # Though, the correctPrimaryTemplateArgs does # that for primary templates. # Is there another case where it would be good? + if parentSymbol.declaration is not None: + if parentSymbol.declaration.objectType == 'templateParam': + raise QualifiedSymbolIsTemplateParam() return None - lookupResult = self._symbol_lookup(nestedName, templateDecls, - onMissingQualifiedSymbol, - strictTemplateParamArgLists=False, - ancestorLookupType=typ, - templateShorthand=templateShorthand, - matchSelf=matchSelf, - recurseInAnon=recurseInAnon, - correctPrimaryTemplateArgs=False) + try: + lookupResult = self._symbol_lookup(nestedName, templateDecls, + onMissingQualifiedSymbol, + strictTemplateParamArgLists=False, + ancestorLookupType=typ, + templateShorthand=templateShorthand, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + correctPrimaryTemplateArgs=False, + searchInSiblings=searchInSiblings) + except QualifiedSymbolIsTemplateParam: + return None, "templateParamInQualified" + if lookupResult is None: # if it was a part of the qualification that could not be found - return None + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None, None res = list(lookupResult.symbols) if len(res) != 0: - return res + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return res, None + + if lookupResult.parentSymbol.declaration is not None: + if lookupResult.parentSymbol.declaration.objectType == 'templateParam': + return None, "templateParamInQualified" # try without template params and args symbol = lookupResult.parentSymbol._find_first_named_symbol( lookupResult.identOrOp, None, None, templateShorthand=templateShorthand, matchSelf=matchSelf, recurseInAnon=recurseInAnon, correctPrimaryTemplateArgs=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 if symbol is not None: - return [symbol] + return [symbol], None else: - return None + return None, None def find_declaration(self, declaration: ASTDeclaration, typ: str, templateShorthand: bool, matchSelf: bool, recurseInAnon: bool) -> "Symbol": # templateShorthand: missing template parameter lists for templates is ok + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_declaration:") nestedName = declaration.name if declaration.templatePrefix: templateDecls = declaration.templatePrefix.templates @@ -4158,8 +4490,10 @@ class Symbol: templateShorthand=templateShorthand, matchSelf=matchSelf, recurseInAnon=recurseInAnon, - correctPrimaryTemplateArgs=False) - + correctPrimaryTemplateArgs=False, + searchInSiblings=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 if lookupResult is None: return None @@ -4185,14 +4519,14 @@ class Symbol: return None def to_string(self, indent: int) -> str: - res = ['\t' * indent] + res = [Symbol.debug_indent_string * indent] if not self.parent: res.append('::') else: if self.templateParams: res.append(str(self.templateParams)) res.append('\n') - res.append('\t' * indent) + res.append(Symbol.debug_indent_string * indent) if self.identOrOp: res.append(str(self.identOrOp)) else: @@ -4218,7 +4552,7 @@ class Symbol: return ''.join(res) -class DefinitionParser: +class DefinitionParser(BaseParser): # those without signedness and size modifiers # see https://en.cppreference.com/w/cpp/language/types _simple_fundemental_types = ( @@ -4228,130 +4562,13 @@ class DefinitionParser: _prefix_keys = ('class', 'struct', 'enum', 'union', 'typename') - def __init__(self, definition: Any, warnEnv: Any, config: "Config") -> None: - self.definition = definition.strip() - self.pos = 0 - self.end = len(self.definition) - self.last_match = None # type: Match - self._previous_state = (0, None) # type: Tuple[int, Match] - self.otherErrors = [] # type: List[DefinitionError] - # in our tests the following is set to False to capture bad parsing - self.allowFallbackExpressionParsing = True - - self.warnEnv = warnEnv + def __init__(self, definition: str, *, + location: Union[nodes.Node, Tuple[str, int]], + config: "Config") -> None: + super().__init__(definition, location=location) self.config = config - def _make_multi_error(self, errors: List[Any], header: str) -> DefinitionError: - if len(errors) == 1: - if len(header) > 0: - return DefinitionError(header + '\n' + str(errors[0][0])) - else: - return DefinitionError(str(errors[0][0])) - result = [header, '\n'] - for e in errors: - if len(e[1]) > 0: - ident = ' ' - result.append(e[1]) - result.append(':\n') - for line in str(e[0]).split('\n'): - if len(line) == 0: - continue - result.append(ident) - result.append(line) - result.append('\n') - else: - result.append(str(e[0])) - return DefinitionError(''.join(result)) - - def status(self, msg: str) -> None: - # for debugging - indicator = '-' * self.pos + '^' - print("%s\n%s\n%s" % (msg, self.definition, indicator)) - - def fail(self, msg: str) -> None: - errors = [] - indicator = '-' * self.pos + '^' - exMain = DefinitionError( - 'Invalid definition: %s [error at %d]\n %s\n %s' % - (msg, self.pos, self.definition, indicator)) - errors.append((exMain, "Main error")) - for err in self.otherErrors: - errors.append((err, "Potential other error")) - self.otherErrors = [] - raise self._make_multi_error(errors, '') - - def warn(self, msg: str) -> None: - if self.warnEnv: - self.warnEnv.warn(msg) - else: - print("Warning: %s" % msg) - - def match(self, regex: Pattern) -> bool: - match = regex.match(self.definition, self.pos) - if match is not None: - self._previous_state = (self.pos, self.last_match) - self.pos = match.end() - self.last_match = match - return True - return False - - def backout(self) -> None: - self.pos, self.last_match = self._previous_state - - def skip_string(self, string: str) -> bool: - strlen = len(string) - if self.definition[self.pos:self.pos + strlen] == string: - self.pos += strlen - return True - return False - - def skip_word(self, word: str) -> bool: - return self.match(re.compile(r'\b%s\b' % re.escape(word))) - - def skip_ws(self) -> bool: - return self.match(_whitespace_re) - - def skip_word_and_ws(self, word: str) -> bool: - if self.skip_word(word): - self.skip_ws() - return True - return False - - def skip_string_and_ws(self, string: str) -> bool: - if self.skip_string(string): - self.skip_ws() - return True - return False - - @property - def eof(self) -> bool: - return self.pos >= self.end - - @property - def current_char(self) -> str: - try: - return self.definition[self.pos] - except IndexError: - return 'EOF' - - @property - def matched_text(self) -> str: - if self.last_match is not None: - return self.last_match.group() - else: - return None - - def read_rest(self) -> str: - rv = self.definition[self.pos:] - self.pos = self.end - return rv - - def assert_end(self) -> None: - self.skip_ws() - if not self.eof: - self.fail('Expected end of definition.') - - def _parse_string(self): + def _parse_string(self) -> str: if self.current_char != '"': return None startPos = self.pos @@ -4390,7 +4607,7 @@ class DefinitionParser: % startPos) return self.definition[startPos:self.pos] - def _parse_attribute(self) -> Any: + def _parse_attribute(self) -> ASTAttribute: self.skip_ws() # try C++11 style startPos = self.pos @@ -4414,7 +4631,7 @@ class DefinitionParser: self.fail("Expected '(' after '__attribute__('.") attrs = [] while 1: - if self.match(_identifier_re): + if self.match(identifier_re): name = self.matched_text self.skip_ws() if self.skip_string_and_ws('('): @@ -4449,7 +4666,7 @@ class DefinitionParser: return None - def _parse_literal(self): + def _parse_literal(self) -> ASTLiteral: # -> integer-literal # | character-literal # | floating-literal @@ -4464,8 +4681,8 @@ class DefinitionParser: return ASTBooleanLiteral(True) if self.skip_word('false'): return ASTBooleanLiteral(False) - for regex in [_float_literal_re, _binary_literal_re, _hex_literal_re, - _integer_literal_re, _octal_literal_re]: + for regex in [float_literal_re, binary_literal_re, hex_literal_re, + integer_literal_re, octal_literal_re]: pos = self.pos if self.match(regex): while self.current_char in 'uUlLfF': @@ -4477,7 +4694,7 @@ class DefinitionParser: return ASTStringLiteral(string) # character-literal - if self.match(_char_literal_re): + if self.match(char_literal_re): prefix = self.last_match.group(1) # may be None when no prefix data = self.last_match.group(2) try: @@ -4491,7 +4708,7 @@ class DefinitionParser: # TODO: user-defined lit return None - def _parse_fold_or_paren_expression(self): + def _parse_fold_or_paren_expression(self) -> ASTExpression: # "(" expression ")" # fold-expression # -> ( cast-expression fold-operator ... ) @@ -4550,7 +4767,7 @@ class DefinitionParser: self.fail("Expected ')' to end binary fold expression.") return ASTFoldExpr(leftExpr, op, rightExpr) - def _parse_primary_expression(self): + def _parse_primary_expression(self) -> ASTExpression: # literal # "this" # lambda-expression @@ -4558,7 +4775,7 @@ class DefinitionParser: # fold-expression # id-expression -> we parse this with _parse_nested_name self.skip_ws() - res = self._parse_literal() + res = self._parse_literal() # type: ASTExpression if res is not None: return res self.skip_ws() @@ -4568,10 +4785,13 @@ class DefinitionParser: res = self._parse_fold_or_paren_expression() if res is not None: return res - return self._parse_nested_name() + nn = self._parse_nested_name() + if nn is not None: + return ASTIdExpression(nn) + return None def _parse_initializer_list(self, name: str, open: str, close: str - ) -> Tuple[List[Any], bool]: + ) -> Tuple[List[ASTExpression], bool]: # Parse open and close with the actual initializer-list inbetween # -> initializer-clause '...'[opt] # | initializer-list ',' initializer-clause '...'[opt] @@ -4581,7 +4801,7 @@ class DefinitionParser: if self.skip_string(close): return [], False - exprs = [] + exprs = [] # type: List[ASTExpression] trailingComma = False while True: self.skip_ws() @@ -4631,7 +4851,7 @@ class DefinitionParser: return paren return self._parse_braced_init_list() - def _parse_postfix_expression(self): + def _parse_postfix_expression(self) -> ASTPostfixExpr: # -> primary # | postfix "[" expression "]" # | postfix "[" braced-init-list [opt] "]" @@ -4737,7 +4957,7 @@ class DefinitionParser: raise self._make_multi_error(errors, header) # and now parse postfixes - postFixes = [] # type: List[Any] + postFixes = [] # type: List[ASTPostfixOp] while True: self.skip_ws() if prefixType in ['expr', 'cast', 'typeid']: @@ -4778,12 +4998,9 @@ class DefinitionParser: postFixes.append(ASTPostfixCallExpr(lst)) continue break - if len(postFixes) == 0: - return prefix - else: - return ASTPostfixExpr(prefix, postFixes) + return ASTPostfixExpr(prefix, postFixes) - def _parse_unary_expression(self): + def _parse_unary_expression(self) -> ASTExpression: # -> postfix # | "++" cast # | "--" cast @@ -4806,7 +5023,7 @@ class DefinitionParser: if self.skip_string_and_ws('...'): if not self.skip_string_and_ws('('): self.fail("Expecting '(' after 'sizeof...'.") - if not self.match(_identifier_re): + if not self.match(identifier_re): self.fail("Expecting identifier for 'sizeof...'.") ident = ASTIdentifier(self.matched_text) self.skip_ws() @@ -4874,7 +5091,7 @@ class DefinitionParser: return ASTDeleteExpr(rooted, array, expr) return self._parse_postfix_expression() - def _parse_cast_expression(self): + def _parse_cast_expression(self) -> ASTExpression: # -> unary | "(" type-id ")" cast pos = self.pos self.skip_ws() @@ -4897,7 +5114,7 @@ class DefinitionParser: else: return self._parse_unary_expression() - def _parse_logical_or_expression(self, inTemplate): + def _parse_logical_or_expression(self, inTemplate: bool) -> ASTExpression: # logical-or = logical-and || # logical-and = inclusive-or && # inclusive-or = exclusive-or | @@ -4909,12 +5126,13 @@ class DefinitionParser: # additive = multiplicative +, - # multiplicative = pm *, /, % # pm = cast .*, ->* - def _parse_bin_op_expr(self, opId, inTemplate): + def _parse_bin_op_expr(self: DefinitionParser, + opId: int, inTemplate: bool) -> ASTExpression: if opId + 1 == len(_expression_bin_ops): - def parser(inTemplate): + def parser(inTemplate: bool) -> ASTExpression: return self._parse_cast_expression() else: - def parser(inTemplate): + def parser(inTemplate: bool) -> ASTExpression: return _parse_bin_op_expr(self, opId + 1, inTemplate=inTemplate) exprs = [] ops = [] @@ -4950,7 +5168,7 @@ class DefinitionParser: # -> "?" expression ":" assignment-expression return None - def _parse_assignment_expression(self, inTemplate): + def _parse_assignment_expression(self, inTemplate: bool) -> ASTExpression: # -> conditional-expression # | logical-or-expression assignment-operator initializer-clause # | throw-expression @@ -4982,19 +5200,21 @@ class DefinitionParser: else: return ASTAssignmentExpr(exprs, ops) - def _parse_constant_expression(self, inTemplate): + def _parse_constant_expression(self, inTemplate: bool) -> ASTExpression: # -> conditional-expression orExpr = self._parse_logical_or_expression(inTemplate=inTemplate) # TODO: use _parse_conditional_expression_tail return orExpr - def _parse_expression(self, inTemplate): + def _parse_expression(self, inTemplate: bool) -> ASTExpression: # -> assignment-expression # | expression "," assignment-expresion # TODO: actually parse the second production return self._parse_assignment_expression(inTemplate=inTemplate) - def _parse_expression_fallback(self, end, parser, allow=True): + def _parse_expression_fallback(self, end: List[str], + parser: Callable[[], ASTExpression], + allow: bool = True) -> ASTExpression: # Stupidly "parse" an expression. # 'end' should be a list of characters which ends the expression. @@ -5034,10 +5254,12 @@ class DefinitionParser: value = self.definition[startPos:self.pos].strip() return ASTFallbackExpr(value.strip()) + # ========================================================================== + def _parse_operator(self) -> ASTOperator: self.skip_ws() # adapted from the old code - # thank god, a regular operator definition + # yay, a regular operator definition if self.match(_operator_re): return ASTOperatorBuildIn(self.matched_text) @@ -5056,7 +5278,7 @@ class DefinitionParser: # user-defined literal? if self.skip_string('""'): self.skip_ws() - if not self.match(_identifier_re): + if not self.match(identifier_re): self.fail("Expected user-defined literal suffix.") identifier = ASTIdentifier(self.matched_text) return ASTOperatorLiteral(identifier) @@ -5073,7 +5295,7 @@ class DefinitionParser: if self.skip_string('>'): return ASTTemplateArgs([]) prevErrors = [] - templateArgs = [] # type: List + templateArgs = [] # type: List[Union[ASTType, ASTTemplateArgConstant]] while 1: pos = self.pos parsedComma = False @@ -5092,9 +5314,13 @@ class DefinitionParser: prevErrors.append((e, "If type argument")) self.pos = pos try: + # actually here we shouldn't use the fallback parser (hence allow=False), + # because if actually took the < in an expression, then we _will_ fail, + # which is handled elsewhere. E.g., :cpp:expr:`A <= 0`. def parser(): return self._parse_constant_expression(inTemplate=True) - value = self._parse_expression_fallback([',', '>'], parser) + value = self._parse_expression_fallback( + [',', '>'], parser, allow=False) self.skip_ws() if self.skip_string('>'): parsedEnd = True @@ -5114,7 +5340,7 @@ class DefinitionParser: return ASTTemplateArgs(templateArgs) def _parse_nested_name(self, memberPointer: bool = False) -> ASTNestedName: - names = [] # type: List[Any] + names = [] # type: List[ASTNestedNameElement] templates = [] # type: List[bool] self.skip_ws() @@ -5132,7 +5358,7 @@ class DefinitionParser: if self.skip_word_and_ws('operator'): identOrOp = self._parse_operator() else: - if not self.match(_identifier_re): + if not self.match(identifier_re): if memberPointer and len(names) > 0: templates.pop() break @@ -5161,7 +5387,9 @@ class DefinitionParser: break return ASTNestedName(names, templates, rooted) - def _parse_trailing_type_spec(self) -> Any: + # ========================================================================== + + def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: # fundemental types self.skip_ws() for t in self._simple_fundemental_types: @@ -5420,7 +5648,7 @@ class DefinitionParser: self.pos = pos declId = None elif named == 'single': - if self.match(_identifier_re): + if self.match(identifier_re): identifier = ASTIdentifier(self.matched_text) nne = ASTNestedNameElement(identifier, None) declId = ASTNestedName([nne], [False], rooted=False) @@ -5464,7 +5692,8 @@ class DefinitionParser: paramQual=paramQual) def _parse_declarator(self, named: Union[bool, str], paramMode: str, - typed: bool = True) -> Any: + typed: bool = True + ) -> ASTDeclarator: # 'typed' here means 'parse return type stuff' if paramMode not in ('type', 'function', 'operatorCast', 'new'): raise Exception( @@ -5706,7 +5935,9 @@ class DefinitionParser: decl = self._parse_declarator(named=named, paramMode=paramMode) return ASTType(declSpecs, decl) - def _parse_type_with_init(self, named: Union[bool, str], outer: str) -> Any: + def _parse_type_with_init( + self, named: Union[bool, str], + outer: str) -> Union[ASTTypeWithInit, ASTTemplateParamConstrainedTypeWithInit]: if outer: assert outer in ('type', 'member', 'function', 'templateParam') type = self._parse_type(outer=outer, named=named) @@ -5820,10 +6051,12 @@ class DefinitionParser: init = ASTInitializer(initVal) return ASTEnumerator(name, init) - def _parse_template_parameter_list(self) -> "ASTTemplateParams": + # ========================================================================== + + def _parse_template_parameter_list(self) -> ASTTemplateParams: # only: '<' parameter-list '>' # we assume that 'template' has just been parsed - templateParams = [] # type: List + templateParams = [] # type: List[ASTTemplateParam] self.skip_ws() if not self.skip_string("<"): self.fail("Expected '<' after 'template'") @@ -5833,7 +6066,6 @@ class DefinitionParser: if self.skip_word('template'): # declare a tenplate template parameter nestedParams = self._parse_template_parameter_list() - nestedParams.isNested = True else: nestedParams = None self.skip_ws() @@ -5850,7 +6082,7 @@ class DefinitionParser: self.skip_ws() parameterPack = self.skip_string('...') self.skip_ws() - if self.match(_identifier_re): + if self.match(identifier_re): identifier = ASTIdentifier(self.matched_text) else: identifier = None @@ -5863,11 +6095,11 @@ class DefinitionParser: parameterPack, default) if nestedParams: # template type - param = ASTTemplateParamTemplateType(nestedParams, data) # type: Any + templateParams.append( + ASTTemplateParamTemplateType(nestedParams, data)) else: # type - param = ASTTemplateParamType(data) - templateParams.append(param) + templateParams.append(ASTTemplateParamType(data)) else: # declare a non-type parameter, or constrained type parameter pos = self.pos @@ -5891,7 +6123,7 @@ class DefinitionParser: prevErrors.append((e, "")) raise self._make_multi_error(prevErrors, header) - def _parse_template_introduction(self) -> "ASTTemplateIntroduction": + def _parse_template_introduction(self) -> ASTTemplateIntroduction: pos = self.pos try: concept = self._parse_nested_name() @@ -5909,7 +6141,7 @@ class DefinitionParser: self.skip_ws() parameterPack = self.skip_string('...') self.skip_ws() - if not self.match(_identifier_re): + if not self.match(identifier_re): self.fail("Expected identifier in template introduction list.") txt_identifier = self.matched_text # make sure there isn't a keyword @@ -5931,14 +6163,15 @@ class DefinitionParser: def _parse_template_declaration_prefix(self, objectType: str ) -> ASTTemplateDeclarationPrefix: - templates = [] # type: List[str] + templates = [] # type: List[Union[ASTTemplateParams, ASTTemplateIntroduction]] while 1: self.skip_ws() # the saved position is only used to provide a better error message + params = None # type: Union[ASTTemplateParams, ASTTemplateIntroduction] pos = self.pos if self.skip_word("template"): try: - params = self._parse_template_parameter_list() # type: Any + params = self._parse_template_parameter_list() except DefinitionError as e: if objectType == 'member' and len(templates) == 0: return ASTTemplateDeclarationPrefix(None) @@ -5990,7 +6223,7 @@ class DefinitionParser: msg += str(nestedName) self.warn(msg) - newTemplates = [] + newTemplates = [] # type: List[Union[ASTTemplateParams, ASTTemplateIntroduction]] for i in range(numExtra): newTemplates.append(ASTTemplateParams([])) if templatePrefix and not isMemberInstantiation: @@ -6067,7 +6300,7 @@ class DefinitionParser: res.objectType = 'namespace' # type: ignore return res - def parse_xref_object(self) -> Tuple[Any, bool]: + def parse_xref_object(self) -> Tuple[Union[ASTNamespace, ASTDeclaration], bool]: pos = self.pos try: templatePrefix = self._parse_template_declaration_prefix(objectType="xref") @@ -6097,25 +6330,26 @@ class DefinitionParser: msg = "Error in cross-reference." raise self._make_multi_error(errs, msg) - def parse_expression(self): + def parse_expression(self) -> Union[ASTExpression, ASTType]: pos = self.pos try: expr = self._parse_expression(False) self.skip_ws() self.assert_end() + return expr except DefinitionError as exExpr: self.pos = pos try: - expr = self._parse_type(False) + typ = self._parse_type(False) self.skip_ws() self.assert_end() + return typ except DefinitionError as exType: header = "Error when parsing (type) expression." errs = [] errs.append((exExpr, "If expression")) errs.append((exType, "If type")) raise self._make_multi_error(errs, header) - return expr def _make_phony_error_name() -> ASTNestedName: @@ -6143,9 +6377,6 @@ class CPPObject(ObjectDescription): option_spec = dict(ObjectDescription.option_spec) option_spec['tparam-line-spec'] = directives.flag - def warn(self, msg: Union[str, Exception]) -> None: - self.state_machine.reporter.warning(msg, line=self.lineno) - def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' # find the parent, if it exists && is an enum @@ -6176,7 +6407,8 @@ class CPPObject(ObjectDescription): return targetSymbol = parentSymbol.parent - s = targetSymbol.find_identifier(symbol.identOrOp, matchSelf=False, recurseInAnon=True) + s = targetSymbol.find_identifier(symbol.identOrOp, matchSelf=False, recurseInAnon=True, + searchInSiblings=False) if s is not None: # something is already declared with that name return @@ -6187,8 +6419,8 @@ class CPPObject(ObjectDescription): declaration=declClone, docname=self.env.docname) - def add_target_and_index(self, ast: ASTDeclaration, sig: str, signode: desc_signature - ) -> None: + def add_target_and_index(self, ast: ASTDeclaration, sig: str, + signode: TextElement) -> None: # general note: name must be lstrip(':')'ed, to remove "::" ids = [] for i in range(1, _max_id + 1): @@ -6202,8 +6434,9 @@ class CPPObject(ObjectDescription): newestId = ids[0] assert newestId # shouldn't be None if not re.compile(r'^[a-zA-Z0-9_]*$').match(newestId): - self.warn('Index id generation for C++ object "%s" failed, please ' - 'report as bug (id=%s).' % (ast, newestId)) + logger.warning('Index id generation for C++ object "%s" failed, please ' + 'report as bug (id=%s).', ast, newestId, + location=self.get_source_info()) name = ast.symbol.get_full_nested_name().get_display_string().lstrip(':') # Add index entry, but not if it's a declaration inside a concept @@ -6231,10 +6464,6 @@ class CPPObject(ObjectDescription): names = self.env.domaindata['cpp']['names'] if name not in names: names[name] = ast.symbol.docname - signode['names'].append(name) - else: - # print("[CPP] non-unique name:", name) - pass # always add the newest id assert newestId signode['ids'].append(newestId) @@ -6244,7 +6473,6 @@ class CPPObject(ObjectDescription): continue if id not in self.state.document.ids: signode['ids'].append(id) - signode['first'] = (not self.names) # hmm, what is this about? self.state.document.note_explicit_target(signode) @property @@ -6261,10 +6489,11 @@ class CPPObject(ObjectDescription): def parse_definition(self, parser: DefinitionParser) -> ASTDeclaration: return parser.parse_declaration(self.object_type, self.objtype) - def describe_signature(self, signode: desc_signature, ast: Any, options: Dict) -> None: + def describe_signature(self, signode: desc_signature, + ast: ASTDeclaration, options: Dict) -> None: ast.describe_signature(signode, 'lastIsName', self.env, options) - def run(self): + def run(self) -> List[Node]: env = self.state.document.settings.env # from ObjectDescription.run if 'cpp:parent_symbol' not in env.temp_data: root = env.domaindata['cpp']['root_symbol'] @@ -6285,23 +6514,29 @@ class CPPObject(ObjectDescription): parentSymbol = env.temp_data['cpp:parent_symbol'] parentDecl = parentSymbol.declaration if parentDecl is not None and parentDecl.objectType == 'function': - self.warn("C++ declarations inside functions are not supported." + - " Parent function is " + str(parentSymbol.get_full_nested_name())) + logger.warning("C++ declarations inside functions are not supported." + + " Parent function is " + + str(parentSymbol.get_full_nested_name()), + location=self.get_source_info()) name = _make_phony_error_name() symbol = parentSymbol.add_name(name) env.temp_data['cpp:last_symbol'] = symbol return [] + # When multiple declarations are made in the same directive + # they need to know about each other to provide symbol lookup for function parameters. + # We use last_symbol to store the latest added declaration in a directive. + env.temp_data['cpp:last_symbol'] = None return super().run() def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: parentSymbol = self.env.temp_data['cpp:parent_symbol'] - parser = DefinitionParser(sig, self, self.env.config) + parser = DefinitionParser(sig, location=signode, config=self.env.config) try: ast = self.parse_definition(parser) parser.assert_end() except DefinitionError as e: - self.warn(e) + logger.warning(e, location=signode) # It is easier to assume some phony name than handling the error in # the possibly inner declarations. name = _make_phony_error_name() @@ -6311,12 +6546,19 @@ class CPPObject(ObjectDescription): try: symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) + # append the new declaration to the sibling list + assert symbol.siblingAbove is None + assert symbol.siblingBelow is None + symbol.siblingAbove = self.env.temp_data['cpp:last_symbol'] + if symbol.siblingAbove is not None: + assert symbol.siblingAbove.siblingBelow is None + symbol.siblingAbove.siblingBelow = symbol self.env.temp_data['cpp:last_symbol'] = symbol except _DuplicateSymbolError as e: # Assume we are actually in the old symbol, # instead of the newly created duplicate. self.env.temp_data['cpp:last_symbol'] = e.symbol - self.warn("Duplicate declaration, %s" % sig) + logger.warning("Duplicate declaration, %s", sig, location=signode) if ast.objectType == 'enumerator': self._add_enumerator_to_parent(ast) @@ -6329,10 +6571,10 @@ class CPPObject(ObjectDescription): return ast def before_content(self) -> None: - lastSymbol = self.env.temp_data['cpp:last_symbol'] + lastSymbol = self.env.temp_data['cpp:last_symbol'] # type: Symbol assert lastSymbol self.oldParentSymbol = self.env.temp_data['cpp:parent_symbol'] - self.oldParentKey = self.env.ref_context['cpp:parent_key'] + self.oldParentKey = self.env.ref_context['cpp:parent_key'] # type: LookupKey self.env.temp_data['cpp:parent_symbol'] = lastSymbol self.env.ref_context['cpp:parent_key'] = lastSymbol.get_lookup_key() @@ -6361,7 +6603,7 @@ class CPPClassObject(CPPObject): object_type = 'class' @property - def display_object_type(self): + def display_object_type(self) -> str: # the distinction between class and struct is only cosmetic assert self.objtype in ('class', 'struct') return self.objtype @@ -6391,21 +6633,20 @@ class CPPNamespaceObject(SphinxDirective): final_argument_whitespace = True option_spec = {} # type: Dict - def warn(self, msg: Union[str, Exception]) -> None: - self.state_machine.reporter.warning(msg, line=self.lineno) - def run(self) -> List[Node]: rootSymbol = self.env.domaindata['cpp']['root_symbol'] if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): symbol = rootSymbol stack = [] # type: List[Symbol] else: - parser = DefinitionParser(self.arguments[0], self, self.config) + parser = DefinitionParser(self.arguments[0], + location=self.get_source_info(), + config=self.config) try: ast = parser.parse_namespace_object() parser.assert_end() except DefinitionError as e: - self.warn(e) + logger.warning(e, location=self.get_source_info()) name = _make_phony_error_name() ast = ASTNamespace(name, None) symbol = rootSymbol.add_name(ast.nestedName, ast.templatePrefix) @@ -6423,18 +6664,17 @@ class CPPNamespacePushObject(SphinxDirective): final_argument_whitespace = True option_spec = {} # type: Dict - def warn(self, msg: Union[str, Exception]) -> None: - self.state_machine.reporter.warning(msg, line=self.lineno) - def run(self) -> List[Node]: if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): return [] - parser = DefinitionParser(self.arguments[0], self, self.config) + parser = DefinitionParser(self.arguments[0], + location=self.get_source_info(), + config=self.config) try: ast = parser.parse_namespace_object() parser.assert_end() except DefinitionError as e: - self.warn(e) + logger.warning(e, location=self.get_source_info()) name = _make_phony_error_name() ast = ASTNamespace(name, None) oldParent = self.env.temp_data.get('cpp:parent_symbol', None) @@ -6456,13 +6696,11 @@ class CPPNamespacePopObject(SphinxDirective): final_argument_whitespace = True option_spec = {} # type: Dict - def warn(self, msg: Union[str, Exception]) -> None: - self.state_machine.reporter.warning(msg, line=self.lineno) - def run(self) -> List[Node]: stack = self.env.temp_data.get('cpp:namespace_stack', None) if not stack or len(stack) == 0: - self.warn("C++ namespace pop on empty stack. Defaulting to gobal scope.") + logger.warning("C++ namespace pop on empty stack. Defaulting to gobal scope.", + location=self.get_source_info()) stack = [] else: stack.pop() @@ -6477,7 +6715,8 @@ class CPPNamespacePopObject(SphinxDirective): class AliasNode(nodes.Element): - def __init__(self, sig, env=None, parentKey=None): + def __init__(self, sig: str, env: "BuildEnvironment" = None, + parentKey: LookupKey = None) -> None: super().__init__() self.sig = sig if env is not None: @@ -6489,8 +6728,8 @@ class AliasNode(nodes.Element): assert parentKey is not None self.parentKey = parentKey - def copy(self): - return self.__class__(self.sig, env=None, parentKey=self.parentKey) + def copy(self: T) -> T: + return self.__class__(self.sig, env=None, parentKey=self.parentKey) # type: ignore class AliasTransform(SphinxTransform): @@ -6498,31 +6737,27 @@ class AliasTransform(SphinxTransform): def apply(self, **kwargs: Any) -> None: for node in self.document.traverse(AliasNode): - class Warner: - def warn(self, msg): - logger.warning(msg, location=node) - warner = Warner() sig = node.sig parentKey = node.parentKey try: - parser = DefinitionParser(sig, warner, self.env.config) + parser = DefinitionParser(sig, location=node, + config=self.env.config) ast, isShorthand = parser.parse_xref_object() parser.assert_end() except DefinitionError as e: - warner.warn(e) + logger.warning(e, location=node) ast, isShorthand = None, None if ast is None: # could not be parsed, so stop here signode = addnodes.desc_signature(sig, '') - signode['first'] = False signode.clear() signode += addnodes.desc_name(sig, sig) node.replace_self(signode) continue - rootSymbol = self.env.domains['cpp'].data['root_symbol'] - parentSymbol = rootSymbol.direct_lookup(parentKey) + rootSymbol = self.env.domains['cpp'].data['root_symbol'] # type: Symbol + parentSymbol = rootSymbol.direct_lookup(parentKey) # type: Symbol if not parentSymbol: print("Target: ", sig) print("ParentKey: ", parentKey) @@ -6531,19 +6766,25 @@ class AliasTransform(SphinxTransform): symbols = [] # type: List[Symbol] if isShorthand: - ns = ast # type: ASTNamespace + assert isinstance(ast, ASTNamespace) + ns = ast name = ns.nestedName if ns.templatePrefix: templateDecls = ns.templatePrefix.templates else: templateDecls = [] - symbols = parentSymbol.find_name(name, templateDecls, 'any', - templateShorthand=True, - matchSelf=True, recurseInAnon=True) + symbols, failReason = parentSymbol.find_name( + nestedName=name, + templateDecls=templateDecls, + typ='any', + templateShorthand=True, + matchSelf=True, recurseInAnon=True, + searchInSiblings=False) if symbols is None: symbols = [] else: - decl = ast # type: ASTDeclaration + assert isinstance(ast, ASTDeclaration) + decl = ast name = decl.name s = parentSymbol.find_declaration(decl, 'any', templateShorthand=True, @@ -6555,7 +6796,6 @@ class AliasTransform(SphinxTransform): if len(symbols) == 0: signode = addnodes.desc_signature(sig, '') - signode['first'] = False node.append(signode) signode.clear() signode += addnodes.desc_name(sig, sig) @@ -6569,7 +6809,6 @@ class AliasTransform(SphinxTransform): options['tparam-line-spec'] = False for s in symbols: signode = addnodes.desc_signature(sig, '') - signode['first'] = False nodes.append(signode) s.declaration.describe_signature(signode, 'markName', self.env, options) node.replace_self(nodes) @@ -6618,7 +6857,7 @@ class CPPXRefRole(XRefRole): if not has_explicit_title: # major hax: replace anon names via simple string manipulation. # Can this actually fail? - title = _anon_identifier_re.sub("[anonymous]", str(title)) + title = anon_identifier_re.sub("[anonymous]", str(title)) if refnode['reftype'] == 'any': # Assume the removal part of fix_parens for :any: refs. @@ -6640,8 +6879,9 @@ class CPPXRefRole(XRefRole): return title, target -class CPPExprRole: - def __init__(self, asCode): +class CPPExprRole(SphinxRole): + def __init__(self, asCode: bool) -> None: + super().__init__() if asCode: # render the expression as inline code self.class_type = 'cpp-expr' @@ -6651,28 +6891,27 @@ class CPPExprRole: self.class_type = 'cpp-texpr' self.node_type = nodes.inline - def __call__(self, typ, rawtext, text, lineno, inliner, options={}, content=[]): - class Warner: - def warn(self, msg): - inliner.reporter.warning(msg, line=lineno) - text = utils.unescape(text).replace('\n', ' ') - env = inliner.document.settings.env - parser = DefinitionParser(text, Warner(), env.config) + def run(self) -> Tuple[List[Node], List[system_message]]: + text = self.text.replace('\n', ' ') + parser = DefinitionParser(text, + location=self.get_source_info(), + config=self.config) # attempt to mimic XRefRole classes, except that... classes = ['xref', 'cpp', self.class_type] try: ast = parser.parse_expression() except DefinitionError as ex: - Warner().warn('Unparseable C++ expression: %r\n%s' % (text, ex)) + logger.warning('Unparseable C++ expression: %r\n%s', text, ex, + location=self.get_source_info()) # see below return [self.node_type(text, text, classes=classes)], [] - parentSymbol = env.temp_data.get('cpp:parent_symbol', None) + parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) if parentSymbol is None: - parentSymbol = env.domaindata['cpp']['root_symbol'] + parentSymbol = self.env.domaindata['cpp']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, # not the container node signode = self.node_type(classes=classes) - ast.describe_signature(signode, 'markType', env, parentSymbol) + ast.describe_signature(signode, 'markType', self.env, parentSymbol) return [signode], [] @@ -6796,25 +7035,24 @@ class CPPDomain(Domain): ourNames[name] = docname def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - typ: str, target: str, node: pending_xref, contnode: Element, - emitWarnings: bool = True) -> Tuple[Element, str]: - class Warner: - def warn(self, msg): - if emitWarnings: - logger.warning(msg, location=node) - warner = Warner() + typ: str, target: str, node: pending_xref, + contnode: Element) -> Tuple[Element, str]: # add parens again for those that could be functions if typ == 'any' or typ == 'func': target += '()' - parser = DefinitionParser(target, warner, env.config) + parser = DefinitionParser(target, location=node, + config=env.config) try: ast, isShorthand = parser.parse_xref_object() except DefinitionError as e: - def findWarning(e): # as arg to stop flake8 from complaining + # as arg to stop flake8 from complaining + def findWarning(e: Exception) -> Tuple[str, Exception]: if typ != 'any' and typ != 'func': return target, e # hax on top of the paren hax to try to get correct errors - parser2 = DefinitionParser(target[:-2], warner, env.config) + parser2 = DefinitionParser(target[:-2], + location=node, + config=env.config) try: parser2.parse_xref_object() except DefinitionError as e2: @@ -6822,34 +7060,49 @@ class CPPDomain(Domain): # strange, that we don't get the error now, use the original return target, e t, ex = findWarning(e) - warner.warn('Unparseable C++ cross-reference: %r\n%s' % (t, ex)) + logger.warning('Unparseable C++ cross-reference: %r\n%s', t, ex, + location=node) return None, None - parentKey = node.get("cpp:parent_key", None) + parentKey = node.get("cpp:parent_key", None) # type: LookupKey rootSymbol = self.data['root_symbol'] if parentKey: - parentSymbol = rootSymbol.direct_lookup(parentKey) + parentSymbol = rootSymbol.direct_lookup(parentKey) # type: Symbol if not parentSymbol: print("Target: ", target) - print("ParentKey: ", parentKey) + print("ParentKey: ", parentKey.data) print(rootSymbol.dump(1)) assert parentSymbol # should be there else: parentSymbol = rootSymbol if isShorthand: - ns = ast # type: ASTNamespace + assert isinstance(ast, ASTNamespace) + ns = ast name = ns.nestedName if ns.templatePrefix: templateDecls = ns.templatePrefix.templates else: templateDecls = [] - symbols = parentSymbol.find_name(name, templateDecls, typ, - templateShorthand=True, - matchSelf=True, recurseInAnon=True) - # just refer to the arbitrarily first symbol - s = None if symbols is None else symbols[0] + # let's be conservative with the sibling lookup for now + searchInSiblings = (not name.rooted) and len(name.names) == 1 + symbols, failReason = parentSymbol.find_name( + name, templateDecls, typ, + templateShorthand=True, + matchSelf=True, recurseInAnon=True, + searchInSiblings=searchInSiblings) + if symbols is None: + if typ == 'identifier': + if failReason == 'templateParamInQualified': + # this is an xref we created as part of a signature, + # so don't warn for names nested in template parameters + raise NoUri(str(name), typ) + s = None + else: + # just refer to the arbitrarily first symbol + s = symbols[0] else: - decl = ast # type: ASTDeclaration + assert isinstance(ast, ASTDeclaration) + decl = ast name = decl.name s = parentSymbol.find_declaration(decl, typ, templateShorthand=True, @@ -6869,7 +7122,7 @@ class CPPDomain(Domain): typ = 'class' declTyp = s.declaration.objectType - def checkType(): + def checkType() -> bool: if typ == 'any' or typ == 'identifier': return True if declTyp == 'templateParam': @@ -6884,9 +7137,10 @@ class CPPDomain(Domain): print("Type is %s (originally: %s), declType is %s" % (typ, origTyp, declTyp)) assert False if not checkType(): - warner.warn("cpp:%s targets a %s (%s)." - % (origTyp, s.declaration.objectType, - s.get_full_nested_name())) + logger.warning("cpp:%s targets a %s (%s).", + origTyp, s.declaration.objectType, + s.get_full_nested_name(), + location=node) declaration = s.declaration if isShorthand: @@ -6949,9 +7203,9 @@ class CPPDomain(Domain): def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element ) -> List[Tuple[str, Element]]: - retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, - 'any', target, node, contnode, - emitWarnings=False) + with logging.suppress_logging(): + retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, + 'any', target, node, contnode) if retnode: if objtype == 'templateParam': return [('cpp:templateParam', retnode)] @@ -6977,8 +7231,8 @@ class CPPDomain(Domain): target = node.get('reftarget', None) if target is None: return None - parentKey = node.get("cpp:parent_key", None) - if parentKey is None or len(parentKey) <= 0: + parentKey = node.get("cpp:parent_key", None) # type: LookupKey + if parentKey is None or len(parentKey.data) <= 0: return None rootSymbol = self.data['root_symbol'] @@ -6996,7 +7250,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/domains/index.py b/sphinx/domains/index.py index d0bdbcfe0..18a256bac 100644 --- a/sphinx/domains/index.py +++ b/sphinx/domains/index.py @@ -12,6 +12,7 @@ from typing import Any, Dict, Iterable, List, Tuple from docutils import nodes from docutils.nodes import Node, system_message +from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.domains import Domain @@ -68,19 +69,27 @@ class IndexDirective(SphinxDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True - option_spec = {} # type: Dict + option_spec = { + 'name': directives.unchanged, + } def run(self) -> List[Node]: arguments = self.arguments[0].split('\n') - targetid = 'index-%s' % self.env.new_serialno('index') - targetnode = nodes.target('', '', ids=[targetid]) + + if 'name' in self.options: + targetname = self.options['name'] + targetnode = nodes.target('', '', names=[targetname]) + else: + targetid = 'index-%s' % self.env.new_serialno('index') + targetnode = nodes.target('', '', ids=[targetid]) + self.state.document.note_explicit_target(targetnode) indexnode = addnodes.index() indexnode['entries'] = [] indexnode['inline'] = False self.set_source_info(indexnode) for entry in arguments: - indexnode['entries'].extend(process_index_entry(entry, targetid)) + indexnode['entries'].extend(process_index_entry(entry, targetnode['ids'][0])) return [indexnode, targetnode] diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index feb39bc9d..d510d7903 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -28,7 +28,7 @@ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import make_refnode +from sphinx.util.nodes import make_id, make_refnode logger = logging.getLogger(__name__) @@ -106,21 +106,23 @@ class JSObject(ObjectDescription): signode: desc_signature) -> None: mod_name = self.env.ref_context.get('js:module') fullname = (mod_name + '.' if mod_name else '') + name_obj[0] - if fullname not in self.state.document.ids: - signode['names'].append(fullname) - signode['ids'].append(fullname.replace('$', '_S_')) - signode['first'] = not self.names - self.state.document.note_explicit_target(signode) + node_id = make_id(self.env, self.state.document, '', fullname) + signode['ids'].append(node_id) - domain = cast(JavaScriptDomain, self.env.get_domain('js')) - domain.note_object(fullname, self.objtype, - location=(self.env.docname, self.lineno)) + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(fullname) + if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']: + signode['ids'].append(old_node_id) + + self.state.document.note_explicit_target(signode) + + domain = cast(JavaScriptDomain, self.env.get_domain('js')) + domain.note_object(fullname, self.objtype, node_id, location=signode) indextext = self.get_index_text(mod_name, name_obj) if indextext: - self.indexnode['entries'].append(('single', indextext, - fullname.replace('$', '_S_'), - '', None)) + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) def get_index_text(self, objectname: str, name_obj: Tuple[str, str]) -> str: name, obj = name_obj @@ -191,6 +193,14 @@ class JSObject(ObjectDescription): self.env.ref_context['js:object'] = (objects[-1] if len(objects) > 0 else None) + def make_old_id(self, fullname: str) -> str: + """Generate old styled node_id for JS objects. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return fullname.replace('$', '_S_') + class JSCallable(JSObject): """Description of a JavaScript function, method or constructor.""" @@ -251,21 +261,36 @@ class JSModule(SphinxDirective): if not noindex: domain = cast(JavaScriptDomain, self.env.get_domain('js')) - domain.note_module(mod_name) + node_id = make_id(self.env, self.state.document, 'module', mod_name) + domain.note_module(mod_name, node_id) # Make a duplicate entry in 'objects' to facilitate searching for # the module in JavaScriptDomain.find_obj() - domain.note_object(mod_name, 'module', location=(self.env.docname, self.lineno)) + domain.note_object(mod_name, 'module', node_id, + location=(self.env.docname, self.lineno)) + + target = nodes.target('', '', ids=[node_id], ismod=True) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(mod_name) + if old_node_id not in self.state.document.ids and old_node_id not in target['ids']: + target['ids'].append(old_node_id) - targetnode = nodes.target('', '', ids=['module-' + mod_name], - ismod=True) - self.state.document.note_explicit_target(targetnode) - ret.append(targetnode) + self.state.document.note_explicit_target(target) + ret.append(target) indextext = _('%s (module)') % mod_name - inode = addnodes.index(entries=[('single', indextext, - 'module-' + mod_name, '', None)]) + inode = addnodes.index(entries=[('single', indextext, node_id, '', None)]) ret.append(inode) return ret + def make_old_id(self, modname: str) -> str: + """Generate old styled node_id for JS modules. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return 'module-' + modname + class JSXRefRole(XRefRole): def process_link(self, env: BuildEnvironment, refnode: Element, @@ -317,47 +342,48 @@ class JavaScriptDomain(Domain): 'mod': JSXRefRole(), } initial_data = { - 'objects': {}, # fullname -> docname, objtype - 'modules': {}, # modname -> docname + 'objects': {}, # fullname -> docname, node_id, objtype + 'modules': {}, # modname -> docname, node_id } # type: Dict[str, Dict[str, Tuple[str, str]]] @property - def objects(self) -> Dict[str, Tuple[str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, objtype + def objects(self) -> Dict[str, Tuple[str, str, str]]: + return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def note_object(self, fullname: str, objtype: str, location: Any = None) -> None: + def note_object(self, fullname: str, objtype: str, node_id: str, + location: Any = None) -> None: if fullname in self.objects: docname = self.objects[fullname][0] - logger.warning(__('duplicate object description of %s, other instance in %s'), - fullname, docname, location=location) - self.objects[fullname] = (self.env.docname, objtype) + logger.warning(__('duplicate %s description of %s, other %s in %s'), + objtype, fullname, objtype, docname, location=location) + self.objects[fullname] = (self.env.docname, node_id, objtype) @property - def modules(self) -> Dict[str, str]: - return self.data.setdefault('modules', {}) # modname -> docname + def modules(self) -> Dict[str, Tuple[str, str]]: + return self.data.setdefault('modules', {}) # modname -> docname, node_id - def note_module(self, modname: str) -> None: - self.modules[modname] = self.env.docname + def note_module(self, modname: str, node_id: str) -> None: + self.modules[modname] = (self.env.docname, node_id) def clear_doc(self, docname: str) -> None: - for fullname, (pkg_docname, _l) in list(self.objects.items()): + for fullname, (pkg_docname, node_id, _l) in list(self.objects.items()): if pkg_docname == docname: del self.objects[fullname] - for modname, pkg_docname in list(self.modules.items()): + for modname, (pkg_docname, node_id) in list(self.modules.items()): if pkg_docname == docname: del self.modules[modname] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates - for fullname, (fn, objtype) in otherdata['objects'].items(): + for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): if fn in docnames: - self.objects[fullname] = (fn, objtype) - for mod_name, pkg_docname in otherdata['modules'].items(): + self.objects[fullname] = (fn, node_id, objtype) + for mod_name, (pkg_docname, node_id) in otherdata['modules'].items(): if pkg_docname in docnames: - self.modules[mod_name] = pkg_docname + self.modules[mod_name] = (pkg_docname, node_id) def find_obj(self, env: BuildEnvironment, mod_name: str, prefix: str, name: str, - typ: str, searchorder: int = 0) -> Tuple[str, Tuple[str, str]]: + typ: str, searchorder: int = 0) -> Tuple[str, Tuple[str, str, str]]: if name[-2:] == '()': name = name[:-2] @@ -389,8 +415,7 @@ class JavaScriptDomain(Domain): name, obj = self.find_obj(env, mod_name, prefix, target, typ, searchorder) if not obj: return None - return make_refnode(builder, fromdocname, obj[0], - name.replace('$', '_S_'), contnode, name) + return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name) def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element @@ -400,13 +425,12 @@ class JavaScriptDomain(Domain): name, obj = self.find_obj(env, mod_name, prefix, target, None, 1) if not obj: return [] - return [('js:' + self.role_for_objtype(obj[1]), - make_refnode(builder, fromdocname, obj[0], - name.replace('$', '_S_'), contnode, name))] + return [('js:' + self.role_for_objtype(obj[2]), + make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))] def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: - for refname, (docname, type) in list(self.objects.items()): - yield refname, refname, type, docname, refname.replace('$', '_S_'), 1 + for refname, (docname, node_id, typ) in list(self.objects.items()): + yield refname, refname, typ, docname, node_id, 1 def get_full_qualified_name(self, node: Element) -> str: modname = node.get('js:module') @@ -423,7 +447,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index b07b2603b..88b6e4eb8 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -15,7 +15,6 @@ from docutils import nodes from docutils.nodes import Element, Node, system_message from docutils.nodes import make_id -from sphinx.addnodes import math_block as displaymath from sphinx.addnodes import pending_xref from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain @@ -54,7 +53,6 @@ class MathDomain(Domain): 'eq': 'equation not found: %(target)s', } enumerable_nodes = { # node_class -> (figtype, title_getter) - displaymath: ('displaymath', None), nodes.math_block: ('displaymath', None), } roles = { diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index be6bf34e3..fae1991c7 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -8,8 +8,12 @@ :license: BSD, see LICENSE for details. """ +import builtins +import inspect import re +import typing import warnings +from inspect import Parameter from typing import Any, Dict, Iterable, Iterator, List, Tuple from typing import cast @@ -17,22 +21,22 @@ from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers.rst import directives -from sphinx import addnodes, locale +from sphinx import addnodes from sphinx.addnodes import pending_xref, desc_signature from sphinx.application import Sphinx from sphinx.builders import Builder -from sphinx.deprecation import ( - DeprecatedDict, RemovedInSphinx30Warning, RemovedInSphinx40Warning -) +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType, Index, IndexEntry from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ +from sphinx.pycode.ast import ast, parse as ast_parse from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import make_refnode +from sphinx.util.inspect import signature_from_str +from sphinx.util.nodes import make_id, make_refnode from sphinx.util.typing import TextlikeNode if False: @@ -63,12 +67,107 @@ pairindextypes = { 'builtin': _('built-in function'), } -locale.pairindextypes = DeprecatedDict( - pairindextypes, - 'sphinx.locale.pairindextypes is deprecated. ' - 'Please use sphinx.domains.python.pairindextypes instead.', - RemovedInSphinx30Warning -) + +def _parse_annotation(annotation: str) -> List[Node]: + """Parse type annotation.""" + def make_xref(text: str) -> addnodes.pending_xref: + return pending_xref('', nodes.Text(text), + refdomain='py', reftype='class', reftarget=text) + + def unparse(node: ast.AST) -> List[Node]: + if isinstance(node, ast.Attribute): + return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))] + elif isinstance(node, ast.Expr): + return unparse(node.value) + elif isinstance(node, ast.Index): + return unparse(node.value) + elif isinstance(node, ast.List): + result = [addnodes.desc_sig_punctuation('', '[')] # type: List[Node] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ', ')) + result.pop() + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + elif isinstance(node, ast.Module): + return sum((unparse(e) for e in node.body), []) + elif isinstance(node, ast.Name): + return [nodes.Text(node.id)] + elif isinstance(node, ast.Subscript): + result = unparse(node.value) + result.append(addnodes.desc_sig_punctuation('', '[')) + result.extend(unparse(node.slice)) + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + elif isinstance(node, ast.Tuple): + result = [] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ', ')) + result.pop() + return result + else: + raise SyntaxError # unsupported syntax + + try: + tree = ast_parse(annotation) + result = unparse(tree) + for i, node in enumerate(result): + if isinstance(node, nodes.Text): + result[i] = make_xref(str(node)) + return result + except SyntaxError: + return [make_xref(annotation)] + + +def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: + """Parse a list of arguments using AST parser""" + params = addnodes.desc_parameterlist(arglist) + sig = signature_from_str('(%s)' % arglist) + last_kind = None + for param in sig.parameters.values(): + if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) + if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + param.POSITIONAL_ONLY, + None): + # PEP-3102: Separator for Keyword Only Parameter: * + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*')) + + node = addnodes.desc_parameter() + if param.kind == param.VAR_POSITIONAL: + node += addnodes.desc_sig_operator('', '*') + node += addnodes.desc_sig_name('', param.name) + elif param.kind == param.VAR_KEYWORD: + node += addnodes.desc_sig_operator('', '**') + node += addnodes.desc_sig_name('', param.name) + else: + node += addnodes.desc_sig_name('', param.name) + + if param.annotation is not param.empty: + children = _parse_annotation(param.annotation) + node += addnodes.desc_sig_punctuation('', ':') + node += nodes.Text(' ') + node += addnodes.desc_sig_name('', '', *children) # type: ignore + if param.default is not param.empty: + if param.annotation is not param.empty: + node += nodes.Text(' ') + node += addnodes.desc_sig_operator('', '=') + node += nodes.Text(' ') + else: + node += addnodes.desc_sig_operator('', '=') + node += nodes.inline('', param.default, classes=['default_value'], + support_smartquotes=False) + + params += node + last_kind = param.kind + + if last_kind == Parameter.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) + + return params def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None: @@ -293,14 +392,24 @@ class PyObject(ObjectDescription): signode += addnodes.desc_name(name, name) if arglist: - _pseudo_parse_arglist(signode, arglist) + try: + signode += _parse_arglist(arglist) + except SyntaxError: + # fallback to parse arglist original parser. + # it supports to represent optional arguments (ex. "func(foo [, bar])") + _pseudo_parse_arglist(signode, arglist) + except NotImplementedError as exc: + logger.warning("could not parse arglist (%r): %s", arglist, exc, + location=signode) + _pseudo_parse_arglist(signode, arglist) else: if self.needs_arglist(): # for callables, add an empty parameter list signode += addnodes.desc_parameterlist() if retann: - signode += addnodes.desc_returns(retann, retann) + children = _parse_annotation(retann) + signode += addnodes.desc_returns(retann, '', *children) anno = self.options.get('annotation') if anno: @@ -316,21 +425,22 @@ class PyObject(ObjectDescription): signode: desc_signature) -> None: modname = self.options.get('module', self.env.ref_context.get('py:module')) fullname = (modname + '.' if modname else '') + name_cls[0] - # note target - if fullname not in self.state.document.ids: - signode['names'].append(fullname) + node_id = make_id(self.env, self.state.document, '', fullname) + signode['ids'].append(node_id) + + # Assign old styled node_id(fullname) not to break old hyperlinks (if possible) + # Note: Will removed in Sphinx-5.0 (RemovedInSphinx50Warning) + if node_id != fullname and fullname not in self.state.document.ids: signode['ids'].append(fullname) - signode['first'] = (not self.names) - self.state.document.note_explicit_target(signode) - domain = cast(PythonDomain, self.env.get_domain('py')) - domain.note_object(fullname, self.objtype, - location=(self.env.docname, self.lineno)) + self.state.document.note_explicit_target(signode) + + domain = cast(PythonDomain, self.env.get_domain('py')) + domain.note_object(fullname, self.objtype, node_id, location=signode) indextext = self.get_index_text(modname, name_cls) if indextext: - self.indexnode['entries'].append(('single', indextext, - fullname, '', None)) + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) def before_content(self) -> None: """Handle object nesting before content @@ -449,6 +559,23 @@ class PyFunction(PyObject): return _('%s() (built-in function)') % name +class PyDecoratorFunction(PyFunction): + """Description of a decorator.""" + + def run(self) -> List[Node]: + # a decorator function is a function after all + self.name = 'py:function' + return super().run() + + def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + ret = super().handle_signature(sig, signode) + signode.insert(0, addnodes.desc_addname('@', '@')) + return ret + + def needs_arglist(self) -> bool: + return False + + class PyVariable(PyObject): """Description of a variable.""" @@ -666,6 +793,22 @@ class PyStaticMethod(PyMethod): return super().run() +class PyDecoratorMethod(PyMethod): + """Description of a decoratormethod.""" + + def run(self) -> List[Node]: + self.name = 'py:method' + return super().run() + + def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + ret = super().handle_signature(sig, signode) + signode.insert(0, addnodes.desc_addname('@', '@')) + return ret + + def needs_arglist(self) -> bool: + return False + + class PyAttribute(PyObject): """Description of an attribute.""" @@ -708,6 +851,15 @@ class PyDecoratorMixin: Mixin for decorator directives. """ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]: + for cls in self.__class__.__mro__: + if cls.__name__ != 'DirectiveAdapter': + warnings.warn('PyDecoratorMixin is deprecated. ' + 'Please check the implementation of %s' % cls, + RemovedInSphinx50Warning) + break + else: + warnings.warn('PyDecoratorMixin is deprecated', RemovedInSphinx50Warning) + ret = super().handle_signature(sig, signode) # type: ignore signode.insert(0, addnodes.desc_addname('@', '@')) return ret @@ -716,25 +868,6 @@ class PyDecoratorMixin: return False -class PyDecoratorFunction(PyDecoratorMixin, PyModulelevel): - """ - Directive to mark functions meant to be used as decorators. - """ - def run(self) -> List[Node]: - # a decorator function is a function after all - self.name = 'py:function' - return super().run() - - -class PyDecoratorMethod(PyDecoratorMixin, PyClassmember): - """ - Directive to mark methods meant to be used as decorators. - """ - def run(self) -> List[Node]: - self.name = 'py:method' - return super().run() - - class PyModule(SphinxDirective): """ Directive to mark description of a new module. @@ -760,24 +893,43 @@ class PyModule(SphinxDirective): ret = [] # type: List[Node] if not noindex: # note module to the domain + node_id = make_id(self.env, self.state.document, 'module', modname) + target = nodes.target('', '', ids=[node_id], ismod=True) + self.set_source_info(target) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(modname) + if node_id != old_node_id and old_node_id not in self.state.document.ids: + target['ids'].append(old_node_id) + + self.state.document.note_explicit_target(target) + domain.note_module(modname, + node_id, self.options.get('synopsis', ''), self.options.get('platform', ''), 'deprecated' in self.options) - domain.note_object(modname, 'module', location=(self.env.docname, self.lineno)) + domain.note_object(modname, 'module', node_id, location=target) - targetnode = nodes.target('', '', ids=['module-' + modname], - ismod=True) - self.state.document.note_explicit_target(targetnode) # the platform and synopsis aren't printed; in fact, they are only # used in the modindex currently - ret.append(targetnode) + ret.append(target) indextext = _('%s (module)') % modname - inode = addnodes.index(entries=[('single', indextext, - 'module-' + modname, '', None)]) + inode = addnodes.index(entries=[('single', indextext, node_id, '', None)]) ret.append(inode) return ret + def make_old_id(self, name: str) -> str: + """Generate old styled node_id. + + Old styled node_id is incompatible with docutils' node_id. + It can contain dots and hyphens. + + .. note:: Old styled node_id was mainly used until Sphinx-3.0. + """ + return 'module-%s' % name + class PyCurrentModule(SphinxDirective): """ @@ -823,6 +975,21 @@ class PyXRefRole(XRefRole): return title, target +def filter_meta_fields(app: Sphinx, domain: str, objtype: str, content: Element) -> None: + """Filter ``:meta:`` field from its docstring.""" + if domain != 'py': + return + + for node in content: + if isinstance(node, nodes.field_list): + fields = cast(List[nodes.field], node) + for field in fields: + field_name = cast(nodes.field_body, field[0]).astext().strip() + if field_name == 'meta' or field_name.startswith('meta '): + node.remove(field) + break + + class PythonModuleIndex(Index): """ Index subclass to provide the Python module index. @@ -845,7 +1012,7 @@ class PythonModuleIndex(Index): # sort out collapsable modules prev_modname = '' num_toplevels = 0 - for modname, (docname, synopsis, platforms, deprecated) in modules: + for modname, (docname, node_id, synopsis, platforms, deprecated) in modules: if docnames and docname not in docnames: continue @@ -882,8 +1049,7 @@ class PythonModuleIndex(Index): qualifier = _('Deprecated') if deprecated else '' entries.append(IndexEntry(stripped + modname, subtype, docname, - 'module-' + stripped + modname, platforms, - qualifier, synopsis)) + node_id, platforms, qualifier, synopsis)) prev_modname = modname # apply heuristics when to collapse modindex at page load: @@ -947,10 +1113,10 @@ class PythonDomain(Domain): ] @property - def objects(self) -> Dict[str, Tuple[str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, objtype + def objects(self) -> Dict[str, Tuple[str, str, str]]: + return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def note_object(self, name: str, objtype: str, location: Any = None) -> None: + def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: """Note a python object for cross reference. .. versionadded:: 2.1 @@ -960,38 +1126,40 @@ class PythonDomain(Domain): logger.warning(__('duplicate object description of %s, ' 'other instance in %s, use :noindex: for one of them'), name, docname, location=location) - self.objects[name] = (self.env.docname, objtype) + self.objects[name] = (self.env.docname, node_id, objtype) @property - def modules(self) -> Dict[str, Tuple[str, str, str, bool]]: - return self.data.setdefault('modules', {}) # modname -> docname, synopsis, platform, deprecated # NOQA + def modules(self) -> Dict[str, Tuple[str, str, str, str, bool]]: + return self.data.setdefault('modules', {}) # modname -> docname, node_id, synopsis, platform, deprecated # NOQA - def note_module(self, name: str, synopsis: str, platform: str, deprecated: bool) -> None: + def note_module(self, name: str, node_id: str, synopsis: str, + platform: str, deprecated: bool) -> None: """Note a python module for cross reference. .. versionadded:: 2.1 """ - self.modules[name] = (self.env.docname, synopsis, platform, deprecated) + self.modules[name] = (self.env.docname, node_id, synopsis, platform, deprecated) def clear_doc(self, docname: str) -> None: - for fullname, (fn, _l) in list(self.objects.items()): + for fullname, (fn, _x, _x) in list(self.objects.items()): if fn == docname: del self.objects[fullname] - for modname, (fn, _x, _x, _y) in list(self.modules.items()): + for modname, (fn, _x, _x, _x, _y) in list(self.modules.items()): if fn == docname: del self.modules[modname] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates? - for fullname, (fn, objtype) in otherdata['objects'].items(): + for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): if fn in docnames: - self.objects[fullname] = (fn, objtype) + self.objects[fullname] = (fn, node_id, objtype) for modname, data in otherdata['modules'].items(): if data[0] in docnames: self.modules[modname] = data def find_obj(self, env: BuildEnvironment, modname: str, classname: str, - name: str, type: str, searchmode: int = 0) -> List[Tuple[str, Any]]: + name: str, type: str, searchmode: int = 0 + ) -> List[Tuple[str, Tuple[str, str, str]]]: """Find a Python object for "name", perhaps using the given module and/or classname. Returns a list of (name, object entry) tuples. """ @@ -1002,7 +1170,7 @@ class PythonDomain(Domain): if not name: return [] - matches = [] # type: List[Tuple[str, Any]] + matches = [] # type: List[Tuple[str, Tuple[str, str, str]]] newname = None if searchmode == 1: @@ -1013,20 +1181,20 @@ class PythonDomain(Domain): if objtypes is not None: if modname and classname: fullname = modname + '.' + classname + '.' + name - if fullname in self.objects and self.objects[fullname][1] in objtypes: + if fullname in self.objects and self.objects[fullname][2] in objtypes: newname = fullname if not newname: if modname and modname + '.' + name in self.objects and \ - self.objects[modname + '.' + name][1] in objtypes: + self.objects[modname + '.' + name][2] in objtypes: newname = modname + '.' + name - elif name in self.objects and self.objects[name][1] in objtypes: + elif name in self.objects and self.objects[name][2] in objtypes: newname = name else: # "fuzzy" searching mode searchname = '.' + name matches = [(oname, self.objects[oname]) for oname in self.objects if oname.endswith(searchname) and - self.objects[oname][1] in objtypes] + self.objects[oname][2] in objtypes] else: # NOTE: searching for exact match, object type is not considered if name in self.objects: @@ -1041,14 +1209,6 @@ class PythonDomain(Domain): elif modname and classname and \ modname + '.' + classname + '.' + name in self.objects: newname = modname + '.' + classname + '.' + name - # special case: builtin exceptions have module "exceptions" set - elif type == 'exc' and '.' not in name and \ - 'exceptions.' + name in self.objects: - newname = 'exceptions.' + name - # special case: object methods - elif type in ('func', 'meth') and '.' not in name and \ - 'object.' + name in self.objects: - newname = 'object.' + name if newname is not None: matches.append((newname, self.objects[newname])) return matches @@ -1074,10 +1234,10 @@ class PythonDomain(Domain): type='ref', subtype='python', location=node) name, obj = matches[0] - if obj[1] == 'module': + if obj[2] == 'module': return self._make_module_refnode(builder, fromdocname, name, contnode) else: - return make_refnode(builder, fromdocname, obj[0], name, contnode, name) + return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name) def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element @@ -1089,20 +1249,20 @@ class PythonDomain(Domain): # always search in "refspecific" mode with the :any: role matches = self.find_obj(env, modname, clsname, target, None, 1) for name, obj in matches: - if obj[1] == 'module': + if obj[2] == 'module': results.append(('py:mod', self._make_module_refnode(builder, fromdocname, name, contnode))) else: - results.append(('py:' + self.role_for_objtype(obj[1]), - make_refnode(builder, fromdocname, obj[0], name, + results.append(('py:' + self.role_for_objtype(obj[2]), + make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))) return results def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, contnode: Node) -> Element: # get additional info for modules - docname, synopsis, platform, deprecated = self.modules[name] + docname, node_id, synopsis, platform, deprecated = self.modules[name] title = name if synopsis: title += ': ' + synopsis @@ -1110,15 +1270,14 @@ class PythonDomain(Domain): title += _(' (deprecated)') if platform: title += ' (' + platform + ')' - return make_refnode(builder, fromdocname, docname, - 'module-' + name, contnode, title) + return make_refnode(builder, fromdocname, docname, node_id, contnode, title) def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: for modname, info in self.modules.items(): - yield (modname, modname, 'module', info[0], 'module-' + modname, 0) - for refname, (docname, type) in self.objects.items(): + yield (modname, modname, 'module', info[0], info[1], 0) + for refname, (docname, node_id, type) in self.objects.items(): if type != 'module': # modules are already handled - yield (refname, refname, type, docname, refname, 1) + yield (refname, refname, type, docname, node_id, 1) def get_full_qualified_name(self, node: Element) -> str: modname = node.get('py:module') @@ -1130,12 +1289,41 @@ class PythonDomain(Domain): return '.'.join(filter(None, [modname, clsname, target])) +def builtin_resolver(app: Sphinx, env: BuildEnvironment, + node: pending_xref, contnode: Element) -> Element: + """Do not emit nitpicky warnings for built-in types.""" + def istyping(s: str) -> bool: + if s.startswith('typing.'): + s = s.split('.', 1)[1] + + return s in typing.__all__ # type: ignore + + if node.get('refdomain') != 'py': + return None + elif node.get('reftype') == 'obj' and node.get('reftarget') == 'None': + return contnode + elif node.get('reftype') in ('class', 'exc'): + reftarget = node.get('reftarget') + if inspect.isclass(getattr(builtins, reftarget, None)): + # built-in class + return contnode + elif istyping(reftarget): + # typing class + return contnode + + return None + + def setup(app: Sphinx) -> Dict[str, Any]: + app.setup_extension('sphinx.directives') + app.add_domain(PythonDomain) + app.connect('object-description-transform', filter_meta_fields) + app.connect('missing-reference', builtin_resolver, priority=900) return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 81287c815..e25b31936 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -25,7 +25,7 @@ from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import logging -from sphinx.util.nodes import make_refnode +from sphinx.util.nodes import make_id, make_refnode logger = logging.getLogger(__name__) @@ -39,24 +39,35 @@ class ReSTMarkup(ObjectDescription): """ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: - targetname = self.objtype + '-' + name - if targetname not in self.state.document.ids: - signode['names'].append(targetname) - signode['ids'].append(targetname) - signode['first'] = (not self.names) - self.state.document.note_explicit_target(signode) + node_id = make_id(self.env, self.state.document, self.objtype, name) + signode['ids'].append(node_id) - domain = cast(ReSTDomain, self.env.get_domain('rst')) - domain.note_object(self.objtype, name, location=(self.env.docname, self.lineno)) + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(name) + if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']: + signode['ids'].append(old_node_id) + + self.state.document.note_explicit_target(signode) + + domain = cast(ReSTDomain, self.env.get_domain('rst')) + domain.note_object(self.objtype, name, node_id, location=signode) indextext = self.get_index_text(self.objtype, name) if indextext: - self.indexnode['entries'].append(('single', indextext, - targetname, '', None)) + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) def get_index_text(self, objectname: str, name: str) -> str: return '' + def make_old_id(self, name: str) -> str: + """Generate old styled node_id for reST markups. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return self.objtype + '-' + name + def parse_directive(d: str) -> Tuple[str, str]: """Parse a directive signature. @@ -128,27 +139,37 @@ class ReSTDirectiveOption(ReSTMarkup): return name def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: + domain = cast(ReSTDomain, self.env.get_domain('rst')) + directive_name = self.current_directive - targetname = '-'.join([self.objtype, self.current_directive, name]) - if targetname not in self.state.document.ids: - signode['names'].append(targetname) - signode['ids'].append(targetname) - signode['first'] = (not self.names) - self.state.document.note_explicit_target(signode) + if directive_name: + prefix = '-'.join([self.objtype, directive_name]) + objname = ':'.join([directive_name, name]) + else: + prefix = self.objtype + objname = name + + node_id = make_id(self.env, self.state.document, prefix, name) + signode['ids'].append(node_id) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(name) + if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']: + signode['ids'].append(old_node_id) - objname = ':'.join(filter(None, [directive_name, name])) - domain = cast(ReSTDomain, self.env.get_domain('rst')) - domain.note_object(self.objtype, objname, location=(self.env.docname, self.lineno)) + self.state.document.note_explicit_target(signode) + domain.note_object(self.objtype, objname, node_id, location=signode) if directive_name: key = name[0].upper() pair = [_('%s (directive)') % directive_name, _(':%s: (directive option)') % name] - self.indexnode['entries'].append(('pair', '; '.join(pair), targetname, '', key)) + self.indexnode['entries'].append(('pair', '; '.join(pair), node_id, '', key)) else: key = name[0].upper() text = _(':%s: (directive option)') % name - self.indexnode['entries'].append(('single', text, targetname, '', key)) + self.indexnode['entries'].append(('single', text, node_id, '', key)) @property def current_directive(self) -> str: @@ -158,6 +179,14 @@ class ReSTDirectiveOption(ReSTMarkup): else: return '' + def make_old_id(self, name: str) -> str: + """Generate old styled node_id for directive options. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return '-'.join([self.objtype, self.current_directive, name]) + class ReSTRole(ReSTMarkup): """ @@ -195,37 +224,36 @@ class ReSTDomain(Domain): } # type: Dict[str, Dict[Tuple[str, str], str]] @property - def objects(self) -> Dict[Tuple[str, str], str]: - return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname + def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]: + return self.data.setdefault('objects', {}) # (objtype, fullname) -> (docname, node_id) - def note_object(self, objtype: str, name: str, location: Any = None) -> None: + def note_object(self, objtype: str, name: str, node_id: str, location: Any = None) -> None: if (objtype, name) in self.objects: - docname = self.objects[objtype, name] + docname, node_id = self.objects[objtype, name] logger.warning(__('duplicate description of %s %s, other instance in %s') % (objtype, name, docname), location=location) - self.objects[objtype, name] = self.env.docname + self.objects[objtype, name] = (self.env.docname, node_id) def clear_doc(self, docname: str) -> None: - for (typ, name), doc in list(self.objects.items()): + for (typ, name), (doc, node_id) in list(self.objects.items()): if doc == docname: del self.objects[typ, name] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates - for (typ, name), doc in otherdata['objects'].items(): + for (typ, name), (doc, node_id) in otherdata['objects'].items(): if doc in docnames: - self.objects[typ, name] = doc + self.objects[typ, name] = (doc, node_id) def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element ) -> Element: objtypes = self.objtypes_for_role(typ) for objtype in objtypes: - todocname = self.objects.get((objtype, target)) + todocname, node_id = self.objects.get((objtype, target), (None, None)) if todocname: - return make_refnode(builder, fromdocname, todocname, - objtype + '-' + target, + return make_refnode(builder, fromdocname, todocname, node_id, contnode, target + ' ' + objtype) return None @@ -234,17 +262,16 @@ class ReSTDomain(Domain): ) -> List[Tuple[str, Element]]: results = [] # type: List[Tuple[str, Element]] for objtype in self.object_types: - todocname = self.objects.get((objtype, target)) + todocname, node_id = self.objects.get((objtype, target), (None, None)) if todocname: results.append(('rst:' + self.role_for_objtype(objtype), - make_refnode(builder, fromdocname, todocname, - objtype + '-' + target, + make_refnode(builder, fromdocname, todocname, node_id, contnode, target + ' ' + objtype))) return results def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: - for (typ, name), docname in self.data['objects'].items(): - yield name, name, typ, docname, typ + '-' + name, 1 + for (typ, name), (docname, node_id) in self.data['objects'].items(): + yield name, name, typ, docname, node_id, 1 def setup(app: Sphinx) -> Dict[str, Any]: @@ -252,7 +279,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 4e46c1551..d820cfe5c 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -22,10 +22,9 @@ from docutils.statemachine import StringList from sphinx import addnodes from sphinx.addnodes import desc_signature, pending_xref -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType -from sphinx.errors import NoUri from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import ws_re, logging, docname_join @@ -63,13 +62,21 @@ class GenericObject(ObjectDescription): signode.clear() signode += addnodes.desc_name(sig, sig) # normalize whitespace like XRefRole does - name = ws_re.sub('', sig) + name = ws_re.sub(' ', sig) return name def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: - targetname = '%s-%s' % (self.objtype, name) - signode['ids'].append(targetname) + node_id = make_id(self.env, self.state.document, self.objtype, name) + signode['ids'].append(node_id) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(name) + if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']: + signode['ids'].append(old_node_id) + self.state.document.note_explicit_target(signode) + if self.indextemplate: colon = self.indextemplate.find(':') if colon != -1: @@ -78,11 +85,18 @@ class GenericObject(ObjectDescription): else: indextype = 'single' indexentry = self.indextemplate % (name,) - self.indexnode['entries'].append((indextype, indexentry, - targetname, '', None)) + self.indexnode['entries'].append((indextype, indexentry, node_id, '', None)) std = cast(StandardDomain, self.env.get_domain('std')) - std.add_object(self.objtype, name, self.env.docname, targetname) + std.note_object(self.objtype, name, node_id, location=signode) + + def make_old_id(self, name: str) -> str: + """Generate old styled node_id for generic objects. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return self.objtype + '-' + name class EnvVar(GenericObject): @@ -125,8 +139,16 @@ class Target(SphinxDirective): def run(self) -> List[Node]: # normalize whitespace in fullname like XRefRole does fullname = ws_re.sub(' ', self.arguments[0].strip()) - targetname = '%s-%s' % (self.name, fullname) - node = nodes.target('', '', ids=[targetname]) + node_id = make_id(self.env, self.state.document, self.name, fullname) + node = nodes.target('', '', ids=[node_id]) + self.set_source_info(node) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(fullname) + if old_node_id not in self.state.document.ids and old_node_id not in node['ids']: + node['ids'].append(old_node_id) + self.state.document.note_explicit_target(node) ret = [node] # type: List[Node] if self.indextemplate: @@ -136,18 +158,25 @@ class Target(SphinxDirective): if colon != -1: indextype = indexentry[:colon].strip() indexentry = indexentry[colon + 1:].strip() - inode = addnodes.index(entries=[(indextype, indexentry, - targetname, '', None)]) + inode = addnodes.index(entries=[(indextype, indexentry, node_id, '', None)]) ret.insert(0, inode) name = self.name if ':' in self.name: _, name = self.name.split(':', 1) std = cast(StandardDomain, self.env.get_domain('std')) - std.add_object(name, fullname, self.env.docname, targetname) + std.note_object(name, fullname, node_id, location=node) return ret + def make_old_id(self, name: str) -> str: + """Generate old styled node_id for targets. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return self.name + '-' + name + class Cmdoption(ObjectDescription): """ @@ -165,7 +194,7 @@ class Cmdoption(ObjectDescription): logger.warning(__('Malformed option description %r, should ' 'look like "opt", "-opt args", "--opt args", ' '"/opt args" or "+opt args"'), potential_option, - location=(self.env.docname, self.lineno)) + location=signode) continue optname, args = m.groups() if count: @@ -185,16 +214,18 @@ class Cmdoption(ObjectDescription): def add_target_and_index(self, firstname: str, sig: str, signode: desc_signature) -> None: currprogram = self.env.ref_context.get('std:program') for optname in signode.get('allnames', []): - targetname = optname.replace('/', '-') - if not targetname.startswith('-'): - targetname = '-arg-' + targetname + prefixes = ['cmdoption'] if currprogram: - targetname = '-' + currprogram + targetname - targetname = 'cmdoption' + targetname - signode['names'].append(targetname) + prefixes.append(currprogram) + if not optname.startswith(('-', '/')): + prefixes.append('arg') + prefix = '-'.join(prefixes) + node_id = make_id(self.env, self.state.document, prefix, optname) + signode['ids'].append(node_id) - domain = cast(StandardDomain, self.env.get_domain('std')) self.state.document.note_explicit_target(signode) + + domain = cast(StandardDomain, self.env.get_domain('std')) for optname in signode.get('allnames', []): domain.add_program_option(currprogram, optname, self.env.docname, signode['ids'][0]) @@ -274,7 +305,7 @@ def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index term['ids'].append(node_id) std = cast(StandardDomain, env.get_domain('std')) - std.add_object('term', termtext.lower(), env.docname, node_id) + std.note_object('term', termtext.lower(), node_id, location=term) # add an index entry too indexnode = addnodes.index() @@ -406,7 +437,9 @@ class Glossary(SphinxDirective): return messages + [node] -def token_xrefs(text: str) -> List[Node]: +def token_xrefs(text: str, productionGroup: str = '') -> List[Node]: + if len(productionGroup) != 0: + productionGroup += ':' retnodes = [] # type: List[Node] pos = 0 for m in token_re.finditer(text): @@ -414,7 +447,7 @@ def token_xrefs(text: str) -> List[Node]: txt = text[pos:m.start()] retnodes.append(nodes.Text(txt, txt)) refnode = pending_xref(m.group(1), reftype='token', refdomain='std', - reftarget=m.group(1)) + reftarget=productionGroup + m.group(1)) refnode += nodes.literal(m.group(1), m.group(1), classes=['xref']) retnodes.append(refnode) pos = m.end() @@ -437,11 +470,16 @@ class ProductionList(SphinxDirective): def run(self) -> List[Node]: domain = cast(StandardDomain, self.env.get_domain('std')) node = addnodes.productionlist() # type: Element - i = 0 + self.set_source_info(node) + # The backslash handling is from ObjectDescription.get_signatures + nl_escape_re = re.compile(r'\\\n') + lines = nl_escape_re.sub('', self.arguments[0]).split('\n') - for rule in self.arguments[0].split('\n'): + productionGroup = "" + i = 0 + for rule in lines: if i == 0 and ':' not in rule: - # production group + productionGroup = rule.strip() continue i += 1 try: @@ -451,15 +489,48 @@ class ProductionList(SphinxDirective): subnode = addnodes.production(rule) subnode['tokenname'] = name.strip() if subnode['tokenname']: - idname = nodes.make_id('grammar-token-%s' % subnode['tokenname']) - if idname not in self.state.document.ids: - subnode['ids'].append(idname) + prefix = 'grammar-token-%s' % productionGroup + node_id = make_id(self.env, self.state.document, prefix, name) + subnode['ids'].append(node_id) + + # Assign old styled node_id not to break old hyperlinks (if possible) + # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) + old_node_id = self.make_old_id(name) + if (old_node_id not in self.state.document.ids and + old_node_id not in subnode['ids']): + subnode['ids'].append(old_node_id) + self.state.document.note_implicit_target(subnode, subnode) - domain.add_object('token', subnode['tokenname'], self.env.docname, idname) - subnode.extend(token_xrefs(tokens)) + + if len(productionGroup) != 0: + objName = "%s:%s" % (productionGroup, name) + else: + objName = name + domain.note_object('token', objName, node_id, location=node) + subnode.extend(token_xrefs(tokens, productionGroup)) node.append(subnode) return [node] + def make_old_id(self, token: str) -> str: + """Generate old styled node_id for tokens. + + .. note:: Old Styled node_id was used until Sphinx-3.0. + This will be removed in Sphinx-5.0. + """ + return nodes.make_id('grammar-token-' + token) + + +class TokenXRefRole(XRefRole): + def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool, + title: str, target: str) -> Tuple[str, str]: + target = target.lstrip('~') # a title-specific thing + if not self.has_explicit_title and title[0] == '~': + if ':' in title: + _, title = title.split(':') + else: + title = title[1:] + return title, target + class StandardDomain(Domain): """ @@ -492,7 +563,7 @@ class StandardDomain(Domain): 'option': OptionXRefRole(warn_dangling=True), 'envvar': EnvVarXRefRole(), # links to tokens in grammar productions - 'token': XRefRole(), + 'token': TokenXRefRole(), # links to terms in glossary 'term': XRefRole(lowercase=True, innernodeclass=nodes.inline, warn_dangling=True), @@ -547,10 +618,51 @@ class StandardDomain(Domain): for node, settings in env.app.registry.enumerable_nodes.items(): self.enumerable_nodes[node] = settings + def note_hyperlink_target(self, name: str, docname: str, node_id: str, + title: str = '') -> None: + """Add a hyperlink target for cross reference. + + .. warning:: + + This is only for internal use. Please don't use this from your extension. + ``document.note_explicit_target()`` or ``note_implicit_target()`` are recommended to + add a hyperlink target to the document. + + This only adds a hyperlink target to the StandardDomain. And this does not add a + node_id to node. Therefore, it is very fragile to calling this without + understanding hyperlink target framework in both docutils and Sphinx. + + .. versionadded:: 3.0 + """ + if name in self.anonlabels and self.anonlabels[name] != (docname, node_id): + logger.warning(__('duplicate label %s, other instance in %s'), + name, self.env.doc2path(self.anonlabels[name][0])) + + self.anonlabels[name] = (docname, node_id) + if title: + self.labels[name] = (docname, node_id, title) + @property def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid + def note_object(self, objtype: str, name: str, labelid: str, location: Any = None + ) -> None: + """Note a generic object for cross reference. + + .. versionadded:: 3.0 + """ + if (objtype, name) in self.objects: + docname = self.objects[objtype, name][0] + logger.warning(__('duplicate %s description of %s, other instance in %s'), + objtype, name, docname, location=location) + self.objects[objtype, name] = (self.env.docname, labelid) + + def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: + warnings.warn('StandardDomain.add_object() is deprecated.', + RemovedInSphinx50Warning) + self.objects[objtype, name] = (docname, labelid) + @property def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid @@ -632,9 +744,6 @@ class StandardDomain(Domain): continue self.labels[name] = docname, labelid, sectname - def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: - self.objects[objtype, name] = (docname, labelid) - def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None: self.progoptions[program, name] = (docname, labelid) @@ -814,30 +923,6 @@ class StandardDomain(Domain): return make_refnode(builder, fromdocname, docname, labelid, contnode) - def _resolve_citation_xref(self, env: "BuildEnvironment", fromdocname: str, - builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: - warnings.warn('StandardDomain._resolve_citation_xref() is deprecated.', - RemovedInSphinx30Warning) - docname, labelid, lineno = self.data['citations'].get(target, ('', '', 0)) - if not docname: - if 'ids' in node: - # remove ids attribute that annotated at - # transforms.CitationReference.apply. - del node['ids'][:] - return None - - try: - return make_refnode(builder, fromdocname, docname, - labelid, contnode) - except NoUri: - # remove the ids we added in the CitationReferences - # transform since they can't be transfered to - # the contnode (if it's a Text node) - if not isinstance(contnode, Element): - del node['ids'][:] - raise - def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: @@ -934,16 +1019,6 @@ class StandardDomain(Domain): figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) return figtype - def get_figtype(self, node: Node) -> str: - """Get figure type of nodes. - - .. deprecated:: 1.8 - """ - warnings.warn('StandardDomain.get_figtype() is deprecated. ' - 'Please use get_enumerable_node_type() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.get_enumerable_node_type(node) - def get_fignumber(self, env: "BuildEnvironment", builder: "Builder", figtype: str, docname: str, target_node: Element) -> Tuple[int, ...]: if figtype == 'section': diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index f1ae6024d..c8735461d 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -13,9 +13,8 @@ import pickle import warnings from collections import defaultdict from copy import copy -from io import BytesIO from os import path -from typing import Any, Callable, Dict, Generator, IO, Iterator, List, Set, Tuple, Union +from typing import Any, Callable, Dict, Generator, Iterator, List, Set, Tuple, Union from typing import cast from docutils import nodes @@ -23,9 +22,7 @@ from docutils.nodes import Node from sphinx import addnodes from sphinx.config import Config -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.domains import Domain from sphinx.environment.adapters.toctree import TocTree from sphinx.errors import SphinxError, BuildEnvironmentError, DocumentError, ExtensionError @@ -221,6 +218,10 @@ class BuildEnvironment: for domain in app.registry.create_domains(self): self.domains[domain.name] = domain + # setup domains (must do after all initialization) + for domain in self.domains.values(): + domain.setup() + # initialize config self._update_config(app.config) @@ -331,7 +332,7 @@ class BuildEnvironment: if suffix: warnings.warn('The suffix argument for doc2path() is deprecated.', RemovedInSphinx40Warning) - if base not in (True, None): + if base not in (True, False, None): warnings.warn('The string style base argument for doc2path() is deprecated.', RemovedInSphinx40Warning) @@ -640,113 +641,6 @@ class BuildEnvironment: domain.check_consistency() self.events.emit('env-check-consistency', self) - # --------- METHODS FOR COMPATIBILITY -------------------------------------- - - def update(self, config: Config, srcdir: str, doctreedir: str) -> List[str]: - warnings.warn('env.update() is deprecated. Please use builder.read() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.app.builder.read() - - def _read_serial(self, docnames: List[str], app: "Sphinx") -> None: - warnings.warn('env._read_serial() is deprecated. Please use builder.read() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.app.builder._read_serial(docnames) - - def _read_parallel(self, docnames: List[str], app: "Sphinx", nproc: int) -> None: - warnings.warn('env._read_parallel() is deprecated. Please use builder.read() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.app.builder._read_parallel(docnames, nproc) - - def read_doc(self, docname: str, app: "Sphinx" = None) -> None: - warnings.warn('env.read_doc() is deprecated. Please use builder.read_doc() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.app.builder.read_doc(docname) - - def write_doctree(self, docname: str, doctree: nodes.document) -> None: - warnings.warn('env.write_doctree() is deprecated. ' - 'Please use builder.write_doctree() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.app.builder.write_doctree(docname, doctree) - - @property - def _nitpick_ignore(self) -> List[str]: - warnings.warn('env._nitpick_ignore is deprecated. ' - 'Please use config.nitpick_ignore instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.config.nitpick_ignore - - @staticmethod - def load(f: IO, app: "Sphinx" = None) -> "BuildEnvironment": - warnings.warn('BuildEnvironment.load() is deprecated. ' - 'Please use pickle.load() instead.', - RemovedInSphinx30Warning, stacklevel=2) - try: - env = pickle.load(f) - except Exception as exc: - # This can happen for example when the pickle is from a - # different version of Sphinx. - raise OSError(exc) - if app: - env.app = app - env.config.values = app.config.values - return env - - @classmethod - def loads(cls, string: bytes, app: "Sphinx" = None) -> "BuildEnvironment": - warnings.warn('BuildEnvironment.loads() is deprecated. ' - 'Please use pickle.loads() instead.', - RemovedInSphinx30Warning, stacklevel=2) - io = BytesIO(string) - return cls.load(io, app) - - @classmethod - def frompickle(cls, filename: str, app: "Sphinx") -> "BuildEnvironment": - warnings.warn('BuildEnvironment.frompickle() is deprecated. ' - 'Please use pickle.load() instead.', - RemovedInSphinx30Warning, stacklevel=2) - with open(filename, 'rb') as f: - return cls.load(f, app) - - @staticmethod - def dump(env: "BuildEnvironment", f: IO) -> None: - warnings.warn('BuildEnvironment.dump() is deprecated. ' - 'Please use pickle.dump() instead.', - RemovedInSphinx30Warning, stacklevel=2) - pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) - - @classmethod - def dumps(cls, env: "BuildEnvironment") -> bytes: - warnings.warn('BuildEnvironment.dumps() is deprecated. ' - 'Please use pickle.dumps() instead.', - RemovedInSphinx30Warning, stacklevel=2) - io = BytesIO() - cls.dump(env, io) - return io.getvalue() - - def topickle(self, filename: str) -> None: - warnings.warn('env.topickle() is deprecated. ' - 'Please use pickle.dump() instead.', - RemovedInSphinx30Warning, stacklevel=2) - with open(filename, 'wb') as f: - self.dump(self, f) - - @property - def versionchanges(self) -> Dict[str, List[Tuple[str, str, int, str, str, str]]]: - warnings.warn('env.versionchanges() is deprecated. ' - 'Please use ChangeSetDomain instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.domaindata['changeset']['changes'] - - def note_versionchange(self, type: str, version: str, - node: addnodes.versionmodified, lineno: int) -> None: - warnings.warn('env.note_versionchange() is deprecated. ' - 'Please use ChangeSetDomain.note_changeset() instead.', - RemovedInSphinx30Warning, stacklevel=2) - node['type'] = type - node['version'] = version - node.line = lineno - self.get_domain('changeset').note_changeset(node) # type: ignore - @property def indexentries(self) -> Dict[str, List[Tuple[str, str, str, str, str]]]: warnings.warn('env.indexentries() is deprecated. Please use IndexDomain instead.', @@ -762,13 +656,3 @@ class BuildEnvironment: from sphinx.domains.index import IndexDomain domain = cast(IndexDomain, self.get_domain('index')) domain.data['entries'] = entries - - -from sphinx.errors import NoUri # NOQA - - -deprecated_alias('sphinx.environment', - { - 'NoUri': NoUri, - }, - RemovedInSphinx30Warning) diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 1d135bd32..5af213932 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -7,7 +7,7 @@ :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import bisect + import re import unicodedata from itertools import groupby @@ -52,8 +52,7 @@ class IndexEntries: except NoUri: pass else: - # maintain links in sorted/deterministic order - bisect.insort(entry[0], (main, uri)) + entry[0].append((main, uri)) domain = cast(IndexDomain, self.env.get_domain('index')) for fn, entries in domain.entries.items(): @@ -89,6 +88,16 @@ class IndexEntries: except ValueError as err: logger.warning(str(err), location=fn) + # sort the index entries for same keyword. + def keyfunc0(entry: Tuple[str, str]) -> Tuple[bool, str]: + main, uri = entry + return (not main, uri) # show main entries at first + + for indexentry in new.values(): + indexentry[0].sort(key=keyfunc0) + for subentry in indexentry[1].values(): + subentry[0].sort(key=keyfunc0) # type: ignore + # sort the index entries; put all symbols at the front, even those # following the letters in ASCII, this is where the chr(127) comes from def keyfunc(entry: Tuple[str, List]) -> Tuple[str, str]: diff --git a/sphinx/events.py b/sphinx/events.py index e6ea379eb..ff49f290c 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -11,8 +11,9 @@ """ import warnings -from collections import OrderedDict, defaultdict -from typing import Any, Callable, Dict, List +from collections import defaultdict +from operator import attrgetter +from typing import Any, Callable, Dict, List, NamedTuple from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError @@ -26,6 +27,10 @@ if False: logger = logging.getLogger(__name__) +EventListener = NamedTuple('EventListener', [('id', int), + ('handler', Callable), + ('priority', int)]) + # List of all known core events. Maps name to arguments description. core_events = { @@ -57,7 +62,7 @@ class EventManager: RemovedInSphinx40Warning) self.app = app self.events = core_events.copy() - self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] + self.listeners = defaultdict(list) # type: Dict[str, List[EventListener]] self.next_listener_id = 0 def add(self, name: str) -> None: @@ -66,20 +71,22 @@ class EventManager: raise ExtensionError(__('Event %r already present') % name) self.events[name] = '' - def connect(self, name: str, callback: Callable) -> int: + def connect(self, name: str, callback: Callable, priority: int) -> int: """Connect a handler to specific event.""" if name not in self.events: raise ExtensionError(__('Unknown event name: %s') % name) listener_id = self.next_listener_id self.next_listener_id += 1 - self.listeners[name][listener_id] = callback + self.listeners[name].append(EventListener(listener_id, callback, priority)) return listener_id def disconnect(self, listener_id: int) -> None: """Disconnect a handler.""" - for event in self.listeners.values(): - event.pop(listener_id, None) + for listeners in self.listeners.values(): + for listener in listeners[:]: + if listener.id == listener_id: + listeners.remove(listener) def emit(self, name: str, *args: Any) -> List: """Emit a Sphinx event.""" @@ -91,12 +98,13 @@ class EventManager: pass results = [] - for callback in self.listeners[name].values(): + listeners = sorted(self.listeners[name], key=attrgetter("priority")) + for listener in listeners: if self.app is None: # for compatibility; RemovedInSphinx40Warning - results.append(callback(*args)) + results.append(listener.handler(*args)) else: - results.append(callback(self.app, *args)) + results.append(listener.handler(self.app, *args)) return results def emit_firstresult(self, name: str, *args: Any) -> Any: diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index a9196d3a6..23be0a00a 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -175,6 +175,7 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files: 'separatemodules': opts.separatemodules, 'automodule_options': options, 'show_headings': not opts.noheadings, + 'maxdepth': opts.maxdepth, } text = ReSTRenderer([user_template_dir, template_dir]).render('package.rst_t', context) write_file(pkgname, text, opts) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 08e4e5301..7b6b3e4aa 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -13,17 +13,17 @@ import importlib import re import warnings +from inspect import Parameter from types import ModuleType -from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union +from unittest.mock import patch from docutils.statemachine import StringList import sphinx from sphinx.application import Sphinx -from sphinx.config import Config, ENUM -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.config import ENUM +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc.importer import import_object, get_module_members, get_object_members from sphinx.ext.autodoc.mock import mock @@ -32,7 +32,7 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect from sphinx.util import logging from sphinx.util import rpartition -from sphinx.util.docstrings import prepare_docstring +from sphinx.util.docstrings import extract_metadata, prepare_docstring from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature from sphinx.util.typing import stringify as stringify_typehint @@ -84,6 +84,14 @@ def members_set_option(arg: Any) -> Union[object, Set[str]]: return {x.strip() for x in arg.split(',') if x.strip()} +def inherited_members_option(arg: Any) -> Union[object, Set[str]]: + """Used to convert the :members: option to auto directives.""" + if arg is None: + return 'object' + else: + return arg + + SUPPRESS = object() @@ -258,11 +266,14 @@ class Documenter: @property def documenters(self) -> Dict[str, "Type[Documenter]"]: """Returns registered Documenter classes""" - return get_documenters(self.env.app) + return self.env.app.registry.documenters def add_line(self, line: str, source: str, *lineno: int) -> None: """Append one line of generated reST to the output.""" - self.directive.result.append(self.indent + line, source, *lineno) + if line.strip(): # not a blank line + self.directive.result.append(self.indent + line, source, *lineno) + else: + self.directive.result.append('', source, *lineno) def resolve_name(self, modname: str, parents: Any, path: str, base: Any ) -> Tuple[str, List[str]]: @@ -516,6 +527,17 @@ class Documenter: The user can override the skipping decision by connecting to the ``autodoc-skip-member`` event. """ + def is_filtered_inherited_member(name: str) -> bool: + if inspect.isclass(self.object): + for cls in self.object.__mro__: + if cls.__name__ == self.options.inherited_members and cls != self.object: + # given member is a member of specified *super class* + return True + elif name in cls.__dict__: + return False + + return False + ret = [] # search for members in source code too @@ -545,32 +567,45 @@ class Documenter: doc = None has_doc = bool(doc) + metadata = extract_metadata(doc) + if 'private' in metadata: + # consider a member private if docstring has "private" metadata + isprivate = True + else: + isprivate = membername.startswith('_') + keep = False if want_all and membername.startswith('__') and \ membername.endswith('__') and len(membername) > 4: # special __methods__ - if self.options.special_members is ALL and \ - membername != '__doc__': - keep = has_doc or self.options.undoc_members - elif self.options.special_members and \ - self.options.special_members is not ALL and \ - membername in self.options.special_members: - keep = has_doc or self.options.undoc_members + if self.options.special_members is ALL: + if membername == '__doc__': + keep = False + elif is_filtered_inherited_member(membername): + keep = False + else: + keep = has_doc or self.options.undoc_members + elif self.options.special_members: + if membername in self.options.special_members: + keep = has_doc or self.options.undoc_members elif (namespace, membername) in attr_docs: - if want_all and membername.startswith('_'): + if want_all and isprivate: # ignore members whose name starts with _ by default keep = self.options.private_members else: # keep documented attributes keep = True isattr = True - elif want_all and membername.startswith('_'): + elif want_all and isprivate: # ignore members whose name starts with _ by default keep = self.options.private_members and \ (has_doc or self.options.undoc_members) else: - # ignore undocumented members if :undoc-members: is not given - keep = has_doc or self.options.undoc_members + if self.options.members is ALL and is_filtered_inherited_member(membername): + keep = False + else: + # ignore undocumented members if :undoc-members: is not given + keep = has_doc or self.options.undoc_members # give the user a chance to decide whether this member # should be skipped @@ -583,7 +618,7 @@ class Documenter: if skip_user is not None: keep = not skip_user except Exception as exc: - logger.warning(__('autodoc: failed to determine %r to be documented.' + logger.warning(__('autodoc: failed to determine %r to be documented, ' 'the following exception was raised:\n%s'), member, exc, type='autodoc') keep = False @@ -744,7 +779,7 @@ class ModuleDocumenter(Documenter): option_spec = { 'members': members_option, 'undoc-members': bool_option, - 'noindex': bool_option, 'inherited-members': bool_option, + 'noindex': bool_option, 'inherited-members': inherited_members_option, 'show-inheritance': bool_option, 'synopsis': identity, 'platform': identity, 'deprecated': bool_option, 'member-order': identity, 'exclude-members': members_set_option, @@ -973,10 +1008,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) def format_args(self, **kwargs: Any) -> str: - if self.env.config.autodoc_typehints == 'none': + if self.env.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) - if inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): + if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and + not inspect.is_cython_function_or_method(self.object)): # cannot introspect arguments of a C function or method return None try: @@ -1011,8 +1047,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ sig = inspect.signature(self.object.__init__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) - # escape backslashes for reST - args = args.replace('\\', '\\\\') + if self.env.config.strip_signature_backslash: + # escape backslashes for reST + args = args.replace('\\', '\\\\') return args def document_members(self, all_members: bool = False) -> None: @@ -1026,6 +1063,63 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ self.add_line(' :async:', sourcename) +class SingledispatchFunctionDocumenter(FunctionDocumenter): + """ + Specialized Documenter subclass for singledispatch'ed functions. + """ + objtype = 'singledispatch_function' + directivetype = 'function' + member_order = 30 + + # before FunctionDocumenter + priority = FunctionDocumenter.priority + 1 + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return (super().can_document_member(member, membername, isattr, parent) and + inspect.is_singledispatch_function(member)) + + def add_directive_header(self, sig: str) -> None: + sourcename = self.get_sourcename() + + # intercept generated directive headers + # TODO: It is very hacky to use mock to intercept header generation + with patch.object(self, 'add_line') as add_line: + super().add_directive_header(sig) + + # output first line of header + self.add_line(*add_line.call_args_list[0][0]) + + # inserts signature of singledispatch'ed functions + for typ, func in self.object.registry.items(): + if typ is object: + pass # default implementation. skipped. + else: + self.annotate_to_first_argument(func, typ) + + documenter = FunctionDocumenter(self.directive, '') + documenter.object = func + self.add_line(' %s%s' % (self.format_name(), + documenter.format_signature()), + sourcename) + + # output remains of directive header + for call in add_line.call_args_list[1:]: + self.add_line(*call[0]) + + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + """Annotate type hint to the first argument of function if needed.""" + sig = inspect.signature(func) + if len(sig.parameters) == 0: + return + + params = list(sig.parameters.values()) + if params[0].annotation is Parameter.empty: + params[0] = params[0].replace(annotation=typ) + func.__signature__ = sig.replace(parameters=params) # type: ignore + + class DecoratorDocumenter(FunctionDocumenter): """ Specialized Documenter subclass for decorator functions. @@ -1035,7 +1129,7 @@ class DecoratorDocumenter(FunctionDocumenter): # must be lower than FunctionDocumenter priority = -1 - def format_args(self, **kwargs): + def format_args(self, **kwargs: Any) -> Any: args = super().format_args(**kwargs) if ',' in args: return args @@ -1051,7 +1145,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: member_order = 20 option_spec = { 'members': members_option, 'undoc-members': bool_option, - 'noindex': bool_option, 'inherited-members': bool_option, + 'noindex': bool_option, 'inherited-members': inherited_members_option, 'show-inheritance': bool_option, 'member-order': identity, 'exclude-members': members_set_option, 'private-members': bool_option, 'special-members': members_option, @@ -1116,7 +1210,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if hasattr(self.object, '__bases__') and len(self.object.__bases__): bases = [':class:`%s`' % b.__name__ if b.__module__ in ('__builtin__', 'builtins') - else ':class:`%s.%s`' % (b.__module__, b.__name__) + else ':class:`%s.%s`' % (b.__module__, b.__qualname__) for b in self.object.__bases__] self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename) @@ -1337,7 +1431,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: if self.env.config.autodoc_typehints == 'none': kwargs.setdefault('show_annotation', False) - if inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): + if ((inspect.isbuiltin(self.object) or inspect.ismethoddescriptor(self.object)) and + not inspect.is_cython_function_or_method(self.object)): # can never get arguments of a C function or method return None if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): @@ -1348,8 +1443,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: sig = inspect.signature(self.object, bound_method=True) args = stringify_signature(sig, **kwargs) - # escape backslashes for reST - args = args.replace('\\', '\\\\') + if self.env.config.strip_signature_backslash: + # escape backslashes for reST + args = args.replace('\\', '\\\\') return args def add_directive_header(self, sig: str) -> None: @@ -1370,6 +1466,67 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: pass +class SingledispatchMethodDocumenter(MethodDocumenter): + """ + Specialized Documenter subclass for singledispatch'ed methods. + """ + objtype = 'singledispatch_method' + directivetype = 'method' + member_order = 50 + + # before MethodDocumenter + priority = MethodDocumenter.priority + 1 + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + if super().can_document_member(member, membername, isattr, parent) and parent.object: + meth = parent.object.__dict__.get(membername) + return inspect.is_singledispatch_method(meth) + else: + return False + + def add_directive_header(self, sig: str) -> None: + sourcename = self.get_sourcename() + + # intercept generated directive headers + # TODO: It is very hacky to use mock to intercept header generation + with patch.object(self, 'add_line') as add_line: + super().add_directive_header(sig) + + # output first line of header + self.add_line(*add_line.call_args_list[0][0]) + + # inserts signature of singledispatch'ed functions + meth = self.parent.__dict__.get(self.objpath[-1]) + for typ, func in meth.dispatcher.registry.items(): + if typ is object: + pass # default implementation. skipped. + else: + self.annotate_to_first_argument(func, typ) + + documenter = MethodDocumenter(self.directive, '') + documenter.object = func + self.add_line(' %s%s' % (self.format_name(), + documenter.format_signature()), + sourcename) + + # output remains of directive header + for call in add_line.call_args_list[1:]: + self.add_line(*call[0]) + + def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + """Annotate type hint to the first argument of function if needed.""" + sig = inspect.signature(func) + if len(sig.parameters) == 1: + return + + params = list(sig.parameters.values()) + if params[1].annotation is Parameter.empty: + params[1] = params[1].replace(annotation=typ) + func.__signature__ = sig.replace(parameters=params) # type: ignore + + class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore """ Specialized Documenter subclass for attributes. @@ -1563,6 +1720,7 @@ class SlotsAttributeDocumenter(AttributeDocumenter): def get_documenters(app: Sphinx) -> Dict[str, "Type[Documenter]"]: """Returns registered Documenter classes""" + warnings.warn("get_documenters() is deprecated.", RemovedInSphinx50Warning) return app.registry.documenters @@ -1575,38 +1733,6 @@ def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any: return safe_getattr(obj, name, *defargs) -def merge_autodoc_default_flags(app: Sphinx, config: Config) -> None: - """This merges the autodoc_default_flags to autodoc_default_options.""" - if not config.autodoc_default_flags: - return - - # Note: this option will be removed in Sphinx-4.0. But I marked this as - # RemovedInSphinx *30* Warning because we have to emit warnings for users - # who will be still in use with Sphinx-3.x. So we should replace this by - # logger.warning() on 3.0.0 release. - warnings.warn('autodoc_default_flags is now deprecated. ' - 'Please use autodoc_default_options instead.', - RemovedInSphinx30Warning, stacklevel=2) - - for option in config.autodoc_default_flags: - if isinstance(option, str): - config.autodoc_default_options[option] = None - else: - logger.warning( - __("Ignoring invalid option in autodoc_default_flags: %r"), - option, type='autodoc' - ) - - -from sphinx.ext.autodoc.mock import _MockImporter # NOQA - -deprecated_alias('sphinx.ext.autodoc', - { - '_MockImporter': _MockImporter, - }, - RemovedInSphinx40Warning) - - def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ModuleDocumenter) app.add_autodocumenter(ClassDocumenter) @@ -1614,8 +1740,10 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(DataDocumenter) app.add_autodocumenter(DataDeclarationDocumenter) app.add_autodocumenter(FunctionDocumenter) + app.add_autodocumenter(SingledispatchFunctionDocumenter) app.add_autodocumenter(DecoratorDocumenter) app.add_autodocumenter(MethodDocumenter) + app.add_autodocumenter(SingledispatchMethodDocumenter) app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(PropertyDocumenter) app.add_autodocumenter(InstanceAttributeDocumenter) @@ -1627,7 +1755,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_default_options', {}, True) app.add_config_value('autodoc_docstring_signature', True, True) app.add_config_value('autodoc_mock_imports', [], True) - app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "none")) + app.add_config_value('autodoc_typehints', "signature", True, + ENUM("signature", "description", "none")) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) app.add_event('autodoc-before-process-signature') @@ -1635,7 +1764,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') - app.connect('config-inited', merge_autodoc_default_flags) app.setup_extension('sphinx.ext.autodoc.type_comment') + app.setup_extension('sphinx.ext.autodoc.typehints') return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index b44bd75b3..3be19f089 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -18,7 +18,7 @@ from docutils.utils import Reporter, assemble_option_dict from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.environment import BuildEnvironment -from sphinx.ext.autodoc import Documenter, Options, get_documenters +from sphinx.ext.autodoc import Documenter, Options from sphinx.util import logging from sphinx.util.docutils import SphinxDirective, switch_source_input from sphinx.util.nodes import nested_parse_with_titles @@ -129,7 +129,7 @@ class AutodocDirective(SphinxDirective): # look up target Documenter objtype = self.name[4:] # strip prefix (auto-). - doccls = get_documenters(self.env.app)[objtype] + doccls = self.env.app.registry.documenters[objtype] # process the options with the selected documenter's option_spec try: @@ -137,7 +137,7 @@ class AutodocDirective(SphinxDirective): except (KeyError, ValueError, TypeError) as exc: # an option is either unknown or has a wrong type logger.error('An option to %s is either unknown or has an invalid value: %s' % - (self.name, exc), location=(source, lineno)) + (self.name, exc), location=(self.env.docname, lineno)) return [] # generate the output diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index c0381d7b4..e98b97915 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -180,12 +180,11 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, from sphinx.ext.autodoc.mock import ( # NOQA - _MockImporter, _MockModule, _MockObject, MockFinder, MockLoader, mock + _MockModule, _MockObject, MockFinder, MockLoader, mock ) deprecated_alias('sphinx.ext.autodoc.importer', { - '_MockImporter': _MockImporter, '_MockModule': _MockModule, '_MockObject': _MockObject, 'MockFinder': MockFinder, diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index 0ee0fddc1..25f50d27e 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -11,13 +11,11 @@ import contextlib import os import sys -import warnings from importlib.abc import Loader, MetaPathFinder from importlib.machinery import ModuleSpec from types import FunctionType, MethodType, ModuleType from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.util import logging logger = logging.getLogger(__name__) @@ -81,15 +79,11 @@ class _MockModule(ModuleType): """Used by autodoc_mock_imports.""" __file__ = os.devnull - def __init__(self, name: str, loader: "_MockImporter" = None) -> None: + def __init__(self, name: str) -> None: super().__init__(name) self.__all__ = [] # type: List[str] self.__path__ = [] # type: List[str] - if loader is not None: - warnings.warn('The loader argument for _MockModule is deprecated.', - RemovedInSphinx30Warning) - def __getattr__(self, name: str) -> _MockObject: return _make_subclass(name, self.__name__)() @@ -97,44 +91,6 @@ class _MockModule(ModuleType): return self.__name__ -class _MockImporter(MetaPathFinder): - def __init__(self, names: List[str]) -> None: - self.names = names - self.mocked_modules = [] # type: List[str] - # enable hook by adding itself to meta_path - sys.meta_path.insert(0, self) - - warnings.warn('_MockImporter is now deprecated.', - RemovedInSphinx30Warning) - - def disable(self) -> None: - # remove `self` from `sys.meta_path` to disable import hook - sys.meta_path = [i for i in sys.meta_path if i is not self] - # remove mocked modules from sys.modules to avoid side effects after - # running auto-documenter - for m in self.mocked_modules: - if m in sys.modules: - del sys.modules[m] - - def find_module(self, name: str, path: Sequence[Union[bytes, str]] = None) -> Any: - # check if name is (or is a descendant of) one of our base_packages - for n in self.names: - if n == name or name.startswith(n + '.'): - return self - return None - - def load_module(self, name: str) -> ModuleType: - if name in sys.modules: - # module has already been imported, return it - return sys.modules[name] - else: - logger.debug('[autodoc] adding a mock module %s!', name) - module = _MockModule(name, self) - sys.modules[name] = module - self.mocked_modules.append(name) - return module - - class MockLoader(Loader): """A loader for mocking.""" def __init__(self, finder: "MockFinder") -> None: diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py index fb0eebfd8..e6a77f24d 100644 --- a/sphinx/ext/autodoc/type_comment.py +++ b/sphinx/ext/autodoc/type_comment.py @@ -14,6 +14,7 @@ from typing import cast import sphinx from sphinx.application import Sphinx +from sphinx.locale import __ from sphinx.pycode.ast import ast from sphinx.pycode.ast import parse as ast_parse from sphinx.pycode.ast import unparse as ast_unparse @@ -128,7 +129,7 @@ def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: if 'return' not in obj.__annotations__: obj.__annotations__['return'] = type_sig.return_annotation except NotImplementedError as exc: # failed to ast.unparse() - logger.warning("Failed to parse type_comment for %r: %s", obj, exc) + logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc) def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 64782dc1c..f917dbdf3 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -18,21 +18,9 @@ from docutils.nodes import Element from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.config import Config, ENUM from sphinx.util import inspect, typing -def config_inited(app: Sphinx, config: Config) -> None: - if config.autodoc_typehints == 'description': - # HACK: override this to make autodoc suppressing typehints in signatures - config.autodoc_typehints = 'none' # type: ignore - - # preserve user settings - app._autodoc_typehints_description = True # type: ignore - else: - app._autodoc_typehints_description = False # type: ignore - - def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, options: Dict, args: str, retann: str) -> None: """Record type hints to env object.""" @@ -53,7 +41,9 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None: if domain != 'py': return - if app._autodoc_typehints_description is False: # type: ignore + if app.config.autodoc_typehints != 'description': + return + if objtype == 'class' and app.config.autoclass_content not in ('init', 'both'): return signature = cast(addnodes.desc_signature, contentnode.parent[0]) @@ -141,10 +131,6 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> No def setup(app: Sphinx) -> Dict[str, Any]: - app.setup_extension('sphinx.ext.autodoc') - app.config.values['autodoc_typehints'] = ('signature', True, - ENUM("signature", "description", "none")) - app.connect('config-inited', config_inited) app.connect('autodoc-process-signature', record_typehints) app.connect('object-description-transform', merge_typehints) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 7a68552df..9c550c622 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -72,10 +72,10 @@ from docutils.statemachine import StringList import sphinx from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.environment.adapters.toctree import TocTree -from sphinx.ext.autodoc import Documenter, get_documenters +from sphinx.ext.autodoc import Documenter from sphinx.ext.autodoc.directive import DocumenterBridge, Options from sphinx.ext.autodoc.importer import import_module from sphinx.ext.autodoc.mock import mock @@ -110,6 +110,8 @@ def process_autosummary_toc(app: Sphinx, doctree: nodes.document) -> None: """Insert items described in autosummary:: to the TOC tree, but do not generate the toctree:: list. """ + warnings.warn('process_autosummary_toc() is deprecated', + RemovedInSphinx50Warning, stacklevel=2) env = app.builder.env crawled = {} @@ -203,7 +205,7 @@ def get_documenter(app: Sphinx, obj: Any, parent: Any) -> "Type[Documenter]": parent_doc = parent_doc_cls(FakeDirective(), "") # Get the corrent documenter class for *obj* - classes = [cls for cls in get_documenters(app).values() + classes = [cls for cls in app.registry.documenters.values() if cls.can_document_member(obj, '', False, parent_doc)] if classes: classes.sort(key=lambda cls: cls.priority) @@ -745,7 +747,8 @@ def process_generate_options(app: Sphinx) -> None: with mock(app.config.autosummary_mock_imports): generate_autosummary_docs(genfiles, builder=app.builder, suffix=suffix, base_path=app.srcdir, - app=app, imported_members=imported_members) + app=app, imported_members=imported_members, + overwrite=app.config.autosummary_generate_overwrite) def setup(app: Sphinx) -> Dict[str, Any]: @@ -765,9 +768,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: texinfo=(autosummary_noop, autosummary_noop)) app.add_directive('autosummary', Autosummary) app.add_role('autolink', AutoLink()) - app.connect('doctree-read', process_autosummary_toc) app.connect('builder-inited', process_generate_options) app.add_config_value('autosummary_generate', [], True, [bool]) + app.add_config_value('autosummary_generate_overwrite', True, False) app.add_config_value('autosummary_mock_imports', lambda config: config.autodoc_mock_imports, 'env') app.add_config_value('autosummary_imported_members', [], False, [bool]) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index a59a97ea7..5a87e4abf 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -24,7 +24,7 @@ import pydoc import re import sys import warnings -from typing import Any, Callable, Dict, List, Set, Tuple +from typing import Any, Callable, Dict, List, NamedTuple, Set, Tuple from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from jinja2.sandbox import SandboxedEnvironment @@ -66,18 +66,25 @@ class DummyApplication: pass +AutosummaryEntry = NamedTuple('AutosummaryEntry', [('name', str), + ('path', str), + ('template', str)]) + + def setup_documenters(app: Any) -> None: from sphinx.ext.autodoc import ( ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, SlotsAttributeDocumenter, DataDeclarationDocumenter, + SingledispatchFunctionDocumenter, ) documenters = [ ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, SlotsAttributeDocumenter, DataDeclarationDocumenter, + SingledispatchFunctionDocumenter, ] # type: List[Type[Documenter]] for documenter in documenters: app.registry.add_documenter(documenter.objtype, documenter) @@ -148,7 +155,7 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, return app.emit_firstresult('autodoc-skip-member', objtype, name, obj, False, {}) except Exception as exc: - logger.warning(__('autosummary: failed to determine %r to be documented.' + logger.warning(__('autosummary: failed to determine %r to be documented, ' 'the following exception was raised:\n%s'), name, exc, type='autosummary') return False @@ -223,7 +230,8 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, suffix: str = '.rst', warn: Callable = None, info: Callable = None, base_path: str = None, builder: Builder = None, template_dir: str = None, - imported_members: bool = False, app: Any = None) -> None: + imported_members: bool = False, app: Any = None, + overwrite: bool = True) -> None: if info: warnings.warn('info argument for generate_autosummary_docs() is deprecated.', RemovedInSphinx40Warning) @@ -259,34 +267,39 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, new_files = [] # write - for name, path, template_name in sorted(set(items), key=str): - if path is None: + for entry in sorted(set(items), key=str): + if entry.path is None: # The corresponding autosummary:: directive did not have # a :toctree: option continue - path = output_dir or os.path.abspath(path) + path = output_dir or os.path.abspath(entry.path) ensuredir(path) try: - name, obj, parent, mod_name = import_by_name(name) + name, obj, parent, mod_name = import_by_name(entry.name) except ImportError as e: - _warn('[autosummary] failed to import %r: %s' % (name, e)) + _warn(__('[autosummary] failed to import %r: %s') % (name, e)) continue - fn = os.path.join(path, name + suffix) - - # skip it if it exists - if os.path.isfile(fn): - continue + content = generate_autosummary_content(name, obj, parent, template, entry.template, + imported_members, app) - new_files.append(fn) + filename = os.path.join(path, name + suffix) + if os.path.isfile(filename): + with open(filename) as f: + old_content = f.read() - with open(fn, 'w') as f: - rendered = generate_autosummary_content(name, obj, parent, - template, template_name, - imported_members, app) - f.write(rendered) + if content == old_content: + continue + elif overwrite: # content has changed + with open(filename, 'w') as f: + f.write(content) + new_files.append(filename) + else: + with open(filename, 'w') as f: + f.write(content) + new_files.append(filename) # descend recursively to new files if new_files: @@ -294,17 +307,18 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, suffix=suffix, warn=warn, info=info, base_path=base_path, builder=builder, template_dir=template_dir, - imported_members=imported_members, app=app) + imported_members=imported_members, app=app, + overwrite=overwrite) # -- Finding documented entries in files --------------------------------------- -def find_autosummary_in_files(filenames: List[str]) -> List[Tuple[str, str, str]]: +def find_autosummary_in_files(filenames: List[str]) -> List[AutosummaryEntry]: """Find out what items are documented in source/*.rst. See `find_autosummary_in_lines`. """ - documented = [] # type: List[Tuple[str, str, str]] + documented = [] # type: List[AutosummaryEntry] for filename in filenames: with open(filename, encoding='utf-8', errors='ignore') as f: lines = f.read().splitlines() @@ -313,7 +327,7 @@ def find_autosummary_in_files(filenames: List[str]) -> List[Tuple[str, str, str] def find_autosummary_in_docstring(name: str, module: Any = None, filename: str = None - ) -> List[Tuple[str, str, str]]: + ) -> List[AutosummaryEntry]: """Find out what items are documented in the given object's docstring. See `find_autosummary_in_lines`. @@ -333,7 +347,7 @@ def find_autosummary_in_docstring(name: str, module: Any = None, filename: str = def find_autosummary_in_lines(lines: List[str], module: Any = None, filename: str = None - ) -> List[Tuple[str, str, str]]: + ) -> List[AutosummaryEntry]: """Find out what items appear in autosummary:: directives in the given lines. @@ -353,7 +367,7 @@ def find_autosummary_in_lines(lines: List[str], module: Any = None, filename: st toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$') - documented = [] # type: List[Tuple[str, str, str]] + documented = [] # type: List[AutosummaryEntry] toctree = None # type: str template = None @@ -387,7 +401,7 @@ def find_autosummary_in_lines(lines: List[str], module: Any = None, filename: st if current_module and \ not name.startswith(current_module + '.'): name = "%s.%s" % (current_module, name) - documented.append((name, toctree, template)) + documented.append(AutosummaryEntry(name, toctree, template)) continue if not line.strip() or line.startswith(base_indent + " "): diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 987cf2919..e8157848f 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -123,7 +123,7 @@ class CoverageBuilder(Builder): op.write(' * %-50s [%9s]\n' % (name, typ)) op.write('\n') - def ignore_pyobj(self, full_name): + def ignore_pyobj(self, full_name: str) -> bool: for exp in self.py_ignorexps: if exp.search(full_name): return True diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py index 02e60cf7e..669baf2f1 100644 --- a/sphinx/ext/duration.py +++ b/sphinx/ext/duration.py @@ -11,8 +11,8 @@ from datetime import datetime, timedelta from itertools import islice from operator import itemgetter +from typing import Any, Dict, List from typing import cast -from typing import Dict, List from docutils import nodes @@ -82,7 +82,7 @@ def on_build_finished(app: Sphinx, error: Exception) -> None: logger.info('%d.%03d %s', d.seconds, d.microseconds / 1000, docname) -def setup(app): +def setup(app: Sphinx) -> Dict[str, Any]: app.add_domain(DurationDomain) app.connect('builder-inited', on_builder_inited) app.connect('source-read', on_source_read) diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index 95d3fe65a..3af93b642 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -38,7 +38,7 @@ class ImagemagickConverter(ImageConverter): subprocess.run(args, stdout=PIPE, stderr=PIPE, check=True) return True except OSError: - logger.warning(__('convert command %r cannot be run.' + logger.warning(__('convert command %r cannot be run, ' 'check the image_converter setting'), self.config.image_converter) return False @@ -62,7 +62,7 @@ class ImagemagickConverter(ImageConverter): subprocess.run(args, stdout=PIPE, stderr=PIPE, check=True) return True except OSError: - logger.warning(__('convert command %r cannot be run.' + logger.warning(__('convert command %r cannot be run, ' 'check the image_converter setting'), self.config.image_converter) return False diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 01803708d..db2a15b14 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -229,7 +229,7 @@ class InheritanceGraph: if module in ('__builtin__', 'builtins'): fullname = cls.__name__ else: - fullname = '%s.%s' % (module, cls.__name__) + fullname = '%s.%s' % (module, cls.__qualname__) if parts == 0: result = fullname else: @@ -247,6 +247,7 @@ class InheritanceGraph: default_graph_attrs = { 'rankdir': 'LR', 'size': '"8.0, 12.0"', + 'bgcolor': 'transparent', } default_node_attrs = { 'shape': 'box', @@ -254,7 +255,8 @@ class InheritanceGraph: 'height': 0.25, 'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, ' 'Arial, Helvetica, sans"', - 'style': '"setlinewidth(0.5)"', + 'style': '"setlinewidth(0.5),filled"', + 'fillcolor': 'white', } default_edge_attrs = { 'arrowsize': 0.5, diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 362583fa2..5aa9c75f0 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -359,7 +359,7 @@ def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: else: config.intersphinx_mapping[key] = (name, (uri, inv)) except Exception as exc: - logger.warning(__('Fail to read intersphinx_mapping[%s], Ignored: %r'), key, exc) + logger.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc) config.intersphinx_mapping.pop(key) diff --git a/sphinx/ext/mathbase.py b/sphinx/ext/mathbase.py deleted file mode 100644 index 99f117bf7..000000000 --- a/sphinx/ext/mathbase.py +++ /dev/null @@ -1,153 +0,0 @@ -""" - sphinx.ext.mathbase - ~~~~~~~~~~~~~~~~~~~ - - Set up math support in source files and LaTeX/text output. - - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import warnings -from typing import Callable, List, Tuple - -from docutils import nodes -from docutils.nodes import Element, Node -from docutils.parsers.rst.roles import math_role as math_role_base - -from sphinx.addnodes import math, math_block as displaymath # NOQA # to keep compatibility -from sphinx.application import Sphinx -from sphinx.builders.latex.nodes import math_reference as eqref # NOQA # to keep compatibility -from sphinx.deprecation import RemovedInSphinx30Warning -from sphinx.directives.patches import MathDirective as MathDirectiveBase -from sphinx.domains.math import MathDomain # NOQA # to keep compatibility -from sphinx.domains.math import MathReferenceRole as EqXRefRole # NOQA # to keep compatibility -from sphinx.writers.html import HTMLTranslator -from sphinx.writers.latex import LaTeXTranslator -from sphinx.writers.manpage import ManualPageTranslator -from sphinx.writers.texinfo import TexinfoTranslator -from sphinx.writers.text import TextTranslator - - -class MathDirective(MathDirectiveBase): - def run(self) -> List[Node]: - warnings.warn('sphinx.ext.mathbase.MathDirective is moved to ' - 'sphinx.directives.patches package.', - RemovedInSphinx30Warning, stacklevel=2) - return super().run() - - -def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): - warnings.warn('sphinx.ext.mathbase.math_role() is deprecated. ' - 'Please use docutils.parsers.rst.roles.math_role() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return math_role_base(role, rawtext, text, lineno, inliner, options, content) - - -def get_node_equation_number(writer: HTMLTranslator, node: nodes.math_block) -> str: - warnings.warn('sphinx.ext.mathbase.get_node_equation_number() is moved to ' - 'sphinx.util.math package.', - RemovedInSphinx30Warning, stacklevel=2) - from sphinx.util.math import get_node_equation_number - return get_node_equation_number(writer, node) - - -def wrap_displaymath(text: str, label: str, numbering: bool) -> str: - warnings.warn('sphinx.ext.mathbase.wrap_displaymath() is moved to ' - 'sphinx.util.math package.', - RemovedInSphinx30Warning, stacklevel=2) - from sphinx.util.math import wrap_displaymath - return wrap_displaymath(text, label, numbering) - - -def is_in_section_title(node: Element) -> bool: - """Determine whether the node is in a section title""" - from sphinx.util.nodes import traverse_parent - - warnings.warn('is_in_section_title() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - for ancestor in traverse_parent(node): - if isinstance(ancestor, nodes.title) and \ - isinstance(ancestor.parent, nodes.section): - return True - return False - - -def latex_visit_math(self: LaTeXTranslator, node: Element) -> None: - warnings.warn('latex_visit_math() is deprecated. ' - 'Please use LaTeXTranslator.visit_math() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math(node) - - -def latex_visit_displaymath(self: LaTeXTranslator, node: Element) -> None: - warnings.warn('latex_visit_displaymath() is deprecated. ' - 'Please use LaTeXTranslator.visit_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math_block(node) - - -def man_visit_math(self: ManualPageTranslator, node: Element) -> None: - warnings.warn('man_visit_math() is deprecated. ' - 'Please use ManualPageTranslator.visit_math() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math(node) - - -def man_visit_displaymath(self: ManualPageTranslator, node: Element) -> None: - warnings.warn('man_visit_displaymath() is deprecated. ' - 'Please use ManualPageTranslator.visit_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math_block(node) - - -def man_depart_displaymath(self: ManualPageTranslator, node: Element) -> None: - warnings.warn('man_depart_displaymath() is deprecated. ' - 'Please use ManualPageTranslator.depart_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.depart_math_block(node) - - -def texinfo_visit_math(self: TexinfoTranslator, node: Element) -> None: - warnings.warn('texinfo_visit_math() is deprecated. ' - 'Please use TexinfoTranslator.visit_math() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math(node) - - -def texinfo_visit_displaymath(self: TexinfoTranslator, node: Element) -> None: - warnings.warn('texinfo_visit_displaymath() is deprecated. ' - 'Please use TexinfoTranslator.visit_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math_block(node) - - -def texinfo_depart_displaymath(self: TexinfoTranslator, node: Element) -> None: - warnings.warn('texinfo_depart_displaymath() is deprecated. ' - 'Please use TexinfoTranslator.depart_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - - -def text_visit_math(self: TextTranslator, node: Element) -> None: - warnings.warn('text_visit_math() is deprecated. ' - 'Please use TextTranslator.visit_math() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math(node) - - -def text_visit_displaymath(self: TextTranslator, node: Element) -> None: - warnings.warn('text_visit_displaymath() is deprecated. ' - 'Please use TextTranslator.visit_math_block() instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.visit_math_block(node) - - -def setup_math(app: Sphinx, - htmlinlinevisitors: Tuple[Callable, Callable], - htmldisplayvisitors: Tuple[Callable, Callable]) -> None: - warnings.warn('setup_math() is deprecated. ' - 'Please use app.add_html_math_renderer() instead.', - RemovedInSphinx30Warning, stacklevel=2) - - app.add_html_math_renderer('unknown', htmlinlinevisitors, htmldisplayvisitors) diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 81f2496cb..820de6ee4 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -561,7 +561,7 @@ class GoogleDocstring: lines = self._consume_to_next_section() self._parsed_lines.extend(lines) - def _parse_admonition(self, admonition, section): + def _parse_admonition(self, admonition: str, section: str) -> List[str]: # type (str, str) -> List[str] lines = self._consume_to_next_section() return self._format_admonition(admonition, lines) @@ -583,7 +583,11 @@ class GoogleDocstring: if _type: lines.append(':vartype %s: %s' % (_name, _type)) else: - lines.extend(['.. attribute:: ' + _name, '']) + lines.append('.. attribute:: ' + _name) + if self._opt and 'noindex' in self._opt: + lines.append(' :noindex:') + lines.append('') + fields = self._format_field('', '', _desc) lines.extend(self._indent(fields, 3)) if _type: @@ -603,7 +607,7 @@ class GoogleDocstring: label = labels.get(section.lower(), section) return self._parse_generic_section(label, use_admonition) - def _parse_custom_generic_section(self, section): + def _parse_custom_generic_section(self, section: str) -> List[str]: # for now, no admonition for simple custom sections return self._parse_generic_section(section, False) @@ -641,6 +645,8 @@ class GoogleDocstring: lines = [] # type: List[str] for _name, _type, _desc in self._consume_fields(parse_type=False): lines.append('.. method:: %s' % _name) + if self._opt and 'noindex' in self._opt: + lines.append(' :noindex:') if _desc: lines.extend([''] + self._indent(_desc, 3)) lines.append('') diff --git a/sphinx/ext/napoleon/iterators.py b/sphinx/ext/napoleon/iterators.py index 72ab74120..e91a3ec14 100644 --- a/sphinx/ext/napoleon/iterators.py +++ b/sphinx/ext/napoleon/iterators.py @@ -60,9 +60,7 @@ class peek_iter: return self def __next__(self, n: int = None) -> Any: - # note: prevent 2to3 to transform self.next() in next(self) which - # causes an infinite loop ! - return getattr(self, 'next')(n) + return self.next(n) def _fillcache(self, n: int) -> None: """Cache `n` items. If `n` is 0 or None, then 1 item is cached.""" diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 82c2111ef..dc24a1993 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -9,7 +9,6 @@ """ import traceback -import warnings from typing import Any, Dict, Iterable, Iterator, Set, Tuple from docutils import nodes @@ -18,8 +17,6 @@ from docutils.nodes import Element, Node import sphinx from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.config import Config -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer @@ -57,10 +54,10 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub: return - def has_tag(modname, fullname, docname, refname): + def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool: entry = env._viewcode_modules.get(modname, None) # type: ignore if entry is False: - return + return False code_tags = app.emit_firstresult('viewcode-find-source', modname) if code_tags is None: @@ -69,7 +66,7 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: analyzer.find_tags() except Exception: env._viewcode_modules[modname] = False # type: ignore - return + return False code = analyzer.code tags = analyzer.tags @@ -84,6 +81,8 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: used[fullname] = docname return True + return False + for objnode in doctree.traverse(addnodes.desc): if objnode.get('domain') != 'py': continue @@ -234,18 +233,10 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: yield ('_modules/index', context, 'page.html') -def migrate_viewcode_import(app: Sphinx, config: Config) -> None: - if config.viewcode_import is not None: - warnings.warn('viewcode_import was renamed to viewcode_follow_imported_members. ' - 'Please update your configuration.', - RemovedInSphinx30Warning, stacklevel=2) - - def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('viewcode_import', None, False) app.add_config_value('viewcode_enable_epub', False, False) app.add_config_value('viewcode_follow_imported_members', True, False) - app.connect('config-inited', migrate_viewcode_import) app.connect('doctree-read', doctree_read) app.connect('env-merge-info', env_merge_info) app.connect('html-collect-pages', collect_pages) diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py index 49a4105bf..4dc9ce543 100644 --- a/sphinx/highlighting.py +++ b/sphinx/highlighting.py @@ -8,8 +8,6 @@ :license: BSD, see LICENSE for details. """ -import html -import warnings from functools import partial from importlib import import_module from typing import Any, Dict @@ -26,8 +24,6 @@ from pygments.style import Style from pygments.styles import get_style_by_name from pygments.util import ClassNotFound -from sphinx.deprecation import RemovedInSphinx30Warning -from sphinx.ext import doctest from sphinx.locale import __ from sphinx.pygments_styles import SphinxStyle, NoneStyle from sphinx.util import logging, texescape @@ -65,7 +61,7 @@ class PygmentsBridge: latex_formatter = LatexFormatter def __init__(self, dest: str = 'html', stylename: str = 'sphinx', - trim_doctest_flags: bool = None, latex_engine: str = None) -> None: + latex_engine: str = None) -> None: self.dest = dest self.latex_engine = latex_engine @@ -77,11 +73,6 @@ class PygmentsBridge: self.formatter = self.latex_formatter self.formatter_args['commandprefix'] = 'PYG' - self.trim_doctest_flags = trim_doctest_flags - if trim_doctest_flags is not None: - warnings.warn('trim_doctest_flags option for PygmentsBridge is now deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - def get_style(self, stylename: str) -> Style: if stylename is None or stylename == 'sphinx': return SphinxStyle @@ -97,19 +88,6 @@ class PygmentsBridge: kwargs.update(self.formatter_args) return self.formatter(**kwargs) - def unhighlighted(self, source: str) -> str: - warnings.warn('PygmentsBridge.unhighlighted() is now deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - if self.dest == 'html': - return '<pre>' + html.escape(source) + '</pre>\n' - else: - # first, escape highlighting characters like Pygments does - source = source.translate(escape_hl_chars) - # then, escape all characters nonrepresentable in LaTeX - source = texescape.escape(source, self.latex_engine) - return '\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n' + \ - source + '\\end{Verbatim}\n' - def get_lexer(self, source: str, lang: str, opts: Dict = None, force: bool = False, location: Any = None) -> Lexer: if not opts: @@ -127,11 +105,6 @@ class PygmentsBridge: lang = 'pycon3' else: lang = 'python3' - elif lang == 'guess': - try: - lexer = guess_lexer(source) - except Exception: - lexer = lexers['none'] if lang in lexers: # just return custom lexers here (without installing raiseonerror filter) @@ -141,7 +114,7 @@ class PygmentsBridge: else: try: if lang == 'guess': - lexer = guess_lexer(lang, **opts) + lexer = guess_lexer(source, **opts) else: lexer = get_lexer_by_name(lang, **opts) except ClassNotFound: @@ -161,11 +134,6 @@ class PygmentsBridge: lexer = self.get_lexer(source, lang, opts, force, location) - # trim doctest options if wanted - if isinstance(lexer, PythonConsoleLexer) and self.trim_doctest_flags: - source = doctest.blankline_re.sub('', source) - source = doctest.doctestopt_re.sub('', source) - # highlight via Pygments formatter = self.get_formatter(**kwargs) try: diff --git a/sphinx/io.py b/sphinx/io.py index b1290ae89..18b4f053e 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -9,7 +9,7 @@ """ import codecs import warnings -from typing import Any, List, Tuple +from typing import Any, List from typing import Type # for python3.5.1 from docutils import nodes @@ -19,14 +19,11 @@ from docutils.io import FileInput, Input, NullOutput from docutils.parsers import Parser from docutils.parsers.rst import Parser as RSTParser from docutils.readers import standalone -from docutils.statemachine import StringList, string2lines from docutils.transforms import Transform from docutils.transforms.references import DanglingReferences from docutils.writers import UnfilteredWriter -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.environment import BuildEnvironment from sphinx.errors import FiletypeNotFoundError from sphinx.transforms import ( @@ -39,7 +36,6 @@ from sphinx.transforms.references import SphinxDomains from sphinx.util import logging, get_filetype from sphinx.util import UnicodeDecodeErrorHandler from sphinx.util.docutils import LoggingReporter -from sphinx.util.rst import append_epilog, docinfo_re, prepend_prolog from sphinx.versioning import UIDTransform if False: @@ -160,17 +156,6 @@ class SphinxI18nReader(SphinxBaseReader): if transform in self.transforms: self.transforms.remove(transform) - def set_lineno_for_reporter(self, lineno: int) -> None: - """Stores the source line number of original text.""" - warnings.warn('SphinxI18nReader.set_lineno_for_reporter() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - @property - def line(self) -> int: - warnings.warn('SphinxI18nReader.line is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return 0 - class SphinxDummyWriter(UnfilteredWriter): """Dummy writer module used for generating doctree.""" @@ -186,93 +171,13 @@ def SphinxDummySourceClass(source: Any, *args: Any, **kwargs: Any) -> Any: return source -class SphinxBaseFileInput(FileInput): - """A base class of SphinxFileInput. - - It supports to replace unknown Unicode characters to '?'. - """ - - def __init__(self, app: "Sphinx", env: BuildEnvironment, - *args: Any, **kwargs: Any) -> None: - self.app = app - self.env = env - - warnings.warn('%s is deprecated.' % self.__class__.__name__, - RemovedInSphinx30Warning, stacklevel=2) - - kwargs['error_handler'] = 'sphinx' # py3: handle error on open. - super().__init__(*args, **kwargs) - - def warn_and_replace(self, error: Any) -> Tuple: - return UnicodeDecodeErrorHandler(self.env.docname)(error) - - class SphinxFileInput(FileInput): """A basic FileInput for Sphinx.""" - supported = ('*',) # RemovedInSphinx30Warning - def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs['error_handler'] = 'sphinx' super().__init__(*args, **kwargs) -class SphinxRSTFileInput(SphinxBaseFileInput): - """A reST FileInput for Sphinx. - - This FileInput automatically prepends and appends text by :confval:`rst_prolog` and - :confval:`rst_epilog`. - - .. important:: - - This FileInput uses an instance of ``StringList`` as a return value of ``read()`` - method to indicate original source filename and line numbers after prepending and - appending. - For that reason, ``sphinx.parsers.RSTParser`` should be used with this to parse - a content correctly. - """ - supported = ('restructuredtext',) - - def prepend_prolog(self, text: StringList, prolog: str) -> None: - docinfo = self.count_docinfo_lines(text) - if docinfo: - # insert a blank line after docinfo - text.insert(docinfo, '', '<generated>', 0) - docinfo += 1 - - # insert prolog (after docinfo if exists) - for lineno, line in enumerate(prolog.splitlines()): - text.insert(docinfo + lineno, line, '<rst_prolog>', lineno) - - text.insert(docinfo + lineno + 1, '', '<generated>', 0) - - def append_epilog(self, text: StringList, epilog: str) -> None: - # append a blank line and rst_epilog - text.append('', '<generated>', 0) - for lineno, line in enumerate(epilog.splitlines()): - text.append(line, '<rst_epilog>', lineno) - - def read(self) -> StringList: # type: ignore - inputstring = super().read() - lines = string2lines(inputstring, convert_whitespace=True) - content = StringList() - for lineno, line in enumerate(lines): - content.append(line, self.source_path, lineno) - - prepend_prolog(content, self.env.config.rst_prolog) - append_epilog(content, self.env.config.rst_epilog) - - return content - - def count_docinfo_lines(self, content: StringList) -> int: - if len(content) == 0: - return 0 - else: - for lineno, line in enumerate(content.data): - if not docinfo_re.match(line): - break - return lineno - - def read_doc(app: "Sphinx", env: BuildEnvironment, filename: str) -> nodes.document: """Parse a document and convert to doctree.""" # set up error_handler for the target document diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 262425a36..9812355ca 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -10,13 +10,10 @@ import gettext import locale -import warnings from collections import UserString, defaultdict from gettext import NullTranslations from typing import Any, Callable, Dict, Iterable, List, Tuple, Union -from sphinx.deprecation import RemovedInSphinx30Warning - class _TranslationProxy(UserString): """ @@ -106,24 +103,6 @@ class _TranslationProxy(UserString): return '<%s broken>' % self.__class__.__name__ -def mygettext(string: str) -> str: - """Used instead of _ when creating TranslationProxies, because _ is - not bound yet at that time. - """ - warnings.warn('sphinx.locale.mygettext() is deprecated. Please use `_()` instead.', - RemovedInSphinx30Warning, stacklevel=2) - return _(string) - - -def lazy_gettext(string: str) -> str: - """A lazy version of `gettext`.""" - # if isinstance(string, _TranslationProxy): - # return string - warnings.warn('sphinx.locale.laxy_gettext() is deprecated. Please use `_()` instead.', - RemovedInSphinx30Warning, stacklevel=2) - return _TranslationProxy(mygettext, string) # type: ignore - - translators = defaultdict(NullTranslations) # type: Dict[Tuple[str, str], NullTranslations] @@ -218,7 +197,7 @@ def _lazy_translate(catalog: str, namespace: str, message: str) -> str: return translator.gettext(message) -def get_translation(catalog, namespace='general'): +def get_translation(catalog: str, namespace: str = 'general') -> Callable: """Get a translation function based on the *catalog* and *namespace*. The extension can use this API to translate the messages on the @@ -266,12 +245,6 @@ _ = get_translation('sphinx') __ = get_translation('sphinx', 'console') -def l_(*args): - warnings.warn('sphinx.locale.l_() is deprecated. Please use `_()` instead.', - RemovedInSphinx30Warning, stacklevel=2) - return _(*args) - - # labels admonitionlabels = { 'attention': _('Attention'), diff --git a/sphinx/locale/sphinx.pot b/sphinx/locale/sphinx.pot index c4f9a54fb..39ed7ccca 100644 --- a/sphinx/locale/sphinx.pot +++ b/sphinx/locale/sphinx.pot @@ -1,126 +1,146 @@ # Translations template for Sphinx. -# Copyright (C) 2019 ORGANIZATION +# Copyright (C) 2020 ORGANIZATION # This file is distributed under the same license as the Sphinx project. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2019. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2020. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Sphinx 2.0.1\n" +"Project-Id-Version: Sphinx 3.0.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2019-03-29 01:05+0900\n" +"POT-Creation-Date: 2020-03-14 19:50+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.6.0\n" +"Generated-By: Babel 2.8.0\n" -#: sphinx/application.py:153 +#: sphinx/application.py:159 #, python-format msgid "config directory doesn't contain a conf.py file (%s)" msgstr "" -#: sphinx/application.py:157 +#: sphinx/application.py:163 #, python-format msgid "Cannot find source directory (%s)" msgstr "" -#: sphinx/application.py:161 +#: sphinx/application.py:167 msgid "Source directory and destination directory cannot be identical" msgstr "" -#: sphinx/application.py:192 +#: sphinx/application.py:198 #, python-format msgid "Running Sphinx v%s" msgstr "" -#: sphinx/application.py:214 +#: sphinx/application.py:202 +msgid "" +"For security reason, parallel mode is disabled on macOS and python3.8 and" +" above. For more details, please read https://github.com/sphinx-" +"doc/sphinx/issues/6803" +msgstr "" + +#: sphinx/application.py:226 #, python-format msgid "" "This project needs at least Sphinx v%s and therefore cannot be built with" " this version." msgstr "" -#: sphinx/application.py:234 +#: sphinx/application.py:246 msgid "making output directory" msgstr "" -#: sphinx/application.py:239 sphinx/registry.py:470 +#: sphinx/application.py:251 sphinx/registry.py:399 #, python-format msgid "while setting up extension %s:" msgstr "" -#: sphinx/application.py:245 +#: sphinx/application.py:257 msgid "" "'setup' as currently defined in conf.py isn't a Python callable. Please " "modify its definition to make it a callable function. This is needed for " "conf.py to behave as a Sphinx extension." msgstr "" -#: sphinx/application.py:269 +#: sphinx/application.py:282 #, python-format msgid "loading translations [%s]... " msgstr "" -#: sphinx/application.py:285 sphinx/builders/html.py:852 -#: sphinx/builders/html.py:870 sphinx/builders/html.py:1133 -#: sphinx/builders/html.py:1151 sphinx/util/__init__.py:702 +#: sphinx/application.py:296 sphinx/util/__init__.py:646 msgid "done" msgstr "" -#: sphinx/application.py:287 +#: sphinx/application.py:298 msgid "not available for built-in messages" msgstr "" -#: sphinx/application.py:298 +#: sphinx/application.py:308 msgid "loading pickled environment" msgstr "" -#: sphinx/application.py:303 +#: sphinx/application.py:313 #, python-format msgid "failed: %s" msgstr "" -#: sphinx/application.py:313 +#: sphinx/application.py:321 msgid "No builder selected, using default: html" msgstr "" -#: sphinx/application.py:344 +#: sphinx/application.py:349 msgid "succeeded" msgstr "" -#: sphinx/application.py:344 +#: sphinx/application.py:350 msgid "finished with problems" msgstr "" -#: sphinx/application.py:346 +#: sphinx/application.py:354 +#, python-format +msgid "build %s, %s warning (with warnings treated as errors)." +msgstr "" + +#: sphinx/application.py:356 +#, python-format +msgid "build %s, %s warnings (with warnings treated as errors)." +msgstr "" + +#: sphinx/application.py:359 #, python-format msgid "build %s, %s warning." msgstr "" -#: sphinx/application.py:350 +#: sphinx/application.py:361 +#, python-format +msgid "build %s, %s warnings." +msgstr "" + +#: sphinx/application.py:365 #, python-format msgid "build %s." msgstr "" -#: sphinx/application.py:557 +#: sphinx/application.py:554 #, python-format msgid "node class %r is already registered, its visitors will be overridden" msgstr "" -#: sphinx/application.py:654 +#: sphinx/application.py:628 #, python-format msgid "directive %r is already registered, it will be overridden" msgstr "" -#: sphinx/application.py:677 sphinx/application.py:696 +#: sphinx/application.py:646 sphinx/application.py:664 #, python-format msgid "role %r is already registered, it will be overridden" msgstr "" -#: sphinx/application.py:1179 +#: sphinx/application.py:1124 #, python-format msgid "" "the %s extension does not declare if it is safe for parallel reading, " @@ -128,7 +148,12 @@ msgid "" "explicit" msgstr "" -#: sphinx/application.py:1185 +#: sphinx/application.py:1128 +#, python-format +msgid "the %s extension is not safe for parallel reading" +msgstr "" + +#: sphinx/application.py:1131 #, python-format msgid "" "the %s extension does not declare if it is safe for parallel writing, " @@ -136,55 +161,60 @@ msgid "" "explicit" msgstr "" -#: sphinx/application.py:1196 +#: sphinx/application.py:1135 +#, python-format +msgid "the %s extension is not safe for parallel writing" +msgstr "" + +#: sphinx/application.py:1143 sphinx/application.py:1147 #, python-format msgid "doing serial %s" msgstr "" -#: sphinx/config.py:220 +#: sphinx/config.py:191 #, python-format msgid "" "cannot override dictionary config setting %r, ignoring (use %r to set " "individual elements)" msgstr "" -#: sphinx/config.py:229 +#: sphinx/config.py:200 #, python-format msgid "invalid number %r for config value %r, ignoring" msgstr "" -#: sphinx/config.py:234 +#: sphinx/config.py:205 #, python-format msgid "cannot override config setting %r with unsupported type, ignoring" msgstr "" -#: sphinx/config.py:264 +#: sphinx/config.py:233 #, python-format msgid "unknown config value %r in override, ignoring" msgstr "" -#: sphinx/config.py:282 +#: sphinx/config.py:250 #, python-format msgid "No such config value: %s" msgstr "" -#: sphinx/config.py:312 +#: sphinx/config.py:274 #, python-format msgid "Config value %r already present" msgstr "" -#: sphinx/config.py:363 +#: sphinx/config.py:321 #, python-format msgid "There is a syntax error in your configuration file: %s\n" msgstr "" -#: sphinx/config.py:366 +#: sphinx/config.py:324 msgid "" "The configuration file (or one of the modules it imports) called " "sys.exit()" msgstr "" -#: sphinx/config.py:370 +#: sphinx/config.py:331 #, python-format msgid "" "There is a programmable error in your configuration file:\n" @@ -192,52 +222,52 @@ msgid "" "%s" msgstr "" -#: sphinx/config.py:397 +#: sphinx/config.py:357 #, python-format msgid "" "The config value `source_suffix' expects a string, list of strings, or " "dictionary. But `%r' is given." msgstr "" -#: sphinx/config.py:405 +#: sphinx/config.py:364 #, python-format msgid "Section %s" msgstr "" -#: sphinx/config.py:406 +#: sphinx/config.py:365 #, python-format msgid "Fig. %s" msgstr "" -#: sphinx/config.py:407 +#: sphinx/config.py:366 #, python-format msgid "Table %s" msgstr "" -#: sphinx/config.py:408 +#: sphinx/config.py:367 #, python-format msgid "Listing %s" msgstr "" -#: sphinx/config.py:447 +#: sphinx/config.py:404 msgid "" "The config value `{name}` has to be a one of {candidates}, but " "`{current}` is given." msgstr "" -#: sphinx/config.py:465 +#: sphinx/config.py:422 msgid "" "The config value `{name}' has type `{current.__name__}'; expected " "{permitted}." msgstr "" -#: sphinx/config.py:478 +#: sphinx/config.py:435 msgid "" "The config value `{name}' has type `{current.__name__}', defaults to " "`{default.__name__}'." msgstr "" -#: sphinx/config.py:497 +#: sphinx/config.py:453 #, python-format msgid "" "the config value %r is set to a string with non-ASCII characters; this " @@ -245,892 +275,898 @@ msgid "" "%r." msgstr "" -#: sphinx/config.py:506 +#: sphinx/config.py:461 #, python-format msgid "primary_domain %r not found, ignored." msgstr "" -#: sphinx/config.py:518 +#: sphinx/config.py:473 msgid "" "Since v2.0, Sphinx uses \"index\" as master_doc by default. Please add " "\"master_doc = 'contents'\" to your conf.py." msgstr "" -#: sphinx/events.py:54 +#: sphinx/events.py:71 #, python-format msgid "Event %r already present" msgstr "" -#: sphinx/events.py:60 +#: sphinx/events.py:77 #, python-format msgid "Unknown event name: %s" msgstr "" -#: sphinx/extension.py:52 +#: sphinx/extension.py:51 #, python-format msgid "" "The %s extension is required by needs_extensions settings, but it is not " "loaded." msgstr "" -#: sphinx/extension.py:57 +#: sphinx/extension.py:56 #, python-format msgid "" "This project needs the extension %s at least in version %s and therefore " "cannot be built with the loaded version (%s)." msgstr "" -#: sphinx/highlighting.py:142 +#: sphinx/highlighting.py:121 #, python-format msgid "Pygments lexer name %r is not known" msgstr "" -#: sphinx/highlighting.py:163 +#: sphinx/highlighting.py:147 #, python-format msgid "Could not lex literal_block as \"%s\". Highlighting skipped." msgstr "" -#: sphinx/project.py:59 +#: sphinx/project.py:61 msgid "document not readable. Ignored." msgstr "" -#: sphinx/registry.py:131 +#: sphinx/registry.py:126 #, python-format msgid "Builder class %s has no \"name\" attribute" msgstr "" -#: sphinx/registry.py:133 +#: sphinx/registry.py:128 #, python-format msgid "Builder %r already exists (in module %s)" msgstr "" -#: sphinx/registry.py:147 +#: sphinx/registry.py:141 #, python-format msgid "Builder name %s not registered or available through entry point" msgstr "" -#: sphinx/registry.py:155 +#: sphinx/registry.py:148 #, python-format msgid "Builder name %s not registered" msgstr "" -#: sphinx/registry.py:163 +#: sphinx/registry.py:155 #, python-format msgid "domain %s already registered" msgstr "" -#: sphinx/registry.py:197 sphinx/registry.py:212 sphinx/registry.py:223 +#: sphinx/registry.py:178 sphinx/registry.py:191 sphinx/registry.py:202 #, python-format msgid "domain %s not yet registered" msgstr "" -#: sphinx/registry.py:201 +#: sphinx/registry.py:182 #, python-format msgid "The %r directive is already registered to domain %s" msgstr "" -#: sphinx/registry.py:215 +#: sphinx/registry.py:194 #, python-format msgid "The %r role is already registered to domain %s" msgstr "" -#: sphinx/registry.py:226 +#: sphinx/registry.py:205 #, python-format msgid "The %r index is already registered to domain %s" msgstr "" -#: sphinx/registry.py:250 +#: sphinx/registry.py:229 #, python-format msgid "The %r object_type is already registered" msgstr "" -#: sphinx/registry.py:270 +#: sphinx/registry.py:249 #, python-format msgid "The %r crossref_type is already registered" msgstr "" -#: sphinx/registry.py:278 +#: sphinx/registry.py:256 #, python-format msgid "source_suffix %r is already registered" msgstr "" -#: sphinx/registry.py:308 +#: sphinx/registry.py:266 #, python-format msgid "source_parser for %r is already registered" msgstr "" -#: sphinx/registry.py:324 +#: sphinx/registry.py:275 #, python-format msgid "Source parser for %s not registered" msgstr "" -#: sphinx/registry.py:344 -#, python-format -msgid "source_input for %r is already registered" -msgstr "" - -#: sphinx/registry.py:363 +#: sphinx/registry.py:301 #, python-format msgid "Translator for %r already exists" msgstr "" -#: sphinx/registry.py:375 +#: sphinx/registry.py:313 #, python-format msgid "kwargs for add_node() must be a (visit, depart) function tuple: %r=%r" msgstr "" -#: sphinx/registry.py:445 +#: sphinx/registry.py:374 #, python-format msgid "enumerable_node %r already registered" msgstr "" -#: sphinx/registry.py:453 +#: sphinx/registry.py:383 #, python-format msgid "math renderer %s is already registred" msgstr "" -#: sphinx/registry.py:464 +#: sphinx/registry.py:393 #, python-format msgid "" "the extension %r was already merged with Sphinx since version %s; this " "extension is ignored." msgstr "" -#: sphinx/registry.py:475 +#: sphinx/registry.py:404 msgid "Original exception:\n" msgstr "" -#: sphinx/registry.py:476 +#: sphinx/registry.py:405 #, python-format msgid "Could not import extension %s" msgstr "" -#: sphinx/registry.py:479 +#: sphinx/registry.py:409 #, python-format msgid "" "extension %r has no setup() function; is it really a Sphinx extension " "module?" msgstr "" -#: sphinx/registry.py:488 +#: sphinx/registry.py:418 #, python-format msgid "" "The %s extension used by this project needs at least Sphinx v%s; it " "therefore cannot be built with this version." msgstr "" -#: sphinx/registry.py:496 +#: sphinx/registry.py:426 #, python-format msgid "" "extension %r returned an unsupported object from its setup() function; it" " should return None or a metadata dictionary" msgstr "" -#: sphinx/roles.py:221 sphinx/roles.py:272 +#: sphinx/roles.py:221 sphinx/roles.py:271 #, python-format msgid "Python Enhancement Proposals; PEP %s" msgstr "" -#: sphinx/theming.py:79 +#: sphinx/theming.py:78 #, python-format msgid "theme %r doesn't have \"theme\" setting" msgstr "" -#: sphinx/theming.py:81 +#: sphinx/theming.py:80 #, python-format msgid "theme %r doesn't have \"inherit\" setting" msgstr "" -#: sphinx/theming.py:87 +#: sphinx/theming.py:86 #, python-format msgid "no theme named %r found, inherited by %r" msgstr "" -#: sphinx/theming.py:112 +#: sphinx/theming.py:109 #, python-format msgid "setting %s.%s occurs in none of the searched theme configs" msgstr "" -#: sphinx/theming.py:132 +#: sphinx/theming.py:128 #, python-format msgid "unsupported theme option %r given" msgstr "" -#: sphinx/theming.py:242 +#: sphinx/theming.py:228 #, python-format msgid "file %r on theme path is not a valid zipfile or contains no theme" msgstr "" -#: sphinx/theming.py:258 +#: sphinx/theming.py:243 msgid "" "sphinx_rtd_theme is no longer a hard dependency since version 1.4.0. " "Please install it manually.(pip install sphinx_rtd_theme)" msgstr "" -#: sphinx/theming.py:262 +#: sphinx/theming.py:247 #, python-format msgid "no theme named %r found (missing theme.conf?)" msgstr "" -#: sphinx/builders/__init__.py:205 +#: sphinx/builders/__init__.py:195 #, python-format msgid "a suitable image for %s builder not found: %s (%s)" msgstr "" -#: sphinx/builders/__init__.py:209 +#: sphinx/builders/__init__.py:199 #, python-format msgid "a suitable image for %s builder not found: %s" msgstr "" -#: sphinx/builders/__init__.py:231 +#: sphinx/builders/__init__.py:219 msgid "building [mo]: " msgstr "" -#: sphinx/builders/__init__.py:232 sphinx/builders/__init__.py:574 -#: sphinx/builders/__init__.py:602 +#: sphinx/builders/__init__.py:220 sphinx/builders/__init__.py:539 +#: sphinx/builders/__init__.py:565 msgid "writing output... " msgstr "" -#: sphinx/builders/__init__.py:245 +#: sphinx/builders/__init__.py:228 #, python-format msgid "all of %d po files" msgstr "" -#: sphinx/builders/__init__.py:266 +#: sphinx/builders/__init__.py:246 #, python-format msgid "targets for %d po files that are specified" msgstr "" -#: sphinx/builders/__init__.py:276 +#: sphinx/builders/__init__.py:253 #, python-format msgid "targets for %d po files that are out of date" msgstr "" -#: sphinx/builders/__init__.py:284 +#: sphinx/builders/__init__.py:260 msgid "all source files" msgstr "" -#: sphinx/builders/__init__.py:298 +#: sphinx/builders/__init__.py:273 #, python-format msgid "file %r given on command line is not under the source directory, ignoring" msgstr "" -#: sphinx/builders/__init__.py:303 +#: sphinx/builders/__init__.py:277 #, python-format msgid "file %r given on command line does not exist, ignoring" msgstr "" -#: sphinx/builders/__init__.py:314 +#: sphinx/builders/__init__.py:288 #, python-format msgid "%d source files given on command line" msgstr "" -#: sphinx/builders/__init__.py:325 +#: sphinx/builders/__init__.py:298 #, python-format msgid "targets for %d source files that are out of date" msgstr "" -#: sphinx/builders/__init__.py:335 +#: sphinx/builders/__init__.py:307 sphinx/builders/gettext.py:267 #, python-format -msgid "building [%s]" +msgid "building [%s]: " msgstr "" -#: sphinx/builders/__init__.py:342 +#: sphinx/builders/__init__.py:314 msgid "looking for now-outdated files... " msgstr "" -#: sphinx/builders/__init__.py:347 +#: sphinx/builders/__init__.py:319 #, python-format msgid "%d found" msgstr "" -#: sphinx/builders/__init__.py:349 +#: sphinx/builders/__init__.py:321 msgid "none found" msgstr "" -#: sphinx/builders/__init__.py:354 +#: sphinx/builders/__init__.py:326 msgid "pickling environment" msgstr "" -#: sphinx/builders/__init__.py:360 +#: sphinx/builders/__init__.py:332 msgid "checking consistency" msgstr "" -#: sphinx/builders/__init__.py:364 +#: sphinx/builders/__init__.py:336 msgid "no targets are out of date." msgstr "" -#: sphinx/builders/__init__.py:404 +#: sphinx/builders/__init__.py:375 msgid "updating environment: " msgstr "" -#: sphinx/builders/__init__.py:423 +#: sphinx/builders/__init__.py:396 #, python-format msgid "%s added, %s changed, %s removed" msgstr "" -#: sphinx/builders/__init__.py:462 sphinx/builders/__init__.py:492 +#: sphinx/builders/__init__.py:434 sphinx/builders/__init__.py:461 msgid "reading sources... " msgstr "" -#: sphinx/builders/__init__.py:497 sphinx/builders/__init__.py:612 +#: sphinx/builders/__init__.py:466 sphinx/builders/__init__.py:575 msgid "waiting for workers..." msgstr "" -#: sphinx/builders/__init__.py:551 +#: sphinx/builders/__init__.py:517 #, python-format msgid "docnames to write: %s" msgstr "" -#: sphinx/builders/__init__.py:560 sphinx/builders/singlehtml.py:166 +#: sphinx/builders/__init__.py:526 sphinx/builders/singlehtml.py:155 msgid "preparing documents" msgstr "" -#: sphinx/builders/_epub_base.py:218 +#: sphinx/builders/_epub_base.py:211 #, python-format msgid "duplicated ToC entry found: %s" msgstr "" -#: sphinx/builders/_epub_base.py:414 sphinx/builders/html.py:761 -#: sphinx/builders/latex/__init__.py:415 sphinx/builders/texinfo.py:190 +#: sphinx/builders/_epub_base.py:395 sphinx/builders/html/__init__.py:688 +#: sphinx/builders/latex/__init__.py:416 sphinx/builders/texinfo.py:178 msgid "copying images... " msgstr "" -#: sphinx/builders/_epub_base.py:421 +#: sphinx/builders/_epub_base.py:402 #, python-format msgid "cannot read image file %r: copying it instead" msgstr "" -#: sphinx/builders/_epub_base.py:427 sphinx/builders/html.py:769 -#: sphinx/builders/latex/__init__.py:423 sphinx/builders/texinfo.py:199 +#: sphinx/builders/_epub_base.py:408 sphinx/builders/html/__init__.py:696 +#: sphinx/builders/latex/__init__.py:424 sphinx/builders/texinfo.py:187 #, python-format msgid "cannot copy image file %r: %s" msgstr "" -#: sphinx/builders/_epub_base.py:444 +#: sphinx/builders/_epub_base.py:425 #, python-format msgid "cannot write image file %r: %s" msgstr "" -#: sphinx/builders/_epub_base.py:455 +#: sphinx/builders/_epub_base.py:435 msgid "Pillow not found - copying image files" msgstr "" -#: sphinx/builders/_epub_base.py:490 sphinx/builders/_epub_base.py:503 -#: sphinx/builders/_epub_base.py:539 sphinx/builders/_epub_base.py:724 -#: sphinx/builders/_epub_base.py:757 sphinx/builders/epub3.py:183 +#: sphinx/builders/_epub_base.py:467 sphinx/builders/_epub_base.py:479 +#: sphinx/builders/_epub_base.py:513 sphinx/builders/_epub_base.py:694 +#: sphinx/builders/_epub_base.py:726 sphinx/builders/epub3.py:173 #, python-format msgid "writing %s file..." msgstr "" -#: sphinx/builders/_epub_base.py:565 +#: sphinx/builders/_epub_base.py:539 #, python-format msgid "unknown mimetype for %s, ignoring" msgstr "" -#: sphinx/builders/changes.py:39 +#: sphinx/builders/changes.py:36 #, python-format msgid "The overview file is in %(outdir)s." msgstr "" -#: sphinx/builders/changes.py:68 +#: sphinx/builders/changes.py:62 #, python-format msgid "no changes in version %s." msgstr "" -#: sphinx/builders/changes.py:70 +#: sphinx/builders/changes.py:64 msgid "writing summary file..." msgstr "" -#: sphinx/builders/changes.py:86 +#: sphinx/builders/changes.py:80 msgid "Builtins" msgstr "" -#: sphinx/builders/changes.py:88 +#: sphinx/builders/changes.py:82 msgid "Module level" msgstr "" -#: sphinx/builders/changes.py:133 +#: sphinx/builders/changes.py:126 msgid "copying source files..." msgstr "" -#: sphinx/builders/changes.py:140 +#: sphinx/builders/changes.py:133 #, python-format msgid "could not read %r for changelog creation" msgstr "" -#: sphinx/builders/dummy.py:24 +#: sphinx/builders/dummy.py:22 msgid "The dummy builder generates no files." msgstr "" -#: sphinx/builders/epub3.py:68 +#: sphinx/builders/epub3.py:65 #, python-format msgid "The ePub file is in %(outdir)s." msgstr "" -#: sphinx/builders/epub3.py:211 +#: sphinx/builders/epub3.py:200 msgid "" "conf value \"epub_language\" (or \"language\") should not be empty for " "EPUB3" msgstr "" -#: sphinx/builders/epub3.py:215 +#: sphinx/builders/epub3.py:204 msgid "conf value \"epub_uid\" should be XML NAME for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:218 +#: sphinx/builders/epub3.py:207 msgid "" "conf value \"epub_title\" (or \"html_title\") should not be empty for " "EPUB3" msgstr "" -#: sphinx/builders/epub3.py:222 +#: sphinx/builders/epub3.py:211 msgid "conf value \"epub_author\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:225 +#: sphinx/builders/epub3.py:214 msgid "conf value \"epub_contributor\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:228 +#: sphinx/builders/epub3.py:217 msgid "conf value \"epub_description\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:231 +#: sphinx/builders/epub3.py:220 msgid "conf value \"epub_publisher\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:234 +#: sphinx/builders/epub3.py:223 msgid "" "conf value \"epub_copyright\" (or \"copyright\")should not be empty for " "EPUB3" msgstr "" -#: sphinx/builders/epub3.py:238 +#: sphinx/builders/epub3.py:227 msgid "conf value \"epub_identifier\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:241 +#: sphinx/builders/epub3.py:230 msgid "conf value \"version\" should not be empty for EPUB3" msgstr "" -#: sphinx/builders/epub3.py:256 sphinx/builders/html.py:1166 +#: sphinx/builders/epub3.py:244 sphinx/builders/html/__init__.py:1060 #, python-format msgid "invalid css_file: %r, ignored" msgstr "" -#: sphinx/builders/gettext.py:221 +#: sphinx/builders/gettext.py:246 #, python-format msgid "The message catalogs are in %(outdir)s." msgstr "" -#: sphinx/builders/gettext.py:245 -#, python-format -msgid "building [%s]: " -msgstr "" - -#: sphinx/builders/gettext.py:246 +#: sphinx/builders/gettext.py:268 #, python-format msgid "targets for %d template files" msgstr "" -#: sphinx/builders/gettext.py:250 +#: sphinx/builders/gettext.py:272 msgid "reading templates... " msgstr "" -#: sphinx/builders/gettext.py:277 +#: sphinx/builders/gettext.py:300 msgid "writing message catalogs... " msgstr "" -#: sphinx/builders/html.py:182 +#: sphinx/builders/linkcheck.py:78 #, python-format -msgid "build info file is broken: %r" +msgid "Look for any errors in the above output or in %(outdir)s/output.txt" msgstr "" -#: sphinx/builders/html.py:217 +#: sphinx/builders/linkcheck.py:150 #, python-format -msgid "The HTML pages are in %(outdir)s." +msgid "Anchor '%s' not found" msgstr "" -#: sphinx/builders/html.py:399 +#: sphinx/builders/linkcheck.py:259 #, python-format -msgid "Failed to read build info file: %r" +msgid "broken link: %s (%s)" msgstr "" -#: sphinx/builders/html.py:488 sphinx/builders/latex/__init__.py:206 -#: sphinx/transforms/__init__.py:121 sphinx/writers/manpage.py:118 -#: sphinx/writers/texinfo.py:243 +#: sphinx/builders/manpage.py:40 #, python-format -msgid "%b %d, %Y" +msgid "The manual pages are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:500 -msgid "html_use_opensearch config value must now be a string" +#: sphinx/builders/manpage.py:47 +msgid "no \"man_pages\" config value found; no manual pages will be written" msgstr "" -#: sphinx/builders/html.py:506 sphinx/themes/basic/defindex.html:30 -msgid "General Index" +#: sphinx/builders/latex/__init__.py:292 sphinx/builders/manpage.py:58 +#: sphinx/builders/singlehtml.py:163 sphinx/builders/texinfo.py:111 +msgid "writing" msgstr "" -#: sphinx/builders/html.py:506 -msgid "index" +#: sphinx/builders/manpage.py:69 +#, python-format +msgid "\"man_pages\" config value references unknown document %s" msgstr "" -#: sphinx/builders/html.py:570 -msgid "next" +#: sphinx/builders/singlehtml.py:36 +#, python-format +msgid "The HTML page is in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:579 -msgid "previous" +#: sphinx/builders/singlehtml.py:158 +msgid "assembling single document" msgstr "" -#: sphinx/builders/html.py:677 -msgid "generating indices..." +#: sphinx/builders/singlehtml.py:176 +msgid "writing additional files" msgstr "" -#: sphinx/builders/html.py:695 -msgid "writing additional pages..." +#: sphinx/builders/texinfo.py:47 +#, python-format +msgid "The Texinfo files are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:780 -msgid "copying downloadable files... " +#: sphinx/builders/texinfo.py:49 +msgid "" +"\n" +"Run 'make' in that directory to run these through makeinfo\n" +"(use 'make info' here to do that automatically)." msgstr "" -#: sphinx/builders/html.py:788 -#, python-format -msgid "cannot copy downloadable file %r: %s" +#: sphinx/builders/texinfo.py:77 +msgid "no \"texinfo_documents\" config value found; no documents will be written" msgstr "" -#: sphinx/builders/html.py:795 -msgid "copying static files... " +#: sphinx/builders/texinfo.py:85 +#, python-format +msgid "\"texinfo_documents\" config value references unknown document %s" msgstr "" -#: sphinx/builders/html.py:830 +#: sphinx/builders/latex/__init__.py:275 sphinx/builders/texinfo.py:107 #, python-format -msgid "html_static_path entry %r does not exist" +msgid "processing %s" msgstr "" -#: sphinx/builders/html.py:839 sphinx/builders/latex/__init__.py:400 -#, python-format -msgid "logo file %r does not exist" +#: sphinx/builders/latex/__init__.py:343 sphinx/builders/texinfo.py:154 +msgid "resolving references..." msgstr "" -#: sphinx/builders/html.py:847 -#, python-format -msgid "favicon file %r does not exist" +#: sphinx/builders/latex/__init__.py:353 sphinx/builders/texinfo.py:163 +msgid " (in " msgstr "" -#: sphinx/builders/html.py:854 -#, python-format -msgid "cannot copy static file %r" +#: sphinx/builders/texinfo.py:192 +msgid "copying Texinfo support files" msgstr "" -#: sphinx/builders/html.py:860 -msgid "copying extra files... " +#: sphinx/builders/texinfo.py:196 +#, python-format +msgid "error writing file Makefile: %s" msgstr "" -#: sphinx/builders/html.py:866 +#: sphinx/builders/text.py:30 #, python-format -msgid "html_extra_path entry %r does not exist" +msgid "The text files are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:872 +#: sphinx/builders/html/__init__.py:1013 sphinx/builders/text.py:77 +#: sphinx/builders/xml.py:96 #, python-format -msgid "cannot copy extra file %r" +msgid "error writing file %s: %s" msgstr "" -#: sphinx/builders/html.py:880 +#: sphinx/builders/xml.py:40 #, python-format -msgid "Failed to write build info file: %r" +msgid "The XML files are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:927 -msgid "" -"search index couldn't be loaded, but not all documents will be built: the" -" index will be incomplete." +#: sphinx/builders/xml.py:108 +#, python-format +msgid "The pseudo-XML files are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:996 +#: sphinx/builders/html/__init__.py:144 #, python-format -msgid "page %s matches two patterns in html_sidebars: %r and %r" +msgid "build info file is broken: %r" msgstr "" -#: sphinx/builders/html.py:1094 +#: sphinx/builders/html/__init__.py:176 #, python-format -msgid "" -"a Unicode error occurred when rendering the page %s. Please make sure all" -" config values that contain non-ASCII content are Unicode strings." +msgid "The HTML pages are in %(outdir)s." msgstr "" -#: sphinx/builders/html.py:1099 +#: sphinx/builders/html/__init__.py:344 #, python-format -msgid "" -"An error happened in rendering the page %s.\n" -"Reason: %r" +msgid "Failed to read build info file: %r" msgstr "" -#: sphinx/builders/html.py:1111 sphinx/builders/text.py:85 -#: sphinx/builders/xml.py:99 +#: sphinx/builders/html/__init__.py:430 sphinx/builders/latex/__init__.py:192 +#: sphinx/transforms/__init__.py:116 sphinx/writers/manpage.py:112 +#: sphinx/writers/texinfo.py:237 #, python-format -msgid "error writing file %s: %s" +msgid "%b %d, %Y" msgstr "" -#: sphinx/builders/html.py:1131 -msgid "dumping object inventory... " +#: sphinx/builders/html/__init__.py:439 +msgid "html_use_opensearch config value must now be a string" msgstr "" -#: sphinx/builders/html.py:1138 -#, python-format -msgid "dumping search index in %s ... " +#: sphinx/builders/html/__init__.py:445 sphinx/themes/basic/defindex.html:29 +msgid "General Index" msgstr "" -#: sphinx/builders/html.py:1184 -#, python-format -msgid "invalid js_file: %r, ignored" +#: sphinx/builders/html/__init__.py:445 +msgid "index" msgstr "" -#: sphinx/builders/html.py:1228 -msgid "Many math_renderers are registered. But no math_renderer is selected." +#: sphinx/builders/html/__init__.py:509 +msgid "next" msgstr "" -#: sphinx/builders/html.py:1231 -#, python-format -msgid "Unknown math_renderer %r is given." +#: sphinx/builders/html/__init__.py:518 +msgid "previous" msgstr "" -#: sphinx/builders/html.py:1264 -#, python-format -msgid "%s %s documentation" +#: sphinx/builders/html/__init__.py:612 +msgid "generating indices" msgstr "" -#: sphinx/builders/linkcheck.py:80 -#, python-format -msgid "Look for any errors in the above output or in %(outdir)s/output.txt" +#: sphinx/builders/html/__init__.py:627 +msgid "writing additional pages" msgstr "" -#: sphinx/builders/linkcheck.py:144 -#, python-format -msgid "Anchor '%s' not found" +#: sphinx/builders/html/__init__.py:706 +msgid "copying downloadable files... " msgstr "" -#: sphinx/builders/linkcheck.py:242 +#: sphinx/builders/html/__init__.py:714 #, python-format -msgid "broken link: %s (%s)" +msgid "cannot copy downloadable file %r: %s" msgstr "" -#: sphinx/builders/manpage.py:43 -#, python-format -msgid "The manual pages are in %(outdir)s." +#: sphinx/builders/html/__init__.py:762 +msgid "copying static files... " msgstr "" -#: sphinx/builders/manpage.py:51 -msgid "no \"man_pages\" config value found; no manual pages will be written" +#: sphinx/builders/html/__init__.py:778 +#, python-format +msgid "cannot copy static file %r" msgstr "" -#: sphinx/builders/latex/__init__.py:272 sphinx/builders/manpage.py:64 -#: sphinx/builders/singlehtml.py:174 sphinx/builders/texinfo.py:120 -msgid "writing" +#: sphinx/builders/html/__init__.py:783 +msgid "copying extra files" msgstr "" -#: sphinx/builders/manpage.py:76 +#: sphinx/builders/html/__init__.py:789 #, python-format -msgid "\"man_pages\" config value references unknown document %s" +msgid "cannot copy extra file %r" msgstr "" -#: sphinx/builders/singlehtml.py:37 +#: sphinx/builders/html/__init__.py:796 #, python-format -msgid "The HTML page is in %(outdir)s." +msgid "Failed to write build info file: %r" msgstr "" -#: sphinx/builders/singlehtml.py:169 -msgid "assembling single document" +#: sphinx/builders/html/__init__.py:844 +msgid "" +"search index couldn't be loaded, but not all documents will be built: the" +" index will be incomplete." msgstr "" -#: sphinx/builders/singlehtml.py:188 -msgid "writing additional files" +#: sphinx/builders/html/__init__.py:913 +#, python-format +msgid "page %s matches two patterns in html_sidebars: %r and %r" msgstr "" -#: sphinx/builders/texinfo.py:50 +#: sphinx/builders/html/__init__.py:996 #, python-format -msgid "The Texinfo files are in %(outdir)s." +msgid "" +"a Unicode error occurred when rendering the page %s. Please make sure all" +" config values that contain non-ASCII content are Unicode strings." msgstr "" -#: sphinx/builders/texinfo.py:52 +#: sphinx/builders/html/__init__.py:1001 +#, python-format msgid "" -"\n" -"Run 'make' in that directory to run these through makeinfo\n" -"(use 'make info' here to do that automatically)." +"An error happened in rendering the page %s.\n" +"Reason: %r" msgstr "" -#: sphinx/builders/texinfo.py:85 -msgid "no \"texinfo_documents\" config value found; no documents will be written" +#: sphinx/builders/html/__init__.py:1030 +msgid "dumping object inventory" msgstr "" -#: sphinx/builders/texinfo.py:93 +#: sphinx/builders/html/__init__.py:1035 #, python-format -msgid "\"texinfo_documents\" config value references unknown document %s" +msgid "dumping search index in %s" msgstr "" -#: sphinx/builders/latex/__init__.py:255 sphinx/builders/texinfo.py:116 +#: sphinx/builders/html/__init__.py:1077 #, python-format -msgid "processing %s" +msgid "invalid js_file: %r, ignored" msgstr "" -#: sphinx/builders/latex/__init__.py:323 sphinx/builders/texinfo.py:164 -msgid "resolving references..." +#: sphinx/builders/html/__init__.py:1118 +msgid "Many math_renderers are registered. But no math_renderer is selected." msgstr "" -#: sphinx/builders/latex/__init__.py:333 sphinx/builders/texinfo.py:173 -msgid " (in " +#: sphinx/builders/html/__init__.py:1121 +#, python-format +msgid "Unknown math_renderer %r is given." msgstr "" -#: sphinx/builders/texinfo.py:205 -msgid "copying Texinfo support files" +#: sphinx/builders/html/__init__.py:1129 +#, python-format +msgid "html_extra_path entry %r does not exist" msgstr "" -#: sphinx/builders/texinfo.py:209 +#: sphinx/builders/html/__init__.py:1133 #, python-format -msgid "error writing file Makefile: %s" +msgid "html_extra_path entry %r is placed inside outdir" msgstr "" -#: sphinx/builders/text.py:33 +#: sphinx/builders/html/__init__.py:1142 #, python-format -msgid "The text files are in %(outdir)s." +msgid "html_static_path entry %r does not exist" msgstr "" -#: sphinx/builders/xml.py:38 +#: sphinx/builders/html/__init__.py:1146 #, python-format -msgid "The XML files are in %(outdir)s." +msgid "html_static_path entry %r is placed inside outdir" msgstr "" -#: sphinx/builders/xml.py:112 +#: sphinx/builders/html/__init__.py:1153 sphinx/builders/latex/__init__.py:428 #, python-format -msgid "The pseudo-XML files are in %(outdir)s." +msgid "logo file %r does not exist" msgstr "" -#: sphinx/builders/latex/__init__.py:123 +#: sphinx/builders/html/__init__.py:1160 +#, python-format +msgid "favicon file %r does not exist" +msgstr "" + +#: sphinx/builders/html/__init__.py:1179 +#, python-format +msgid "%s %s documentation" +msgstr "" + +#: sphinx/builders/latex/__init__.py:115 #, python-format msgid "The LaTeX files are in %(outdir)s." msgstr "" -#: sphinx/builders/latex/__init__.py:125 +#: sphinx/builders/latex/__init__.py:117 msgid "" "\n" "Run 'make' in that directory to run these through (pdf)latex\n" "(use `make latexpdf' here to do that automatically)." msgstr "" -#: sphinx/builders/latex/__init__.py:165 +#: sphinx/builders/latex/__init__.py:154 msgid "no \"latex_documents\" config value found; no documents will be written" msgstr "" -#: sphinx/builders/latex/__init__.py:173 +#: sphinx/builders/latex/__init__.py:162 #, python-format msgid "\"latex_documents\" config value references unknown document %s" msgstr "" -#: sphinx/builders/latex/__init__.py:213 sphinx/domains/std.py:501 -#: sphinx/templates/latex/latex.tex_t:79 -#: sphinx/themes/basic/genindex-single.html:30 -#: sphinx/themes/basic/genindex-single.html:55 -#: sphinx/themes/basic/genindex-split.html:11 -#: sphinx/themes/basic/genindex-split.html:14 -#: sphinx/themes/basic/genindex.html:30 sphinx/themes/basic/genindex.html:33 -#: sphinx/themes/basic/genindex.html:66 sphinx/themes/basic/layout.html:150 -#: sphinx/writers/texinfo.py:522 +#: sphinx/builders/latex/__init__.py:199 sphinx/domains/std.py:586 +#: sphinx/templates/latex/latex.tex_t:68 +#: sphinx/themes/basic/genindex-single.html:19 +#: sphinx/themes/basic/genindex-single.html:36 +#: sphinx/themes/basic/genindex-split.html:10 +#: sphinx/themes/basic/genindex-split.html:13 +#: sphinx/themes/basic/genindex.html:19 sphinx/themes/basic/genindex.html:22 +#: sphinx/themes/basic/genindex.html:44 sphinx/themes/basic/layout.html:51 +#: sphinx/writers/texinfo.py:502 msgid "Index" msgstr "" -#: sphinx/builders/latex/__init__.py:216 sphinx/templates/latex/latex.tex_t:64 +#: sphinx/builders/latex/__init__.py:202 sphinx/templates/latex/latex.tex_t:55 msgid "Release" msgstr "" -#: sphinx/builders/latex/__init__.py:224 sphinx/writers/latex.py:549 +#: sphinx/builders/latex/__init__.py:209 sphinx/writers/latex.py:388 #, python-format msgid "no Babel option known for language %r" msgstr "" -#: sphinx/builders/latex/__init__.py:363 +#: sphinx/builders/latex/__init__.py:374 msgid "copying TeX support files" msgstr "" -#: sphinx/builders/latex/__init__.py:384 +#: sphinx/builders/latex/__init__.py:394 msgid "copying TeX support files..." msgstr "" -#: sphinx/builders/latex/__init__.py:404 +#: sphinx/builders/latex/__init__.py:407 msgid "copying additional files" msgstr "" -#: sphinx/builders/latex/__init__.py:447 +#: sphinx/builders/latex/__init__.py:489 +#, python-format +msgid "Unknown configure key: latex_elements[%r], ignored." +msgstr "" + +#: sphinx/builders/latex/theming.py:79 #, python-format -msgid "Unknown configure key: latex_elements[%r]. ignored." +msgid "%r doesn't have \"theme\" setting" msgstr "" -#: sphinx/cmd/build.py:38 +#: sphinx/builders/latex/theming.py:81 +#, python-format +msgid "%r doesn't have \"%s\" setting" +msgstr "" + +#: sphinx/cmd/build.py:34 msgid "Exception occurred while building, starting debugger:" msgstr "" -#: sphinx/cmd/build.py:48 -msgid "interrupted!" +#: sphinx/cmd/build.py:44 +msgid "Interrupted!" msgstr "" -#: sphinx/cmd/build.py:50 +#: sphinx/cmd/build.py:46 msgid "reST markup error:" msgstr "" -#: sphinx/cmd/build.py:56 +#: sphinx/cmd/build.py:52 msgid "Encoding error:" msgstr "" -#: sphinx/cmd/build.py:59 sphinx/cmd/build.py:74 +#: sphinx/cmd/build.py:55 sphinx/cmd/build.py:70 #, python-format msgid "" "The full traceback has been saved in %s, if you want to report the issue " "to the developers." msgstr "" -#: sphinx/cmd/build.py:63 +#: sphinx/cmd/build.py:59 msgid "Recursion error:" msgstr "" -#: sphinx/cmd/build.py:66 +#: sphinx/cmd/build.py:62 msgid "" "This can happen with very large or deeply nested source files. You can " "carefully increase the default Python recursion limit of 1000 in conf.py " "with e.g.:" msgstr "" -#: sphinx/cmd/build.py:69 -msgid " import sys; sys.setrecursionlimit(1500)" -msgstr "" - -#: sphinx/cmd/build.py:71 +#: sphinx/cmd/build.py:67 msgid "Exception occurred:" msgstr "" -#: sphinx/cmd/build.py:77 +#: sphinx/cmd/build.py:73 msgid "" "Please also report this if it was a user error, so that a better error " "message can be provided next time." msgstr "" -#: sphinx/cmd/build.py:80 +#: sphinx/cmd/build.py:76 msgid "" "A bug report can be filed in the tracker at <https://github.com/sphinx-" "doc/sphinx/issues>. Thanks!" msgstr "" -#: sphinx/cmd/build.py:97 +#: sphinx/cmd/build.py:92 msgid "job number should be a positive number" msgstr "" -#: sphinx/cmd/build.py:106 sphinx/cmd/quickstart.py:497 -#: sphinx/ext/apidoc.py:298 sphinx/ext/autosummary/generate.py:363 +#: sphinx/cmd/build.py:100 sphinx/cmd/quickstart.py:478 +#: sphinx/ext/apidoc.py:350 sphinx/ext/autosummary/generate.py:434 msgid "For more information, visit <http://sphinx-doc.org/>." msgstr "" -#: sphinx/cmd/build.py:107 +#: sphinx/cmd/build.py:101 msgid "" "\n" "Generate documentation from source files.\n" @@ -1153,262 +1189,255 @@ msgid "" "files can be built by specifying individual filenames.\n" msgstr "" -#: sphinx/cmd/build.py:128 +#: sphinx/cmd/build.py:122 msgid "path to documentation source files" msgstr "" -#: sphinx/cmd/build.py:130 +#: sphinx/cmd/build.py:124 msgid "path to output directory" msgstr "" -#: sphinx/cmd/build.py:132 +#: sphinx/cmd/build.py:126 msgid "a list of specific files to rebuild. Ignored if -a is specified" msgstr "" -#: sphinx/cmd/build.py:135 +#: sphinx/cmd/build.py:129 msgid "general options" msgstr "" -#: sphinx/cmd/build.py:138 +#: sphinx/cmd/build.py:132 msgid "builder to use (default: html)" msgstr "" -#: sphinx/cmd/build.py:140 +#: sphinx/cmd/build.py:134 msgid "write all files (default: only write new and changed files)" msgstr "" -#: sphinx/cmd/build.py:143 +#: sphinx/cmd/build.py:137 msgid "don't use a saved environment, always read all files" msgstr "" -#: sphinx/cmd/build.py:146 +#: sphinx/cmd/build.py:140 msgid "" "path for the cached environment and doctree files (default: " "OUTPUTDIR/.doctrees)" msgstr "" -#: sphinx/cmd/build.py:149 +#: sphinx/cmd/build.py:143 msgid "" "build in parallel with N processes where possible (special value \"auto\"" " will set N to cpu-count)" msgstr "" -#: sphinx/cmd/build.py:153 +#: sphinx/cmd/build.py:147 msgid "" "path where configuration file (conf.py) is located (default: same as " "SOURCEDIR)" msgstr "" -#: sphinx/cmd/build.py:156 +#: sphinx/cmd/build.py:150 msgid "use no config file at all, only -D options" msgstr "" -#: sphinx/cmd/build.py:159 +#: sphinx/cmd/build.py:153 msgid "override a setting in configuration file" msgstr "" -#: sphinx/cmd/build.py:162 +#: sphinx/cmd/build.py:156 msgid "pass a value into HTML templates" msgstr "" -#: sphinx/cmd/build.py:165 +#: sphinx/cmd/build.py:159 msgid "define tag: include \"only\" blocks with TAG" msgstr "" -#: sphinx/cmd/build.py:167 +#: sphinx/cmd/build.py:161 msgid "nit-picky mode, warn about all missing references" msgstr "" -#: sphinx/cmd/build.py:170 +#: sphinx/cmd/build.py:164 msgid "console output options" msgstr "" -#: sphinx/cmd/build.py:172 +#: sphinx/cmd/build.py:166 msgid "increase verbosity (can be repeated)" msgstr "" -#: sphinx/cmd/build.py:174 +#: sphinx/cmd/build.py:168 sphinx/ext/apidoc.py:373 msgid "no output on stdout, just warnings on stderr" msgstr "" -#: sphinx/cmd/build.py:176 +#: sphinx/cmd/build.py:170 msgid "no output at all, not even warnings" msgstr "" -#: sphinx/cmd/build.py:179 +#: sphinx/cmd/build.py:173 msgid "do emit colored output (default: auto-detect)" msgstr "" -#: sphinx/cmd/build.py:182 +#: sphinx/cmd/build.py:176 msgid "do not emit colored output (default: auto-detect)" msgstr "" -#: sphinx/cmd/build.py:185 +#: sphinx/cmd/build.py:179 msgid "write warnings (and errors) to given file" msgstr "" -#: sphinx/cmd/build.py:187 +#: sphinx/cmd/build.py:181 msgid "turn warnings into errors" msgstr "" -#: sphinx/cmd/build.py:189 -msgid "With -W, Keep going when getting warnings" +#: sphinx/cmd/build.py:183 +msgid "with -W, keep going when getting warnings" msgstr "" -#: sphinx/cmd/build.py:191 +#: sphinx/cmd/build.py:185 msgid "show full traceback on exception" msgstr "" -#: sphinx/cmd/build.py:193 +#: sphinx/cmd/build.py:187 msgid "run Pdb on exception" msgstr "" -#: sphinx/cmd/build.py:227 +#: sphinx/cmd/build.py:219 #, python-format msgid "cannot find files %r" msgstr "" -#: sphinx/cmd/build.py:230 +#: sphinx/cmd/build.py:222 msgid "cannot combine -a option and filenames" msgstr "" -#: sphinx/cmd/build.py:249 +#: sphinx/cmd/build.py:241 #, python-format msgid "cannot open warning file %r: %s" msgstr "" -#: sphinx/cmd/build.py:259 +#: sphinx/cmd/build.py:251 msgid "-D option argument must be in the form name=value" msgstr "" -#: sphinx/cmd/build.py:266 +#: sphinx/cmd/build.py:258 msgid "-A option argument must be in the form name=value" msgstr "" -#: sphinx/cmd/quickstart.py:52 +#: sphinx/cmd/quickstart.py:49 msgid "automatically insert docstrings from modules" msgstr "" -#: sphinx/cmd/quickstart.py:53 +#: sphinx/cmd/quickstart.py:50 msgid "automatically test code snippets in doctest blocks" msgstr "" -#: sphinx/cmd/quickstart.py:54 +#: sphinx/cmd/quickstart.py:51 msgid "link between Sphinx documentation of different projects" msgstr "" -#: sphinx/cmd/quickstart.py:55 +#: sphinx/cmd/quickstart.py:52 msgid "write \"todo\" entries that can be shown or hidden on build" msgstr "" -#: sphinx/cmd/quickstart.py:56 +#: sphinx/cmd/quickstart.py:53 msgid "checks for documentation coverage" msgstr "" -#: sphinx/cmd/quickstart.py:57 +#: sphinx/cmd/quickstart.py:54 msgid "include math, rendered as PNG or SVG images" msgstr "" -#: sphinx/cmd/quickstart.py:58 +#: sphinx/cmd/quickstart.py:55 msgid "include math, rendered in the browser by MathJax" msgstr "" -#: sphinx/cmd/quickstart.py:59 +#: sphinx/cmd/quickstart.py:56 msgid "conditional inclusion of content based on config values" msgstr "" -#: sphinx/cmd/quickstart.py:61 +#: sphinx/cmd/quickstart.py:57 msgid "include links to the source code of documented Python objects" msgstr "" -#: sphinx/cmd/quickstart.py:63 +#: sphinx/cmd/quickstart.py:58 msgid "create .nojekyll file to publish the document on GitHub pages" msgstr "" -#: sphinx/cmd/quickstart.py:107 +#: sphinx/cmd/quickstart.py:100 msgid "Please enter a valid path name." msgstr "" -#: sphinx/cmd/quickstart.py:119 +#: sphinx/cmd/quickstart.py:110 msgid "Please enter some text." msgstr "" -#: sphinx/cmd/quickstart.py:128 +#: sphinx/cmd/quickstart.py:117 #, python-format msgid "Please enter one of %s." msgstr "" -#: sphinx/cmd/quickstart.py:136 +#: sphinx/cmd/quickstart.py:124 msgid "Please enter either 'y' or 'n'." msgstr "" -#: sphinx/cmd/quickstart.py:143 +#: sphinx/cmd/quickstart.py:130 msgid "Please enter a file suffix, e.g. '.rst' or '.txt'." msgstr "" -#: sphinx/cmd/quickstart.py:169 +#: sphinx/cmd/quickstart.py:153 msgid "" "* Note: non-ASCII characters entered and terminal encoding unknown -- " "assuming UTF-8 or Latin-1." msgstr "" -#: sphinx/cmd/quickstart.py:248 +#: sphinx/cmd/quickstart.py:227 #, python-format msgid "Welcome to the Sphinx %s quickstart utility." msgstr "" -#: sphinx/cmd/quickstart.py:249 +#: sphinx/cmd/quickstart.py:229 msgid "" -"\n" "Please enter values for the following settings (just press Enter to\n" "accept a default value, if one is given in brackets)." msgstr "" -#: sphinx/cmd/quickstart.py:254 +#: sphinx/cmd/quickstart.py:234 #, python-format -msgid "" -"\n" -"Selected root path: %s" +msgid "Selected root path: %s" msgstr "" -#: sphinx/cmd/quickstart.py:257 -msgid "" -"\n" -"Enter the root path for documentation." +#: sphinx/cmd/quickstart.py:237 +msgid "Enter the root path for documentation." msgstr "" -#: sphinx/cmd/quickstart.py:259 +#: sphinx/cmd/quickstart.py:238 msgid "Root path for the documentation" msgstr "" -#: sphinx/cmd/quickstart.py:264 +#: sphinx/cmd/quickstart.py:243 msgid "Error: an existing conf.py has been found in the selected root path." msgstr "" -#: sphinx/cmd/quickstart.py:266 +#: sphinx/cmd/quickstart.py:245 msgid "sphinx-quickstart will not overwrite existing Sphinx projects." msgstr "" -#: sphinx/cmd/quickstart.py:268 +#: sphinx/cmd/quickstart.py:247 msgid "Please enter a new root path (or just Enter to exit)" msgstr "" -#: sphinx/cmd/quickstart.py:274 +#: sphinx/cmd/quickstart.py:254 msgid "" -"\n" "You have two options for placing the build directory for Sphinx output.\n" "Either, you use a directory \"_build\" within the root path, or you " "separate\n" "\"source\" and \"build\" directories within the root path." msgstr "" -#: sphinx/cmd/quickstart.py:278 +#: sphinx/cmd/quickstart.py:257 msgid "Separate source and build directories (y/n)" msgstr "" -#: sphinx/cmd/quickstart.py:282 +#: sphinx/cmd/quickstart.py:261 msgid "" -"\n" "Inside the root directory, two more directories will be created; " "\"_templates\"\n" "for custom HTML templates and \"_static\" for custom stylesheets and " @@ -1417,27 +1446,24 @@ msgid "" "underscore." msgstr "" -#: sphinx/cmd/quickstart.py:286 +#: sphinx/cmd/quickstart.py:264 msgid "Name prefix for templates and static dir" msgstr "" -#: sphinx/cmd/quickstart.py:289 -msgid "" -"\n" -"The project name will occur in several places in the built documentation." +#: sphinx/cmd/quickstart.py:268 +msgid "The project name will occur in several places in the built documentation." msgstr "" -#: sphinx/cmd/quickstart.py:291 +#: sphinx/cmd/quickstart.py:269 msgid "Project name" msgstr "" -#: sphinx/cmd/quickstart.py:293 +#: sphinx/cmd/quickstart.py:271 msgid "Author name(s)" msgstr "" -#: sphinx/cmd/quickstart.py:296 +#: sphinx/cmd/quickstart.py:275 msgid "" -"\n" "Sphinx has the notion of a \"version\" and a \"release\" for the\n" "software. Each version can have multiple releases. For example, for\n" "Python the version is something like 2.5 or 3.0, while the release is\n" @@ -1445,138 +1471,134 @@ msgid "" "just set both to the same value." msgstr "" -#: sphinx/cmd/quickstart.py:302 +#: sphinx/cmd/quickstart.py:280 msgid "Project version" msgstr "" -#: sphinx/cmd/quickstart.py:304 +#: sphinx/cmd/quickstart.py:282 msgid "Project release" msgstr "" -#: sphinx/cmd/quickstart.py:307 +#: sphinx/cmd/quickstart.py:286 msgid "" -"\n" "If the documents are to be written in a language other than English,\n" "you can select a language here by its language code. Sphinx will then\n" "translate text that it generates into that language.\n" "\n" "For a list of supported codes, see\n" -"https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language." +"https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-" +"language." msgstr "" -#: sphinx/cmd/quickstart.py:314 +#: sphinx/cmd/quickstart.py:292 msgid "Project language" msgstr "" -#: sphinx/cmd/quickstart.py:319 +#: sphinx/cmd/quickstart.py:298 msgid "" -"\n" "The file name suffix for source files. Commonly, this is either \".txt\"\n" "or \".rst\". Only files with this suffix are considered documents." msgstr "" -#: sphinx/cmd/quickstart.py:322 +#: sphinx/cmd/quickstart.py:300 msgid "Source file suffix" msgstr "" -#: sphinx/cmd/quickstart.py:325 +#: sphinx/cmd/quickstart.py:304 msgid "" -"\n" "One document is special in that it is considered the top node of the\n" "\"contents tree\", that is, it is the root of the hierarchical structure\n" "of the documents. Normally, this is \"index\", but if your \"index\"\n" "document is a custom template, you can also set this to another filename." msgstr "" -#: sphinx/cmd/quickstart.py:330 +#: sphinx/cmd/quickstart.py:308 msgid "Name of your master document (without suffix)" msgstr "" -#: sphinx/cmd/quickstart.py:336 +#: sphinx/cmd/quickstart.py:313 #, python-format msgid "" "Error: the master file %s has already been found in the selected root " "path." msgstr "" -#: sphinx/cmd/quickstart.py:338 +#: sphinx/cmd/quickstart.py:315 msgid "sphinx-quickstart will not overwrite the existing file." msgstr "" -#: sphinx/cmd/quickstart.py:340 +#: sphinx/cmd/quickstart.py:317 msgid "Please enter a new file name, or rename the existing file and press Enter" msgstr "" -#: sphinx/cmd/quickstart.py:344 +#: sphinx/cmd/quickstart.py:321 msgid "Indicate which of the following Sphinx extensions should be enabled:" msgstr "" -#: sphinx/cmd/quickstart.py:353 +#: sphinx/cmd/quickstart.py:329 msgid "" "Note: imgmath and mathjax cannot be enabled at the same time. imgmath has" " been deselected." msgstr "" -#: sphinx/cmd/quickstart.py:358 +#: sphinx/cmd/quickstart.py:335 msgid "" -"\n" "A Makefile and a Windows command file can be generated for you so that " "you\n" "only have to run e.g. `make html' instead of invoking sphinx-build\n" "directly." msgstr "" -#: sphinx/cmd/quickstart.py:362 +#: sphinx/cmd/quickstart.py:338 msgid "Create Makefile? (y/n)" msgstr "" -#: sphinx/cmd/quickstart.py:365 +#: sphinx/cmd/quickstart.py:341 msgid "Create Windows command file? (y/n)" msgstr "" -#: sphinx/cmd/quickstart.py:408 sphinx/ext/apidoc.py:74 +#: sphinx/cmd/quickstart.py:382 sphinx/ext/apidoc.py:107 #, python-format msgid "Creating file %s." msgstr "" -#: sphinx/cmd/quickstart.py:413 sphinx/ext/apidoc.py:72 +#: sphinx/cmd/quickstart.py:387 sphinx/ext/apidoc.py:104 #, python-format msgid "File %s already exists, skipping." msgstr "" -#: sphinx/cmd/quickstart.py:449 +#: sphinx/cmd/quickstart.py:423 msgid "Finished: An initial directory structure has been created." msgstr "" -#: sphinx/cmd/quickstart.py:450 +#: sphinx/cmd/quickstart.py:425 #, python-format msgid "" -"\n" "You should now populate your master file %s and create other " "documentation\n" "source files. " msgstr "" -#: sphinx/cmd/quickstart.py:452 +#: sphinx/cmd/quickstart.py:428 msgid "" "Use the Makefile to build the docs, like so:\n" -" make builder\n" +" make builder" msgstr "" -#: sphinx/cmd/quickstart.py:455 +#: sphinx/cmd/quickstart.py:431 #, python-format msgid "" "Use the sphinx-build command to build the docs, like so:\n" -" sphinx-build -b builder %s %s\n" +" sphinx-build -b builder %s %s" msgstr "" -#: sphinx/cmd/quickstart.py:458 +#: sphinx/cmd/quickstart.py:433 msgid "" "where \"builder\" is one of the supported builders, e.g. html, latex or " -"linkcheck.\n" +"linkcheck." msgstr "" -#: sphinx/cmd/quickstart.py:498 +#: sphinx/cmd/quickstart.py:468 msgid "" "\n" "Generate required files for a Sphinx project.\n" @@ -1587,137 +1609,137 @@ msgid "" "Makefile to be used with sphinx-build.\n" msgstr "" -#: sphinx/cmd/quickstart.py:508 +#: sphinx/cmd/quickstart.py:483 msgid "quiet mode" msgstr "" -#: sphinx/cmd/quickstart.py:513 -msgid "output path" +#: sphinx/cmd/quickstart.py:488 +msgid "project root" msgstr "" -#: sphinx/cmd/quickstart.py:515 +#: sphinx/cmd/quickstart.py:490 msgid "Structure options" msgstr "" -#: sphinx/cmd/quickstart.py:517 +#: sphinx/cmd/quickstart.py:492 msgid "if specified, separate source and build dirs" msgstr "" -#: sphinx/cmd/quickstart.py:519 +#: sphinx/cmd/quickstart.py:494 msgid "replacement for dot in _templates etc." msgstr "" -#: sphinx/cmd/quickstart.py:521 +#: sphinx/cmd/quickstart.py:496 msgid "Project basic options" msgstr "" -#: sphinx/cmd/quickstart.py:523 +#: sphinx/cmd/quickstart.py:498 msgid "project name" msgstr "" -#: sphinx/cmd/quickstart.py:525 +#: sphinx/cmd/quickstart.py:500 msgid "author names" msgstr "" -#: sphinx/cmd/quickstart.py:527 +#: sphinx/cmd/quickstart.py:502 msgid "version of project" msgstr "" -#: sphinx/cmd/quickstart.py:529 +#: sphinx/cmd/quickstart.py:504 msgid "release of project" msgstr "" -#: sphinx/cmd/quickstart.py:531 +#: sphinx/cmd/quickstart.py:506 msgid "document language" msgstr "" -#: sphinx/cmd/quickstart.py:533 +#: sphinx/cmd/quickstart.py:508 msgid "source file suffix" msgstr "" -#: sphinx/cmd/quickstart.py:535 +#: sphinx/cmd/quickstart.py:510 msgid "master document name" msgstr "" -#: sphinx/cmd/quickstart.py:537 +#: sphinx/cmd/quickstart.py:512 msgid "use epub" msgstr "" -#: sphinx/cmd/quickstart.py:539 +#: sphinx/cmd/quickstart.py:514 msgid "Extension options" msgstr "" -#: sphinx/cmd/quickstart.py:543 sphinx/ext/apidoc.py:379 +#: sphinx/cmd/quickstart.py:518 sphinx/ext/apidoc.py:433 #, python-format msgid "enable %s extension" msgstr "" -#: sphinx/cmd/quickstart.py:545 sphinx/ext/apidoc.py:375 +#: sphinx/cmd/quickstart.py:520 sphinx/ext/apidoc.py:429 msgid "enable arbitrary extensions" msgstr "" -#: sphinx/cmd/quickstart.py:547 +#: sphinx/cmd/quickstart.py:522 msgid "Makefile and Batchfile creation" msgstr "" -#: sphinx/cmd/quickstart.py:549 +#: sphinx/cmd/quickstart.py:524 msgid "create makefile" msgstr "" -#: sphinx/cmd/quickstart.py:551 +#: sphinx/cmd/quickstart.py:526 msgid "do not create makefile" msgstr "" -#: sphinx/cmd/quickstart.py:553 +#: sphinx/cmd/quickstart.py:528 msgid "create batchfile" msgstr "" -#: sphinx/cmd/quickstart.py:556 +#: sphinx/cmd/quickstart.py:531 msgid "do not create batchfile" msgstr "" -#: sphinx/cmd/quickstart.py:559 +#: sphinx/cmd/quickstart.py:534 msgid "use make-mode for Makefile/make.bat" msgstr "" -#: sphinx/cmd/quickstart.py:562 +#: sphinx/cmd/quickstart.py:537 msgid "do not use make-mode for Makefile/make.bat" msgstr "" -#: sphinx/cmd/quickstart.py:564 +#: sphinx/cmd/quickstart.py:539 sphinx/ext/apidoc.py:435 msgid "Project templating" msgstr "" -#: sphinx/cmd/quickstart.py:567 +#: sphinx/cmd/quickstart.py:542 sphinx/ext/apidoc.py:438 msgid "template directory for template files" msgstr "" -#: sphinx/cmd/quickstart.py:570 +#: sphinx/cmd/quickstart.py:545 msgid "define a template variable" msgstr "" -#: sphinx/cmd/quickstart.py:604 +#: sphinx/cmd/quickstart.py:578 msgid "" "\"quiet\" is specified, but any of \"project\" or \"author\" is not " "specified." msgstr "" -#: sphinx/cmd/quickstart.py:618 +#: sphinx/cmd/quickstart.py:592 msgid "Error: specified path is not a directory, or sphinx files already exist." msgstr "" -#: sphinx/cmd/quickstart.py:620 +#: sphinx/cmd/quickstart.py:594 msgid "" "sphinx-quickstart only generate into a empty directory. Please specify a " "new root path." msgstr "" -#: sphinx/cmd/quickstart.py:635 +#: sphinx/cmd/quickstart.py:609 #, python-format msgid "Invalid template variable: %s" msgstr "" -#: sphinx/directives/code.py:74 +#: sphinx/directives/code.py:75 msgid "Over dedent has detected" msgstr "" @@ -1726,110 +1748,110 @@ msgstr "" msgid "Invalid caption: %s" msgstr "" -#: sphinx/directives/code.py:140 sphinx/directives/code.py:292 -#: sphinx/directives/code.py:462 +#: sphinx/directives/code.py:140 sphinx/directives/code.py:284 +#: sphinx/directives/code.py:450 #, python-format msgid "line number spec is out of range(1-%d): %r" msgstr "" -#: sphinx/directives/code.py:222 +#: sphinx/directives/code.py:219 #, python-format msgid "Cannot use both \"%s\" and \"%s\" options" msgstr "" -#: sphinx/directives/code.py:235 +#: sphinx/directives/code.py:231 #, python-format msgid "Include file %r not found or reading it failed" msgstr "" -#: sphinx/directives/code.py:237 +#: sphinx/directives/code.py:233 #, python-format msgid "" "Encoding %r used for reading included file %r seems to be wrong, try " "giving an :encoding: option" msgstr "" -#: sphinx/directives/code.py:275 +#: sphinx/directives/code.py:268 #, python-format msgid "Object named %r not found in include file %r" msgstr "" -#: sphinx/directives/code.py:301 +#: sphinx/directives/code.py:293 msgid "Cannot use \"lineno-match\" with a disjoint set of \"lines\"" msgstr "" -#: sphinx/directives/code.py:306 +#: sphinx/directives/code.py:298 #, python-format msgid "Line spec %r: no lines pulled from include file %r" msgstr "" -#: sphinx/directives/other.py:172 +#: sphinx/directives/other.py:171 msgid "Section author: " msgstr "" -#: sphinx/directives/other.py:174 +#: sphinx/directives/other.py:173 msgid "Module author: " msgstr "" -#: sphinx/directives/other.py:176 +#: sphinx/directives/other.py:175 msgid "Code author: " msgstr "" -#: sphinx/directives/other.py:178 +#: sphinx/directives/other.py:177 msgid "Author: " msgstr "" -#: sphinx/domains/__init__.py:344 +#: sphinx/domains/__init__.py:393 #, python-format msgid "%s %s" msgstr "" -#: sphinx/domains/c.py:64 sphinx/domains/cpp.py:6445 -#: sphinx/domains/python.py:210 sphinx/ext/napoleon/docstring.py:696 +#: sphinx/domains/c.py:67 sphinx/domains/cpp.py:6410 +#: sphinx/domains/python.py:249 sphinx/ext/napoleon/docstring.py:661 msgid "Parameters" msgstr "" -#: sphinx/domains/c.py:67 sphinx/domains/cpp.py:6454 -#: sphinx/domains/javascript.py:209 sphinx/domains/python.py:222 +#: sphinx/domains/c.py:70 sphinx/domains/cpp.py:6419 +#: sphinx/domains/javascript.py:216 sphinx/domains/python.py:261 msgid "Returns" msgstr "" -#: sphinx/domains/c.py:69 sphinx/domains/javascript.py:211 -#: sphinx/domains/python.py:224 +#: sphinx/domains/c.py:72 sphinx/domains/javascript.py:218 +#: sphinx/domains/python.py:263 msgid "Return type" msgstr "" -#: sphinx/domains/c.py:189 +#: sphinx/domains/c.py:187 #, python-format msgid "%s (C function)" msgstr "" -#: sphinx/domains/c.py:191 +#: sphinx/domains/c.py:189 #, python-format msgid "%s (C member)" msgstr "" -#: sphinx/domains/c.py:193 +#: sphinx/domains/c.py:191 #, python-format msgid "%s (C macro)" msgstr "" -#: sphinx/domains/c.py:195 +#: sphinx/domains/c.py:193 #, python-format msgid "%s (C type)" msgstr "" -#: sphinx/domains/c.py:197 +#: sphinx/domains/c.py:195 #, python-format msgid "%s (C variable)" msgstr "" -#: sphinx/domains/c.py:258 sphinx/domains/cpp.py:7029 -#: sphinx/domains/javascript.py:297 sphinx/domains/python.py:742 +#: sphinx/domains/c.py:258 sphinx/domains/cpp.py:6991 +#: sphinx/domains/javascript.py:321 sphinx/domains/python.py:1015 msgid "function" msgstr "" -#: sphinx/domains/c.py:259 sphinx/domains/cpp.py:7030 +#: sphinx/domains/c.py:259 sphinx/domains/cpp.py:6992 msgid "member" msgstr "" @@ -1837,7 +1859,7 @@ msgstr "" msgid "macro" msgstr "" -#: sphinx/domains/c.py:261 sphinx/domains/cpp.py:7031 +#: sphinx/domains/c.py:261 sphinx/domains/cpp.py:6993 msgid "type" msgstr "" @@ -1845,370 +1867,435 @@ msgstr "" msgid "variable" msgstr "" -#: sphinx/domains/changeset.py:33 +#: sphinx/domains/c.py:290 +#, python-format +msgid "" +"duplicate C object description of %s, other instance in %s, use :noindex:" +" for one of them" +msgstr "" + +#: sphinx/domains/changeset.py:31 #, python-format msgid "New in version %s" msgstr "" -#: sphinx/domains/changeset.py:34 +#: sphinx/domains/changeset.py:32 #, python-format msgid "Changed in version %s" msgstr "" -#: sphinx/domains/changeset.py:35 +#: sphinx/domains/changeset.py:33 #, python-format msgid "Deprecated since version %s" msgstr "" -#: sphinx/domains/cpp.py:4323 +#: sphinx/domains/citation.py:77 +#, python-format +msgid "duplicate citation %s, other instance in %s" +msgstr "" + +#: sphinx/domains/citation.py:88 +#, python-format +msgid "Citation [%s] is not referenced." +msgstr "" + +#: sphinx/domains/cpp.py:4220 #, python-format msgid "" "Duplicate declaration, also defined in '%s'.\n" "Declaration is '%s'." msgstr "" -#: sphinx/domains/cpp.py:6448 +#: sphinx/domains/cpp.py:6413 msgid "Template Parameters" msgstr "" -#: sphinx/domains/cpp.py:6451 sphinx/domains/javascript.py:206 +#: sphinx/domains/cpp.py:6416 sphinx/domains/javascript.py:213 msgid "Throws" msgstr "" -#: sphinx/domains/cpp.py:6579 +#: sphinx/domains/cpp.py:6539 #, python-format msgid "%s (C++ %s)" msgstr "" -#: sphinx/domains/cpp.py:7027 sphinx/domains/javascript.py:299 -#: sphinx/domains/python.py:744 +#: sphinx/domains/cpp.py:6989 sphinx/domains/javascript.py:323 +#: sphinx/domains/python.py:1017 msgid "class" msgstr "" -#: sphinx/domains/cpp.py:7028 +#: sphinx/domains/cpp.py:6990 msgid "union" msgstr "" -#: sphinx/domains/cpp.py:7032 +#: sphinx/domains/cpp.py:6994 msgid "concept" msgstr "" -#: sphinx/domains/cpp.py:7033 +#: sphinx/domains/cpp.py:6995 msgid "enum" msgstr "" -#: sphinx/domains/cpp.py:7034 +#: sphinx/domains/cpp.py:6996 msgid "enumerator" msgstr "" -#: sphinx/domains/cpp.py:7127 +#: sphinx/domains/cpp.py:7086 #, python-format msgid "" "Duplicate declaration, also defined in '%s'.\n" "Name of declaration is '%s'." msgstr "" -#: sphinx/domains/javascript.py:130 sphinx/domains/python.py:430 +#: sphinx/domains/javascript.py:131 sphinx/domains/python.py:471 +#: sphinx/domains/python.py:503 #, python-format msgid "%s() (built-in function)" msgstr "" -#: sphinx/domains/javascript.py:131 sphinx/domains/python.py:495 +#: sphinx/domains/javascript.py:132 sphinx/domains/python.py:615 +#: sphinx/domains/python.py:713 #, python-format msgid "%s() (%s method)" msgstr "" -#: sphinx/domains/javascript.py:133 +#: sphinx/domains/javascript.py:134 #, python-format msgid "%s() (class)" msgstr "" -#: sphinx/domains/javascript.py:135 +#: sphinx/domains/javascript.py:136 #, python-format msgid "%s (global variable or constant)" msgstr "" -#: sphinx/domains/javascript.py:137 sphinx/domains/python.py:533 +#: sphinx/domains/javascript.py:138 sphinx/domains/python.py:653 +#: sphinx/domains/python.py:790 #, python-format msgid "%s (%s attribute)" msgstr "" -#: sphinx/domains/javascript.py:203 +#: sphinx/domains/javascript.py:210 msgid "Arguments" msgstr "" -#: sphinx/domains/javascript.py:264 sphinx/domains/python.py:610 +#: sphinx/domains/javascript.py:281 sphinx/domains/python.py:862 #, python-format msgid "%s (module)" msgstr "" -#: sphinx/domains/javascript.py:298 sphinx/domains/python.py:746 +#: sphinx/domains/javascript.py:322 sphinx/domains/python.py:1019 msgid "method" msgstr "" -#: sphinx/domains/javascript.py:300 sphinx/domains/python.py:743 +#: sphinx/domains/javascript.py:324 sphinx/domains/python.py:1016 msgid "data" msgstr "" -#: sphinx/domains/javascript.py:301 sphinx/domains/python.py:749 +#: sphinx/domains/javascript.py:325 sphinx/domains/python.py:1022 msgid "attribute" msgstr "" -#: sphinx/domains/javascript.py:302 sphinx/domains/python.py:49 -#: sphinx/domains/python.py:750 +#: sphinx/domains/javascript.py:326 sphinx/domains/python.py:60 +#: sphinx/domains/python.py:1023 msgid "module" msgstr "" -#: sphinx/domains/math.py:101 sphinx/writers/latex.py:2459 +#: sphinx/domains/javascript.py:357 #, python-format -msgid "Invalid math_eqref_format: %r" +msgid "duplicate %s description of %s, other %s in %s" msgstr "" -#: sphinx/domains/math.py:126 +#: sphinx/domains/math.py:69 sphinx/domains/math.py:148 #, python-format msgid "duplicate label of equation %s, other instance in %s" msgstr "" -#: sphinx/domains/python.py:50 +#: sphinx/domains/math.py:123 sphinx/writers/latex.py:2031 +#, python-format +msgid "Invalid math_eqref_format: %r" +msgstr "" + +#: sphinx/domains/python.py:61 msgid "keyword" msgstr "" -#: sphinx/domains/python.py:51 +#: sphinx/domains/python.py:62 msgid "operator" msgstr "" -#: sphinx/domains/python.py:52 +#: sphinx/domains/python.py:63 msgid "object" msgstr "" -#: sphinx/domains/python.py:53 sphinx/domains/python.py:745 +#: sphinx/domains/python.py:64 sphinx/domains/python.py:1018 msgid "exception" msgstr "" -#: sphinx/domains/python.py:54 +#: sphinx/domains/python.py:65 msgid "statement" msgstr "" -#: sphinx/domains/python.py:55 +#: sphinx/domains/python.py:66 msgid "built-in function" msgstr "" -#: sphinx/domains/python.py:215 +#: sphinx/domains/python.py:254 msgid "Variables" msgstr "" -#: sphinx/domains/python.py:219 +#: sphinx/domains/python.py:258 msgid "Raises" msgstr "" -#: sphinx/domains/python.py:431 sphinx/domains/python.py:489 -#: sphinx/domains/python.py:501 sphinx/domains/python.py:514 +#: sphinx/domains/python.py:472 sphinx/domains/python.py:501 +#: sphinx/domains/python.py:609 sphinx/domains/python.py:621 +#: sphinx/domains/python.py:634 sphinx/domains/python.py:702 #, python-format msgid "%s() (in module %s)" msgstr "" -#: sphinx/domains/python.py:434 +#: sphinx/domains/python.py:475 sphinx/domains/python.py:550 #, python-format msgid "%s (built-in variable)" msgstr "" -#: sphinx/domains/python.py:435 sphinx/domains/python.py:527 +#: sphinx/domains/python.py:476 sphinx/domains/python.py:548 +#: sphinx/domains/python.py:647 sphinx/domains/python.py:786 #, python-format msgid "%s (in module %s)" msgstr "" -#: sphinx/domains/python.py:455 +#: sphinx/domains/python.py:566 #, python-format msgid "%s (built-in class)" msgstr "" -#: sphinx/domains/python.py:456 +#: sphinx/domains/python.py:567 #, python-format msgid "%s (class in %s)" msgstr "" -#: sphinx/domains/python.py:493 +#: sphinx/domains/python.py:613 #, python-format msgid "%s() (%s.%s method)" msgstr "" -#: sphinx/domains/python.py:505 +#: sphinx/domains/python.py:625 #, python-format msgid "%s() (%s.%s static method)" msgstr "" -#: sphinx/domains/python.py:508 +#: sphinx/domains/python.py:628 sphinx/domains/python.py:711 #, python-format msgid "%s() (%s static method)" msgstr "" -#: sphinx/domains/python.py:518 +#: sphinx/domains/python.py:638 #, python-format msgid "%s() (%s.%s class method)" msgstr "" -#: sphinx/domains/python.py:521 +#: sphinx/domains/python.py:641 sphinx/domains/python.py:707 #, python-format msgid "%s() (%s class method)" msgstr "" -#: sphinx/domains/python.py:531 +#: sphinx/domains/python.py:651 #, python-format msgid "%s (%s.%s attribute)" msgstr "" -#: sphinx/domains/python.py:668 +#: sphinx/domains/python.py:709 +#, python-format +msgid "%s() (%s property)" +msgstr "" + +#: sphinx/domains/python.py:943 msgid "Python Module Index" msgstr "" -#: sphinx/domains/python.py:669 +#: sphinx/domains/python.py:944 msgid "modules" msgstr "" -#: sphinx/domains/python.py:720 +#: sphinx/domains/python.py:994 msgid "Deprecated" msgstr "" -#: sphinx/domains/python.py:747 +#: sphinx/domains/python.py:1020 msgid "class method" msgstr "" -#: sphinx/domains/python.py:748 +#: sphinx/domains/python.py:1021 msgid "static method" msgstr "" -#: sphinx/domains/python.py:880 +#: sphinx/domains/python.py:1070 +#, python-format +msgid "" +"duplicate object description of %s, other instance in %s, use :noindex: " +"for one of them" +msgstr "" + +#: sphinx/domains/python.py:1176 #, python-format msgid "more than one target found for cross-reference %r: %s" msgstr "" -#: sphinx/domains/python.py:918 +#: sphinx/domains/python.py:1214 msgid " (deprecated)" msgstr "" -#: sphinx/domains/rst.py:62 +#: sphinx/domains/rst.py:105 sphinx/domains/rst.py:166 #, python-format msgid "%s (directive)" msgstr "" -#: sphinx/domains/rst.py:64 +#: sphinx/domains/rst.py:167 sphinx/domains/rst.py:171 +#, python-format +msgid ":%s: (directive option)" +msgstr "" + +#: sphinx/domains/rst.py:200 #, python-format msgid "%s (role)" msgstr "" -#: sphinx/domains/rst.py:116 +#: sphinx/domains/rst.py:209 msgid "directive" msgstr "" -#: sphinx/domains/rst.py:117 +#: sphinx/domains/rst.py:210 +msgid "directive-option" +msgstr "" + +#: sphinx/domains/rst.py:211 msgid "role" msgstr "" -#: sphinx/domains/std.py:88 sphinx/domains/std.py:105 +#: sphinx/domains/rst.py:233 +#, python-format +msgid "duplicate description of %s %s, other instance in %s" +msgstr "" + +#: sphinx/domains/std.py:103 sphinx/domains/std.py:120 #, python-format msgid "environment variable; %s" msgstr "" -#: sphinx/domains/std.py:166 +#: sphinx/domains/std.py:194 #, python-format msgid "" "Malformed option description %r, should look like \"opt\", \"-opt args\"," " \"--opt args\", \"/opt args\" or \"+opt args\"" msgstr "" -#: sphinx/domains/std.py:207 +#: sphinx/domains/std.py:235 #, python-format -msgid "%scommand line option; %s" +msgid "%s command line option" msgstr "" -#: sphinx/domains/std.py:458 +#: sphinx/domains/std.py:237 +msgid "command line option" +msgstr "" + +#: sphinx/domains/std.py:368 +msgid "glossary term must be preceded by empty line" +msgstr "" + +#: sphinx/domains/std.py:376 +msgid "glossary terms must not be separated by empty lines" +msgstr "" + +#: sphinx/domains/std.py:382 sphinx/domains/std.py:395 +msgid "glossary seems to be misformatted, check indentation" +msgstr "" + +#: sphinx/domains/std.py:545 msgid "glossary term" msgstr "" -#: sphinx/domains/std.py:459 +#: sphinx/domains/std.py:546 msgid "grammar token" msgstr "" -#: sphinx/domains/std.py:460 +#: sphinx/domains/std.py:547 msgid "reference label" msgstr "" -#: sphinx/domains/std.py:462 +#: sphinx/domains/std.py:549 msgid "environment variable" msgstr "" -#: sphinx/domains/std.py:463 +#: sphinx/domains/std.py:550 msgid "program option" msgstr "" -#: sphinx/domains/std.py:464 +#: sphinx/domains/std.py:551 msgid "document" msgstr "" -#: sphinx/domains/std.py:502 +#: sphinx/domains/std.py:587 msgid "Module Index" msgstr "" -#: sphinx/domains/std.py:503 sphinx/themes/basic/defindex.html:25 +#: sphinx/domains/std.py:588 sphinx/themes/basic/defindex.html:24 msgid "Search Page" msgstr "" -#: sphinx/domains/std.py:598 -#, python-format -msgid "duplicate citation %s, other instance in %s" -msgstr "" - -#: sphinx/domains/std.py:631 sphinx/ext/autosectionlabel.py:56 +#: sphinx/domains/std.py:638 sphinx/domains/std.py:727 +#: sphinx/ext/autosectionlabel.py:53 #, python-format msgid "duplicate label %s, other instance in %s" msgstr "" -#: sphinx/domains/std.py:665 +#: sphinx/domains/std.py:657 #, python-format -msgid "Citation [%s] is not referenced." +msgid "duplicate %s description of %s, other instance in %s" msgstr "" -#: sphinx/domains/std.py:748 +#: sphinx/domains/std.py:833 msgid "numfig is disabled. :numref: is ignored." msgstr "" -#: sphinx/domains/std.py:756 +#: sphinx/domains/std.py:841 #, python-format msgid "no number is assigned for %s: %s" msgstr "" -#: sphinx/domains/std.py:767 +#: sphinx/domains/std.py:852 #, python-format msgid "the link has no caption: %s" msgstr "" -#: sphinx/domains/std.py:781 +#: sphinx/domains/std.py:866 #, python-format msgid "invalid numfig_format: %s (%r)" msgstr "" -#: sphinx/domains/std.py:784 +#: sphinx/domains/std.py:869 #, python-format msgid "invalid numfig_format: %s" msgstr "" -#: sphinx/environment/__init__.py:69 +#: sphinx/environment/__init__.py:73 msgid "new config" msgstr "" -#: sphinx/environment/__init__.py:70 +#: sphinx/environment/__init__.py:74 msgid "config changed" msgstr "" -#: sphinx/environment/__init__.py:71 +#: sphinx/environment/__init__.py:75 msgid "extensions changed" msgstr "" -#: sphinx/environment/__init__.py:210 +#: sphinx/environment/__init__.py:202 msgid "build environment version not current" msgstr "" -#: sphinx/environment/__init__.py:212 +#: sphinx/environment/__init__.py:204 msgid "source directory has changed" msgstr "" @@ -2218,40 +2305,40 @@ msgid "" " another doctree directory." msgstr "" -#: sphinx/environment/__init__.py:404 +#: sphinx/environment/__init__.py:396 #, python-format msgid "Failed to scan documents in %s: %r" msgstr "" -#: sphinx/environment/__init__.py:532 +#: sphinx/environment/__init__.py:515 #, python-format msgid "Domain %r is not registered" msgstr "" -#: sphinx/environment/__init__.py:617 +#: sphinx/environment/__init__.py:596 msgid "self referenced toctree found. Ignored." msgstr "" -#: sphinx/environment/__init__.py:658 +#: sphinx/environment/__init__.py:636 msgid "document isn't included in any toctree" msgstr "" -#: sphinx/environment/adapters/indexentries.py:82 +#: sphinx/environment/adapters/indexentries.py:80 #, python-format msgid "see %s" msgstr "" -#: sphinx/environment/adapters/indexentries.py:86 +#: sphinx/environment/adapters/indexentries.py:84 #, python-format msgid "see also %s" msgstr "" -#: sphinx/environment/adapters/indexentries.py:89 +#: sphinx/environment/adapters/indexentries.py:87 #, python-format msgid "unknown index entry type %r" msgstr "" -#: sphinx/environment/adapters/indexentries.py:156 +#: sphinx/environment/adapters/indexentries.py:172 #: sphinx/templates/latex/sphinxmessages.sty_t:11 msgid "Symbols" msgstr "" @@ -2278,7 +2365,7 @@ msgstr "" msgid "toctree contains reference to nonexisting document %r" msgstr "" -#: sphinx/environment/collectors/asset.py:90 +#: sphinx/environment/collectors/asset.py:87 #, python-format msgid "image file not readable: %s" msgstr "" @@ -2288,22 +2375,22 @@ msgstr "" msgid "image file %s not readable: %s" msgstr "" -#: sphinx/environment/collectors/asset.py:134 +#: sphinx/environment/collectors/asset.py:132 #, python-format msgid "download file not readable: %s" msgstr "" -#: sphinx/environment/collectors/toctree.py:196 +#: sphinx/environment/collectors/toctree.py:191 #, python-format msgid "%s is already assigned section numbers (nested numbered toctree?)" msgstr "" -#: sphinx/ext/apidoc.py:69 +#: sphinx/ext/apidoc.py:100 #, python-format msgid "Would create file %s." msgstr "" -#: sphinx/ext/apidoc.py:299 +#: sphinx/ext/apidoc.py:351 msgid "" "\n" "Look recursively in <MODULE_PATH> for Python modules and packages and " @@ -2318,177 +2405,181 @@ msgid "" "Note: By default this script will not overwrite already created files." msgstr "" -#: sphinx/ext/apidoc.py:312 +#: sphinx/ext/apidoc.py:364 msgid "path to module to document" msgstr "" -#: sphinx/ext/apidoc.py:314 +#: sphinx/ext/apidoc.py:366 msgid "fnmatch-style file and/or directory patterns to exclude from generation" msgstr "" -#: sphinx/ext/apidoc.py:319 +#: sphinx/ext/apidoc.py:371 msgid "directory to place all output" msgstr "" -#: sphinx/ext/apidoc.py:322 +#: sphinx/ext/apidoc.py:376 msgid "maximum depth of submodules to show in the TOC (default: 4)" msgstr "" -#: sphinx/ext/apidoc.py:325 +#: sphinx/ext/apidoc.py:379 msgid "overwrite existing files" msgstr "" -#: sphinx/ext/apidoc.py:328 +#: sphinx/ext/apidoc.py:382 msgid "" "follow symbolic links. Powerful when combined with " "collective.recipe.omelette." msgstr "" -#: sphinx/ext/apidoc.py:331 +#: sphinx/ext/apidoc.py:385 msgid "run the script without creating files" msgstr "" -#: sphinx/ext/apidoc.py:334 +#: sphinx/ext/apidoc.py:388 msgid "put documentation for each module on its own page" msgstr "" -#: sphinx/ext/apidoc.py:337 +#: sphinx/ext/apidoc.py:391 msgid "include \"_private\" modules" msgstr "" -#: sphinx/ext/apidoc.py:339 +#: sphinx/ext/apidoc.py:393 msgid "filename of table of contents (default: modules)" msgstr "" -#: sphinx/ext/apidoc.py:341 +#: sphinx/ext/apidoc.py:395 msgid "don't create a table of contents file" msgstr "" -#: sphinx/ext/apidoc.py:344 +#: sphinx/ext/apidoc.py:398 msgid "" "don't create headings for the module/package packages (e.g. when the " "docstrings already contain them)" msgstr "" -#: sphinx/ext/apidoc.py:349 +#: sphinx/ext/apidoc.py:403 msgid "put module documentation before submodule documentation" msgstr "" -#: sphinx/ext/apidoc.py:353 +#: sphinx/ext/apidoc.py:407 msgid "" "interpret module paths according to PEP-0420 implicit namespaces " "specification" msgstr "" -#: sphinx/ext/apidoc.py:357 +#: sphinx/ext/apidoc.py:411 msgid "file suffix (default: rst)" msgstr "" -#: sphinx/ext/apidoc.py:359 +#: sphinx/ext/apidoc.py:413 msgid "generate a full project with sphinx-quickstart" msgstr "" -#: sphinx/ext/apidoc.py:362 +#: sphinx/ext/apidoc.py:416 msgid "append module_path to sys.path, used when --full is given" msgstr "" -#: sphinx/ext/apidoc.py:364 +#: sphinx/ext/apidoc.py:418 msgid "project name (default: root module name)" msgstr "" -#: sphinx/ext/apidoc.py:366 +#: sphinx/ext/apidoc.py:420 msgid "project author(s), used when --full is given" msgstr "" -#: sphinx/ext/apidoc.py:368 +#: sphinx/ext/apidoc.py:422 msgid "project version, used when --full is given" msgstr "" -#: sphinx/ext/apidoc.py:370 +#: sphinx/ext/apidoc.py:424 msgid "project release, used when --full is given, defaults to --doc-version" msgstr "" -#: sphinx/ext/apidoc.py:373 +#: sphinx/ext/apidoc.py:427 msgid "extension options" msgstr "" -#: sphinx/ext/apidoc.py:402 +#: sphinx/ext/apidoc.py:460 #, python-format msgid "%s is not a directory." msgstr "" -#: sphinx/ext/coverage.py:46 +#: sphinx/ext/coverage.py:42 #, python-format msgid "invalid regex %r in %s" msgstr "" -#: sphinx/ext/coverage.py:55 +#: sphinx/ext/coverage.py:51 #, python-format msgid "" "Testing of coverage in the sources finished, look at the results in " "%(outdir)spython.txt." msgstr "" -#: sphinx/ext/coverage.py:70 +#: sphinx/ext/coverage.py:65 #, python-format msgid "invalid regex %r in coverage_c_regexes" msgstr "" -#: sphinx/ext/coverage.py:152 +#: sphinx/ext/coverage.py:150 #, python-format msgid "module %s could not be imported: %s" msgstr "" -#: sphinx/ext/doctest.py:132 +#: sphinx/ext/doctest.py:131 #, python-format msgid "missing '+' or '-' in '%s' option." msgstr "" -#: sphinx/ext/doctest.py:137 +#: sphinx/ext/doctest.py:136 #, python-format msgid "'%s' is not a valid option." msgstr "" -#: sphinx/ext/doctest.py:151 +#: sphinx/ext/doctest.py:150 #, python-format msgid "'%s' is not a valid pyversion option" msgstr "" -#: sphinx/ext/doctest.py:222 +#: sphinx/ext/doctest.py:219 msgid "invalid TestCode type" msgstr "" -#: sphinx/ext/doctest.py:283 +#: sphinx/ext/doctest.py:277 #, python-format msgid "" "Testing of doctests in the sources finished, look at the results in " "%(outdir)s/output.txt." msgstr "" -#: sphinx/ext/doctest.py:446 +#: sphinx/ext/doctest.py:427 #, python-format msgid "no code/output in %s block at %s:%s" msgstr "" -#: sphinx/ext/doctest.py:535 +#: sphinx/ext/doctest.py:513 #, python-format msgid "ignoring invalid doctest code: %r" msgstr "" -#: sphinx/ext/graphviz.py:140 +#: sphinx/ext/duration.py:80 +msgid "====================== slowest reading durations =======================" +msgstr "" + +#: sphinx/ext/graphviz.py:132 msgid "Graphviz directive cannot have both content and a filename argument" msgstr "" -#: sphinx/ext/graphviz.py:150 +#: sphinx/ext/graphviz.py:142 #, python-format msgid "External Graphviz file %r not found or reading it failed" msgstr "" -#: sphinx/ext/graphviz.py:156 +#: sphinx/ext/graphviz.py:148 msgid "Ignoring \"graphviz\" directive without content." msgstr "" -#: sphinx/ext/graphviz.py:250 +#: sphinx/ext/graphviz.py:248 #, python-format msgid "" "dot did not produce an output file:\n" @@ -2498,14 +2589,14 @@ msgid "" "%r" msgstr "" -#: sphinx/ext/graphviz.py:254 +#: sphinx/ext/graphviz.py:252 #, python-format msgid "" "dot command %r cannot be run (needed for graphviz output), check the " "graphviz_dot setting" msgstr "" -#: sphinx/ext/graphviz.py:261 +#: sphinx/ext/graphviz.py:259 #, python-format msgid "" "dot exited with error:\n" @@ -2515,32 +2606,32 @@ msgid "" "%r" msgstr "" -#: sphinx/ext/graphviz.py:271 +#: sphinx/ext/graphviz.py:269 #, python-format msgid "graphviz_output_format must be one of 'png', 'svg', but is %r" msgstr "" -#: sphinx/ext/graphviz.py:275 sphinx/ext/graphviz.py:329 -#: sphinx/ext/graphviz.py:367 +#: sphinx/ext/graphviz.py:273 sphinx/ext/graphviz.py:324 +#: sphinx/ext/graphviz.py:361 #, python-format msgid "dot code %r: %s" msgstr "" -#: sphinx/ext/graphviz.py:382 sphinx/ext/graphviz.py:391 +#: sphinx/ext/graphviz.py:374 sphinx/ext/graphviz.py:382 #, python-format msgid "[graph: %s]" msgstr "" -#: sphinx/ext/graphviz.py:384 sphinx/ext/graphviz.py:393 +#: sphinx/ext/graphviz.py:376 sphinx/ext/graphviz.py:384 msgid "[graph]" msgstr "" -#: sphinx/ext/imgconverter.py:43 sphinx/ext/imgconverter.py:68 +#: sphinx/ext/imgconverter.py:41 sphinx/ext/imgconverter.py:65 #, python-format -msgid "convert command %r cannot be run.check the image_converter setting" +msgid "convert command %r cannot be run, check the image_converter setting" msgstr "" -#: sphinx/ext/imgconverter.py:48 sphinx/ext/imgconverter.py:73 +#: sphinx/ext/imgconverter.py:46 sphinx/ext/imgconverter.py:70 #, python-format msgid "" "convert exited with error:\n" @@ -2550,75 +2641,75 @@ msgid "" "%r" msgstr "" -#: sphinx/ext/imgmath.py:139 +#: sphinx/ext/imgmath.py:170 #, python-format msgid "" "LaTeX command %r cannot be run (needed for math display), check the " "imgmath_latex setting" msgstr "" -#: sphinx/ext/imgmath.py:154 +#: sphinx/ext/imgmath.py:184 #, python-format msgid "" "%s command %r cannot be run (needed for math display), check the " "imgmath_%s setting" msgstr "" -#: sphinx/ext/imgmath.py:289 +#: sphinx/ext/imgmath.py:329 #, python-format msgid "display latex %r: %s" msgstr "" -#: sphinx/ext/imgmath.py:316 +#: sphinx/ext/imgmath.py:355 #, python-format msgid "inline latex %r: %s" msgstr "" -#: sphinx/ext/imgmath.py:323 sphinx/ext/mathjax.py:54 +#: sphinx/ext/imgmath.py:362 sphinx/ext/mathjax.py:49 msgid "Permalink to this equation" msgstr "" -#: sphinx/ext/intersphinx.py:182 +#: sphinx/ext/intersphinx.py:173 #, python-format msgid "intersphinx inventory has moved: %s -> %s" msgstr "" -#: sphinx/ext/intersphinx.py:217 +#: sphinx/ext/intersphinx.py:204 #, python-format msgid "loading intersphinx inventory from %s..." msgstr "" -#: sphinx/ext/intersphinx.py:232 +#: sphinx/ext/intersphinx.py:218 msgid "" "encountered some issues with some of the inventories, but they had " "working alternatives:" msgstr "" -#: sphinx/ext/intersphinx.py:238 +#: sphinx/ext/intersphinx.py:224 msgid "failed to reach any of the inventories with the following issues:" msgstr "" -#: sphinx/ext/intersphinx.py:311 +#: sphinx/ext/intersphinx.py:314 #, python-format msgid "(in %s v%s)" msgstr "" -#: sphinx/ext/intersphinx.py:313 +#: sphinx/ext/intersphinx.py:316 #, python-format msgid "(in %s)" msgstr "" -#: sphinx/ext/intersphinx.py:347 +#: sphinx/ext/intersphinx.py:349 #, python-format msgid "intersphinx identifier %r is not string. Ignored" msgstr "" -#: sphinx/ext/intersphinx.py:360 +#: sphinx/ext/intersphinx.py:362 #, python-format -msgid "Fail to read intersphinx_mapping[%s], Ignored: %r" +msgid "Failed to read intersphinx_mapping[%s], ignored: %r" msgstr "" -#: sphinx/ext/linkcode.py:72 sphinx/ext/viewcode.py:117 +#: sphinx/ext/linkcode.py:70 sphinx/ext/viewcode.py:113 msgid "[source]" msgstr "" @@ -2626,73 +2717,73 @@ msgstr "" msgid "Todo" msgstr "" -#: sphinx/ext/todo.py:111 +#: sphinx/ext/todo.py:103 sphinx/ext/todo.py:130 #, python-format msgid "TODO entry found: %s" msgstr "" -#: sphinx/ext/todo.py:160 +#: sphinx/ext/todo.py:193 sphinx/ext/todo.py:242 msgid "<<original entry>>" msgstr "" -#: sphinx/ext/todo.py:163 +#: sphinx/ext/todo.py:195 sphinx/ext/todo.py:245 #, python-format msgid "(The <<original entry>> is located in %s, line %d.)" msgstr "" -#: sphinx/ext/todo.py:172 +#: sphinx/ext/todo.py:205 sphinx/ext/todo.py:253 msgid "original entry" msgstr "" -#: sphinx/ext/viewcode.py:158 +#: sphinx/ext/viewcode.py:153 msgid "highlighting module code... " msgstr "" -#: sphinx/ext/viewcode.py:187 +#: sphinx/ext/viewcode.py:182 msgid "[docs]" msgstr "" -#: sphinx/ext/viewcode.py:201 +#: sphinx/ext/viewcode.py:196 msgid "Module code" msgstr "" -#: sphinx/ext/viewcode.py:207 +#: sphinx/ext/viewcode.py:202 #, python-format msgid "<h1>Source code for %s</h1>" msgstr "" -#: sphinx/ext/viewcode.py:233 +#: sphinx/ext/viewcode.py:228 msgid "Overview: module code" msgstr "" -#: sphinx/ext/viewcode.py:234 +#: sphinx/ext/viewcode.py:229 msgid "<h1>All modules for which code is available</h1>" msgstr "" -#: sphinx/ext/autodoc/__init__.py:300 +#: sphinx/ext/autodoc/__init__.py:298 #, python-format msgid "invalid signature for auto%s (%r)" msgstr "" -#: sphinx/ext/autodoc/__init__.py:400 +#: sphinx/ext/autodoc/__init__.py:395 #, python-format msgid "error while formatting arguments for %s: %s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:512 +#: sphinx/ext/autodoc/__init__.py:502 #, python-format msgid "missing attribute %s in object %s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:600 +#: sphinx/ext/autodoc/__init__.py:617 #, python-format msgid "" -"autodoc: failed to determine %r to be documented.the following exception " -"was raised:\n" +"autodoc: failed to determine %r to be documented, the following exception" +" was raised:\n" "%s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:692 +#: sphinx/ext/autodoc/__init__.py:706 #, python-format msgid "" "don't know which module to import for autodocumenting %r (try placing a " @@ -2700,86 +2791,106 @@ msgid "" "explicit module name)" msgstr "" -#: sphinx/ext/autodoc/__init__.py:786 +#: sphinx/ext/autodoc/__init__.py:799 msgid "\"::\" in automodule name doesn't make sense" msgstr "" -#: sphinx/ext/autodoc/__init__.py:794 +#: sphinx/ext/autodoc/__init__.py:806 #, python-format msgid "signature arguments or return annotation given for automodule %s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:827 +#: sphinx/ext/autodoc/__init__.py:837 #, python-format msgid "" "__all__ should be a list of strings, not %r (in module %s) -- ignoring " "__all__" msgstr "" -#: sphinx/ext/autodoc/__init__.py:842 +#: sphinx/ext/autodoc/__init__.py:852 #, python-format msgid "" "missing attribute mentioned in :members: or __all__: module %s, attribute" " %s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:1126 +#: sphinx/ext/autodoc/__init__.py:1208 #, python-format msgid "Bases: %s" msgstr "" -#: sphinx/ext/autodoc/__init__.py:1183 +#: sphinx/ext/autodoc/__init__.py:1267 #, python-format msgid "alias of :class:`%s`" msgstr "" -#: sphinx/ext/autodoc/__init__.py:1468 +#: sphinx/ext/autodoc/type_comment.py:132 #, python-format -msgid "Ignoring invalid option in autodoc_default_flags: %r" +msgid "Failed to parse type_comment for %r: %s" msgstr "" -#: sphinx/ext/autosummary/__init__.py:256 +#: sphinx/ext/autosummary/__init__.py:255 #, python-format -msgid "toctree references excluded document %r" +msgid "autosummary references excluded document %r. Ignored." msgstr "" -#: sphinx/ext/autosummary/__init__.py:258 +#: sphinx/ext/autosummary/__init__.py:257 #, python-format -msgid "toctree references unknown document %r" +msgid "" +"autosummary: stub file not found %r. Check your autosummary_generate " +"setting." msgstr "" -#: sphinx/ext/autosummary/__init__.py:292 +#: sphinx/ext/autosummary/__init__.py:296 #, python-format msgid "failed to import %s" msgstr "" -#: sphinx/ext/autosummary/__init__.py:307 +#: sphinx/ext/autosummary/__init__.py:311 #, python-format msgid "failed to parse name %s" msgstr "" -#: sphinx/ext/autosummary/__init__.py:311 +#: sphinx/ext/autosummary/__init__.py:315 #, python-format msgid "failed to import object %s" msgstr "" -#: sphinx/ext/autosummary/__init__.py:702 +#: sphinx/ext/autosummary/__init__.py:730 +#, python-format +msgid "autosummary_generate: file not found: %s" +msgstr "" + +#: sphinx/ext/autosummary/__init__.py:738 msgid "" "autosummary generats .rst files internally. But your source_suffix does " "not contain .rst. Skipped." msgstr "" -#: sphinx/ext/autosummary/generate.py:100 +#: sphinx/ext/autosummary/generate.py:153 +#, python-format +msgid "" +"autosummary: failed to determine %r to be documented, the following " +"exception was raised:\n" +"%s" +msgstr "" + +#: sphinx/ext/autosummary/generate.py:247 #, python-format msgid "[autosummary] generating autosummary for: %s" msgstr "" -#: sphinx/ext/autosummary/generate.py:104 +#: sphinx/ext/autosummary/generate.py:251 #, python-format msgid "[autosummary] writing to %s" msgstr "" -#: sphinx/ext/autosummary/generate.py:364 +#: sphinx/ext/autosummary/generate.py:277 +#, python-format +msgid "[autosummary] failed to import %r: %s" +msgstr "" + +#: sphinx/ext/autosummary/generate.py:435 msgid "" "\n" "Generate ReStructuredText using autosummary directives.\n" @@ -2796,110 +2907,107 @@ msgid "" " pydoc sphinx.ext.autosummary\n" msgstr "" -#: sphinx/ext/autosummary/generate.py:381 +#: sphinx/ext/autosummary/generate.py:452 msgid "source files to generate rST files for" msgstr "" -#: sphinx/ext/autosummary/generate.py:385 +#: sphinx/ext/autosummary/generate.py:456 msgid "directory to place all output in" msgstr "" -#: sphinx/ext/autosummary/generate.py:388 +#: sphinx/ext/autosummary/generate.py:459 #, python-format msgid "default suffix for files (default: %(default)s)" msgstr "" -#: sphinx/ext/autosummary/generate.py:392 +#: sphinx/ext/autosummary/generate.py:463 #, python-format msgid "custom template directory (default: %(default)s)" msgstr "" -#: sphinx/ext/autosummary/generate.py:396 +#: sphinx/ext/autosummary/generate.py:467 #, python-format msgid "document imported members (default: %(default)s)" msgstr "" -#: sphinx/ext/napoleon/__init__.py:330 sphinx/ext/napoleon/docstring.py:669 +#: sphinx/ext/napoleon/__init__.py:325 sphinx/ext/napoleon/docstring.py:638 msgid "Keyword Arguments" msgstr "" -#: sphinx/ext/napoleon/docstring.py:627 +#: sphinx/ext/napoleon/docstring.py:599 msgid "Example" msgstr "" -#: sphinx/ext/napoleon/docstring.py:628 +#: sphinx/ext/napoleon/docstring.py:600 msgid "Examples" msgstr "" -#: sphinx/ext/napoleon/docstring.py:684 +#: sphinx/ext/napoleon/docstring.py:651 msgid "Notes" msgstr "" -#: sphinx/ext/napoleon/docstring.py:688 +#: sphinx/ext/napoleon/docstring.py:654 msgid "Other Parameters" msgstr "" -#: sphinx/ext/napoleon/docstring.py:717 +#: sphinx/ext/napoleon/docstring.py:680 msgid "References" msgstr "" -#: sphinx/ext/napoleon/docstring.py:754 +#: sphinx/ext/napoleon/docstring.py:714 msgid "Warns" msgstr "" -#: sphinx/ext/napoleon/docstring.py:759 +#: sphinx/ext/napoleon/docstring.py:718 msgid "Yields" msgstr "" -#: sphinx/locale/__init__.py:307 +#: sphinx/locale/__init__.py:250 msgid "Attention" msgstr "" -#: sphinx/locale/__init__.py:308 +#: sphinx/locale/__init__.py:251 msgid "Caution" msgstr "" -#: sphinx/locale/__init__.py:309 +#: sphinx/locale/__init__.py:252 msgid "Danger" msgstr "" -#: sphinx/locale/__init__.py:310 +#: sphinx/locale/__init__.py:253 msgid "Error" msgstr "" -#: sphinx/locale/__init__.py:311 +#: sphinx/locale/__init__.py:254 msgid "Hint" msgstr "" -#: sphinx/locale/__init__.py:312 +#: sphinx/locale/__init__.py:255 msgid "Important" msgstr "" -#: sphinx/locale/__init__.py:313 +#: sphinx/locale/__init__.py:256 msgid "Note" msgstr "" -#: sphinx/locale/__init__.py:314 +#: sphinx/locale/__init__.py:257 msgid "See also" msgstr "" -#: sphinx/locale/__init__.py:315 +#: sphinx/locale/__init__.py:258 msgid "Tip" msgstr "" -#: sphinx/locale/__init__.py:316 +#: sphinx/locale/__init__.py:259 msgid "Warning" msgstr "" -#: sphinx/templates/latex/longtable.tex_t:23 +#: sphinx/templates/latex/longtable.tex_t:18 #: sphinx/templates/latex/sphinxmessages.sty_t:8 msgid "continued from previous page" msgstr "" -#: sphinx/templates/latex/longtable.tex_t:29 -msgid "Continued on next page" -msgstr "" - +#: sphinx/templates/latex/longtable.tex_t:24 #: sphinx/templates/latex/sphinxmessages.sty_t:9 msgid "continues on next page" msgstr "" @@ -2916,122 +3024,121 @@ msgstr "" msgid "page" msgstr "" -#: sphinx/themes/agogo/layout.html:46 sphinx/themes/basic/globaltoc.html:10 -#: sphinx/themes/basic/localtoc.html:11 sphinx/themes/scrolls/layout.html:41 +#: sphinx/themes/agogo/layout.html:27 sphinx/themes/basic/globaltoc.html:10 +#: sphinx/themes/basic/localtoc.html:10 sphinx/themes/scrolls/layout.html:31 msgid "Table of Contents" msgstr "" -#: sphinx/themes/agogo/layout.html:51 sphinx/themes/basic/layout.html:153 -#: sphinx/themes/basic/search.html:11 sphinx/themes/basic/search.html:21 -#: sphinx/themes/basic/searchresults.html:10 +#: sphinx/themes/agogo/layout.html:30 sphinx/themes/basic/layout.html:52 +#: sphinx/themes/basic/search.html:10 sphinx/themes/basic/search.html:18 msgid "Search" msgstr "" -#: sphinx/themes/agogo/layout.html:54 sphinx/themes/basic/searchbox.html:16 +#: sphinx/themes/agogo/layout.html:33 sphinx/themes/basic/searchbox.html:15 msgid "Go" msgstr "" -#: sphinx/themes/agogo/layout.html:79 sphinx/themes/basic/sourcelink.html:15 +#: sphinx/themes/agogo/layout.html:66 sphinx/themes/basic/sourcelink.html:14 msgid "Show Source" msgstr "" -#: sphinx/themes/basic/defindex.html:11 +#: sphinx/themes/basic/defindex.html:10 msgid "Overview" msgstr "" -#: sphinx/themes/basic/defindex.html:15 +#: sphinx/themes/basic/defindex.html:14 msgid "Welcome! This is" msgstr "" -#: sphinx/themes/basic/defindex.html:16 +#: sphinx/themes/basic/defindex.html:15 msgid "the documentation for" msgstr "" -#: sphinx/themes/basic/defindex.html:17 +#: sphinx/themes/basic/defindex.html:16 msgid "last updated" msgstr "" -#: sphinx/themes/basic/defindex.html:20 +#: sphinx/themes/basic/defindex.html:19 msgid "Indices and tables:" msgstr "" -#: sphinx/themes/basic/defindex.html:23 +#: sphinx/themes/basic/defindex.html:22 msgid "Complete Table of Contents" msgstr "" -#: sphinx/themes/basic/defindex.html:24 +#: sphinx/themes/basic/defindex.html:23 msgid "lists all sections and subsections" msgstr "" -#: sphinx/themes/basic/defindex.html:26 +#: sphinx/themes/basic/defindex.html:25 msgid "search this documentation" msgstr "" -#: sphinx/themes/basic/defindex.html:28 +#: sphinx/themes/basic/defindex.html:27 msgid "Global Module Index" msgstr "" -#: sphinx/themes/basic/defindex.html:29 +#: sphinx/themes/basic/defindex.html:28 msgid "quick access to all modules" msgstr "" -#: sphinx/themes/basic/defindex.html:31 +#: sphinx/themes/basic/defindex.html:30 msgid "all functions, classes, terms" msgstr "" -#: sphinx/themes/basic/genindex-single.html:33 +#: sphinx/themes/basic/genindex-single.html:22 #, python-format msgid "Index – %(key)s" msgstr "" -#: sphinx/themes/basic/genindex-single.html:61 -#: sphinx/themes/basic/genindex-split.html:24 -#: sphinx/themes/basic/genindex-split.html:38 -#: sphinx/themes/basic/genindex.html:72 +#: sphinx/themes/basic/genindex-single.html:41 +#: sphinx/themes/basic/genindex-split.html:22 +#: sphinx/themes/basic/genindex-split.html:35 +#: sphinx/themes/basic/genindex.html:49 msgid "Full index on one page" msgstr "" -#: sphinx/themes/basic/genindex-split.html:16 +#: sphinx/themes/basic/genindex-split.html:15 msgid "Index pages by letter" msgstr "" -#: sphinx/themes/basic/genindex-split.html:25 +#: sphinx/themes/basic/genindex-split.html:23 msgid "can be huge" msgstr "" -#: sphinx/themes/basic/layout.html:31 +#: sphinx/themes/basic/layout.html:16 msgid "Navigation" msgstr "" -#: sphinx/themes/basic/layout.html:138 +#: sphinx/themes/basic/layout.html:47 #, python-format msgid "Search within %(docstitle)s" msgstr "" -#: sphinx/themes/basic/layout.html:147 +#: sphinx/themes/basic/layout.html:50 msgid "About these documents" msgstr "" -#: sphinx/themes/basic/layout.html:156 +#: sphinx/themes/basic/layout.html:53 msgid "Copyright" msgstr "" -#: sphinx/themes/basic/layout.html:201 +#: sphinx/themes/basic/layout.html:69 #, python-format msgid "© <a href=\"%(path)s\">Copyright</a> %(copyright)s." msgstr "" -#: sphinx/themes/basic/layout.html:203 +#: sphinx/themes/basic/layout.html:70 #, python-format msgid "© Copyright %(copyright)s." msgstr "" -#: sphinx/themes/basic/layout.html:207 +#: sphinx/themes/basic/layout.html:71 #, python-format msgid "Last updated on %(last_updated)s." msgstr "" -#: sphinx/themes/basic/layout.html:210 +#: sphinx/themes/basic/layout.html:72 #, python-format msgid "" "Created using <a href=\"http://sphinx-doc.org/\">Sphinx</a> " @@ -3043,57 +3150,55 @@ msgstr "" msgid "Search %(docstitle)s" msgstr "" -#: sphinx/themes/basic/relations.html:11 +#: sphinx/themes/basic/relations.html:10 msgid "Previous topic" msgstr "" -#: sphinx/themes/basic/relations.html:13 +#: sphinx/themes/basic/relations.html:12 msgid "previous chapter" msgstr "" -#: sphinx/themes/basic/relations.html:16 +#: sphinx/themes/basic/relations.html:13 msgid "Next topic" msgstr "" -#: sphinx/themes/basic/relations.html:18 +#: sphinx/themes/basic/relations.html:15 msgid "next chapter" msgstr "" -#: sphinx/themes/basic/search.html:25 +#: sphinx/themes/basic/search.html:22 msgid "" "Please activate JavaScript to enable the search\n" " functionality." msgstr "" -#: sphinx/themes/basic/search.html:30 +#: sphinx/themes/basic/search.html:27 msgid "" -"From here you can search these documents. Enter your search\n" -" words into the box below and click \"search\". Note that the search\n" -" function will automatically search for all of the words. Pages\n" -" containing fewer words won't appear in the result list." +"Searching for multiple words only shows matches that contain\n" +" all words." msgstr "" -#: sphinx/themes/basic/search.html:37 sphinx/themes/basic/searchresults.html:17 +#: sphinx/themes/basic/search.html:32 msgid "search" msgstr "" -#: sphinx/themes/basic/search.html:41 sphinx/themes/basic/searchresults.html:21 -#: sphinx/themes/basic/static/searchtools.js:285 +#: sphinx/themes/basic/search.html:36 +#: sphinx/themes/basic/static/searchtools.js:304 msgid "Search Results" msgstr "" -#: sphinx/themes/basic/search.html:43 sphinx/themes/basic/searchresults.html:23 -#: sphinx/themes/basic/static/searchtools.js:287 +#: sphinx/themes/basic/search.html:38 +#: sphinx/themes/basic/static/searchtools.js:306 msgid "" "Your search did not match any documents. Please make sure that all words " "are spelled correctly and that you've selected enough categories." msgstr "" -#: sphinx/themes/basic/searchbox.html:12 +#: sphinx/themes/basic/searchbox.html:11 msgid "Quick search" msgstr "" -#: sphinx/themes/basic/sourcelink.html:12 +#: sphinx/themes/basic/sourcelink.html:11 msgid "This Page" msgstr "" @@ -3125,15 +3230,15 @@ msgstr "" msgid "Other changes" msgstr "" -#: sphinx/themes/basic/static/doctools.js:194 sphinx/writers/html.py:454 -#: sphinx/writers/html.py:459 sphinx/writers/html5.py:400 -#: sphinx/writers/html5.py:405 +#: sphinx/themes/basic/static/doctools.js:194 sphinx/writers/html.py:403 +#: sphinx/writers/html.py:408 sphinx/writers/html5.py:355 +#: sphinx/writers/html5.py:360 msgid "Permalink to this headline" msgstr "" -#: sphinx/themes/basic/static/doctools.js:200 sphinx/writers/html.py:134 -#: sphinx/writers/html.py:145 sphinx/writers/html5.py:103 -#: sphinx/writers/html5.py:114 +#: sphinx/themes/basic/static/doctools.js:200 sphinx/writers/html.py:121 +#: sphinx/writers/html.py:130 sphinx/writers/html5.py:93 +#: sphinx/writers/html5.py:102 msgid "Permalink to this definition" msgstr "" @@ -3141,20 +3246,20 @@ msgstr "" msgid "Hide Search Matches" msgstr "" -#: sphinx/themes/basic/static/searchtools.js:121 +#: sphinx/themes/basic/static/searchtools.js:136 msgid "Searching" msgstr "" -#: sphinx/themes/basic/static/searchtools.js:126 +#: sphinx/themes/basic/static/searchtools.js:141 msgid "Preparing search..." msgstr "" -#: sphinx/themes/basic/static/searchtools.js:289 +#: sphinx/themes/basic/static/searchtools.js:308 #, python-format msgid "Search finished, found %s page(s) matching the search query." msgstr "" -#: sphinx/themes/basic/static/searchtools.js:342 +#: sphinx/themes/basic/static/searchtools.js:362 msgid ", in " msgstr "" @@ -3167,224 +3272,215 @@ msgstr "" msgid "Collapse sidebar" msgstr "" -#: sphinx/themes/haiku/layout.html:24 +#: sphinx/themes/haiku/layout.html:19 msgid "Contents" msgstr "" -#: sphinx/transforms/__init__.py:261 +#: sphinx/transforms/__init__.py:224 #, python-format msgid "4 column based index found. It might be a bug of extensions you use: %r" msgstr "" -#: sphinx/transforms/__init__.py:303 +#: sphinx/transforms/__init__.py:263 #, python-format msgid "Footnote [%s] is not referenced." msgstr "" -#: sphinx/transforms/__init__.py:309 +#: sphinx/transforms/__init__.py:269 msgid "Footnote [#] is not referenced." msgstr "" -#: sphinx/transforms/i18n.py:293 sphinx/transforms/i18n.py:363 +#: sphinx/transforms/i18n.py:296 sphinx/transforms/i18n.py:366 msgid "" "inconsistent footnote references in translated message. original: {0}, " "translated: {1}" msgstr "" -#: sphinx/transforms/i18n.py:335 +#: sphinx/transforms/i18n.py:338 msgid "" "inconsistent references in translated message. original: {0}, translated:" " {1}" msgstr "" -#: sphinx/transforms/i18n.py:382 +#: sphinx/transforms/i18n.py:385 msgid "" "inconsistent citation references in translated message. original: {0}, " "translated: {1}" msgstr "" -#: sphinx/transforms/i18n.py:402 +#: sphinx/transforms/i18n.py:405 msgid "" "inconsistent term references in translated message. original: {0}, " "translated: {1}" msgstr "" -#: sphinx/transforms/post_transforms/__init__.py:110 +#: sphinx/transforms/post_transforms/__init__.py:139 #, python-format msgid "more than one target found for 'any' cross-reference %r: could be %s" msgstr "" -#: sphinx/transforms/post_transforms/__init__.py:142 +#: sphinx/transforms/post_transforms/__init__.py:171 #, python-format msgid "%s:%s reference target not found: %%(target)s" msgstr "" -#: sphinx/transforms/post_transforms/__init__.py:145 +#: sphinx/transforms/post_transforms/__init__.py:174 #, python-format msgid "%r reference target not found: %%(target)s" msgstr "" -#: sphinx/transforms/post_transforms/images.py:92 +#: sphinx/transforms/post_transforms/images.py:86 #, python-format msgid "Could not fetch remote image: %s [%d]" msgstr "" -#: sphinx/transforms/post_transforms/images.py:120 +#: sphinx/transforms/post_transforms/images.py:114 #, python-format msgid "Could not fetch remote image: %s [%s]" msgstr "" -#: sphinx/transforms/post_transforms/images.py:140 +#: sphinx/transforms/post_transforms/images.py:132 #, python-format msgid "Unknown image format: %s..." msgstr "" -#: sphinx/util/__init__.py:415 +#: sphinx/util/__init__.py:379 #, python-format msgid "undecodable source characters, replacing with \"?\": %r" msgstr "" -#: sphinx/util/__init__.py:695 +#: sphinx/util/__init__.py:639 msgid "skipped" msgstr "" -#: sphinx/util/__init__.py:700 +#: sphinx/util/__init__.py:644 msgid "failed" msgstr "" -#: sphinx/util/docutils.py:320 -msgid "when adding directive classes, no additional arguments may be given" -msgstr "" - -#: sphinx/util/i18n.py:74 +#: sphinx/util/i18n.py:68 #, python-format msgid "reading error: %s, %s" msgstr "" -#: sphinx/util/i18n.py:81 +#: sphinx/util/i18n.py:75 #, python-format msgid "writing error: %s, %s" msgstr "" -#: sphinx/util/i18n.py:215 +#: sphinx/util/i18n.py:260 #, python-format msgid "" "Invalid date format. Quote the string by single quote if you want to " "output it directly: %s" msgstr "" -#: sphinx/util/nodes.py:428 +#: sphinx/util/nodes.py:426 #, python-format msgid "toctree contains ref to nonexisting file %r" msgstr "" -#: sphinx/util/nodes.py:501 +#: sphinx/util/nodes.py:526 #, python-format msgid "exception while evaluating only directive expression: %s" msgstr "" -#: sphinx/util/pycompat.py:82 +#: sphinx/util/pycompat.py:77 #, python-format msgid "" "Support for evaluating Python 2 syntax is deprecated and will be removed " "in Sphinx 4.0. Convert %s to Python 3 syntax." msgstr "" -#: sphinx/util/rst.py:49 +#: sphinx/util/rst.py:74 #, python-format msgid "default role %s not found" msgstr "" -#: sphinx/writers/html.py:345 sphinx/writers/html5.py:313 +#: sphinx/writers/html.py:306 sphinx/writers/html5.py:278 #, python-format msgid "numfig_format is not defined for %s" msgstr "" -#: sphinx/writers/html.py:355 sphinx/writers/html5.py:323 +#: sphinx/writers/html.py:316 sphinx/writers/html5.py:288 #, python-format msgid "Any IDs not assigned for %s node" msgstr "" -#: sphinx/writers/html.py:463 sphinx/writers/html5.py:409 +#: sphinx/writers/html.py:412 sphinx/writers/html5.py:364 msgid "Permalink to this table" msgstr "" -#: sphinx/writers/html.py:510 sphinx/writers/html5.py:456 +#: sphinx/writers/html.py:456 sphinx/writers/html5.py:408 msgid "Permalink to this code" msgstr "" -#: sphinx/writers/html.py:512 sphinx/writers/html5.py:458 +#: sphinx/writers/html.py:458 sphinx/writers/html5.py:410 msgid "Permalink to this image" msgstr "" -#: sphinx/writers/html.py:514 sphinx/writers/html5.py:460 +#: sphinx/writers/html.py:460 sphinx/writers/html5.py:412 msgid "Permalink to this toctree" msgstr "" -#: sphinx/writers/html.py:672 sphinx/writers/html5.py:606 +#: sphinx/writers/html.py:584 sphinx/writers/html5.py:525 msgid "Could not obtain image size. :scale: option is ignored." msgstr "" -#: sphinx/writers/latex.py:510 +#: sphinx/writers/latex.py:352 #, python-format msgid "unknown %r toplevel_sectioning for class %r" msgstr "" -#: sphinx/writers/latex.py:603 +#: sphinx/writers/latex.py:404 msgid "too large :maxdepth:, ignored." msgstr "" -#: sphinx/writers/latex.py:893 +#: sphinx/writers/latex.py:653 msgid "document title is not a single Text node" msgstr "" -#: sphinx/writers/latex.py:926 sphinx/writers/texinfo.py:658 +#: sphinx/writers/latex.py:685 sphinx/writers/texinfo.py:626 msgid "encountered title node not in section, topic, table, admonition or sidebar" msgstr "" -#: sphinx/writers/latex.py:1102 sphinx/writers/manpage.py:277 -#: sphinx/writers/texinfo.py:675 +#: sphinx/writers/latex.py:827 sphinx/writers/manpage.py:238 +#: sphinx/writers/texinfo.py:641 msgid "Footnotes" msgstr "" -#: sphinx/writers/latex.py:1150 +#: sphinx/writers/latex.py:879 msgid "both tabularcolumns and :widths: option are given. :widths: is ignored." msgstr "" -#: sphinx/writers/latex.py:1521 +#: sphinx/writers/latex.py:1202 #, python-format msgid "dimension unit %s is invalid. Ignored." msgstr "" -#: sphinx/writers/latex.py:1847 +#: sphinx/writers/latex.py:1513 #, python-format msgid "unknown index entry type %s found" msgstr "" -#: sphinx/writers/latex.py:2560 -#, python-format -msgid "Unknown configure key: latex_elements[%r] is ignored." -msgstr "" - -#: sphinx/writers/manpage.py:333 sphinx/writers/text.py:887 +#: sphinx/writers/manpage.py:287 sphinx/writers/text.py:788 #, python-format msgid "[image: %s]" msgstr "" -#: sphinx/writers/manpage.py:334 sphinx/writers/text.py:888 +#: sphinx/writers/manpage.py:288 sphinx/writers/text.py:789 msgid "[image]" msgstr "" -#: sphinx/writers/texinfo.py:1330 +#: sphinx/writers/texinfo.py:1185 msgid "caption not inside a figure." msgstr "" -#: sphinx/writers/texinfo.py:1422 +#: sphinx/writers/texinfo.py:1261 #, python-format msgid "unimplemented node type: %r" msgstr "" -#: sphinx/writers/texinfo.py:1427 +#: sphinx/writers/texinfo.py:1265 #, python-format msgid "unknown node type: %r" msgstr "" diff --git a/sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po b/sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po index 0d402ede1..c651d7510 100644 --- a/sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po +++ b/sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po @@ -1,7 +1,7 @@ # Translations template for Sphinx. # Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the Sphinx project. -# +# # Translators: # Yinian Chin <yinian1992@live.com>, 2015,2017-2018 # Hsiaoming Yang <me@lepture.com>, 2018 @@ -28,6 +28,7 @@ msgstr "" "Generated-By: Babel 2.6.0\n" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 2.2.4\n" #: sphinx/application.py:153 #, python-format @@ -50,9 +51,7 @@ msgstr "正在运行 Sphinx v%s" #: sphinx/application.py:214 #, python-format -msgid "" -"This project needs at least Sphinx v%s and therefore cannot be built with " -"this version." +msgid "This project needs at least Sphinx v%s and therefore cannot be built with this version." msgstr "该项目需要 Sphinx v%s 及以上版本,使用现有版本不能构建文档。" #: sphinx/application.py:234 @@ -66,9 +65,8 @@ msgstr "同时设置扩展名 %s:" #: sphinx/application.py:245 msgid "" -"'setup' as currently defined in conf.py isn't a Python callable. Please " -"modify its definition to make it a callable function. This is needed for " -"conf.py to behave as a Sphinx extension." +"'setup' as currently defined in conf.py isn't a Python callable. Please modify its definition to make it a callable function. This is needed for conf.py to " +"behave as a Sphinx extension." msgstr "当前 conf.py 中定义的 'setup' 不是一个可调用的 Python 对象。请把其定义改为一个可调用的函数。Sphinx 扩展的 conf.py 必须这样配置。" #: sphinx/application.py:269 @@ -76,9 +74,8 @@ msgstr "当前 conf.py 中定义的 'setup' 不是一个可调用的 Python 对 msgid "loading translations [%s]... " msgstr "正在加载翻译 [%s]... " -#: sphinx/application.py:285 sphinx/builders/html.py:852 -#: sphinx/builders/html.py:870 sphinx/builders/html.py:1133 -#: sphinx/builders/html.py:1151 sphinx/util/__init__.py:702 +#: sphinx/application.py:285 sphinx/builders/html.py:852 sphinx/builders/html.py:870 sphinx/builders/html.py:1133 sphinx/builders/html.py:1151 +#: sphinx/util/__init__.py:702 msgid "done" msgstr "完成" @@ -134,18 +131,12 @@ msgstr "角色 %r 已注册,将被覆盖" #: sphinx/application.py:1179 #, python-format -msgid "" -"the %s extension does not declare if it is safe for parallel reading, " -"assuming it isn't - please ask the extension author to check and make it " -"explicit" +msgid "the %s extension does not declare if it is safe for parallel reading, assuming it isn't - please ask the extension author to check and make it explicit" msgstr "扩展 %s 没有声明是否并行读取安全,默认假定为否 - 请联系扩展作者检查是否支持该特性并显式声明" #: sphinx/application.py:1185 #, python-format -msgid "" -"the %s extension does not declare if it is safe for parallel writing, " -"assuming it isn't - please ask the extension author to check and make it " -"explicit" +msgid "the %s extension does not declare if it is safe for parallel writing, assuming it isn't - please ask the extension author to check and make it explicit" msgstr "%s 扩展没有声明是否并行写入安全,默认假定为否 - 请联系扩展作者检查是否支持该特性并显式声明" #: sphinx/application.py:1196 @@ -155,9 +146,7 @@ msgstr "执行顺序 %s" #: sphinx/config.py:220 #, python-format -msgid "" -"cannot override dictionary config setting %r, ignoring (use %r to set " -"individual elements)" +msgid "cannot override dictionary config setting %r, ignoring (use %r to set individual elements)" msgstr "不能覆盖字典配置项 %r,已忽略 (请用 %r 设置单个字典元素)" #: sphinx/config.py:229 @@ -191,8 +180,7 @@ msgid "There is a syntax error in your configuration file: %s\n" msgstr "配置文件中存在语法错误: %s\n" #: sphinx/config.py:366 -msgid "" -"The configuration file (or one of the modules it imports) called sys.exit()" +msgid "The configuration file (or one of the modules it imports) called sys.exit()" msgstr "配置文件(或配置文件导入的模块)调用了 sys.exit()" #: sphinx/config.py:370 @@ -201,7 +189,10 @@ msgid "" "There is a programmable error in your configuration file:\n" "\n" "%s" -msgstr "配置文件中有程序上的错误:\n\n%s" +msgstr "" +"配置文件中有程序上的错误:\n" +"\n" +"%s" #: sphinx/config.py:397 #, python-format @@ -231,9 +222,7 @@ msgid "Listing %s" msgstr "列表 %s" #: sphinx/config.py:447 -msgid "" -"The config value `{name}` has to be a one of {candidates}, but `{current}` " -"is given." +msgid "The config value `{name}` has to be a one of {candidates}, but `{current}` is given." msgstr "配置项 `{name}` 必须设置为 {candidates} 之一,但现在是 `{current}` 。" #: sphinx/config.py:465 @@ -243,16 +232,12 @@ msgid "" msgstr "配置值\"[name]\"的类型为\"[当前._name];但\"当前\"为\"当前\"。\"当前\"为\"当前\"。\"当前\"为\"当前\"。=================================预期 [允许]。" #: sphinx/config.py:478 -msgid "" -"The config value `{name}' has type `{current.__name__}', defaults to " -"`{default.__name__}'." +msgid "The config value `{name}' has type `{current.__name__}', defaults to `{default.__name__}'." msgstr "配置项 `{name}' 的类型是 `{current.__name__}',默认为 `{default.__name__}'。" #: sphinx/config.py:497 #, python-format -msgid "" -"the config value %r is set to a string with non-ASCII characters; this can " -"lead to Unicode errors occurring. Please use Unicode strings, e.g. %r." +msgid "the config value %r is set to a string with non-ASCII characters; this can lead to Unicode errors occurring. Please use Unicode strings, e.g. %r." msgstr "配置项 %r 的值包含了非 ASCII 字符,这会导致 Unicode 错误。请使用 Unicode 字符串,例如 %r。" #: sphinx/config.py:506 @@ -278,16 +263,12 @@ msgstr "未知事件名称:%s" #: sphinx/extension.py:52 #, python-format -msgid "" -"The %s extension is required by needs_extensions settings, but it is not " -"loaded." +msgid "The %s extension is required by needs_extensions settings, but it is not loaded." msgstr "未能加载 needs_extensions 配置项所需的 %s。" #: sphinx/extension.py:57 #, python-format -msgid "" -"This project needs the extension %s at least in version %s and therefore " -"cannot be built with the loaded version (%s)." +msgid "This project needs the extension %s at least in version %s and therefore cannot be built with the loaded version (%s)." msgstr "该项目所需扩展 %s 最低要求版本 %s ,当前加载版本 (%s) 无法构建文档。" #: sphinx/highlighting.py:142 @@ -417,23 +398,17 @@ msgstr "无法导入扩展 %s" #: sphinx/registry.py:479 #, python-format -msgid "" -"extension %r has no setup() function; is it really a Sphinx extension " -"module?" +msgid "extension %r has no setup() function; is it really a Sphinx extension module?" msgstr "扩展 %r 未包含setup() 函数;它确实是一个 Sphinx 扩展模块吗?" #: sphinx/registry.py:488 #, python-format -msgid "" -"The %s extension used by this project needs at least Sphinx v%s; it " -"therefore cannot be built with this version." +msgid "The %s extension used by this project needs at least Sphinx v%s; it therefore cannot be built with this version." msgstr "该项目所用扩展 %s 需要 Sphinx 版本 %s 以上;当前版本无法构建文档。" #: sphinx/registry.py:496 #, python-format -msgid "" -"extension %r returned an unsupported object from its setup() function; it " -"should return None or a metadata dictionary" +msgid "extension %r returned an unsupported object from its setup() function; it should return None or a metadata dictionary" msgstr "扩展 %r 在其 setup() 函数中返回了一个不支持的对象;该函数应返回 None 或一个元数据字典" #: sphinx/roles.py:221 sphinx/roles.py:272 @@ -472,9 +447,7 @@ msgid "file %r on theme path is not a valid zipfile or contains no theme" msgstr "主题路径指定的文件 %r 是一个无效的或不包含主题的 zip 文件" #: sphinx/theming.py:258 -msgid "" -"sphinx_rtd_theme is no longer a hard dependency since version 1.4.0. Please " -"install it manually.(pip install sphinx_rtd_theme)" +msgid "sphinx_rtd_theme is no longer a hard dependency since version 1.4.0. Please install it manually.(pip install sphinx_rtd_theme)" msgstr "sphinx_rtd_theme 从 1.4.0 版本开始不再作为强依赖。请手动安装。(pip install sphinx_rtd_theme)" #: sphinx/theming.py:262 @@ -496,8 +469,7 @@ msgstr "没有找到适合 %s 构建器的图像:%s" msgid "building [mo]: " msgstr "构建 [mo]: " -#: sphinx/builders/__init__.py:232 sphinx/builders/__init__.py:574 -#: sphinx/builders/__init__.py:602 +#: sphinx/builders/__init__.py:232 sphinx/builders/__init__.py:574 sphinx/builders/__init__.py:602 msgid "writing output... " msgstr "写入输出... " @@ -522,8 +494,7 @@ msgstr "所有源文件" #: sphinx/builders/__init__.py:298 #, python-format -msgid "" -"file %r given on command line is not under the source directory, ignoring" +msgid "file %r given on command line is not under the source directory, ignoring" msgstr "源文件目录下没有命令行给出的 %r 文件,将被忽略" #: sphinx/builders/__init__.py:303 @@ -602,8 +573,7 @@ msgstr "准备文件" msgid "duplicated ToC entry found: %s" msgstr "找到重复的ToC条目: %s" -#: sphinx/builders/_epub_base.py:414 sphinx/builders/html.py:761 -#: sphinx/builders/latex/__init__.py:415 sphinx/builders/texinfo.py:190 +#: sphinx/builders/_epub_base.py:414 sphinx/builders/html.py:761 sphinx/builders/latex/__init__.py:415 sphinx/builders/texinfo.py:190 msgid "copying images... " msgstr "复制图像... " @@ -612,8 +582,7 @@ msgstr "复制图像... " msgid "cannot read image file %r: copying it instead" msgstr "无法读取图像文件 %r:直接复制" -#: sphinx/builders/_epub_base.py:427 sphinx/builders/html.py:769 -#: sphinx/builders/latex/__init__.py:423 sphinx/builders/texinfo.py:199 +#: sphinx/builders/_epub_base.py:427 sphinx/builders/html.py:769 sphinx/builders/latex/__init__.py:423 sphinx/builders/texinfo.py:199 #, python-format msgid "cannot copy image file %r: %s" msgstr "无法复制图像文件 %r:%s" @@ -627,8 +596,7 @@ msgstr "无法写入图像文件 %r:%s" msgid "Pillow not found - copying image files" msgstr "未找到Pillow - 复制图像文件" -#: sphinx/builders/_epub_base.py:490 sphinx/builders/_epub_base.py:503 -#: sphinx/builders/_epub_base.py:539 sphinx/builders/_epub_base.py:724 +#: sphinx/builders/_epub_base.py:490 sphinx/builders/_epub_base.py:503 sphinx/builders/_epub_base.py:539 sphinx/builders/_epub_base.py:724 #: sphinx/builders/_epub_base.py:757 sphinx/builders/epub3.py:183 #, python-format msgid "writing %s file..." @@ -762,9 +730,7 @@ msgstr "HTML 页面保存在 %(outdir)s 目录。" msgid "Failed to read build info file: %r" msgstr "读取构建信息文件失败:%r" -#: sphinx/builders/html.py:488 sphinx/builders/latex/__init__.py:206 -#: sphinx/transforms/__init__.py:121 sphinx/writers/manpage.py:118 -#: sphinx/writers/texinfo.py:243 +#: sphinx/builders/html.py:488 sphinx/builders/latex/__init__.py:206 sphinx/transforms/__init__.py:121 sphinx/writers/manpage.py:118 sphinx/writers/texinfo.py:243 #, python-format msgid "%b %d, %Y" msgstr "%Y 年 %m 月 %d 日" @@ -850,9 +816,7 @@ msgid "Failed to write build info file: %r" msgstr "写入构建信息文件失败:%r" #: sphinx/builders/html.py:927 -msgid "" -"search index couldn't be loaded, but not all documents will be built: the " -"index will be incomplete." +msgid "search index couldn't be loaded, but not all documents will be built: the index will be incomplete." msgstr "无法加载搜索索引,不会构建所有文档:索引将不完整。" #: sphinx/builders/html.py:996 @@ -862,9 +826,7 @@ msgstr "页面 %s 匹配了 html_sidebars 中的两条规则:%r 和 %r" #: sphinx/builders/html.py:1094 #, python-format -msgid "" -"a Unicode error occurred when rendering the page %s. Please make sure all " -"config values that contain non-ASCII content are Unicode strings." +msgid "a Unicode error occurred when rendering the page %s. Please make sure all config values that contain non-ASCII content are Unicode strings." msgstr "渲染页面 %s 时发生了 Unicode 错误。请确保所有包含非 ASCII 字符的配置项是 Unicode 字符串。" #: sphinx/builders/html.py:1099 @@ -872,10 +834,11 @@ msgstr "渲染页面 %s 时发生了 Unicode 错误。请确保所有包含非 A msgid "" "An error happened in rendering the page %s.\n" "Reason: %r" -msgstr "渲染页面 %s 时发生了错误。\n原因:%r" +msgstr "" +"渲染页面 %s 时发生了错误。\n" +"原因:%r" -#: sphinx/builders/html.py:1111 sphinx/builders/text.py:85 -#: sphinx/builders/xml.py:99 +#: sphinx/builders/html.py:1111 sphinx/builders/text.py:85 sphinx/builders/xml.py:99 #, python-format msgid "error writing file %s: %s" msgstr "写入文件 %s 时发生错误:%s" @@ -932,8 +895,7 @@ msgstr "手册页保存在 %(outdir)s 目录。" msgid "no \"man_pages\" config value found; no manual pages will be written" msgstr "未找到“man_pages”配置项,不会写入手册页" -#: sphinx/builders/latex/__init__.py:272 sphinx/builders/manpage.py:64 -#: sphinx/builders/singlehtml.py:174 sphinx/builders/texinfo.py:120 +#: sphinx/builders/latex/__init__.py:272 sphinx/builders/manpage.py:64 sphinx/builders/singlehtml.py:174 sphinx/builders/texinfo.py:120 msgid "writing" msgstr "写作" @@ -965,7 +927,10 @@ msgid "" "\n" "Run 'make' in that directory to run these through makeinfo\n" "(use 'make info' here to do that automatically)." -msgstr "\n在该目录下运行“make”命令以通过 makeinfo 运行这些 Texinfo文件\n(在此处用“make info”即可自动执行)。" +msgstr "" +"\n" +"在该目录下运行“make”命令以通过 makeinfo 运行这些 Texinfo文件\n" +"(在此处用“make info”即可自动执行)。" #: sphinx/builders/texinfo.py:85 msgid "no \"texinfo_documents\" config value found; no documents will be written" @@ -1023,7 +988,10 @@ msgid "" "\n" "Run 'make' in that directory to run these through (pdf)latex\n" "(use `make latexpdf' here to do that automatically)." -msgstr "\n在该目录下运行“make”以通过 (pdf)latex 运行这些 LaTex 文件\n(在此处用“make latexpdf”即可自动执行)。" +msgstr "" +"\n" +"在该目录下运行“make”以通过 (pdf)latex 运行这些 LaTex 文件\n" +"(在此处用“make latexpdf”即可自动执行)。" #: sphinx/builders/latex/__init__.py:165 msgid "no \"latex_documents\" config value found; no documents will be written" @@ -1034,14 +1002,9 @@ msgstr "未找到“latex_documents”配置项,不会写入文档" msgid "\"latex_documents\" config value references unknown document %s" msgstr "配置项“latex_documents”引用了未知文档 %s" -#: sphinx/builders/latex/__init__.py:213 sphinx/domains/std.py:501 -#: sphinx/templates/latex/latex.tex_t:79 -#: sphinx/themes/basic/genindex-single.html:30 -#: sphinx/themes/basic/genindex-single.html:55 -#: sphinx/themes/basic/genindex-split.html:11 -#: sphinx/themes/basic/genindex-split.html:14 -#: sphinx/themes/basic/genindex.html:30 sphinx/themes/basic/genindex.html:33 -#: sphinx/themes/basic/genindex.html:66 sphinx/themes/basic/layout.html:150 +#: sphinx/builders/latex/__init__.py:213 sphinx/domains/std.py:501 sphinx/templates/latex/latex.tex_t:79 sphinx/themes/basic/genindex-single.html:30 +#: sphinx/themes/basic/genindex-single.html:55 sphinx/themes/basic/genindex-split.html:11 sphinx/themes/basic/genindex-split.html:14 +#: sphinx/themes/basic/genindex.html:30 sphinx/themes/basic/genindex.html:33 sphinx/themes/basic/genindex.html:66 sphinx/themes/basic/layout.html:150 #: sphinx/writers/texinfo.py:522 msgid "Index" msgstr "索引" @@ -1090,9 +1053,7 @@ msgstr "编码错误:" #: sphinx/cmd/build.py:59 sphinx/cmd/build.py:74 #, python-format -msgid "" -"The full traceback has been saved in %s, if you want to report the issue to " -"the developers." +msgid "The full traceback has been saved in %s, if you want to report the issue to the developers." msgstr "如果你想向开发者报告问题,可以查阅已经保存在 %s 的完整 Traceback 信息 。" #: sphinx/cmd/build.py:63 @@ -1100,10 +1061,7 @@ msgid "Recursion error:" msgstr "递归错误:" #: sphinx/cmd/build.py:66 -msgid "" -"This can happen with very large or deeply nested source files. You can " -"carefully increase the default Python recursion limit of 1000 in conf.py " -"with e.g.:" +msgid "This can happen with very large or deeply nested source files. You can carefully increase the default Python recursion limit of 1000 in conf.py with e.g.:" msgstr "在源文件过大或嵌套层数过深时会出现此错误。你可以在 conf.py 中增大默认的Python 递归 1000 层限制,像这样:" #: sphinx/cmd/build.py:69 @@ -1115,23 +1073,18 @@ msgid "Exception occurred:" msgstr "抛出异常:" #: sphinx/cmd/build.py:77 -msgid "" -"Please also report this if it was a user error, so that a better error " -"message can be provided next time." +msgid "Please also report this if it was a user error, so that a better error message can be provided next time." msgstr "如果此处抛出了用户的错误,也请向我们报告,这样以后可以显示更友好、更详细的错误信息。" #: sphinx/cmd/build.py:80 -msgid "" -"A bug report can be filed in the tracker at <https://github.com/sphinx-" -"doc/sphinx/issues>. Thanks!" +msgid "A bug report can be filed in the tracker at <https://github.com/sphinx-doc/sphinx/issues>. Thanks!" msgstr "请向 Bug 追踪系统 <https://github.com/sphinx-doc/sphinx/issues> 投递 Bug 报告。谢谢!" #: sphinx/cmd/build.py:97 msgid "job number should be a positive number" msgstr "工作编号应为正值" -#: sphinx/cmd/build.py:106 sphinx/cmd/quickstart.py:497 -#: sphinx/ext/apidoc.py:298 sphinx/ext/autosummary/generate.py:363 +#: sphinx/cmd/build.py:106 sphinx/cmd/quickstart.py:497 sphinx/ext/apidoc.py:298 sphinx/ext/autosummary/generate.py:363 msgid "For more information, visit <http://sphinx-doc.org/>." msgstr "更多信息请访问 <http://sphinx-doc.org/>。" @@ -1152,7 +1105,20 @@ msgid "" "\n" "By default, everything that is outdated is built. Output only for selected\n" "files can be built by specifying individual filenames.\n" -msgstr "\n从源文件生成文档。\n\nsphinx-build 从 SOURCEDIR 中的文件生成文档,并保存在 OUTPUTDIR。\n它从 SOURCEDIR 的“conf.py” 中读取配置。“sphinx-quickstart”工具可以生\n成包括“conf.py”在内的模板文件。\n\nsphinx-build 可以生成多种格式的文档。在命令行中指定构建器名称即可\n选择文档格式,默认是 HTML。构建器也可以执行文档处理相关的其他\n任务。\n\n默认只会重新构建过期内容。如果指定了文件名,那么只会产生这些文件\n的输出。\n" +msgstr "" +"\n" +"从源文件生成文档。\n" +"\n" +"sphinx-build 从 SOURCEDIR 中的文件生成文档,并保存在 OUTPUTDIR。\n" +"它从 SOURCEDIR 的“conf.py” 中读取配置。“sphinx-quickstart”工具可以生\n" +"成包括“conf.py”在内的模板文件。\n" +"\n" +"sphinx-build 可以生成多种格式的文档。在命令行中指定构建器名称即可\n" +"选择文档格式,默认是 HTML。构建器也可以执行文档处理相关的其他\n" +"任务。\n" +"\n" +"默认只会重新构建过期内容。如果指定了文件名,那么只会产生这些文件\n" +"的输出。\n" #: sphinx/cmd/build.py:128 msgid "path to documentation source files" @@ -1183,21 +1149,15 @@ msgid "don't use a saved environment, always read all files" msgstr "不使用已保存的环境,始终读取全部文件" #: sphinx/cmd/build.py:146 -msgid "" -"path for the cached environment and doctree files (default: " -"OUTPUTDIR/.doctrees)" +msgid "path for the cached environment and doctree files (default: OUTPUTDIR/.doctrees)" msgstr "缓存环境和 doctree 文件路径(默认:OUTPUTDIR/.doctrees)" #: sphinx/cmd/build.py:149 -msgid "" -"build in parallel with N processes where possible (special value \"auto\" " -"will set N to cpu-count)" +msgid "build in parallel with N processes where possible (special value \"auto\" will set N to cpu-count)" msgstr "如果可能,用 N 个进程并行构建文档(如果指定为“auto”,则 N 为 CPU 数量)" #: sphinx/cmd/build.py:153 -msgid "" -"path where configuration file (conf.py) is located (default: same as " -"SOURCEDIR)" +msgid "path where configuration file (conf.py) is located (default: same as SOURCEDIR)" msgstr "配置文件(conf.py)所在目录路径(默认:与 SOURCEDIR 相同)" #: sphinx/cmd/build.py:156 @@ -1348,9 +1308,7 @@ msgid "Please enter a file suffix, e.g. '.rst' or '.txt'." msgstr "请输入文件后缀,例如:“.rst”或者“.txt”。" #: sphinx/cmd/quickstart.py:169 -msgid "" -"* Note: non-ASCII characters entered and terminal encoding unknown -- " -"assuming UTF-8 or Latin-1." +msgid "* Note: non-ASCII characters entered and terminal encoding unknown -- assuming UTF-8 or Latin-1." msgstr "* 提示:输入了非 ASCII 字符并且终端编码未知——假定为 UTF-8 或 Latin-1。" #: sphinx/cmd/quickstart.py:248 @@ -1363,20 +1321,27 @@ msgid "" "\n" "Please enter values for the following settings (just press Enter to\n" "accept a default value, if one is given in brackets)." -msgstr "\n请输入接下来各项设置的值(如果方括号中指定了默认值,直接\n按回车即可使用默认值)。" +msgstr "" +"\n" +"请输入接下来各项设置的值(如果方括号中指定了默认值,直接\n" +"按回车即可使用默认值)。" #: sphinx/cmd/quickstart.py:254 #, python-format msgid "" "\n" "Selected root path: %s" -msgstr "\n已选择根路径:%s" +msgstr "" +"\n" +"已选择根路径:%s" #: sphinx/cmd/quickstart.py:257 msgid "" "\n" "Enter the root path for documentation." -msgstr "\n输入文档的根路径。" +msgstr "" +"\n" +"输入文档的根路径。" #: sphinx/cmd/quickstart.py:259 msgid "Root path for the documentation" @@ -1400,7 +1365,11 @@ msgid "" "You have two options for placing the build directory for Sphinx output.\n" "Either, you use a directory \"_build\" within the root path, or you separate\n" "\"source\" and \"build\" directories within the root path." -msgstr "\n布置用于保存 Sphinx 输出的构建目录,有两种选择。\n一是在根路径下创建“_build”目录,二是在根路径下创建“source”\n和“build”两个独立的目录。" +msgstr "" +"\n" +"布置用于保存 Sphinx 输出的构建目录,有两种选择。\n" +"一是在根路径下创建“_build”目录,二是在根路径下创建“source”\n" +"和“build”两个独立的目录。" #: sphinx/cmd/quickstart.py:278 msgid "Separate source and build directories (y/n)" @@ -1412,7 +1381,11 @@ msgid "" "Inside the root directory, two more directories will be created; \"_templates\"\n" "for custom HTML templates and \"_static\" for custom stylesheets and other static\n" "files. You can enter another prefix (such as \".\") to replace the underscore." -msgstr "\n在根目录下,还会创建两个目录:“_templates”用于自定义 HTML 模板、\n“_static”用于自定义 CSS 和其他静态文件。你可以输入其他前缀(比如“.”)\n代替默认的下划线。" +msgstr "" +"\n" +"在根目录下,还会创建两个目录:“_templates”用于自定义 HTML 模板、\n" +"“_static”用于自定义 CSS 和其他静态文件。你可以输入其他前缀(比如“.”)\n" +"代替默认的下划线。" #: sphinx/cmd/quickstart.py:286 msgid "Name prefix for templates and static dir" @@ -1422,7 +1395,9 @@ msgstr "模板目录名和静态目录名的前缀" msgid "" "\n" "The project name will occur in several places in the built documentation." -msgstr "\n项目名称会出现在文档的许多地方。" +msgstr "" +"\n" +"项目名称会出现在文档的许多地方。" #: sphinx/cmd/quickstart.py:291 msgid "Project name" @@ -1440,7 +1415,12 @@ msgid "" "Python the version is something like 2.5 or 3.0, while the release is\n" "something like 2.5.1 or 3.0a1. If you don't need this dual structure,\n" "just set both to the same value." -msgstr "\n在 Sphinx 中,会区分“版本”和“发行版本”两个概念。同一版本可以\n有多个发行版本。例如,Python 版本可以是 2.5 或 3.0,而发行版\n本则是 2.5.1 或 3.0a1。如果你不需要这样的双重版本结构,请把这\n两个选项设置为相同值。" +msgstr "" +"\n" +"在 Sphinx 中,会区分“版本”和“发行版本”两个概念。同一版本可以\n" +"有多个发行版本。例如,Python 版本可以是 2.5 或 3.0,而发行版\n" +"本则是 2.5.1 或 3.0a1。如果你不需要这样的双重版本结构,请把这\n" +"两个选项设置为相同值。" #: sphinx/cmd/quickstart.py:302 msgid "Project version" @@ -1470,7 +1450,10 @@ msgid "" "\n" "The file name suffix for source files. Commonly, this is either \".txt\"\n" "or \".rst\". Only files with this suffix are considered documents." -msgstr "\n源文件的文件名后缀。一般是“.txt”或“.rst”。只有此后缀的文件才会\n被视为文档的源文件。" +msgstr "" +"\n" +"源文件的文件名后缀。一般是“.txt”或“.rst”。只有此后缀的文件才会\n" +"被视为文档的源文件。" #: sphinx/cmd/quickstart.py:322 msgid "Source file suffix" @@ -1483,7 +1466,11 @@ msgid "" "\"contents tree\", that is, it is the root of the hierarchical structure\n" "of the documents. Normally, this is \"index\", but if your \"index\"\n" "document is a custom template, you can also set this to another filename." -msgstr "\n有一个特殊的文档将被视为“目录树”的树顶节点,也即是文档层级\n结构的根。一般用“index”作为这个特殊文档,如果你的“index”文\n档使用了自定义模板,你也可以指定其他的文件名。" +msgstr "" +"\n" +"有一个特殊的文档将被视为“目录树”的树顶节点,也即是文档层级\n" +"结构的根。一般用“index”作为这个特殊文档,如果你的“index”文\n" +"档使用了自定义模板,你也可以指定其他的文件名。" #: sphinx/cmd/quickstart.py:330 msgid "Name of your master document (without suffix)" @@ -1491,8 +1478,7 @@ msgstr "主文档文件名(不含后缀)" #: sphinx/cmd/quickstart.py:336 #, python-format -msgid "" -"Error: the master file %s has already been found in the selected root path." +msgid "Error: the master file %s has already been found in the selected root path." msgstr "错误:选择的根目录下已存在主文档文件 %s。" #: sphinx/cmd/quickstart.py:338 @@ -1500,8 +1486,7 @@ msgid "sphinx-quickstart will not overwrite the existing file." msgstr "sphinx-quickstart 不会覆盖已有的文件。" #: sphinx/cmd/quickstart.py:340 -msgid "" -"Please enter a new file name, or rename the existing file and press Enter" +msgid "Please enter a new file name, or rename the existing file and press Enter" msgstr "请输入新文件名,若要重命名现有文件请按回车" #: sphinx/cmd/quickstart.py:344 @@ -1509,9 +1494,7 @@ msgid "Indicate which of the following Sphinx extensions should be enabled:" msgstr "启用 Sphinx 扩展:" #: sphinx/cmd/quickstart.py:353 -msgid "" -"Note: imgmath and mathjax cannot be enabled at the same time. imgmath has " -"been deselected." +msgid "Note: imgmath and mathjax cannot be enabled at the same time. imgmath has been deselected." msgstr "注意:imgmath 和 mathjax 不能同时启用。已取消选择 imgmath。" #: sphinx/cmd/quickstart.py:358 @@ -1520,7 +1503,10 @@ msgid "" "A Makefile and a Windows command file can be generated for you so that you\n" "only have to run e.g. `make html' instead of invoking sphinx-build\n" "directly." -msgstr "\n生成 Makefile 和 Windows 批处理文件,可以直接像“make html”这样\n运行,而不需要直接调用 sphinx-build。" +msgstr "" +"\n" +"生成 Makefile 和 Windows 批处理文件,可以直接像“make html”这样\n" +"运行,而不需要直接调用 sphinx-build。" #: sphinx/cmd/quickstart.py:362 msgid "Create Makefile? (y/n)" @@ -1556,19 +1542,21 @@ msgstr "\n你现在可以填写主文档文件 %s 并创建其他文档源文件 msgid "" "Use the Makefile to build the docs, like so:\n" " make builder\n" -msgstr "用 Makefile 构建文档,像这样:\n make builder\n" +msgstr "" +"用 Makefile 构建文档,像这样:\n" +" make builder\n" #: sphinx/cmd/quickstart.py:455 #, python-format msgid "" "Use the sphinx-build command to build the docs, like so:\n" " sphinx-build -b builder %s %s\n" -msgstr "用 sphinx-build 命令构建文档,像这样:\n sphinx-build -b builder %s %s\n" +msgstr "" +"用 sphinx-build 命令构建文档,像这样:\n" +" sphinx-build -b builder %s %s\n" #: sphinx/cmd/quickstart.py:458 -msgid "" -"where \"builder\" is one of the supported builders, e.g. html, latex or " -"linkcheck.\n" +msgid "where \"builder\" is one of the supported builders, e.g. html, latex or linkcheck.\n" msgstr "此处的“builder”是支持的构建器名,比如 html、latex 或 linkcheck。\n" #: sphinx/cmd/quickstart.py:498 @@ -1579,7 +1567,12 @@ msgid "" "sphinx-quickstart is an interactive tool that asks some questions about your\n" "project and then generates a complete documentation directory and sample\n" "Makefile to be used with sphinx-build.\n" -msgstr "\n生成 Sphinx 项目的必需文件。\n\nsphinx-quickstart 是一个交互式工具,询问一些关于项目的问题,生成\n完整的文档目录和用于 sphinx-build 的示例 Makefile。\n" +msgstr "" +"\n" +"生成 Sphinx 项目的必需文件。\n" +"\n" +"sphinx-quickstart 是一个交互式工具,询问一些关于项目的问题,生成\n" +"完整的文档目录和用于 sphinx-build 的示例 Makefile。\n" #: sphinx/cmd/quickstart.py:508 msgid "quiet mode" @@ -1695,14 +1688,11 @@ msgid "\"quiet\" is specified, but any of \"project\" or \"author\" is not speci msgstr "指定了“quiet”,但是没有指定“project”和“author”。" #: sphinx/cmd/quickstart.py:618 -msgid "" -"Error: specified path is not a directory, or sphinx files already exist." +msgid "Error: specified path is not a directory, or sphinx files already exist." msgstr "错误:指定的路径不是一个目录,或是 Sphinx 文件已存在。" #: sphinx/cmd/quickstart.py:620 -msgid "" -"sphinx-quickstart only generate into a empty directory. Please specify a new" -" root path." +msgid "sphinx-quickstart only generate into a empty directory. Please specify a new root path." msgstr "sphinx-quickstart 只会在空目录中生成文件。请指定一个新的根路径。" #: sphinx/cmd/quickstart.py:635 @@ -1719,8 +1709,7 @@ msgstr "检测到过度的去缩进" msgid "Invalid caption: %s" msgstr "无效的标题:%s" -#: sphinx/directives/code.py:140 sphinx/directives/code.py:292 -#: sphinx/directives/code.py:462 +#: sphinx/directives/code.py:140 sphinx/directives/code.py:292 sphinx/directives/code.py:462 #, python-format msgid "line number spec is out of range(1-%d): %r" msgstr "指定的行号超出范围(1-%d):%r" @@ -1777,18 +1766,15 @@ msgstr "作者: " msgid "%s %s" msgstr "%s %s" -#: sphinx/domains/c.py:64 sphinx/domains/cpp.py:6445 -#: sphinx/domains/python.py:210 sphinx/ext/napoleon/docstring.py:696 +#: sphinx/domains/c.py:64 sphinx/domains/cpp.py:6445 sphinx/domains/python.py:210 sphinx/ext/napoleon/docstring.py:696 msgid "Parameters" msgstr "参数" -#: sphinx/domains/c.py:67 sphinx/domains/cpp.py:6454 -#: sphinx/domains/javascript.py:209 sphinx/domains/python.py:222 +#: sphinx/domains/c.py:67 sphinx/domains/cpp.py:6454 sphinx/domains/javascript.py:209 sphinx/domains/python.py:222 msgid "Returns" msgstr "返回" -#: sphinx/domains/c.py:69 sphinx/domains/javascript.py:211 -#: sphinx/domains/python.py:224 +#: sphinx/domains/c.py:69 sphinx/domains/javascript.py:211 sphinx/domains/python.py:224 msgid "Return type" msgstr "返回类型" @@ -1817,8 +1803,7 @@ msgstr "%s (C 类型)" msgid "%s (C variable)" msgstr "%s (C 变量)" -#: sphinx/domains/c.py:258 sphinx/domains/cpp.py:7029 -#: sphinx/domains/javascript.py:297 sphinx/domains/python.py:742 +#: sphinx/domains/c.py:258 sphinx/domains/cpp.py:7029 sphinx/domains/javascript.py:297 sphinx/domains/python.py:742 msgid "function" msgstr "函数" @@ -1858,7 +1843,9 @@ msgstr "%s 版后已移除" msgid "" "Duplicate declaration, also defined in '%s'.\n" "Declaration is '%s'." -msgstr "重复的声明,已经在“%s”处定义。\n定义为“%s”。" +msgstr "" +"重复的声明,已经在“%s”处定义。\n" +"定义为“%s”。" #: sphinx/domains/cpp.py:6448 msgid "Template Parameters" @@ -1873,8 +1860,7 @@ msgstr "抛出" msgid "%s (C++ %s)" msgstr "%s (C++ %s)" -#: sphinx/domains/cpp.py:7027 sphinx/domains/javascript.py:299 -#: sphinx/domains/python.py:744 +#: sphinx/domains/cpp.py:7027 sphinx/domains/javascript.py:299 sphinx/domains/python.py:744 msgid "class" msgstr "类" @@ -1899,7 +1885,9 @@ msgstr "枚举子" msgid "" "Duplicate declaration, also defined in '%s'.\n" "Name of declaration is '%s'." -msgstr "重复的声明,已经在“%s”处定义。\n声明名称为“%s”。" +msgstr "" +"重复的声明,已经在“%s”处定义。\n" +"声明名称为“%s”。" #: sphinx/domains/javascript.py:130 sphinx/domains/python.py:430 #, python-format @@ -1947,8 +1935,7 @@ msgstr "数据" msgid "attribute" msgstr "属性" -#: sphinx/domains/javascript.py:302 sphinx/domains/python.py:49 -#: sphinx/domains/python.py:750 +#: sphinx/domains/javascript.py:302 sphinx/domains/python.py:49 sphinx/domains/python.py:750 msgid "module" msgstr "模块" @@ -1994,8 +1981,7 @@ msgstr "变量" msgid "Raises" msgstr "引发" -#: sphinx/domains/python.py:431 sphinx/domains/python.py:489 -#: sphinx/domains/python.py:501 sphinx/domains/python.py:514 +#: sphinx/domains/python.py:431 sphinx/domains/python.py:489 sphinx/domains/python.py:501 sphinx/domains/python.py:514 #, python-format msgid "%s() (in module %s)" msgstr "%s() (在 %s 模块中)" @@ -2104,9 +2090,7 @@ msgstr "环境变量; %s" #: sphinx/domains/std.py:166 #, python-format -msgid "" -"Malformed option description %r, should look like \"opt\", \"-opt args\", \"" -"--opt args\", \"/opt args\" or \"+opt args\"" +msgid "Malformed option description %r, should look like \"opt\", \"-opt args\", \"--opt args\", \"/opt args\" or \"+opt args\"" msgstr "畸形的选项描述 %r,应是“opt”、“-opt args”、“--opt args”、“/opt args”或“+opt args”形式" #: sphinx/domains/std.py:207 @@ -2206,9 +2190,7 @@ msgid "source directory has changed" msgstr "源文件目录已变化" #: sphinx/environment/__init__.py:283 -msgid "" -"This environment is incompatible with the selected builder, please choose " -"another doctree directory." +msgid "This environment is incompatible with the selected builder, please choose another doctree directory." msgstr "本环境与选择的构建器不兼容,请选择其他的文档树目录。" #: sphinx/environment/__init__.py:404 @@ -2244,8 +2226,7 @@ msgstr "参见 %s" msgid "unknown index entry type %r" msgstr "未知的索引条目类型 %r" -#: sphinx/environment/adapters/indexentries.py:156 -#: sphinx/templates/latex/sphinxmessages.sty_t:11 +#: sphinx/environment/adapters/indexentries.py:156 sphinx/templates/latex/sphinxmessages.sty_t:11 msgid "Symbols" msgstr "符号" @@ -2256,9 +2237,7 @@ msgstr "在文档树中检测到循环引用,已忽略:%s <- %s" #: sphinx/environment/adapters/toctree.py:172 #, python-format -msgid "" -"toctree contains reference to document %r that doesn't have a title: no link" -" will be generated" +msgid "toctree contains reference to document %r that doesn't have a title: no link will be generated" msgstr "目录树引用的文档 %r 缺少标题:不会生成链接" #: sphinx/environment/adapters/toctree.py:178 @@ -2306,15 +2285,21 @@ msgid "" "excluded from generation.\n" "\n" "Note: By default this script will not overwrite already created files." -msgstr "\n在 <MODULE_PATH> 中递归查找 Python 模块和包,然后在 <OUTPUT_PATH> 中为每个使用了\nautomodule 指令的包创建一个 reST 文件。\n\n<EXCLUDE_PATTERN> 可以排除生成符合规则的文件/目录的文档。\n\n提示:本脚本默认不会覆盖已有文件。" +msgstr "" +"\n" +"在 <MODULE_PATH> 中递归查找 Python 模块和包,然后在 <OUTPUT_PATH> 中为每个使用了\n" +"automodule 指令的包创建一个 reST 文件。\n" +"\n" +"<EXCLUDE_PATTERN> 可以排除生成符合规则的文件/目录的文档。\n" +"\n" +"提示:本脚本默认不会覆盖已有文件。" #: sphinx/ext/apidoc.py:312 msgid "path to module to document" msgstr "要生成文档的模块路径" #: sphinx/ext/apidoc.py:314 -msgid "" -"fnmatch-style file and/or directory patterns to exclude from generation" +msgid "fnmatch-style file and/or directory patterns to exclude from generation" msgstr "排除的文件/目录,fnmatch 风格的规则" #: sphinx/ext/apidoc.py:319 @@ -2330,9 +2315,7 @@ msgid "overwrite existing files" msgstr "覆盖已有文件" #: sphinx/ext/apidoc.py:328 -msgid "" -"follow symbolic links. Powerful when combined with " -"collective.recipe.omelette." +msgid "follow symbolic links. Powerful when combined with collective.recipe.omelette." msgstr "遵循符号链接。配合 collective.recipe.omelette 使用尤其奏效。" #: sphinx/ext/apidoc.py:331 @@ -2356,9 +2339,7 @@ msgid "don't create a table of contents file" msgstr "不创建目录文件" #: sphinx/ext/apidoc.py:344 -msgid "" -"don't create headings for the module/package packages (e.g. when the " -"docstrings already contain them)" +msgid "don't create headings for the module/package packages (e.g. when the docstrings already contain them)" msgstr "不创建模块/包的标题(比如当 Docstring 中已经有标题时,可以使用这个选项)" #: sphinx/ext/apidoc.py:349 @@ -2366,9 +2347,7 @@ msgid "put module documentation before submodule documentation" msgstr "模块文档先于子模块文档" #: sphinx/ext/apidoc.py:353 -msgid "" -"interpret module paths according to PEP-0420 implicit namespaces " -"specification" +msgid "interpret module paths according to PEP-0420 implicit namespaces specification" msgstr "根据 PEP-0420 隐式命名空间规范解释模块路径" #: sphinx/ext/apidoc.py:357 @@ -2415,9 +2394,7 @@ msgstr "无效的正则表达式 %r 在 %s" #: sphinx/ext/coverage.py:55 #, python-format -msgid "" -"Testing of coverage in the sources finished, look at the results in " -"%(outdir)spython.txt." +msgid "Testing of coverage in the sources finished, look at the results in %(outdir)spython.txt." msgstr "已完成源文件的覆盖率测试,请在 %(outdir)s/python.txt 中查看结果。" #: sphinx/ext/coverage.py:70 @@ -2451,9 +2428,7 @@ msgstr "无效的 TestCode 类型" #: sphinx/ext/doctest.py:283 #, python-format -msgid "" -"Testing of doctests in the sources finished, look at the results in " -"%(outdir)s/output.txt." +msgid "Testing of doctests in the sources finished, look at the results in %(outdir)s/output.txt." msgstr "已完成源文件的文档测试,请在 %(outdir)s/output.txt 中查看结果。" #: sphinx/ext/doctest.py:446 @@ -2491,9 +2466,7 @@ msgstr "dot没有生成输出文件:\n[stderr]\n%r\n[stdout]\n%r" #: sphinx/ext/graphviz.py:254 #, python-format -msgid "" -"dot command %r cannot be run (needed for graphviz output), check the " -"graphviz_dot setting" +msgid "dot command %r cannot be run (needed for graphviz output), check the graphviz_dot setting" msgstr "无法运行 Dot 命令 %r (Graphviz 输出所需),请检查 graphviz_dot 配置" #: sphinx/ext/graphviz.py:261 @@ -2511,8 +2484,7 @@ msgstr "点退出错误:\n[stderr]\n%r\n[stdout]\n%r" msgid "graphviz_output_format must be one of 'png', 'svg', but is %r" msgstr "graphviz_output_format 必须是 'png' 或 'svg' 中之一,现为 %r" -#: sphinx/ext/graphviz.py:275 sphinx/ext/graphviz.py:329 -#: sphinx/ext/graphviz.py:367 +#: sphinx/ext/graphviz.py:275 sphinx/ext/graphviz.py:329 sphinx/ext/graphviz.py:367 #, python-format msgid "dot code %r: %s" msgstr "点 代码 %r: %s" @@ -2543,16 +2515,12 @@ msgstr "转换退出时出错:\n[stderr]\n%r\n[stdout]\n%r" #: sphinx/ext/imgmath.py:139 #, python-format -msgid "" -"LaTeX command %r cannot be run (needed for math display), check the " -"imgmath_latex setting" +msgid "LaTeX command %r cannot be run (needed for math display), check the imgmath_latex setting" msgstr "无法运行 LaTeX 命令 %r (数学公式显示必需),请检查 imgmath_latex 设置" #: sphinx/ext/imgmath.py:154 #, python-format -msgid "" -"%s command %r cannot be run (needed for math display), check the imgmath_%s " -"setting" +msgid "%s command %r cannot be run (needed for math display), check the imgmath_%s setting" msgstr "无法运行 %s 命令 %r (数学公式显示必需),请检查 imgmath_%s 设置" #: sphinx/ext/imgmath.py:289 @@ -2680,14 +2648,15 @@ msgstr "属性 %s 不存在,在对象 %s 上" msgid "" "autodoc: failed to determine %r to be documented.the following exception was raised:\n" "%s" -msgstr "autodoc:无法判断是否生成 %r 的文档。抛出了下列异常:\n%s" +msgstr "" +"autodoc:无法判断是否生成 %r 的文档。抛出了下列异常:\n" +"%s" #: sphinx/ext/autodoc/__init__.py:692 #, python-format msgid "" -"don't know which module to import for autodocumenting %r (try placing a " -"\"module\" or \"currentmodule\" directive in the document, or giving an " -"explicit module name)" +"don't know which module to import for autodocumenting %r (try placing a \"module\" or \"currentmodule\" directive in the document, or giving an explicit module " +"name)" msgstr "无法判断导入哪个模块来自动生成文档 %r(尝试在文档中使用“module”或“currentmodule”指令,或者显式给定模块名)" #: sphinx/ext/autodoc/__init__.py:786 @@ -2701,15 +2670,12 @@ msgstr "automodule %s 给定了函数签名参数或返回类型标注" #: sphinx/ext/autodoc/__init__.py:827 #, python-format -msgid "" -"__all__ should be a list of strings, not %r (in module %s) -- ignoring " -"__all__" +msgid "__all__ should be a list of strings, not %r (in module %s) -- ignoring __all__" msgstr "__all__ 应是一个字符串列表,而不是 %r (出现在模块 %s 中) -- 已忽略__all__" #: sphinx/ext/autodoc/__init__.py:842 #, python-format -msgid "" -"missing attribute mentioned in :members: or __all__: module %s, attribute %s" +msgid "missing attribute mentioned in :members: or __all__: module %s, attribute %s" msgstr ":members: 或 __all__ 提及的属性不存在:模块 %s,属性 %s" #: sphinx/ext/autodoc/__init__.py:1126 @@ -2753,9 +2719,7 @@ msgid "failed to import object %s" msgstr "无法导入对象 %s" #: sphinx/ext/autosummary/__init__.py:702 -msgid "" -"autosummary generats .rst files internally. But your source_suffix does not " -"contain .rst. Skipped." +msgid "autosummary generats .rst files internally. But your source_suffix does not contain .rst. Skipped." msgstr "autosummary 内部生成 .rst 文件,但是 source_suffix 中不包含 .rst,已跳过。" #: sphinx/ext/autosummary/generate.py:100 @@ -2781,7 +2745,17 @@ msgid "" "``sphinx.ext.autosummary`` Python module and can be read using::\n" "\n" " pydoc sphinx.ext.autosummary\n" -msgstr "\n用 autosummary 指令生成 ReStructuredText\n\nsphinx-autogen 是 sphinx.ext.autosummary.generate 的前端,它根据给定\n的输入文件中的 autosummary 指令生成 reStructuredText 文件\n\nautosummary 指令的格式见 Python 模块 ``sphinx.ext.autosummary`` 的文\n档,并且可以这样调出文档阅读::\n\n pydoc sphinx.ext.autosummary\n" +msgstr "" +"\n" +"用 autosummary 指令生成 ReStructuredText\n" +"\n" +"sphinx-autogen 是 sphinx.ext.autosummary.generate 的前端,它根据给定\n" +"的输入文件中的 autosummary 指令生成 reStructuredText 文件\n" +"\n" +"autosummary 指令的格式见 Python 模块 ``sphinx.ext.autosummary`` 的文\n" +"档,并且可以这样调出文档阅读::\n" +"\n" +" pydoc sphinx.ext.autosummary\n" #: sphinx/ext/autosummary/generate.py:381 msgid "source files to generate rST files for" @@ -2878,8 +2852,7 @@ msgstr "小技巧" msgid "Warning" msgstr "警告" -#: sphinx/templates/latex/longtable.tex_t:23 -#: sphinx/templates/latex/sphinxmessages.sty_t:8 +#: sphinx/templates/latex/longtable.tex_t:23 sphinx/templates/latex/sphinxmessages.sty_t:8 msgid "continued from previous page" msgstr "续上页" @@ -2903,13 +2876,11 @@ msgstr "数值" msgid "page" msgstr "页" -#: sphinx/themes/agogo/layout.html:46 sphinx/themes/basic/globaltoc.html:10 -#: sphinx/themes/basic/localtoc.html:11 sphinx/themes/scrolls/layout.html:41 +#: sphinx/themes/agogo/layout.html:46 sphinx/themes/basic/globaltoc.html:10 sphinx/themes/basic/localtoc.html:11 sphinx/themes/scrolls/layout.html:41 msgid "Table of Contents" msgstr "目录" -#: sphinx/themes/agogo/layout.html:51 sphinx/themes/basic/layout.html:153 -#: sphinx/themes/basic/search.html:11 sphinx/themes/basic/search.html:21 +#: sphinx/themes/agogo/layout.html:51 sphinx/themes/basic/layout.html:153 sphinx/themes/basic/search.html:11 sphinx/themes/basic/search.html:21 #: sphinx/themes/basic/searchresults.html:10 msgid "Search" msgstr "搜索" @@ -2971,9 +2942,7 @@ msgstr "所的函数,类,术语" msgid "Index – %(key)s" msgstr "索引 – %(key)s" -#: sphinx/themes/basic/genindex-single.html:61 -#: sphinx/themes/basic/genindex-split.html:24 -#: sphinx/themes/basic/genindex-split.html:38 +#: sphinx/themes/basic/genindex-single.html:61 sphinx/themes/basic/genindex-split.html:24 sphinx/themes/basic/genindex-split.html:38 #: sphinx/themes/basic/genindex.html:72 msgid "Full index on one page" msgstr "一页的全部索引" @@ -3020,9 +2989,7 @@ msgstr "最后更新于 %(last_updated)s." #: sphinx/themes/basic/layout.html:210 #, python-format -msgid "" -"Created using <a href=\"http://sphinx-doc.org/\">Sphinx</a> " -"%(sphinx_version)s." +msgid "Created using <a href=\"http://sphinx-doc.org/\">Sphinx</a> %(sphinx_version)s." msgstr "由 <a href=\"http://sphinx-doc.org/\">Sphinx</a> %(sphinx_version)s 创建。" #: sphinx/themes/basic/opensearch.xml:4 @@ -3060,23 +3027,16 @@ msgid "" " containing fewer words won't appear in the result list." msgstr "在这儿,你可以对这些文档进行搜索。向搜索框中输入你所要搜索的关键字并点击“搜索”。注意:搜索引擎会自动搜索所有的关键字。将不会搜索到部分关键字的页面." -#: sphinx/themes/basic/search.html:37 -#: sphinx/themes/basic/searchresults.html:17 +#: sphinx/themes/basic/search.html:37 sphinx/themes/basic/searchresults.html:17 msgid "search" msgstr "搜索" -#: sphinx/themes/basic/search.html:41 -#: sphinx/themes/basic/searchresults.html:21 -#: sphinx/themes/basic/static/searchtools.js:285 +#: sphinx/themes/basic/search.html:41 sphinx/themes/basic/searchresults.html:21 sphinx/themes/basic/static/searchtools.js:285 msgid "Search Results" msgstr "搜索结果" -#: sphinx/themes/basic/search.html:43 -#: sphinx/themes/basic/searchresults.html:23 -#: sphinx/themes/basic/static/searchtools.js:287 -msgid "" -"Your search did not match any documents. Please make sure that all words are" -" spelled correctly and that you've selected enough categories." +#: sphinx/themes/basic/search.html:43 sphinx/themes/basic/searchresults.html:23 sphinx/themes/basic/static/searchtools.js:287 +msgid "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." msgstr "没有任何文档匹配您的搜索。请确保你输入的词拼写正确并选择了合适的分类。" #: sphinx/themes/basic/searchbox.html:12 @@ -3087,8 +3047,7 @@ msgstr "快速搜索" msgid "This Page" msgstr "本页" -#: sphinx/themes/basic/changes/frameset.html:5 -#: sphinx/themes/basic/changes/versionchanges.html:12 +#: sphinx/themes/basic/changes/frameset.html:5 sphinx/themes/basic/changes/versionchanges.html:12 #, python-format msgid "Changes in Version %(version)s — %(docstitle)s" msgstr "更改发生在版本 %(version)s— %(docstitle)s" @@ -3115,15 +3074,11 @@ msgstr "C API 更改" msgid "Other changes" msgstr "其他更改" -#: sphinx/themes/basic/static/doctools.js:194 sphinx/writers/html.py:454 -#: sphinx/writers/html.py:459 sphinx/writers/html5.py:400 -#: sphinx/writers/html5.py:405 +#: sphinx/themes/basic/static/doctools.js:194 sphinx/writers/html.py:454 sphinx/writers/html.py:459 sphinx/writers/html5.py:400 sphinx/writers/html5.py:405 msgid "Permalink to this headline" msgstr "永久链接至标题" -#: sphinx/themes/basic/static/doctools.js:200 sphinx/writers/html.py:134 -#: sphinx/writers/html.py:145 sphinx/writers/html5.py:103 -#: sphinx/writers/html5.py:114 +#: sphinx/themes/basic/static/doctools.js:200 sphinx/writers/html.py:134 sphinx/writers/html.py:145 sphinx/writers/html5.py:103 sphinx/writers/html5.py:114 msgid "Permalink to this definition" msgstr "永久链接至目标" @@ -3152,8 +3107,7 @@ msgstr ", 在 " msgid "Expand sidebar" msgstr "展开边栏" -#: sphinx/themes/classic/static/sidebar.js_t:96 -#: sphinx/themes/classic/static/sidebar.js_t:124 +#: sphinx/themes/classic/static/sidebar.js_t:96 sphinx/themes/classic/static/sidebar.js_t:124 msgid "Collapse sidebar" msgstr "折叠边栏" @@ -3163,8 +3117,7 @@ msgstr "目录" #: sphinx/transforms/__init__.py:261 #, python-format -msgid "" -"4 column based index found. It might be a bug of extensions you use: %r" +msgid "4 column based index found. It might be a bug of extensions you use: %r" msgstr "发现使用了 4 列布局的索引页。可能是你所用的扩展出现了 Bug:%r" #: sphinx/transforms/__init__.py:303 @@ -3177,27 +3130,19 @@ msgid "Footnote [#] is not referenced." msgstr "脚注 [#] 没有被引用过。" #: sphinx/transforms/i18n.py:293 sphinx/transforms/i18n.py:363 -msgid "" -"inconsistent footnote references in translated message. original: {0}, " -"translated: {1}" +msgid "inconsistent footnote references in translated message. original: {0}, translated: {1}" msgstr "译文中的脚注引用与原文不一致。原始为:{0},翻译后为:{1}" #: sphinx/transforms/i18n.py:335 -msgid "" -"inconsistent references in translated message. original: {0}, translated: " -"{1}" +msgid "inconsistent references in translated message. original: {0}, translated: {1}" msgstr "译文中的引用与原文不一致。原始为:{0},翻译后为:{1}" #: sphinx/transforms/i18n.py:382 -msgid "" -"inconsistent citation references in translated message. original: {0}, " -"translated: {1}" +msgid "inconsistent citation references in translated message. original: {0}, translated: {1}" msgstr "译文中的引文引用与原文不一致。原始为:{0},翻译后为:{1}" #: sphinx/transforms/i18n.py:402 -msgid "" -"inconsistent term references in translated message. original: {0}, " -"translated: {1}" +msgid "inconsistent term references in translated message. original: {0}, translated: {1}" msgstr "译文中的术语引用与原文不一致。原始为:{0},翻译后为:{1}" #: sphinx/transforms/post_transforms/__init__.py:110 @@ -3259,9 +3204,7 @@ msgstr "写入时发生错误:%s,%s" #: sphinx/util/i18n.py:215 #, python-format -msgid "" -"Invalid date format. Quote the string by single quote if you want to output " -"it directly: %s" +msgid "Invalid date format. Quote the string by single quote if you want to output it directly: %s" msgstr "无效的日期格式。如果你想直接输出日期字符串,请用单引号:%s" #: sphinx/util/nodes.py:428 @@ -3330,12 +3273,10 @@ msgid "document title is not a single Text node" msgstr "文档标题不是一个单纯文本节点" #: sphinx/writers/latex.py:926 sphinx/writers/texinfo.py:658 -msgid "" -"encountered title node not in section, topic, table, admonition or sidebar" +msgid "encountered title node not in section, topic, table, admonition or sidebar" msgstr "在节、话题、表格、警示或边栏以外的位置发现标题节点" -#: sphinx/writers/latex.py:1102 sphinx/writers/manpage.py:277 -#: sphinx/writers/texinfo.py:675 +#: sphinx/writers/latex.py:1102 sphinx/writers/manpage.py:277 sphinx/writers/texinfo.py:675 msgid "Footnotes" msgstr "脚注" diff --git a/sphinx/make_mode.py b/sphinx/make_mode.py deleted file mode 100644 index 792f4ab85..000000000 --- a/sphinx/make_mode.py +++ /dev/null @@ -1,38 +0,0 @@ -""" - sphinx.make_mode - ~~~~~~~~~~~~~~~~ - - sphinx-build -M command-line handling. - - This replaces the old, platform-dependent and once-generated content - of Makefile / make.bat. - - This is in its own module so that importing it is fast. It should not - import the main Sphinx modules (like sphinx.applications, sphinx.builders). - - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import warnings - -from sphinx.cmd import make_mode -from sphinx.deprecation import RemovedInSphinx30Warning - - -BUILDERS = make_mode.BUILDERS - - -class Make(make_mode.Make): - def __init__(self, *args): - warnings.warn('sphinx.make_mode.Make is deprecated. ' - 'Please use sphinx.cmd.make_mode.Make instead.', - RemovedInSphinx30Warning, stacklevel=2) - super().__init__(*args) - - -def run_make_mode(args): - warnings.warn('sphinx.make_mode.run_make_mode() is deprecated. ' - 'Please use sphinx.cmd.make_mode.run_make_mode() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return make_mode.run_make_mode(args) diff --git a/sphinx/parsers.py b/sphinx/parsers.py index 2a61971f3..3974d1c66 100644 --- a/sphinx/parsers.py +++ b/sphinx/parsers.py @@ -8,6 +8,7 @@ :license: BSD, see LICENSE for details. """ +import warnings from typing import Any, Dict, List, Union import docutils.parsers @@ -17,6 +18,7 @@ from docutils.parsers.rst import states from docutils.statemachine import StringList from docutils.transforms.universal import SmartQuotes +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.util.rst import append_epilog, prepend_prolog if False: @@ -47,6 +49,8 @@ class Parser(docutils.parsers.Parser): .. deprecated:: 1.6 ``warn()`` and ``info()`` is deprecated. Use :mod:`sphinx.util.logging` instead. + .. deprecated:: 3.0 + parser.app is deprecated. """ def set_application(self, app: "Sphinx") -> None: @@ -54,10 +58,15 @@ class Parser(docutils.parsers.Parser): :param sphinx.application.Sphinx app: Sphinx application object """ - self.app = app + self._app = app self.config = app.config self.env = app.env + @property + def app(self) -> "Sphinx": + warnings.warn('parser.app is deprecated.', RemovedInSphinx50Warning) + return self._app + class RSTParser(docutils.parsers.rst.Parser, Parser): """A reST parser for Sphinx.""" diff --git a/sphinx/project.py b/sphinx/project.py index d63af1fcb..f4afdadad 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -9,6 +9,7 @@ """ import os +from glob import glob from sphinx.locale import __ from sphinx.util import get_matching_files @@ -55,7 +56,13 @@ class Project: for filename in get_matching_files(self.srcdir, excludes): # type: ignore docname = self.path2doc(filename) if docname: - if os.access(os.path.join(self.srcdir, filename), os.R_OK): + if docname in self.docnames: + pattern = os.path.join(self.srcdir, docname) + '.*' + files = [relpath(f, self.srcdir) for f in glob(pattern)] + logger.warning(__('multiple files found for the document "%s": %r\n' + 'Use %r for the build.'), + docname, files, self.doc2path(docname), once=True) + elif os.access(os.path.join(self.srcdir, filename), os.R_OK): self.docnames.add(docname) else: logger.warning(__("document not readable. Ignored."), location=docname) diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 22207b715..4d8aa8955 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -9,6 +9,7 @@ """ import sys +from typing import Dict, List, Type if sys.version_info > (3, 8): import ast @@ -20,6 +21,29 @@ else: import ast # type: ignore +OPERATORS = { + ast.Add: "+", + ast.And: "and", + ast.BitAnd: "&", + ast.BitOr: "|", + ast.BitXor: "^", + ast.Div: "/", + ast.FloorDiv: "//", + ast.Invert: "~", + ast.LShift: "<<", + ast.MatMult: "@", + ast.Mult: "*", + ast.Mod: "%", + ast.Not: "not", + ast.Pow: "**", + ast.Or: "or", + ast.RShift: ">>", + ast.Sub: "-", + ast.UAdd: "+", + ast.USub: "-", +} # type: Dict[Type[ast.AST], str] + + def parse(code: str, mode: str = 'exec') -> "ast.AST": """Parse the *code* using built-in ast or typed_ast. @@ -40,8 +64,22 @@ def unparse(node: ast.AST) -> str: return None elif isinstance(node, str): return node + elif node.__class__ in OPERATORS: + return OPERATORS[node.__class__] + elif isinstance(node, ast.arg): + if node.annotation: + return "%s: %s" % (node.arg, unparse(node.annotation)) + else: + return node.arg + elif isinstance(node, ast.arguments): + return unparse_arguments(node) elif isinstance(node, ast.Attribute): return "%s.%s" % (unparse(node.value), node.attr) + elif isinstance(node, ast.BinOp): + return " ".join(unparse(e) for e in [node.left, node.op, node.right]) + elif isinstance(node, ast.BoolOp): + op = " %s " % unparse(node.op) + return op.join(unparse(e) for e in node.values) elif isinstance(node, ast.Bytes): return repr(node.s) elif isinstance(node, ast.Call): @@ -58,7 +96,7 @@ def unparse(node: ast.AST) -> str: elif isinstance(node, ast.Index): return unparse(node.value) elif isinstance(node, ast.Lambda): - return "<function <lambda>>" # TODO + return "lambda %s: ..." % unparse(node.args) elif isinstance(node, ast.List): return "[" + ", ".join(unparse(e) for e in node.elts) + "]" elif isinstance(node, ast.Name): @@ -73,6 +111,8 @@ def unparse(node: ast.AST) -> str: return repr(node.s) elif isinstance(node, ast.Subscript): return "%s[%s]" % (unparse(node.value), unparse(node.slice)) + elif isinstance(node, ast.UnaryOp): + return "%s %s" % (unparse(node.op), unparse(node.operand)) elif isinstance(node, ast.Tuple): return ", ".join(unparse(e) for e in node.elts) elif sys.version_info > (3, 6) and isinstance(node, ast.Constant): @@ -80,3 +120,61 @@ def unparse(node: ast.AST) -> str: return repr(node.value) else: raise NotImplementedError('Unable to parse %s object' % type(node).__name__) + + +def unparse_arguments(node: ast.arguments) -> str: + """Unparse an arguments to string.""" + defaults = list(node.defaults) + positionals = len(node.args) + posonlyargs = 0 + if hasattr(node, "posonlyargs"): # for py38+ + posonlyargs += len(node.posonlyargs) # type:ignore + positionals += posonlyargs + for _ in range(len(defaults), positionals): + defaults.insert(0, None) + + kw_defaults = list(node.kw_defaults) + for _ in range(len(kw_defaults), len(node.kwonlyargs)): + kw_defaults.insert(0, None) + + args = [] # type: List[str] + if hasattr(node, "posonlyargs"): # for py38+ + for i, arg in enumerate(node.posonlyargs): # type: ignore + name = unparse(arg) + if defaults[i]: + if arg.annotation: + name += " = %s" % unparse(defaults[i]) + else: + name += "=%s" % unparse(defaults[i]) + args.append(name) + + if node.posonlyargs: # type: ignore + args.append('/') + + for i, arg in enumerate(node.args): + name = unparse(arg) + if defaults[i + posonlyargs]: + if arg.annotation: + name += " = %s" % unparse(defaults[i + posonlyargs]) + else: + name += "=%s" % unparse(defaults[i + posonlyargs]) + args.append(name) + + if node.vararg: + args.append("*" + unparse(node.vararg)) + + if node.kwonlyargs and not node.vararg: + args.append('*') + for i, arg in enumerate(node.kwonlyargs): + name = unparse(arg) + if kw_defaults[i]: + if arg.annotation: + name += " = %s" % unparse(kw_defaults[i]) + else: + name += "=%s" % unparse(kw_defaults[i]) + args.append(name) + + if node.kwarg: + args.append("**" + unparse(node.kwarg)) + + return ", ".join(args) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 1f803e950..cb3cf0cc1 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -142,7 +142,7 @@ class TokenProcessor: def fetch_token(self) -> Token: """Fetch a next token from source code. - Returns ``False`` if sequence finished. + Returns ``None`` if sequence finished. """ try: self.previous = self.current diff --git a/sphinx/registry.py b/sphinx/registry.py index ea423298f..93a7ebc25 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -9,9 +9,7 @@ """ import traceback -import warnings from importlib import import_module -from inspect import isclass from types import MethodType from typing import Any, Callable, Dict, Iterator, List, Tuple, Union @@ -25,18 +23,15 @@ from pkg_resources import iter_entry_points from sphinx.builders import Builder from sphinx.config import Config -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.domains import Domain, Index, ObjType from sphinx.domains.std import GenericObject, Target from sphinx.environment import BuildEnvironment from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError from sphinx.extension import Extension -from sphinx.io import SphinxFileInput from sphinx.locale import __ from sphinx.parsers import Parser as SphinxParser from sphinx.roles import XRefRole from sphinx.util import logging -from sphinx.util.docutils import directive_helper from sphinx.util.logging import prefixed_warnings from sphinx.util.typing import RoleFunction, TitleGetter @@ -176,17 +171,9 @@ class SphinxComponentRegistry: yield domain - def override_domain(self, domain: "Type[Domain]") -> None: - warnings.warn('registry.override_domain() is deprecated. ' - 'Use app.add_domain(domain, override=True) instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.add_domain(domain, override=True) - - def add_directive_to_domain(self, domain: str, name: str, obj: Any, - has_content: bool = None, argument_spec: Any = None, - override: bool = False, **option_spec: Any) -> None: - logger.debug('[app] adding directive to domain: %r', - (domain, name, obj, has_content, argument_spec, option_spec)) + def add_directive_to_domain(self, domain: str, name: str, + cls: "Type[Directive]", override: bool = False) -> None: + logger.debug('[app] adding directive to domain: %r', (domain, name, cls)) if domain not in self.domains: raise ExtensionError(__('domain %s not yet registered') % domain) @@ -194,10 +181,7 @@ class SphinxComponentRegistry: if name in directives and not override: raise ExtensionError(__('The %r directive is already registered to domain %s') % (name, domain)) - if not isclass(obj) or not issubclass(obj, Directive): - directives[name] = directive_helper(obj, has_content, argument_spec, **option_spec) - else: - directives[name] = obj + directives[name] = cls def add_role_to_domain(self, domain: str, name: str, role: Union[RoleFunction, XRefRole], override: bool = False @@ -273,27 +257,8 @@ class SphinxComponentRegistry: else: self.source_suffix[suffix] = filetype - def add_source_parser(self, *args: Any, **kwargs: Any) -> None: - logger.debug('[app] adding search source_parser: %r', args) - if len(args) == 1: - # new sytle arguments: (source_parser) - suffix = None # type: str - parser = args[0] # type: Type[Parser] - else: - # old style arguments: (suffix, source_parser) - warnings.warn('app.add_source_parser() does not support suffix argument. ' - 'Use app.add_source_suffix() instead.', - RemovedInSphinx30Warning, stacklevel=3) - suffix = args[0] - parser = args[1] - - if suffix: - self.add_source_suffix(suffix, suffix, override=True) - - if len(parser.supported) == 0: - warnings.warn('Old source_parser has been detected. Please fill Parser.supported ' - 'attribute: %s' % parser.__name__, - RemovedInSphinx30Warning, stacklevel=3) + def add_source_parser(self, parser: "Type[Parser]", **kwargs: Any) -> None: + logger.debug('[app] adding search source_parser: %r', parser) # create a map from filetype to parser for filetype in parser.supported: @@ -303,12 +268,6 @@ class SphinxComponentRegistry: else: self.source_parsers[filetype] = parser - # also maps suffix to parser - # - # This rescues old styled parsers which does not have ``supported`` filetypes. - if suffix: - self.source_parsers[suffix] = parser - def get_source_parser(self, filetype: str) -> "Type[Parser]": try: return self.source_parsers[filetype] @@ -325,16 +284,6 @@ class SphinxComponentRegistry: parser.set_application(app) return parser - def add_source_input(self, input_class: "Type[SphinxFileInput]", override: bool = False - ) -> None: - warnings.warn('registry.source_input() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - for filetype in input_class.supported: - if filetype in self.source_inputs and not override: - raise ExtensionError(__('source_input for %r is already registered') % - filetype) - self.source_inputs[filetype] = input_class - def get_source_input(self, filetype: str) -> "Type[Input]": try: return self.source_inputs[filetype] @@ -407,7 +356,7 @@ class SphinxComponentRegistry: attrgetter: Callable[[Any, str, Any], Any]) -> None: self.autodoc_attrgettrs[typ] = attrgetter - def add_css_files(self, filename, **attributes): + def add_css_files(self, filename: str, **attributes: str) -> None: self.css_files.append((filename, attributes)) def add_js_file(self, filename: str, **attributes: str) -> None: diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index aab4297a0..d9853ff06 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -43,6 +43,14 @@ class SearchLanguage: This is a set of stop words of the target language. Default `stopwords` is empty. This word is used for building index and embedded in JS. + .. attribute:: js_splitter_code + + Return splitter funcion of JavaScript version. The function should be + named as ``splitQuery``. And it should take a string and return list of + strings. + + .. versionadded:: 3.0 + .. attribute:: js_stemmer_code Return stemmer class of JavaScript version. This class' name should be @@ -55,6 +63,7 @@ class SearchLanguage: lang = None # type: str language_name = None # type: str stopwords = set() # type: Set[str] + js_splitter_code = None # type: str js_stemmer_rawcode = None # type: str js_stemmer_code = """ /** @@ -425,11 +434,16 @@ class IndexBuilder: self._mapping.setdefault(stemmed_word, set()).add(docname) def context_for_searchtool(self) -> Dict[str, Any]: + if self.lang.js_splitter_code: + js_splitter_code = self.lang.js_splitter_code + else: + js_splitter_code = self.js_splitter_code + return { 'search_language_stemming_code': self.lang.js_stemmer_code, 'search_language_stop_words': jsdump.dumps(sorted(self.lang.stopwords)), 'search_scorer_tool': self.js_scorer_code, - 'search_word_splitter_code': self.js_splitter_code, + 'search_word_splitter_code': js_splitter_code, } def get_js_stemmer_rawcode(self) -> str: diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index 668248b6b..d1f444be1 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -19,7 +19,6 @@ import os import re import sys -import warnings from typing import Any, Dict, List try: @@ -34,7 +33,6 @@ try: except ImportError: janome_module = False -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.errors import SphinxError, ExtensionError from sphinx.search import SearchLanguage from sphinx.util import import_object @@ -525,21 +523,9 @@ class SearchJapanese(SearchLanguage): """ lang = 'ja' language_name = 'Japanese' - splitters = { - 'default': 'sphinx.search.ja.DefaultSplitter', - 'mecab': 'sphinx.search.ja.MecabSplitter', - 'janome': 'sphinx.search.ja.JanomeSplitter', - } def init(self, options: Dict) -> None: - type = options.get('type', 'sphinx.search.ja.DefaultSplitter') - if type in self.splitters: - dotted_path = self.splitters[type] - warnings.warn('html_search_options["type"]: %s is deprecated. ' - 'Please give "%s" instead.' % (type, dotted_path), - RemovedInSphinx30Warning, stacklevel=2) - else: - dotted_path = type + dotted_path = options.get('type', 'sphinx.search.ja.DefaultSplitter') try: self.splitter = import_object(dotted_path)(options) except ExtensionError: diff --git a/sphinx/templates/apidoc/package.rst_t b/sphinx/templates/apidoc/package.rst_t index ed9f669ea..8630a87b7 100644 --- a/sphinx/templates/apidoc/package.rst_t +++ b/sphinx/templates/apidoc/package.rst_t @@ -7,6 +7,7 @@ {%- macro toctree(docnames) -%} .. toctree:: + :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} diff --git a/sphinx/templates/latex/longtable.tex_t b/sphinx/templates/latex/longtable.tex_t index f9e8802e4..8d4cd748c 100644 --- a/sphinx/templates/latex/longtable.tex_t +++ b/sphinx/templates/latex/longtable.tex_t @@ -26,7 +26,7 @@ \endhead \hline -\multicolumn{<%= table.colcount %>}{r}{\makebox[0pt][r]{\sphinxtablecontinued{<%= _('Continued on next page') %>}}}\\ +\multicolumn{<%= table.colcount %>}{r}{\makebox[0pt][r]{\sphinxtablecontinued{<%= _('continues on next page') %>}}}\\ \endfoot \endlastfoot diff --git a/sphinx/testing/comparer.py b/sphinx/testing/comparer.py index 0bcd194a8..ddd818394 100644 --- a/sphinx/testing/comparer.py +++ b/sphinx/testing/comparer.py @@ -9,7 +9,7 @@ """ import difflib import pathlib -from typing import List, Union +from typing import Any, List, Union class PathComparer: @@ -38,13 +38,13 @@ class PathComparer: """ self.path = pathlib.Path(path) - def __str__(self): + def __str__(self) -> str: return self.path.as_posix() - def __repr__(self): + def __repr__(self) -> str: return "<{0.__class__.__name__}: '{0}'>".format(self) - def __eq__(self, other): + def __eq__(self, other: Union[str, pathlib.Path]) -> bool: # type: ignore return not bool(self.ldiff(other)) def diff(self, other: Union[str, pathlib.Path]) -> List[str]: @@ -94,8 +94,10 @@ class PathComparer: return [line.strip() for line in difflib.Differ().compare([s_path], [o_path])] -def pytest_assertrepr_compare(op, left, right): +def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> List[str]: if isinstance(left, PathComparer) and op == "==": return ['Comparing path:'] + left.ldiff(right) - if isinstance(right, PathComparer) and op == "==": + elif isinstance(right, PathComparer) and op == "==": return ['Comparing path:'] + right.rdiff(left) + else: + return [] diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index c6dd7ecdf..eec3b4208 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -14,20 +14,44 @@ import sys from collections import namedtuple from io import StringIO from subprocess import PIPE -from typing import Any, Dict +from typing import Any, Callable, Dict, Generator, Tuple import pytest -from . import util +from sphinx.testing import util +from sphinx.testing.util import SphinxTestApp, SphinxTestAppWrapperForSkipBuilding @pytest.fixture(scope='session') -def rootdir() -> None: +def rootdir() -> str: return None +class SharedResult: + cache = {} # type: Dict[str, Dict[str, str]] + + def store(self, key: str, app_: SphinxTestApp) -> Any: + if key in self.cache: + return + data = { + 'status': app_._status.getvalue(), + 'warning': app_._warning.getvalue(), + } + self.cache[key] = data + + def restore(self, key: str) -> Dict[str, StringIO]: + if key not in self.cache: + return {} + data = self.cache[key] + return { + 'status': StringIO(data['status']), + 'warning': StringIO(data['warning']), + } + + @pytest.fixture -def app_params(request, test_params, shared_result, sphinx_test_tempdir, rootdir): +def app_params(request: Any, test_params: Dict, shared_result: SharedResult, + sphinx_test_tempdir: str, rootdir: str) -> Tuple[Dict, Dict]: """ parameters that is specified by 'pytest.mark.sphinx' for sphinx.application.Sphinx initialization @@ -40,7 +64,7 @@ def app_params(request, test_params, shared_result, sphinx_test_tempdir, rootdir else: markers = request.node.get_marker("sphinx") pargs = {} - kwargs = {} # type: Dict[str, str] + kwargs = {} # type: Dict[str, Any] if markers is not None: # to avoid stacking positional args @@ -75,7 +99,7 @@ def app_params(request, test_params, shared_result, sphinx_test_tempdir, rootdir @pytest.fixture -def test_params(request): +def test_params(request: Any) -> Dict: """ test parameters that is specified by 'pytest.mark.test_params' @@ -102,7 +126,8 @@ def test_params(request): @pytest.fixture(scope='function') -def app(test_params, app_params, make_app, shared_result): +def app(test_params: Dict, app_params: Tuple[Dict, Dict], make_app: Callable, + shared_result: SharedResult) -> Generator[SphinxTestApp, None, None]: """ provides sphinx.application.Sphinx object """ @@ -122,7 +147,7 @@ def app(test_params, app_params, make_app, shared_result): @pytest.fixture(scope='function') -def status(app): +def status(app: SphinxTestApp) -> StringIO: """ compat for testing with previous @with_app decorator """ @@ -130,7 +155,7 @@ def status(app): @pytest.fixture(scope='function') -def warning(app): +def warning(app: SphinxTestApp) -> StringIO: """ compat for testing with previous @with_app decorator """ @@ -138,7 +163,7 @@ def warning(app): @pytest.fixture() -def make_app(test_params, monkeypatch): +def make_app(test_params: Dict, monkeypatch: Any) -> Generator[Callable, None, None]: """ provides make_app function to initialize SphinxTestApp instance. if you want to initialize 'app' in your test function. please use this @@ -153,10 +178,10 @@ def make_app(test_params, monkeypatch): status, warning = StringIO(), StringIO() kwargs.setdefault('status', status) kwargs.setdefault('warning', warning) - app_ = util.SphinxTestApp(*args, **kwargs) # type: Any + app_ = SphinxTestApp(*args, **kwargs) # type: Any apps.append(app_) if test_params['shared_result']: - app_ = util.SphinxTestAppWrapperForSkipBuilding(app_) + app_ = SphinxTestAppWrapperForSkipBuilding(app_) return app_ yield make @@ -165,40 +190,18 @@ def make_app(test_params, monkeypatch): app_.cleanup() -class SharedResult: - cache = {} # type: Dict[str, Dict[str, str]] - - def store(self, key, app_): - if key in self.cache: - return - data = { - 'status': app_._status.getvalue(), - 'warning': app_._warning.getvalue(), - } - self.cache[key] = data - - def restore(self, key): - if key not in self.cache: - return {} - data = self.cache[key] - return { - 'status': StringIO(data['status']), - 'warning': StringIO(data['warning']), - } - - @pytest.fixture -def shared_result(): +def shared_result() -> SharedResult: return SharedResult() @pytest.fixture(scope='module', autouse=True) -def _shared_result_cache(): +def _shared_result_cache() -> None: SharedResult.cache.clear() @pytest.fixture -def if_graphviz_found(app): +def if_graphviz_found(app: SphinxTestApp) -> None: """ The test will be skipped when using 'if_graphviz_found' fixture and graphviz dot command is not found. @@ -215,7 +218,7 @@ def if_graphviz_found(app): @pytest.fixture(scope='session') -def sphinx_test_tempdir(tmpdir_factory): +def sphinx_test_tempdir(tmpdir_factory: Any) -> "util.path": """ temporary directory that wrapped with `path` class. """ @@ -227,7 +230,7 @@ def sphinx_test_tempdir(tmpdir_factory): @pytest.fixture -def tempdir(tmpdir): +def tempdir(tmpdir: str) -> "util.path": """ temporary directory that wrapped with `path` class. this fixture is for compat with old test implementation. diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 1c883af2f..4c3702e3d 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -10,8 +10,11 @@ import builtins import os import shutil import sys +import warnings from typing import Any, Callable, IO, List +from sphinx.deprecation import RemovedInSphinx50Warning + FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() @@ -138,6 +141,14 @@ class path(str): """ Returns the text in the file. """ + warnings.warn('Path.text() is deprecated. Please use read_text() instead.', + RemovedInSphinx50Warning, stacklevel=2) + return self.read_text(encoding, **kwargs) + + def read_text(self, encoding: str = 'utf-8', **kwargs: Any) -> str: + """ + Returns the text in the file. + """ with open(self, encoding=encoding, **kwargs) as f: return f.read() @@ -145,6 +156,14 @@ class path(str): """ Returns the bytes in the file. """ + warnings.warn('Path.bytes() is deprecated. Please use read_bytes() instead.', + RemovedInSphinx50Warning, stacklevel=2) + return self.read_bytes() + + def read_bytes(self) -> builtins.bytes: + """ + Returns the bytes in the file. + """ with open(self, mode='rb') as f: return f.read() diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index f4ef35d61..450241f55 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -11,10 +11,12 @@ import os import re import sys import warnings +from io import StringIO from typing import Any, Dict, Generator, IO, List, Pattern from xml.etree import ElementTree from docutils import nodes +from docutils.nodes import Node from docutils.parsers.rst import directives, roles from sphinx import application, locale @@ -47,7 +49,7 @@ def assert_startswith(thing: str, prefix: str) -> None: assert False, '%r does not start with %r' % (thing, prefix) -def assert_node(node: nodes.Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> None: +def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> None: if cls: if isinstance(cls, list): assert_node(node, cls[0], xpath=xpath, **kwargs) @@ -61,7 +63,7 @@ def assert_node(node: nodes.Node, cls: Any = None, xpath: str = "", **kwargs: An 'The node%s has %d child nodes, not one' % (xpath, len(node)) assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs) elif isinstance(cls, tuple): - assert isinstance(node, nodes.Element), \ + assert isinstance(node, (list, nodes.Element)), \ 'The node%s does not have any items' % xpath assert len(node) == len(cls), \ 'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls)) @@ -101,6 +103,8 @@ class SphinxTestApp(application.Sphinx): A subclass of :class:`Sphinx` that runs on the test root, with some better default values for the initialization parameters. """ + _status = None # type: StringIO + _warning = None # type: StringIO def __init__(self, buildername: str = 'html', srcdir: path = None, freshenv: bool = False, confoverrides: Dict = None, status: IO = None, warning: IO = None, diff --git a/sphinx/texinputs/Makefile_t b/sphinx/texinputs/Makefile_t index 08ddd2f0a..96bb0fed1 100644 --- a/sphinx/texinputs/Makefile_t +++ b/sphinx/texinputs/Makefile_t @@ -10,7 +10,6 @@ ALLDVI = $(addsuffix .dvi,$(ALLDOCS)) ALLXDV = {% endif -%} ALLPS = $(addsuffix .ps,$(ALLDOCS)) -ALLIMGS = $(wildcard *.png *.gif *.jpg *.jpeg) # Prefix for archive names ARCHIVEPREFIX = @@ -46,15 +45,7 @@ LATEX = latexmk -dvi PDFLATEX = latexmk -pdf -dvi- -ps- {% endif %} -%.png %.gif %.jpg %.jpeg: FORCE_MAKE - extractbb '$@' - -{% if latex_engine in ('platex', 'uplatex') -%} -%.dvi: %.tex $(ALLIMGS) FORCE_MAKE - for f in *.pdf; do extractbb "$$f"; done - $(LATEX) $(LATEXMKOPTS) '$<' - -{% elif latex_engine != 'xelatex' -%} +{% if latex_engine != 'xelatex' -%} %.dvi: %.tex FORCE_MAKE $(LATEX) $(LATEXMKOPTS) '$<' @@ -62,12 +53,7 @@ PDFLATEX = latexmk -pdf -dvi- -ps- %.ps: %.dvi dvips '$<' -{% if latex_engine in ('platex', 'uplatex') -%} -%.pdf: %.tex $(ALLIMGS) FORCE_MAKE - for f in *.pdf; do extractbb "$$f"; done -{%- else -%} %.pdf: %.tex FORCE_MAKE -{%- endif %} $(PDFLATEX) $(LATEXMKOPTS) '$<' all: $(ALLPDF) diff --git a/sphinx/texinputs/make.bat_t b/sphinx/texinputs/make.bat_t index 83dd449c2..9dfa38c13 100644 --- a/sphinx/texinputs/make.bat_t +++ b/sphinx/texinputs/make.bat_t @@ -31,11 +31,6 @@ if "%1" == "" goto all-pdf if "%1" == "all-pdf" ( :all-pdf -{%- if latex_engine == 'platex' %} - for %%i in (*.png *.gif *.jpg *.jpeg *.pdf) do ( - extractbb %%i - ) -{%- endif %} for %%i in (*.tex) do ( %PDFLATEX% %LATEXMKOPTS% %%i ) diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 714d98e05..63676d46d 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -1837,6 +1837,7 @@ \protected\def\sphinxtitleref#1{\emph{#1}} \protected\def\sphinxmenuselection#1{\emph{#1}} \protected\def\sphinxguilabel#1{\emph{#1}} +\protected\def\sphinxkeyboard#1{\sphinxcode{#1}} \protected\def\sphinxaccelerator#1{\underline{#1}} \protected\def\sphinxcrossref#1{\emph{#1}} \protected\def\sphinxtermref#1{\emph{#1}} diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 02ee85abc..2673369f2 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -27,10 +27,8 @@ </p> </div> <p> - {% trans %}From here you can search these documents. Enter your search - words into the box below and click "search". Note that the search - function will automatically search for all of the words. Pages - containing fewer words won't appear in the result list.{% endtrans %} + {% trans %}Searching for multiple words only shows matches that contain + all words.{% endtrans %} </p> <form action="" method="get"> <input type="text" name="q" aria-labelledby="search-documentation" value="" /> diff --git a/sphinx/themes/basic/searchresults.html b/sphinx/themes/basic/searchresults.html deleted file mode 100644 index 50700c158..000000000 --- a/sphinx/themes/basic/searchresults.html +++ /dev/null @@ -1,36 +0,0 @@ -{# - basic/searchresults.html - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Template for the body of the search results page. - - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -<h1 id="search-documentation">{{ _('Search') }}</h1> -<p> - From here you can search these documents. Enter your search - words into the box below and click "search". -</p> -<form action="" method="get"> - <input type="text" name="q" value="" /> - <input type="submit" value="{{ _('search') }}" /> - <span id="search-progress" style="padding-left: 10px"></span> -</form> -{%- if search_performed %} - <h2>{{ _('Search Results') }}</h2> - {%- if not search_results %} - <p>{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}</p> - {%- endif %} -{%- endif %} -<div id="search-results"> - {%- if search_results %} - <ul class="search"> - {% for href, caption, context in search_results %} - <li><a href="{{ docroot }}{{ href }}/?highlight={{ q }}">{{ caption }}</a> - <div class="context">{{ context|e }}</div> - </li> - {% endfor %} - </ul> - {%- endif %} -</div> diff --git a/sphinx/themes/basic/static/documentation_options.js_t b/sphinx/themes/basic/static/documentation_options.js_t index 059e8871c..8afaac2f8 100644 --- a/sphinx/themes/basic/static/documentation_options.js_t +++ b/sphinx/themes/basic/static/documentation_options.js_t @@ -5,6 +5,7 @@ var DOCUMENTATION_OPTIONS = { COLLAPSE_INDEX: false, BUILDER: '{{ builder }}', FILE_SUFFIX: '{{ file_suffix }}', + LINK_SUFFIX: '{{ link_suffix }}', HAS_SOURCE: {{ has_source|lower }}, SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}', NAVIGATION_WITH_KEYS: {{ 'true' if theme_navigation_with_keys|tobool else 'false'}} diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index d11b33a78..ab5649965 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -251,6 +251,7 @@ var Search = { var item = results.pop(); var listItem = $('<li style="display:none"></li>'); var requestUrl = ""; + var linkUrl = ""; if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') { // dirhtml builder var dirname = item[0] + '/'; @@ -260,13 +261,15 @@ var Search = { dirname = ''; } requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname; + linkUrl = requestUrl; } else { // normal html builders requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX; + linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX; } listItem.append($('<a/>').attr('href', - requestUrl + + linkUrl + highlightstring + item[2]).html(item[1])); if (item[3]) { listItem.append($('<span> (' + item[3] + ')</span>')); diff --git a/sphinx/themes/bizstyle/static/bizstyle.css_t b/sphinx/themes/bizstyle/static/bizstyle.css_t index b1ae49689..b13395b4f 100644 --- a/sphinx/themes/bizstyle/static/bizstyle.css_t +++ b/sphinx/themes/bizstyle/static/bizstyle.css_t @@ -51,9 +51,13 @@ div.body { {%- if theme_rightsidebar|tobool %} div.bodywrapper { - margin: 0 240px 0 0; + margin: 0 calc({{ theme_sidebarwidth|todim }} + 30px) 0 0; border-right: 1px solid #ccc; } +{%- else %} +div.bodywrapper { + margin: 0 0 0 calc({{ theme_sidebarwidth|todim }} + 30px); +} {%- endif %} div.related { @@ -103,7 +107,7 @@ div.sphinxsidebarwrapper { div.sphinxsidebar { margin: 0; padding: 0.5em 12px 12px 12px; - width: 210px; + width: {{ theme_sidebarwidth|todim }}; {%- if theme_rightsidebar|tobool %} float: right; {%- endif %} diff --git a/sphinx/themes/bizstyle/theme.conf b/sphinx/themes/bizstyle/theme.conf index 724eb1270..3c21b0fe0 100644 --- a/sphinx/themes/bizstyle/theme.conf +++ b/sphinx/themes/bizstyle/theme.conf @@ -5,5 +5,6 @@ pygments_style = friendly [options] rightsidebar = false +sidebarwidth = 210 maincolor = #336699 diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index ee459cc56..48f5dc248 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Type from typing import cast from docutils import nodes @@ -22,6 +22,7 @@ from sphinx.errors import NoUri from sphinx.locale import __ from sphinx.transforms import SphinxTransform from sphinx.util import logging +from sphinx.util.docutils import SphinxTranslator from sphinx.util.nodes import process_only_nodes @@ -131,7 +132,7 @@ class ReferencesResolver(SphinxPostTransform): if not results: return None if len(results) > 1: - def stringify(name, node): + def stringify(name: str, node: Element) -> str: reftitle = node.get('reftitle', node.astext()) return ':%s:`%s`' % (name, reftitle) candidates = ' or '.join(stringify(name, role) for name, role in results) @@ -186,9 +187,41 @@ class OnlyNodeTransform(SphinxPostTransform): process_only_nodes(self.document, self.app.builder.tags) +class SigElementFallbackTransform(SphinxPostTransform): + """Fallback desc_sig_element nodes to inline if translator does not supported them.""" + default_priority = 200 + + SIG_ELEMENTS = [addnodes.desc_sig_name, + addnodes.desc_sig_operator, + addnodes.desc_sig_punctuation] + + def run(self, **kwargs: Any) -> None: + def has_visitor(translator: Type[nodes.NodeVisitor], node: Type[Element]) -> bool: + return hasattr(translator, "visit_%s" % node.__name__) + + translator = self.app.builder.get_translator_class() + if isinstance(translator, SphinxTranslator): + # subclass of SphinxTranslator supports desc_sig_element nodes automatically. + return + + if all(has_visitor(translator, node) for node in self.SIG_ELEMENTS): + # the translator supports all desc_sig_element nodes + return + else: + self.fallback() + + def fallback(self): + for node in self.document.traverse(addnodes.desc_sig_element): + newnode = nodes.inline() + newnode.update_all_atts(node) + newnode.extend(node) + node.replace_self(newnode) + + def setup(app: Sphinx) -> Dict[str, Any]: app.add_post_transform(ReferencesResolver) app.add_post_transform(OnlyNodeTransform) + app.add_post_transform(SigElementFallbackTransform) return { 'version': 'builtin', diff --git a/sphinx/transforms/post_transforms/compat.py b/sphinx/transforms/post_transforms/compat.py deleted file mode 100644 index c71191dec..000000000 --- a/sphinx/transforms/post_transforms/compat.py +++ /dev/null @@ -1,86 +0,0 @@ -""" - sphinx.transforms.post_transforms.compat - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Post transforms for compatibility - - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import warnings -from typing import Any, Dict - -from docutils import nodes -from docutils.writers.docutils_xml import XMLTranslator - -from sphinx.addnodes import math_block, displaymath -from sphinx.application import Sphinx -from sphinx.deprecation import RemovedInSphinx30Warning -from sphinx.transforms import SphinxTransform -from sphinx.util import logging - - -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: 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: Sphinx) -> Dict[str, Any]: - app.add_post_transform(MathNodeMigrator) - - return { - 'version': 'builtin', - 'parallel_read_safe': True, - 'parallel_write_safe': True, - } diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 262837b50..12ae051f1 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -28,16 +28,13 @@ from time import mktime, strptime from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Set, Tuple 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, FiletypeNotFoundError ) 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.typing import PathMatcher from sphinx.util import smartypants # noqa @@ -45,7 +42,7 @@ from sphinx.util import smartypants # noqa # 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) @@ -56,7 +53,7 @@ if False: # For type annotation from typing import Type # for python3.5.1 from sphinx.application import Sphinx - from sphinx.builders import Builder + logger = logging.getLogger(__name__) @@ -201,35 +198,6 @@ class DownloadFiles(dict): self.add_file(docname, filename) -def copy_static_entry(source: str, targetdir: str, builder: "Builder", context: Dict = {}, - exclude_matchers: Tuple[PathMatcher, ...] = (), level: int = 0) -> 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) @@ -681,7 +649,7 @@ class progress_message: def __call__(self, f: Callable) -> Callable: @functools.wraps(f) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: with self: return f(*args, **kwargs) diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py new file mode 100644 index 000000000..516221cf9 --- /dev/null +++ b/sphinx/util/cfamily.py @@ -0,0 +1,250 @@ +""" + sphinx.util.cfamily + ~~~~~~~~~~~~~~~~~~~ + + Utility functions common to the C and C++ domains. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +import warnings +from copy import deepcopy +from typing import ( + Any, Callable, List, Match, Pattern, Tuple, Union +) + +from docutils import nodes + +from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.util import logging + +logger = logging.getLogger(__name__) + +StringifyTransform = Callable[[Any], str] + + +_whitespace_re = re.compile(r'(?u)\s+') +anon_identifier_re = re.compile(r'(@[a-zA-Z0-9_])[a-zA-Z0-9_]*\b') +identifier_re = re.compile(r'''(?x) + ( # This 'extends' _anon_identifier_re with the ordinary identifiers, + # make sure they are in sync. + (~?\b[a-zA-Z_]) # ordinary identifiers + | (@[a-zA-Z0-9_]) # our extension for names of anonymous entities + ) + [a-zA-Z0-9_]*\b +''') +integer_literal_re = re.compile(r'[1-9][0-9]*') +octal_literal_re = re.compile(r'0[0-7]*') +hex_literal_re = re.compile(r'0[xX][0-9a-fA-F][0-9a-fA-F]*') +binary_literal_re = re.compile(r'0[bB][01][01]*') +float_literal_re = re.compile(r'''(?x) + [+-]?( + # decimal + ([0-9]+[eE][+-]?[0-9]+) + | ([0-9]*\.[0-9]+([eE][+-]?[0-9]+)?) + | ([0-9]+\.([eE][+-]?[0-9]+)?) + # hex + | (0[xX][0-9a-fA-F]+[pP][+-]?[0-9a-fA-F]+) + | (0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+([pP][+-]?[0-9a-fA-F]+)?) + | (0[xX][0-9a-fA-F]+\.([pP][+-]?[0-9a-fA-F]+)?) + ) +''') +char_literal_re = re.compile(r'''(?x) + ((?:u8)|u|U|L)? + '( + (?:[^\\']) + | (\\( + (?:['"?\\abfnrtv]) + | (?:[0-7]{1,3}) + | (?:x[0-9a-fA-F]{2}) + | (?:u[0-9a-fA-F]{4}) + | (?:U[0-9a-fA-F]{8}) + )) + )' +''') + + +def verify_description_mode(mode: str) -> None: + if mode not in ('lastIsName', 'noneIsName', 'markType', 'markName', 'param'): + raise Exception("Description mode '%s' is invalid." % mode) + + +class NoOldIdError(Exception): + # Used to avoid implementing unneeded id generation for old id schemes. + @property + def description(self) -> str: + warnings.warn('%s.description is deprecated. ' + 'Coerce the instance to a string instead.' % self.__class__.__name__, + RemovedInSphinx40Warning, stacklevel=2) + return str(self) + + +class ASTBaseBase: + def __eq__(self, other: Any) -> bool: + if type(self) is not type(other): + return False + try: + for key, value in self.__dict__.items(): + if value != getattr(other, key): + return False + except AttributeError: + return False + return True + + __hash__ = None # type: Callable[[], int] + + def clone(self) -> Any: + """Clone a definition expression node.""" + return deepcopy(self) + + def _stringify(self, transform: StringifyTransform) -> str: + raise NotImplementedError(repr(self)) + + def __str__(self) -> str: + return self._stringify(lambda ast: str(ast)) + + def get_display_string(self) -> str: + return self._stringify(lambda ast: ast.get_display_string()) + + def __repr__(self) -> str: + return '<%s>' % self.__class__.__name__ + + +class UnsupportedMultiCharacterCharLiteral(Exception): + @property + def decoded(self) -> str: + warnings.warn('%s.decoded is deprecated. ' + 'Coerce the instance to a string instead.' % self.__class__.__name__, + RemovedInSphinx40Warning, stacklevel=2) + return str(self) + + +class DefinitionError(Exception): + @property + def description(self) -> str: + warnings.warn('%s.description is deprecated. ' + 'Coerce the instance to a string instead.' % self.__class__.__name__, + RemovedInSphinx40Warning, stacklevel=2) + return str(self) + + +class BaseParser: + def __init__(self, definition: str, *, + location: Union[nodes.Node, Tuple[str, int]]) -> None: + self.definition = definition.strip() + self.location = location # for warnings + + self.pos = 0 + self.end = len(self.definition) + self.last_match = None # type: Match + self._previous_state = (0, None) # type: Tuple[int, Match] + self.otherErrors = [] # type: List[DefinitionError] + + # in our tests the following is set to False to capture bad parsing + self.allowFallbackExpressionParsing = True + + def _make_multi_error(self, errors: List[Any], header: str) -> DefinitionError: + if len(errors) == 1: + if len(header) > 0: + return DefinitionError(header + '\n' + str(errors[0][0])) + else: + return DefinitionError(str(errors[0][0])) + result = [header, '\n'] + for e in errors: + if len(e[1]) > 0: + ident = ' ' + result.append(e[1]) + result.append(':\n') + for line in str(e[0]).split('\n'): + if len(line) == 0: + continue + result.append(ident) + result.append(line) + result.append('\n') + else: + result.append(str(e[0])) + return DefinitionError(''.join(result)) + + def status(self, msg: str) -> None: + # for debugging + indicator = '-' * self.pos + '^' + print("%s\n%s\n%s" % (msg, self.definition, indicator)) + + def fail(self, msg: str) -> None: + errors = [] + indicator = '-' * self.pos + '^' + exMain = DefinitionError( + 'Invalid definition: %s [error at %d]\n %s\n %s' % + (msg, self.pos, self.definition, indicator)) + errors.append((exMain, "Main error")) + for err in self.otherErrors: + errors.append((err, "Potential other error")) + self.otherErrors = [] + raise self._make_multi_error(errors, '') + + def warn(self, msg: str) -> None: + logger.warning(msg, location=self.location) + + def match(self, regex: Pattern) -> bool: + match = regex.match(self.definition, self.pos) + if match is not None: + self._previous_state = (self.pos, self.last_match) + self.pos = match.end() + self.last_match = match + return True + return False + + def skip_string(self, string: str) -> bool: + strlen = len(string) + if self.definition[self.pos:self.pos + strlen] == string: + self.pos += strlen + return True + return False + + def skip_word(self, word: str) -> bool: + return self.match(re.compile(r'\b%s\b' % re.escape(word))) + + def skip_ws(self) -> bool: + return self.match(_whitespace_re) + + def skip_word_and_ws(self, word: str) -> bool: + if self.skip_word(word): + self.skip_ws() + return True + return False + + def skip_string_and_ws(self, string: str) -> bool: + if self.skip_string(string): + self.skip_ws() + return True + return False + + @property + def eof(self) -> bool: + return self.pos >= self.end + + @property + def current_char(self) -> str: + try: + return self.definition[self.pos] + except IndexError: + return 'EOF' + + @property + def matched_text(self) -> str: + if self.last_match is not None: + return self.last_match.group() + else: + return None + + def read_rest(self) -> str: + rv = self.definition[self.pos:] + self.pos = self.end + return rv + + def assert_end(self) -> None: + self.skip_ws() + if not self.eof: + self.fail('Expected end of definition.') diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py index ba3c4d27c..0135899eb 100644 --- a/sphinx/util/compat.py +++ b/sphinx/util/compat.py @@ -15,27 +15,14 @@ from typing import Any, Dict from docutils.utils import get_source_line from sphinx import addnodes -from sphinx.config import Config -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 from sphinx.application import Sphinx -def deprecate_source_parsers(app: "Sphinx", config: Config) -> None: - if config.source_parsers: - warnings.warn('The config variable "source_parsers" is deprecated. ' - 'Please update your extension for the parser and remove the setting.', - 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: "Sphinx") -> None: """Register application object to autosummary module. @@ -65,7 +52,6 @@ class IndexEntriesMigrator(SphinxTransform): def setup(app: "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/console.py b/sphinx/util/console.py index 08aad8ab1..98563f58e 100644 --- a/sphinx/util/console.py +++ b/sphinx/util/console.py @@ -132,9 +132,9 @@ _colors = [ ('lightgray', 'white'), ] -for i, (dark, light) in enumerate(_colors): - codes[dark] = '\x1b[%im' % (i + 30) - codes[light] = '\x1b[%i;01m' % (i + 30) +for i, (dark, light) in enumerate(_colors, 30): + codes[dark] = '\x1b[%im' % i + codes[light] = '\x1b[%im' % (i + 60) _orig_codes = codes.copy() diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 8854a1f98..7b3f011d1 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -8,8 +8,38 @@ :license: BSD, see LICENSE for details. """ +import re import sys -from typing import List +from typing import Dict, List + +from docutils.parsers.rst.states import Body + + +field_list_item_re = re.compile(Body.patterns['field_marker']) + + +def extract_metadata(s: str) -> Dict[str, str]: + """Extract metadata from docstring.""" + in_other_element = False + metadata = {} # type: Dict[str, str] + + if not s: + return metadata + + for line in prepare_docstring(s): + if line.strip() == '': + in_other_element = False + else: + matched = field_list_item_re.match(line) + if matched and not in_other_element: + field_name = matched.group()[1:].split(':', 1)[0] + if field_name.startswith('meta '): + name = field_name[5:].strip() + metadata[name] = line[matched.end():].strip() + else: + in_other_element = True + + return metadata def prepare_docstring(s: str, ignore: int = 1, tabsize: int = 8) -> List[str]: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index ed93939a7..7069189e3 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 @@ -24,14 +22,12 @@ import docutils from docutils import nodes from docutils.io import FileOutput from docutils.nodes import Element, Node, system_message -from docutils.parsers.rst import Directive, directives, roles, convert_directive_function +from docutils.parsers.rst import Directive, directives, roles from docutils.parsers.rst.states import Inliner from docutils.statemachine import StateMachine, State, StringList 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 from sphinx.util.typing import RoleFunction @@ -279,25 +275,6 @@ def is_html5_writer_available() -> bool: return __version_info__ > (0, 13, 0) -def directive_helper(obj: Any, has_content: bool = None, - argument_spec: Tuple[int, int, bool] = None, **option_spec: 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: State, content: StringList) -> Generator[None, None, None]: """Switch current source input of state temporarily.""" @@ -353,9 +330,13 @@ class SphinxDirective(Directive): """Reference to the :class:`.Config` object.""" return self.env.config + def get_source_info(self) -> Tuple[str, int]: + """Get source and line number.""" + return self.state_machine.get_source_and_line(self.lineno) + def set_source_info(self, node: Node) -> None: """Set source and line number to the node.""" - node.source, node.line = self.state_machine.get_source_and_line(self.lineno) + node.source, node.line = self.get_source_info() class SphinxRole: @@ -411,12 +392,13 @@ class SphinxRole: """Reference to the :class:`.Config` object.""" return self.env.config - def set_source_info(self, node: Node, lineno: int = None) -> None: + def get_source_info(self, lineno: int = None) -> Tuple[str, int]: if lineno is None: lineno = self.lineno + return self.inliner.reporter.get_source_and_line(lineno) # type: ignore - source_info = self.inliner.reporter.get_source_and_line(lineno) # type: ignore - node.source, node.line = source_info + def set_source_info(self, node: Node, lineno: int = None) -> None: + node.source, node.line = self.get_source_info(lineno) class ReferenceRole(SphinxRole): @@ -467,7 +449,7 @@ class SphinxTranslator(nodes.NodeVisitor): self.config = builder.config self.settings = document.settings - def dispatch_visit(self, node): + def dispatch_visit(self, node: Node) -> None: """ Dispatch node to appropriate visitor method. The priority of visitor method is: @@ -479,11 +461,12 @@ class SphinxTranslator(nodes.NodeVisitor): for node_class in node.__class__.__mro__: method = getattr(self, 'visit_%s' % (node_class.__name__), None) if method: - return method(node) + method(node) + break else: super().dispatch_visit(node) - def dispatch_departure(self, node): + def dispatch_departure(self, node: Node) -> None: """ Dispatch node to appropriate departure method. The priority of departure method is: @@ -495,7 +478,8 @@ class SphinxTranslator(nodes.NodeVisitor): for node_class in node.__class__.__mro__: method = getattr(self, 'depart_%s' % (node_class.__name__), None) if method: - return method(node) + method(node) + break else: super().dispatch_departure(node) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 096a200bf..1cb75637c 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -20,7 +20,7 @@ import babel.dates from babel.messages.mofile import write_mo from babel.messages.pofile import read_po -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import SphinxError from sphinx.locale import __ from sphinx.util import logging @@ -151,9 +151,8 @@ def find_catalog_files(docname: str, srcdir: str, locale_dirs: List[str], def find_catalog_source_files(locale_dirs: List[str], locale: str, domains: List[str] = None, - gettext_compact: bool = None, charset: str = 'utf-8', - force_all: bool = False, excluded: Matcher = Matcher([]) - ) -> Set[CatalogInfo]: + charset: str = 'utf-8', force_all: bool = False, + excluded: Matcher = Matcher([])) -> Set[CatalogInfo]: """ :param list locale_dirs: list of path as `['locale_dir1', 'locale_dir2', ...]` to find @@ -169,9 +168,6 @@ def find_catalog_source_files(locale_dirs: List[str], locale: str, domains: List """ warnings.warn('find_catalog_source_files() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) - if gettext_compact is not None: - warnings.warn('gettext_compact argument for find_catalog_source_files() ' - 'is deprecated.', RemovedInSphinx30Warning, stacklevel=2) catalogs = set() # type: Set[CatalogInfo] diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 506e4a931..568682b37 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 IO, NamedTuple, Tuple import imagesize -from sphinx.deprecation import RemovedInSphinx30Warning - try: from PIL import Image except ImportError: @@ -49,12 +45,8 @@ def get_image_size(filename: str) -> Tuple[int, int]: size = (int(size[0]), int(size[1])) if size is None and Image: # fallback to Pillow - im = Image.open(filename) - size = im.size - try: - im.fp.close() - except Exception: - pass + with Image.open(filename) as im: + size = im.size return size except Exception: @@ -69,16 +61,10 @@ def guess_mimetype_for_stream(stream: IO, default: str = None) -> str: return default -def guess_mimetype(filename: str = '', content: bytes = None, default: str = None) -> str: +def guess_mimetype(filename: str = '', default: str = None) -> 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 2bcccdd60..855b11d83 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -17,12 +17,15 @@ import typing import warnings from functools import partial, partialmethod from inspect import ( # NOQA - isclass, ismethod, ismethoddescriptor, isroutine + Parameter, isclass, ismethod, ismethoddescriptor, isroutine ) from io import StringIO from typing import Any, Callable, Mapping, List, Tuple +from typing import cast -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning +from sphinx.pycode.ast import ast # for py35-37 +from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging from sphinx.util.typing import stringify as stringify_annotation @@ -51,9 +54,11 @@ memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software # Foundation; All Rights Reserved -def getargspec(func): +def getargspec(func: Callable) -> Any: """Like inspect.getfullargspec but supports bound methods, and wrapped methods.""" + warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', + RemovedInSphinx50Warning) # On 3.5+, signature(int) or similar raises ValueError. On 3.4, it # succeeds with a bogus signature. We want a TypeError uniformly, to # match historical behavior. @@ -81,19 +86,19 @@ def getargspec(func): kind = param.kind name = param.name - if kind is inspect.Parameter.POSITIONAL_ONLY: + if kind is Parameter.POSITIONAL_ONLY: args.append(name) - elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: + elif kind is Parameter.POSITIONAL_OR_KEYWORD: args.append(name) if param.default is not param.empty: defaults += (param.default,) # type: ignore - elif kind is inspect.Parameter.VAR_POSITIONAL: + elif kind is Parameter.VAR_POSITIONAL: varargs = name - elif kind is inspect.Parameter.KEYWORD_ONLY: + elif kind is Parameter.KEYWORD_ONLY: kwonlyargs.append(name) if param.default is not param.empty: kwdefaults[name] = param.default - elif kind is inspect.Parameter.VAR_KEYWORD: + elif kind is Parameter.VAR_KEYWORD: varkw = name if param.annotation is not param.empty: @@ -192,6 +197,14 @@ def isabstractmethod(obj: Any) -> bool: return safe_getattr(obj, '__isabstractmethod__', False) is True +def is_cython_function_or_method(obj: Any) -> bool: + """Check if the object is a function or method in cython.""" + try: + return obj.__class__.__name__ == 'cython_function_or_method' + except AttributeError: + return False + + def isattributedescriptor(obj: Any) -> bool: """Check if the object is an attribute like descriptor.""" if inspect.isdatadescriptor(object): @@ -202,6 +215,9 @@ def isattributedescriptor(obj: Any) -> bool: if isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj): # attribute must not be either function, builtin and method return False + elif is_cython_function_or_method(obj): + # attribute must not be either function and method (for cython) + return False elif inspect.isclass(obj): # attribute must not be a class return False @@ -219,6 +235,26 @@ def isattributedescriptor(obj: Any) -> bool: return False +def is_singledispatch_function(obj: Any) -> bool: + """Check if the object is singledispatch function.""" + if (inspect.isfunction(obj) and + hasattr(obj, 'dispatch') and + hasattr(obj, 'register') and + obj.dispatch.__module__ == 'functools'): + return True + else: + return False + + +def is_singledispatch_method(obj: Any) -> bool: + """Check if the object is singledispatch method.""" + try: + from functools import singledispatchmethod # type: ignore + return isinstance(obj, singledispatchmethod) + except ImportError: # py35-37 + return False + + def isfunction(obj: Any) -> bool: """Check if the object is function.""" return inspect.isfunction(unwrap(obj)) @@ -359,7 +395,7 @@ def signature(subject: Callable, bound_method: bool = False) -> inspect.Signatur # https://bugs.python.org/issue33009 if hasattr(subject, '_partialmethod'): parameters = [] - return_annotation = inspect.Parameter.empty + return_annotation = Parameter.empty else: raise @@ -427,11 +463,11 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, args.append(arg.getvalue()) last_kind = param.kind - if last_kind == inspect.Parameter.POSITIONAL_ONLY: + if last_kind == Parameter.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / args.append('/') - if (sig.return_annotation is inspect.Parameter.empty or + if (sig.return_annotation is Parameter.empty or show_annotation is False or show_return_annotation is False): return '(%s)' % ', '.join(args) @@ -440,24 +476,50 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, return '(%s) -> %s' % (', '.join(args), annotation) -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 signature_from_str(signature: str) -> inspect.Signature: + """Create a Signature object from string.""" + module = ast.parse('def func' + signature + ': pass') + definition = cast(ast.FunctionDef, module.body[0]) # type: ignore + + # parameters + args = definition.args + params = [] + + if hasattr(args, "posonlyargs"): + for arg in args.posonlyargs: # type: ignore + annotation = ast_unparse(arg.annotation) or Parameter.empty + params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, + annotation=annotation)) + + for i, arg in enumerate(args.args): + if len(args.args) - i <= len(args.defaults): + default = ast_unparse(args.defaults[-len(args.args) + i]) + else: + default = Parameter.empty + + annotation = ast_unparse(arg.annotation) or Parameter.empty + params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, + default=default, annotation=annotation)) + + if args.vararg: + annotation = ast_unparse(args.vararg.annotation) or Parameter.empty + params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL, + annotation=annotation)) + + for i, arg in enumerate(args.kwonlyargs): + default = ast_unparse(args.kw_defaults[i]) + annotation = ast_unparse(arg.annotation) or Parameter.empty + params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default, + annotation=annotation)) + + if args.kwarg: + annotation = ast_unparse(args.kwarg.annotation) or Parameter.empty + params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD, + annotation=annotation)) - def __init__(self, name: str, kind: int = POSITIONAL_OR_KEYWORD, - default: Any = empty) -> None: - self.name = name - self.kind = kind - self.default = default - self.annotation = self.empty + return_annotation = ast_unparse(definition.returns) or Parameter.empty - warnings.warn('sphinx.util.inspect.Parameter is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) + return inspect.Signature(params, return_annotation=return_annotation) class Signature: @@ -528,12 +590,12 @@ class Signature: if self.has_retval: return self.signature.return_annotation else: - return inspect.Parameter.empty + return Parameter.empty else: return None def format_args(self, show_annotation: bool = True) -> str: - def get_annotation(param: inspect.Parameter) -> Any: + def get_annotation(param: Parameter) -> Any: if isinstance(param.annotation, str) and param.name in self.annotations: return self.annotations[param.name] else: @@ -585,7 +647,7 @@ class Signature: args.append(arg.getvalue()) last_kind = param.kind - if self.return_annotation is inspect.Parameter.empty or show_annotation is False: + if self.return_annotation is Parameter.empty or show_annotation is False: return '(%s)' % ', '.join(args) else: if 'return' in self.annotations: diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index fb2ec2900..fbf161ec0 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -118,6 +118,7 @@ class SphinxWarningLogRecord(SphinxLogRecord): class SphinxLoggerAdapter(logging.LoggerAdapter): """LoggerAdapter allowing ``type`` and ``subtype`` keywords.""" + KEYWORDS = ['type', 'subtype', 'location', 'nonl', 'color', 'once'] def log(self, level: Union[int, str], msg: str, *args: Any, **kwargs: Any) -> None: if isinstance(level, int): @@ -131,16 +132,9 @@ class SphinxLoggerAdapter(logging.LoggerAdapter): def process(self, msg: str, kwargs: Dict) -> Tuple[str, Dict]: # type: ignore extra = kwargs.setdefault('extra', {}) - if 'type' in kwargs: - extra['type'] = kwargs.pop('type') - if 'subtype' in kwargs: - extra['subtype'] = kwargs.pop('subtype') - if 'location' in kwargs: - extra['location'] = kwargs.pop('location') - if 'nonl' in kwargs: - extra['nonl'] = kwargs.pop('nonl') - if 'color' in kwargs: - extra['color'] = kwargs.pop('color') + for keyword in self.KEYWORDS: + if keyword in kwargs: + extra[keyword] = kwargs.pop(keyword) return msg, kwargs @@ -220,16 +214,15 @@ def pending_warnings() -> Generator[logging.Handler, None, None]: @contextmanager -def pending_logging() -> Generator[MemoryHandler, None, None]: - """Contextmanager to pend logging all logs temporary. +def suppress_logging() -> Generator[MemoryHandler, None, None]: + """Contextmanager to suppress logging all logs temporary. For example:: - >>> with pending_logging(): - >>> logger.warning('Warning message!') # not flushed yet + >>> with suppress_logging(): + >>> logger.warning('Warning message!') # suppressed >>> some_long_process() >>> - Warning message! # the warning is flushed here """ logger = logging.getLogger(NAMESPACE) memhandler = MemoryHandler() @@ -248,6 +241,24 @@ def pending_logging() -> Generator[MemoryHandler, None, None]: for handler in handlers: logger.addHandler(handler) + +@contextmanager +def pending_logging() -> Generator[MemoryHandler, None, None]: + """Contextmanager to pend logging all logs temporary. + + For example:: + + >>> with pending_logging(): + >>> logger.warning('Warning message!') # not flushed yet + >>> some_long_process() + >>> + Warning message! # the warning is flushed here + """ + logger = logging.getLogger(NAMESPACE) + try: + with suppress_logging() as memhandler: + yield memhandler + finally: memhandler.flushTo(logger) @@ -429,6 +440,26 @@ class MessagePrefixFilter(logging.Filter): return True +class OnceFilter(logging.Filter): + """Show the message only once.""" + + def __init__(self, name: str = '') -> None: + super().__init__(name) + self.messages = {} # type: Dict[str, List] + + def filter(self, record: logging.LogRecord) -> bool: + once = getattr(record, 'once', '') + if not once: + return True + else: + params = self.messages.setdefault(record.msg, []) + if record.args in params: + return False + + params.append(record.args) + return True + + class SphinxLogRecordTranslator(logging.Filter): """Converts a log record to one Sphinx expects @@ -546,6 +577,7 @@ def setup(app: "Sphinx", status: IO, warning: IO) -> None: warning_handler.addFilter(WarningSuppressor(app)) warning_handler.addFilter(WarningLogRecordTranslator(app)) warning_handler.addFilter(WarningIsErrorFilter(app)) + warning_handler.addFilter(OnceFilter()) warning_handler.setLevel(logging.WARNING) warning_handler.setFormatter(ColorizeFormatter()) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index a5ab83f5d..b39a7ca01 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -9,6 +9,7 @@ """ import re +import unicodedata import warnings from typing import Any, Callable, Iterable, List, Set, Tuple from typing import cast @@ -436,6 +437,79 @@ def inline_all_toctrees(builder: "Builder", docnameset: Set[str], docname: str, return tree +def _make_id(string: str) -> str: + """Convert `string` into an identifier and return it. + + This function is a modified version of ``docutils.nodes.make_id()`` of + docutils-0.16. + + Changes: + + * Allow to use dots (".") and underscores ("_") for an identifier + without a leading character. + + # Author: David Goodger <goodger@python.org> + # Maintainer: docutils-develop@lists.sourceforge.net + # Copyright: This module has been placed in the public domain. + """ + id = string.lower() + id = id.translate(_non_id_translate_digraphs) + id = id.translate(_non_id_translate) + # get rid of non-ascii characters. + # 'ascii' lowercase to prevent problems with turkish locale. + id = unicodedata.normalize('NFKD', id).encode('ascii', 'ignore').decode('ascii') + # shrink runs of whitespace and replace by hyphen + id = _non_id_chars.sub('-', ' '.join(id.split())) + id = _non_id_at_ends.sub('', id) + return str(id) + + +_non_id_chars = re.compile('[^a-z0-9._]+') +_non_id_at_ends = re.compile('^[-0-9._]+|-+$') +_non_id_translate = { + 0x00f8: u'o', # o with stroke + 0x0111: u'd', # d with stroke + 0x0127: u'h', # h with stroke + 0x0131: u'i', # dotless i + 0x0142: u'l', # l with stroke + 0x0167: u't', # t with stroke + 0x0180: u'b', # b with stroke + 0x0183: u'b', # b with topbar + 0x0188: u'c', # c with hook + 0x018c: u'd', # d with topbar + 0x0192: u'f', # f with hook + 0x0199: u'k', # k with hook + 0x019a: u'l', # l with bar + 0x019e: u'n', # n with long right leg + 0x01a5: u'p', # p with hook + 0x01ab: u't', # t with palatal hook + 0x01ad: u't', # t with hook + 0x01b4: u'y', # y with hook + 0x01b6: u'z', # z with stroke + 0x01e5: u'g', # g with stroke + 0x0225: u'z', # z with hook + 0x0234: u'l', # l with curl + 0x0235: u'n', # n with curl + 0x0236: u't', # t with curl + 0x0237: u'j', # dotless j + 0x023c: u'c', # c with stroke + 0x023f: u's', # s with swash tail + 0x0240: u'z', # z with swash tail + 0x0247: u'e', # e with stroke + 0x0249: u'j', # j with stroke + 0x024b: u'q', # q with hook tail + 0x024d: u'r', # r with stroke + 0x024f: u'y', # y with stroke +} +_non_id_translate_digraphs = { + 0x00df: u'sz', # ligature sz + 0x00e6: u'ae', # ae + 0x0153: u'oe', # ligature oe + 0x0238: u'db', # db digraph + 0x0239: u'qp', # qp digraph +} + + def make_id(env: "BuildEnvironment", document: nodes.document, prefix: str = '', term: str = None) -> str: """Generate an appropriate node_id for given *prefix* and *term*.""" @@ -443,14 +517,18 @@ def make_id(env: "BuildEnvironment", document: nodes.document, if prefix: idformat = prefix + "-%s" else: - idformat = document.settings.id_prefix + "%s" + idformat = (document.settings.id_prefix or "id") + "%s" # try to generate node_id by *term* if prefix and term: - node_id = nodes.make_id(idformat % term) + node_id = _make_id(idformat % term) if node_id == prefix: # *term* is not good to generate a node_id. node_id = None + elif term: + node_id = _make_id(term) + if node_id == '': + node_id = None # fallback to None while node_id is None or node_id in document.ids: node_id = idformat % env.new_serialno(prefix) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 23fa208d7..af41df48b 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -15,13 +15,12 @@ import os import re import shutil import sys -import time import warnings from io import StringIO from os import path from typing import Any, Generator, Iterator, List, Tuple -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning try: # for ALT Linux (#6712) @@ -60,8 +59,8 @@ def relative_uri(base: str, to: str) -> str: """Return a relative URL from ``base`` to ``to``.""" if to.startswith(SEP): return to - b2 = base.split(SEP) - t2 = to.split(SEP) + b2 = base.split('#')[0].split(SEP) + t2 = to.split('#')[0].split(SEP) # remove common segments (except the last segment) for x, y in zip(b2[:-1], t2[:-1]): if x != y: @@ -144,27 +143,6 @@ def make_filename_from_project(project: str) -> str: return make_filename(project_suffix_re.sub('', project)).lower() -def ustrftime(format: str, *args: 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: str, start: str = os.curdir) -> str: """Return a relative filepath to *path* either from the current directory or from an optional *start* directory. diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index ba4f83a71..061bbcb6d 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -52,7 +52,7 @@ class UnicodeMixin: .. deprecated:: 2.0 """ - def __str__(self): + def __str__(self) -> str: warnings.warn('UnicodeMixin is deprecated', RemovedInSphinx40Warning, stacklevel=2) return self.__unicode__() # type: ignore diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py index 884e7c997..d9d1d1b2c 100644 --- a/sphinx/util/requests.py +++ b/sphinx/util/requests.py @@ -14,7 +14,6 @@ from contextlib import contextmanager from typing import Any, Generator, Union from urllib.parse import urlsplit -import pkg_resources import requests import sphinx @@ -37,28 +36,6 @@ except ImportError: # for requests < 2.4.0 InsecureRequestWarning = None # type: ignore -try: - from requests.packages.urllib3.exceptions import InsecurePlatformWarning -except ImportError: - try: - # for Debian-jessie - from urllib3.exceptions import InsecurePlatformWarning # type: ignore - except ImportError: - # for requests < 2.4.0 - InsecurePlatformWarning = None # type: ignore - -# try to load requests[security] (but only if SSL is available) -try: - import ssl # NOQA -except ImportError: - pass -else: - try: - pkg_resources.require(['requests[security]']) - except (pkg_resources.DistributionNotFound, - pkg_resources.VersionConflict): - pass # ignored - useragent_header = [('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0')] diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index ccceefed6..eb38d232c 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -49,7 +49,8 @@ def stringify(annotation: Any) -> str: return repr(annotation) elif annotation is NoneType: # type: ignore return 'None' - elif getattr(annotation, '__module__', None) == 'builtins': + elif (getattr(annotation, '__module__', None) == 'builtins' and + hasattr(annotation, '__qualname__')): return annotation.__qualname__ elif annotation is Ellipsis: return '...' @@ -88,6 +89,8 @@ def _stringify_py37(annotation: Any) -> str: args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) returns = stringify(annotation.__args__[-1]) return '%s[[%s], %s]' % (qualname, args, returns) + elif str(annotation).startswith('typing.Annotated'): # for py39+ + return stringify(annotation.__args__[0]) elif annotation._special: return qualname else: diff --git a/sphinx/versioning.py b/sphinx/versioning.py index e5fa3b9b3..502bf361c 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -9,17 +9,14 @@ :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 typing import Any, Dict, Iterator from uuid import uuid4 -from docutils import nodes from docutils.nodes import Node -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.transforms import SphinxTransform if False: @@ -177,14 +174,6 @@ class UIDTransform(SphinxTransform): list(merge_doctrees(old_doctree, self.document, env.versioning_condition)) -def prepare(document: 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: "Sphinx") -> Dict[str, Any]: app.add_transform(UIDTransform) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index ab5712410..85eeb4376 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -11,7 +11,7 @@ import copy import os import posixpath -import sys +import re import warnings from typing import Any, Iterable, Tuple from typing import cast @@ -22,7 +22,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 @@ -39,6 +39,19 @@ logger = logging.getLogger(__name__) # http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html +def multiply_length(length: str, scale: int) -> str: + """Multiply *length* (width or height) by *scale*.""" + matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) + if not matched: + return length + elif scale == 100: + return length + else: + amount, unit = matched.groups() + result = float(amount) * scale / 100 + return "%s%s" % (int(result), unit) + + class HTMLWriter(Writer): # override embed-stylesheet default value to 0. @@ -116,10 +129,6 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def visit_desc_signature(self, node: Element) -> None: # the id is set automatically self.body.append(self.starttag(node, 'dt')) - # anchor for per-desc interactive data - if node.parent['objtype'] != 'describe' \ - and node['ids'] and node['first']: - self.body.append('<!--[%s]-->' % node['ids'][0]) def depart_desc_signature(self, node: Element) -> None: if not node.get('is_multiline'): @@ -602,11 +611,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): if 'height' in node: atts['height'] = node['height'] if 'scale' in node: - scale = node['scale'] / 100.0 if 'width' in atts: - atts['width'] = int(atts['width']) * scale + atts['width'] = multiply_length(atts['width'], node['scale']) if 'height' in atts: - atts['height'] = int(atts['height']) * scale + atts['height'] = multiply_length(atts['height'], node['scale']) atts['alt'] = node.get('alt', uri) if 'align' in node: atts['class'] = 'align-%s' % node['align'] @@ -831,29 +839,3 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node: Node) -> None: raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - # --------- METHODS FOR COMPATIBILITY -------------------------------------- - - @property - def highlightlang(self) -> str: - warnings.warn('HTMLTranslator.highlightlang is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightlang_base(self) -> str: - warnings.warn('HTMLTranslator.highlightlang_base is deprecated.', - RemovedInSphinx30Warning) - return self.builder.config.highlight_language - - @property - def highlightopts(self) -> str: - warnings.warn('HTMLTranslator.highlightopts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_options - - @property - def highlightlinenothreshold(self) -> 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 9f38bf4f3..f94dd60a3 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -10,7 +10,7 @@ import os import posixpath -import sys +import re import warnings from typing import Any, Iterable, Tuple from typing import cast @@ -21,7 +21,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 @@ -38,6 +38,19 @@ logger = logging.getLogger(__name__) # http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html +def multiply_length(length: str, scale: int) -> str: + """Multiply *length* (width or height) by *scale*.""" + matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) + if not matched: + return length + elif scale == 100: + return length + else: + amount, unit = matched.groups() + result = float(amount) * scale / 100 + return "%s%s" % (int(result), unit) + + class HTML5Translator(SphinxTranslator, BaseTranslator): """ Our custom HTML translator. @@ -88,10 +101,6 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def visit_desc_signature(self, node: Element) -> None: # the id is set automatically self.body.append(self.starttag(node, 'dt')) - # anchor for per-desc interactive data - if node.parent['objtype'] != 'describe' \ - and node['ids'] and node['first']: - self.body.append('<!--[%s]-->' % node['ids'][0]) def depart_desc_signature(self, node: Element) -> None: if not node.get('is_multiline'): @@ -543,11 +552,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): if 'height' in node: atts['height'] = node['height'] if 'scale' in node: - scale = node['scale'] / 100.0 if 'width' in atts: - atts['width'] = int(atts['width']) * scale + atts['width'] = multiply_length(atts['width'], node['scale']) if 'height' in atts: - atts['height'] = int(atts['height']) * scale + atts['height'] = multiply_length(atts['height'], node['scale']) atts['alt'] = node.get('alt', uri) if 'align' in node: atts['class'] = 'align-%s' % node['align'] @@ -777,29 +785,3 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node: Node) -> None: raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - # --------- METHODS FOR COMPATIBILITY -------------------------------------- - - @property - def highlightlang(self) -> str: - warnings.warn('HTMLTranslator.highlightlang is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightlang_base(self) -> str: - warnings.warn('HTMLTranslator.highlightlang_base is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightopts(self) -> str: - warnings.warn('HTMLTranslator.highlightopts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_options - - @property - def highlightlinenothreshold(self) -> 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 85579a65a..9390ba5de 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -12,11 +12,10 @@ """ import re -import sys import warnings from collections import defaultdict from os import path -from typing import Any, Callable, Dict, Iterable, Iterator, List, Pattern, Tuple, Set, Union +from typing import Any, Dict, Iterable, Iterator, List, Tuple, Set, Union from typing import cast from docutils import nodes, writers @@ -25,7 +24,7 @@ from docutils.nodes import Element, Node, Text from sphinx import addnodes from sphinx import highlighting from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias + RemovedInSphinx40Warning, RemovedInSphinx50Warning, deprecated_alias ) from sphinx.domains import IndexEntry from sphinx.domains.std import StandardDomain @@ -46,17 +45,11 @@ except ImportError: if False: # For type annotation from sphinx.builders.latex import LaTeXBuilder + from sphinx.builders.latex.theming import Theme logger = logging.getLogger(__name__) -SHORTHANDOFF = r''' -\ifdefined\shorthandoff - \ifnum\catcode`\=\string=\active\shorthandoff{=}\fi - \ifnum\catcode`\"=\active\shorthandoff{"}\fi -\fi -''' - MAX_CITATION_LABEL_LENGTH = 8 LATEXSECTIONNAMES = ["part", "chapter", "section", "subsection", "subsubsection", "paragraph", "subparagraph"] @@ -96,9 +89,16 @@ class LaTeXWriter(writers.Writer): def __init__(self, builder: "LaTeXBuilder") -> None: super().__init__() self.builder = builder + self.theme = None # type: Theme def translate(self) -> None: - visitor = self.builder.create_translator(self.document, self.builder) + try: + visitor = self.builder.create_translator(self.document, self.builder, self.theme) + except TypeError: + warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', + RemovedInSphinx50Warning) + visitor = self.builder.create_translator(self.document, self.builder) + self.document.walkabout(visitor) self.output = cast(LaTeXTranslator, visitor).astext() @@ -132,18 +132,6 @@ class Table: # (cell = rectangular area) self.cell_id = 0 # last assigned cell_id - @property - def caption_footnotetexts(self) -> List[str]: - warnings.warn('table.caption_footnotetexts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def header_footnotetexts(self) -> List[str]: - warnings.warn('table.header_footnotetexts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - def is_longtable(self) -> bool: """True if and only if table uses longtable environment.""" return self.row > 30 or 'longtable' in self.classes @@ -296,9 +284,15 @@ class LaTeXTranslator(SphinxTranslator): # sphinx specific document classes docclasses = ('howto', 'manual') - def __init__(self, document: nodes.document, builder: "LaTeXBuilder") -> None: + def __init__(self, document: nodes.document, builder: "LaTeXBuilder", + theme: "Theme" = None) -> None: super().__init__(document, builder) self.body = [] # type: List[str] + self.theme = theme + + if theme is None: + warnings.warn('LaTeXTranslator now takes 3rd argument; "theme".', + RemovedInSphinx50Warning) # flags self.in_title = 0 @@ -321,21 +315,32 @@ class LaTeXTranslator(SphinxTranslator): # sort out some elements self.elements = self.builder.context.copy() - # but some have other interface in config file - self.elements['wrapperclass'] = self.format_docclass(document.get('docclass')) - - # we assume LaTeX class provides \chapter command except in case - # of non-Japanese 'howto' case + # initial section names self.sectionnames = LATEXSECTIONNAMES[:] - if document.get('docclass') == 'howto': - docclass = self.config.latex_docclass.get('howto', 'article') - if docclass[0] == 'j': # Japanese class... - pass - else: + + if self.theme: + # new style: control sectioning via theme's setting + # + # .. note:: template variables(elements) are already assigned in builder + docclass = self.theme.docclass + if self.theme.toplevel_sectioning == 'section': self.sectionnames.remove('chapter') else: - docclass = self.config.latex_docclass.get('manual', 'report') - self.elements['docclass'] = docclass + # old style: sectioning control is hard-coded + # but some have other interface in config file + self.elements['wrapperclass'] = self.format_docclass(self.settings.docclass) + + # we assume LaTeX class provides \chapter command except in case + # of non-Japanese 'howto' case + if document.get('docclass') == 'howto': + docclass = self.config.latex_docclass.get('howto', 'article') + if docclass[0] == 'j': # Japanese class... + pass + else: + self.sectionnames.remove('chapter') + else: + docclass = self.config.latex_docclass.get('manual', 'report') + self.elements['docclass'] = docclass # determine top section level self.top_sectionlevel = 1 @@ -383,44 +388,6 @@ class LaTeXTranslator(SphinxTranslator): logger.warning(__('no Babel option known for language %r'), self.config.language) - # set up multilingual module... - if self.elements['latex_engine'] == 'pdflatex': - if not self.babel.uses_cyrillic(): - if 'X2' in self.elements['fontenc']: - self.elements['substitutefont'] = '\\usepackage{substitutefont}' - self.elements['textcyrillic'] = ('\\usepackage[Xtwo]' - '{sphinxcyrillic}') - elif 'T2A' in self.elements['fontenc']: - self.elements['substitutefont'] = '\\usepackage{substitutefont}' - self.elements['textcyrillic'] = ('\\usepackage[TtwoA]' - '{sphinxcyrillic}') - if 'LGR' in self.elements['fontenc']: - self.elements['substitutefont'] = '\\usepackage{substitutefont}' - else: - self.elements['textgreek'] = '' - # 'babel' key is public and user setting must be obeyed - if self.elements['babel']: - self.elements['classoptions'] += ',' + self.babel.get_language() - # this branch is not taken for xelatex/lualatex if default settings - self.elements['multilingual'] = self.elements['babel'] - if self.config.language: - self.elements['shorthandoff'] = SHORTHANDOFF - - # Times fonts don't work with Cyrillic languages - if self.babel.uses_cyrillic() and 'fontpkg' not in self.config.latex_elements: - self.elements['fontpkg'] = '' - elif self.elements['polyglossia']: - self.elements['classoptions'] += ',' + self.babel.get_language() - options = self.babel.get_mainlanguage_options() - if options: - mainlanguage = r'\setmainlanguage[%s]{%s}' % (options, - self.babel.get_language()) - else: - mainlanguage = r'\setmainlanguage{%s}' % self.babel.get_language() - - self.elements['multilingual'] = '%s\n%s' % (self.elements['polyglossia'], - mainlanguage) - minsecnumdepth = self.secnumdepth # 2 from legacy sphinx manual/howto if self.document.get('tocdepth'): # reduce tocdepth if `part` or `chapter` is used for top_sectionlevel @@ -484,28 +451,11 @@ class LaTeXTranslator(SphinxTranslator): self.body = self.bodystack.pop() return body - def restrict_footnote(self, node: 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: 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: str) -> str: """ prepends prefix to sphinx document classes """ + warnings.warn('LaTeXWriter.format_docclass() is deprecated.', + RemovedInSphinx50Warning, stacklevel=2) if docclass in self.docclasses: docclass = 'sphinx' + docclass return docclass @@ -1501,8 +1451,8 @@ class LaTeXTranslator(SphinxTranslator): def depart_attribution(self, node: Element) -> None: self.body.append('\n\\end{flushright}\n') - def visit_index(self, node: Element, scre: Pattern = None) -> None: - def escape(value): + def visit_index(self, node: Element) -> None: + def escape(value: str) -> str: value = self.encode(value) value = value.replace(r'\{', r'\sphinxleftcurlybrace{}') value = value.replace(r'\}', r'\sphinxrightcurlybrace{}') @@ -1512,17 +1462,13 @@ class LaTeXTranslator(SphinxTranslator): value = value.replace('|', r'\textbar{}') return value - def style(string): + def style(string: str) -> str: match = EXTRA_RE.match(string) if match: return match.expand(r'\\spxentry{\1}\\spxextra{\2}') 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'] @@ -1758,6 +1704,8 @@ class LaTeXTranslator(SphinxTranslator): def visit_literal(self, node: Element) -> None: if self.in_title: self.body.append(r'\sphinxstyleliteralintitle{\sphinxupquote{') + elif 'kbd' in node['classes']: + self.body.append(r'\sphinxkeyboard{\sphinxupquote{') else: self.body.append(r'\sphinxcode{\sphinxupquote{') @@ -2122,61 +2070,6 @@ class LaTeXTranslator(SphinxTranslator): RemovedInSphinx40Warning, stacklevel=2) return 0 - @property - def footnotestack(self) -> List[Dict[str, List[Union[collected_footnote, bool]]]]: - warnings.warn('LaTeXWriter.footnotestack is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def bibitems(self) -> List[List[str]]: - warnings.warn('LaTeXTranslator.bibitems() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def in_container_literal_block(self) -> int: - warnings.warn('LaTeXTranslator.in_container_literal_block is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return 0 - - @property - def next_section_ids(self) -> Set[str]: - warnings.warn('LaTeXTranslator.next_section_ids is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return set() - - @property - def next_hyperlink_ids(self) -> Dict: - warnings.warn('LaTeXTranslator.next_hyperlink_ids is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return {} - - def push_hyperlink_ids(self, figtype: str, ids: Set[str]) -> None: - warnings.warn('LaTeXTranslator.push_hyperlink_ids() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - pass - - def pop_hyperlink_ids(self, figtype: str) -> Set[str]: - warnings.warn('LaTeXTranslator.pop_hyperlink_ids() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return set() - - @property - def hlsettingstack(self) -> 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) -> 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: str, definition: str) -> str: warnings.warn('babel_defmacro() is deprecated.', RemovedInSphinx40Warning) @@ -2190,15 +2083,6 @@ class LaTeXTranslator(SphinxTranslator): return ('%s\\def%s{%s}%s\n' % (prefix, name, definition, suffix)) - def _make_visit_admonition(name: str) -> Callable[["LaTeXTranslator", Element], None]: # type: ignore # NOQA - warnings.warn('LaTeXTranslator._make_visit_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def visit_admonition(self: "LaTeXTranslator", node: Element) -> None: - self.body.append('\n\\begin{sphinxadmonition}{%s}{%s:}' % - (name, admonitionlabels[name])) - return visit_admonition - def generate_numfig_format(self, builder: "LaTeXBuilder") -> str: warnings.warn('generate_numfig_format() is deprecated.', RemovedInSphinx40Warning) @@ -2239,22 +2123,16 @@ class LaTeXTranslator(SphinxTranslator): # Import old modules here for compatibility from sphinx.builders.latex import constants # NOQA -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', - { 'ADDITIONAL_SETTINGS': constants.ADDITIONAL_SETTINGS, 'DEFAULT_SETTINGS': constants.DEFAULT_SETTINGS, 'LUALATEX_DEFAULT_FONTPKG': constants.LUALATEX_DEFAULT_FONTPKG, 'PDFLATEX_DEFAULT_FONTPKG': constants.PDFLATEX_DEFAULT_FONTPKG, + 'SHORTHANDOFF': constants.SHORTHANDOFF, 'XELATEX_DEFAULT_FONTPKG': constants.XELATEX_DEFAULT_FONTPKG, 'XELATEX_GREEK_DEFAULT_FONTPKG': constants.XELATEX_GREEK_DEFAULT_FONTPKG, 'ExtBabel': ExtBabel, diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 65b4f1a0e..9c30244e9 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -10,16 +10,14 @@ import re import textwrap -import warnings from os import path -from typing import Any, Callable, Dict, Iterable, Iterator, List, Pattern, Set, Tuple, Union +from typing import Any, Dict, Iterable, Iterator, List, Pattern, Set, Tuple, Union from typing import cast from docutils import nodes, writers from docutils.nodes import Element, Node, Text from sphinx import addnodes, __display_version__ -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.domains import IndexEntry from sphinx.domains.index import IndexDomain from sphinx.errors import ExtensionError @@ -1528,11 +1526,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: str) -> Callable[["TexinfoTranslator", Element], None]: # type: ignore # NOQA - warnings.warn('TexinfoTranslator._make_visit_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def visit(self: "TexinfoTranslator", node: Element) -> None: - self.visit_admonition(node, admonitionlabels[name]) - return visit diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 882007d7e..b2ccd7b89 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -11,9 +11,8 @@ import math import os import re import textwrap -import warnings from itertools import groupby, chain -from typing import Any, Callable, Dict, List, Iterable, Optional, Set, Tuple, Union +from typing import Any, Dict, Generator, List, Iterable, Optional, Set, Tuple, Union from typing import cast from docutils import nodes, writers @@ -21,7 +20,6 @@ from docutils.nodes import Element, Node, Text 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 @@ -34,23 +32,23 @@ class Cell: """Represents a cell in a table. It can span on multiple columns or on multiple lines. """ - def __init__(self, text="", rowspan=1, colspan=1): + def __init__(self, text: str = "", rowspan: int = 1, colspan: int = 1) -> None: self.text = text self.wrapped = [] # type: List[str] self.rowspan = rowspan self.colspan = colspan - self.col = None - self.row = None + self.col = None # type: Optional[int] + self.row = None # type: Optional[int] - def __repr__(self): + def __repr__(self) -> str: return "<Cell {!r} {}v{}/{}>{}>".format( self.text, self.row, self.rowspan, self.col, self.colspan ) - def __hash__(self): + def __hash__(self) -> int: return hash((self.col, self.row)) - def wrap(self, width): + def wrap(self, width: int) -> None: self.wrapped = my_wrap(self.text, width) @@ -100,7 +98,7 @@ class Table: +--------+--------+ """ - def __init__(self, colwidth=None): + def __init__(self, colwidth: List[int] = None) -> None: self.lines = [] # type: List[List[Cell]] self.separator = 0 self.colwidth = (colwidth if colwidth is not None @@ -108,19 +106,19 @@ class Table: self.current_line = 0 self.current_col = 0 - def add_row(self): + def add_row(self) -> None: """Add a row to the table, to use with ``add_cell()``. It is not needed to call ``add_row()`` before the first ``add_cell()``. """ self.current_line += 1 self.current_col = 0 - def set_separator(self): + def set_separator(self) -> None: """Sets the separator below the current line. """ self.separator = len(self.lines) - def add_cell(self, cell): + def add_cell(self, cell: Cell) -> None: """Add a cell to the current line, to use with ``add_row()``. To add a cell spanning on multiple lines or rows, simply set the ``cell.colspan`` or ``cell.rowspan`` BEFORE inserting it to @@ -131,13 +129,13 @@ class Table: self[self.current_line, self.current_col] = cell self.current_col += cell.colspan - def __getitem__(self, pos): + def __getitem__(self, pos: Tuple[int, int]) -> Cell: line, col = pos self._ensure_has_line(line + 1) self._ensure_has_column(col + 1) return self.lines[line][col] - def __setitem__(self, pos, cell): + def __setitem__(self, pos: Tuple[int, int], cell: Cell) -> None: line, col = pos self._ensure_has_line(line + cell.rowspan) self._ensure_has_column(col + cell.colspan) @@ -147,19 +145,19 @@ class Table: cell.row = line cell.col = col - def _ensure_has_line(self, line): + def _ensure_has_line(self, line: int) -> None: while len(self.lines) < line: self.lines.append([]) - def _ensure_has_column(self, col): + def _ensure_has_column(self, col: int) -> None: for line in self.lines: while len(line) < col: line.append(None) - def __repr__(self): + def __repr__(self) -> str: return "\n".join(repr(line) for line in self.lines) - def cell_width(self, cell, source): + def cell_width(self, cell: Cell, source: List[int]) -> int: """Give the cell width, according to the given source (either ``self.colwidth`` or ``self.measured_widths``). This take into account cells spanning on multiple columns. @@ -170,7 +168,7 @@ class Table: return width + (cell.colspan - 1) * 3 @property - def cells(self): + def cells(self) -> Generator[Cell, None, None]: seen = set() # type: Set[Cell] for lineno, line in enumerate(self.lines): for colno, cell in enumerate(line): @@ -178,7 +176,7 @@ class Table: yield cell seen.add(cell) - def rewrap(self): + def rewrap(self) -> None: """Call ``cell.wrap()`` on all cells, and measure each column width after wrapping (result written in ``self.measured_widths``). """ @@ -191,7 +189,7 @@ class Table: for col in range(cell.col, cell.col + cell.colspan): self.measured_widths[col] = max(self.measured_widths[col], width) - def physical_lines_for_line(self, line): + def physical_lines_for_line(self, line: List[Cell]) -> int: """From a given line, compute the number of physical lines it spans due to text wrapping. """ @@ -200,7 +198,7 @@ class Table: physical_lines = max(physical_lines, len(cell.wrapped)) return physical_lines - def __str__(self): + def __str__(self) -> str: out = [] self.rewrap() @@ -1176,11 +1174,3 @@ class TextTranslator(SphinxTranslator): def unknown_visit(self, node: Node) -> None: raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - def _make_depart_admonition(name: str) -> Callable[["TextTranslator", Element], None]: # type: ignore # NOQA - warnings.warn('TextTranslator._make_depart_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def depart_admonition(self: "TextTranslator", node: Element) -> None: - self.end_state(first=admonitionlabels[name] + ': ') - return depart_admonition 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-domain-c/conf.py b/tests/roots/test-domain-c/conf.py new file mode 100644 index 000000000..a45d22e28 --- /dev/null +++ b/tests/roots/test-domain-c/conf.py @@ -0,0 +1 @@ +exclude_patterns = ['_build'] diff --git a/tests/roots/test-domain-c/index.rst b/tests/roots/test-domain-c/index.rst new file mode 100644 index 000000000..7e2c18be9 --- /dev/null +++ b/tests/roots/test-domain-c/index.rst @@ -0,0 +1,52 @@ +test-domain-c +============= + +directives +---------- + +.. c:function:: int hello(const char *name) + + :rtype: int + +.. c:function:: MyStruct hello2(char *name) + + :rtype: MyStruct + +.. c:member:: float Sphinx.version +.. c:var:: int version + +.. c:macro:: IS_SPHINX +.. c:macro:: SPHINX(arg1, arg2) + +.. c:struct:: MyStruct +.. c:union:: MyUnion +.. c:enum:: MyEnum + + .. c:enumerator:: MyEnumerator + + :c:enumerator:`MyEnumerator` + + :c:enumerator:`MyEnumerator` + +:c:enumerator:`MyEnumerator` + +.. c:type:: Sphinx +.. c:type:: int SphinxVersionNum + + +.. c:struct:: A + + .. c:union:: @data + + .. c:member:: int a + +- :c:member:`A.@data.a` +- :c:member:`A.a` + +- :c:expr:`unsigned int` +- :c:texpr:`unsigned int` + +.. c:var:: A a + +- :c:expr:`a->b` +- :c:texpr:`a->b` diff --git a/tests/roots/test-domain-cpp/backslash.rst b/tests/roots/test-domain-cpp/backslash.rst new file mode 100644 index 000000000..c93e68e94 --- /dev/null +++ b/tests/roots/test-domain-cpp/backslash.rst @@ -0,0 +1 @@ +.. cpp:var:: char c = '\\' diff --git a/tests/roots/test-domain-cpp/lookup-key-overload.rst b/tests/roots/test-domain-cpp/lookup-key-overload.rst new file mode 100644 index 000000000..2011e26d6 --- /dev/null +++ b/tests/roots/test-domain-cpp/lookup-key-overload.rst @@ -0,0 +1,8 @@ +.. default-domain:: cpp + +.. namespace:: lookup_key_overload + +.. function:: void g(int a) +.. function:: void g(double b) + + :var:`b` diff --git a/tests/roots/test-domain-cpp/multi-decl-lookup.rst b/tests/roots/test-domain-cpp/multi-decl-lookup.rst new file mode 100644 index 000000000..9706d1822 --- /dev/null +++ b/tests/roots/test-domain-cpp/multi-decl-lookup.rst @@ -0,0 +1,24 @@ +.. default-domain:: cpp + +.. namespace:: multi_decl_lookup + +.. function:: void f1(int a) + void f1(double b) + + - a: :var:`a` + - b: :var:`b` + +.. function:: template<typename T> void f2(int a) + template<typename U> void f2(double b) + + - T: :type:`T` + - U: :type:`U` + + +.. class:: template<typename T> A + template<typename U> B + + .. function:: void f3() + + - T: :type:`T` + - U: :type:`U` diff --git a/tests/roots/test-domain-cpp/warn-template-param-qualified-name.rst b/tests/roots/test-domain-cpp/warn-template-param-qualified-name.rst new file mode 100644 index 000000000..49a650de0 --- /dev/null +++ b/tests/roots/test-domain-cpp/warn-template-param-qualified-name.rst @@ -0,0 +1,11 @@ +.. default-domain:: cpp + +.. class:: template<typename T> A + + .. type:: N1 = T::typeOk + + - Not ok, warn: :type:`T::typeWarn` + + .. type:: N2 = T::U::typeOk + + - Not ok, warn: :type:`T::U::typeWarn` diff --git a/tests/roots/test-domain-py/module.rst b/tests/roots/test-domain-py/module.rst index c01032b26..dce3fa5ac 100644 --- a/tests/roots/test-domain-py/module.rst +++ b/tests/roots/test-domain-py/module.rst @@ -51,3 +51,11 @@ module .. py:attribute:: attr2 :type: :doc:`index` + +.. py:module:: exceptions + +.. py:exception:: Exception + +.. py:module:: object + +.. py:function:: sum() diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py index f6970a36c..e28eeef8a 100644 --- a/tests/roots/test-ext-autodoc/target/__init__.py +++ b/tests/roots/test-ext-autodoc/target/__init__.py @@ -110,6 +110,10 @@ class Outer(object): factory = dict +class InnerChild(Outer.Inner): + """InnerChild docstring""" + + class DocstringSig(object): def meth(self): """meth(FOO, BAR=1) -> BAZ diff --git a/tests/roots/test-ext-autodoc/target/annotated.py b/tests/roots/test-ext-autodoc/target/annotated.py new file mode 100644 index 000000000..427188256 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/annotated.py @@ -0,0 +1,6 @@ +from typing import Annotated + + +def hello(name: Annotated[str, "attribute"]) -> None: + """docstring""" + pass diff --git a/tests/roots/test-ext-autodoc/target/cython.pyx b/tests/roots/test-ext-autodoc/target/cython.pyx new file mode 100644 index 000000000..1457db3c9 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/cython.pyx @@ -0,0 +1,12 @@ +# cython: binding=True + +def foo(*args, **kwargs): + """Docstring.""" + + +class Class: + """Docstring.""" + + def meth(self, name: str, age: int = 0) -> None: + """Docstring.""" + pass diff --git a/tests/roots/test-ext-autodoc/target/private.py b/tests/roots/test-ext-autodoc/target/private.py new file mode 100644 index 000000000..38f276663 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/private.py @@ -0,0 +1,5 @@ +def private_function(name): + """private_function is a docstring(). + + :meta private: + """ diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py new file mode 100644 index 000000000..33dcae43a --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/singledispatch.py @@ -0,0 +1,27 @@ +from functools import singledispatch +import inspect + + +def assign_signature(func): + # This is intended to cover more complex signature-rewriting decorators. + func.__signature__ = inspect.signature(func) + return func + + +@singledispatch +def func(arg, kwarg=None): + """A function for general use.""" + pass + + +@func.register(int) +def _func_int(arg, kwarg=None): + """A function for int.""" + pass + + +@func.register(str) +@assign_signature +def _func_str(arg, kwarg=None): + """A function for str.""" + pass diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py new file mode 100644 index 000000000..b5ccbb2f0 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py @@ -0,0 +1,20 @@ +from functools import singledispatchmethod + + +class Foo: + """docstring""" + + @singledispatchmethod + def meth(self, arg, kwarg=None): + """A method for general use.""" + pass + + @meth.register(int) + def _meth_int(self, arg, kwarg=None): + """A method for int.""" + pass + + @meth.register(str) + def _meth_str(self, arg, kwarg=None): + """A method for str.""" + pass 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-html_scaled_image_link/conf.py b/tests/roots/test-html_scaled_image_link/conf.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/roots/test-html_scaled_image_link/conf.py diff --git a/tests/roots/test-html_scaled_image_link/img.png b/tests/roots/test-html_scaled_image_link/img.png Binary files differnew file mode 100644 index 000000000..a97e86d66 --- /dev/null +++ b/tests/roots/test-html_scaled_image_link/img.png diff --git a/tests/roots/test-html_scaled_image_link/index.rst b/tests/roots/test-html_scaled_image_link/index.rst new file mode 100644 index 000000000..0e4794058 --- /dev/null +++ b/tests/roots/test-html_scaled_image_link/index.rst @@ -0,0 +1,11 @@ +test-html_scaled_image_link +=========================== + +.. image:: img.png + +.. image:: img.png + :scale: 50% + +.. image:: img.png + :scale: 50% + :class: no-scaled-link diff --git a/tests/roots/test-inheritance/diagram_w_nested_classes.rst b/tests/roots/test-inheritance/diagram_w_nested_classes.rst new file mode 100644 index 000000000..7fa02175f --- /dev/null +++ b/tests/roots/test-inheritance/diagram_w_nested_classes.rst @@ -0,0 +1,5 @@ +Diagram with Nested Classes +=========================== + +.. inheritance-diagram:: + dummy.test_nested diff --git a/tests/roots/test-inheritance/dummy/test_nested.py b/tests/roots/test-inheritance/dummy/test_nested.py new file mode 100644 index 000000000..1e732aab5 --- /dev/null +++ b/tests/roots/test-inheritance/dummy/test_nested.py @@ -0,0 +1,12 @@ +""" + Test with nested classes. +""" + + +class A(object): + class B(object): + pass + + +class C(A.B): + pass diff --git a/tests/roots/test-latex-table/expects/longtable.tex b/tests/roots/test-latex-table/expects/longtable.tex index 9ba11269e..9febfcef5 100644 --- a/tests/roots/test-latex-table/expects/longtable.tex +++ b/tests/roots/test-latex-table/expects/longtable.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_align.tex b/tests/roots/test-latex-table/expects/longtable_having_align.tex index 0165a3829..1969e19d2 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_align.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_align.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_caption.tex b/tests/roots/test-latex-table/expects/longtable_having_caption.tex index c5acb1be8..f0041e9ec 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_caption.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_caption.tex @@ -24,7 +24,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex index 23bce882c..050527b69 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_problematic_cell.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex index d46bd1539..68e74c5f4 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_stub_columns_and_problematic_cell.tex @@ -26,7 +26,7 @@ header3 \endhead \hline -\multicolumn{3}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{3}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex b/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex index 0d67bbe35..c7213b906 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_verbatim.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_widths.tex b/tests/roots/test-latex-table/expects/longtable_having_widths.tex index 134b4465c..884fd9f8a 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_widths.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_widths.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex b/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex index 98a8ec915..17c5ec4cc 100644 --- a/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex +++ b/tests/roots/test-latex-table/expects/longtable_having_widths_and_problematic_cell.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex b/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex index 642ae596a..2fbbbc4ef 100644 --- a/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex +++ b/tests/roots/test-latex-table/expects/longtable_with_tabularcolumn.tex @@ -22,7 +22,7 @@ header2 \endhead \hline -\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{Continued on next page}}}\\ +\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\ \endfoot \endlastfoot diff --git a/tests/roots/test-latex-theme/conf.py b/tests/roots/test-latex-theme/conf.py new file mode 100644 index 000000000..196307ae6 --- /dev/null +++ b/tests/roots/test-latex-theme/conf.py @@ -0,0 +1,2 @@ +latex_theme = 'custom' +latex_theme_path = ['theme'] diff --git a/tests/roots/test-latex-theme/index.rst b/tests/roots/test-latex-theme/index.rst new file mode 100644 index 000000000..f5b1d5380 --- /dev/null +++ b/tests/roots/test-latex-theme/index.rst @@ -0,0 +1,2 @@ +latex_theme +=========== diff --git a/tests/roots/test-latex-theme/theme/custom/theme.conf b/tests/roots/test-latex-theme/theme/custom/theme.conf new file mode 100644 index 000000000..8961fac75 --- /dev/null +++ b/tests/roots/test-latex-theme/theme/custom/theme.conf @@ -0,0 +1,4 @@ +[theme] +docclass = book +wrapperclass = sphinxbook +toplevel_sectioning = chapter diff --git a/tests/roots/test-productionlist/Bare.rst b/tests/roots/test-productionlist/Bare.rst new file mode 100644 index 000000000..8ea9213f1 --- /dev/null +++ b/tests/roots/test-productionlist/Bare.rst @@ -0,0 +1,6 @@ +Bare +==== + +.. productionlist:: + A: `A` | somethingA + B: `B` | somethingB diff --git a/tests/roots/test-productionlist/Dup1.rst b/tests/roots/test-productionlist/Dup1.rst new file mode 100644 index 000000000..5cd09cb54 --- /dev/null +++ b/tests/roots/test-productionlist/Dup1.rst @@ -0,0 +1,5 @@ +Dup1 +==== + +.. productionlist:: + Dup: `Dup` | somethingDup diff --git a/tests/roots/test-productionlist/Dup2.rst b/tests/roots/test-productionlist/Dup2.rst new file mode 100644 index 000000000..1d663757f --- /dev/null +++ b/tests/roots/test-productionlist/Dup2.rst @@ -0,0 +1,5 @@ +Dup2 +==== + +.. productionlist:: + Dup: `Dup` | somethingDup diff --git a/tests/roots/test-productionlist/LineContinuation.rst b/tests/roots/test-productionlist/LineContinuation.rst new file mode 100644 index 000000000..4943e8bd2 --- /dev/null +++ b/tests/roots/test-productionlist/LineContinuation.rst @@ -0,0 +1,6 @@ +LineContinuation +================ + +.. productionlist:: lineContinuation + A: B C D \ + E F G diff --git a/tests/roots/test-productionlist/P1.rst b/tests/roots/test-productionlist/P1.rst new file mode 100644 index 000000000..6f9a863a7 --- /dev/null +++ b/tests/roots/test-productionlist/P1.rst @@ -0,0 +1,6 @@ +P1 +== + +.. productionlist:: P1 + A: `A` | somethingA + B: `B` | somethingB diff --git a/tests/roots/test-productionlist/P2.rst b/tests/roots/test-productionlist/P2.rst new file mode 100644 index 000000000..e6c3bc144 --- /dev/null +++ b/tests/roots/test-productionlist/P2.rst @@ -0,0 +1,6 @@ +P2 +== + +.. productionlist:: P2 + A: `A` | somethingA + B: `B` | somethingB diff --git a/tests/roots/test-productionlist/conf.py b/tests/roots/test-productionlist/conf.py new file mode 100644 index 000000000..a45d22e28 --- /dev/null +++ b/tests/roots/test-productionlist/conf.py @@ -0,0 +1 @@ +exclude_patterns = ['_build'] diff --git a/tests/roots/test-productionlist/firstLineRule.rst b/tests/roots/test-productionlist/firstLineRule.rst new file mode 100644 index 000000000..30ea6e005 --- /dev/null +++ b/tests/roots/test-productionlist/firstLineRule.rst @@ -0,0 +1,5 @@ +FirstLineRule +============= + +.. productionlist:: FirstLine: something + SecondLine: somethingElse diff --git a/tests/roots/test-productionlist/index.rst b/tests/roots/test-productionlist/index.rst new file mode 100644 index 000000000..4a0b9789c --- /dev/null +++ b/tests/roots/test-productionlist/index.rst @@ -0,0 +1,27 @@ +.. toctree:: + + P1 + P2 + Bare + Dup1 + Dup2 + firstLineRule + LineContinuation + +- A: :token:`A` +- B: :token:`B` +- P1:A: :token:`P1:A` +- P1:B: :token:`P1:B` +- P2:A: :token:`P1:A` +- P2:B: :token:`P2:B` +- Explicit title A, plain: :token:`MyTitle <A>` +- Explicit title A, colon: :token:`My:Title <A>` +- Explicit title P1:A, plain: :token:`MyTitle <P1:A>` +- Explicit title P1:A, colon: :token:`My:Title <P1:A>` +- Tilde A: :token:`~A`. +- Tilde P1:A: :token:`~P1:A`. +- Tilde explicit title P1:A: :token:`~MyTitle <P1:A>` +- Tilde, explicit title P1:A: :token:`MyTitle <~P1:A>` +- Dup: :token:`Dup` +- FirstLine: :token:`FirstLine` +- SecondLine: :token:`SecondLine` diff --git a/tests/roots/test-root/objects.txt b/tests/roots/test-root/objects.txt index 7e9bc15a9..f713e076c 100644 --- a/tests/roots/test-root/objects.txt +++ b/tests/roots/test-root/objects.txt @@ -107,15 +107,15 @@ Referring to :func:`nothing <>`. C items ======= -.. c:function:: Sphinx_DoSomething() +.. c:function:: void Sphinx_DoSomething() -.. c:member:: SphinxStruct.member +.. c:member:: int SphinxStruct.member .. c:macro:: SPHINX_USE_PYTHON .. c:type:: SphinxType -.. c:var:: sphinx_global +.. c:var:: int sphinx_global Javascript items diff --git a/tests/roots/test-search/nosearch.rst b/tests/roots/test-search/nosearch.rst new file mode 100644 index 000000000..9aa48b374 --- /dev/null +++ b/tests/roots/test-search/nosearch.rst @@ -0,0 +1,7 @@ +:nosearch: + +nosearch +======== + +zfs +latex diff --git a/tests/roots/test-theming/test_theme/test-theme/theme.conf b/tests/roots/test-theming/test_theme/test-theme/theme.conf index b7518bc9c..2ad2c33e1 100644 --- a/tests/roots/test-theming/test_theme/test-theme/theme.conf +++ b/tests/roots/test-theming/test_theme/test-theme/theme.conf @@ -1,3 +1,4 @@ [theme] inherit = classic sidebars = globaltoc.html, searchbox.html +pygments_dark_style = monokai diff --git a/tests/test_application.py b/tests/test_application.py index 86fff9d57..a089a4bc0 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -61,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) == {'.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() diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index a86211f18..741c4bb60 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -22,6 +22,13 @@ from sphinx.testing.util import SphinxTestApp, Struct # NOQA from sphinx.util import logging from sphinx.util.docutils import LoggingReporter +try: + # Enable pyximport to test cython module + import pyximport + pyximport.install() +except ImportError: + pyximport = None + app = None @@ -193,7 +200,7 @@ def test_format_signature(): assert formatsig('function', 'f', f, None, None) == '(a, b, c=1, **d)' assert formatsig('function', 'f', f, 'a, b, c, d', None) == '(a, b, c, d)' assert formatsig('function', 'f', f, None, 'None') == '(a, b, c=1, **d) -> None' - assert formatsig('function', 'g', g, None, None) == r"(a='\\n')" + assert formatsig('function', 'g', g, None, None) == r"(a='\n')" # test for classes class D: @@ -247,12 +254,12 @@ def test_format_signature(): assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)' assert formatsig('method', 'H.foo', H.foo1, 'a', None) == '(a)' assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' - assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')" + assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\n')" # test bound methods interpreted as functions assert formatsig('function', 'foo', H().foo1, None, None) == '(b, *c)' assert formatsig('function', 'foo', H().foo2, None, None) == '(*c)' - assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\\n')" + assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\n')" # test exception handling (exception is caught and args is '') directive.env.config.autodoc_docstring_signature = False @@ -361,7 +368,7 @@ def test_new_documenter(app): ' :module: target', '', ' documentation for the integer', - ' ' + '', ] @@ -420,7 +427,7 @@ def test_py_module(app, warning): ' :module: target', '', ' Function.', - ' ' + '', ] assert ("don't know which module to import for autodocumenting 'Class.meth'" not in warning.getvalue()) @@ -435,7 +442,7 @@ def test_autodoc_decorator(app): ' :module: target.decorator', '', ' docstring for deco1', - ' ' + '', ] actual = do_autodoc(app, 'decorator', 'target.decorator.deco2') @@ -445,7 +452,7 @@ def test_autodoc_decorator(app): ' :module: target.decorator', '', ' docstring for deco2', - ' ' + '', ] @@ -458,7 +465,7 @@ def test_autodoc_exception(app): ' :module: target', '', ' My custom exception.', - ' ' + '', ] @@ -582,6 +589,30 @@ def test_autodoc_inherited_members(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherited_members_Base(app): + options = {"members": None, + "inherited-members": "Base", + "special-members": None} + + # check methods for object class are shown + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert ' .. py:method:: Derived.inheritedmeth()' in actual + assert ' .. py:method:: Derived.inheritedclassmeth' not in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_inherited_members_None(app): + options = {"members": None, + "inherited-members": "None", + "special-members": None} + + # check methods for object class are shown + actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options) + assert ' .. py:method:: Derived.__init__' in actual + assert ' .. py:method:: Derived.__str__' in actual + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_imported_members(app): options = {"members": None, "imported-members": None, @@ -661,6 +692,7 @@ def test_autodoc_ignore_module_all(app): assert list(filter(lambda l: 'class::' in l, actual)) == [ '.. py:class:: Class(arg)', '.. py:class:: CustomDict', + '.. py:class:: InnerChild', '.. py:class:: InstAttCls()', '.. py:class:: Outer', ' .. py:class:: Outer.Inner', @@ -701,7 +733,7 @@ def test_autodoc_subclass_of_builtin_class(app): ' :module: target', '', ' Docstring.', - ' ' + '', ] @@ -715,23 +747,23 @@ def test_autodoc_inner_class(app): ' :module: target', '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:class:: Outer.Inner', ' :module: target', - ' ', + '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:method:: Outer.Inner.meth()', ' :module: target', - ' ', + '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:attribute:: Outer.factory', ' :module: target', - ' ', + '', ' alias of :class:`builtins.dict`' ] @@ -742,13 +774,25 @@ def test_autodoc_inner_class(app): ' :module: target.Outer', '', ' Foo', - ' ', - ' ', + '', + '', ' .. py:method:: Inner.meth()', ' :module: target.Outer', - ' ', + '', ' Foo', - ' ', + '', + ] + + options['show-inheritance'] = True + actual = do_autodoc(app, 'class', 'target.InnerChild', options) + assert list(actual) == [ + '', + '.. py:class:: InnerChild', + ' :module: target', '', + ' Bases: :class:`target.Outer.Inner`', + '', + ' InnerChild docstring', + '', ] @@ -762,7 +806,7 @@ def test_autodoc_classmethod(app): ' :classmethod:', '', ' Inherited class method.', - ' ' + '', ] @@ -776,7 +820,7 @@ def test_autodoc_staticmethod(app): ' :staticmethod:', '', ' Inherited static method.', - ' ' + '', ] @@ -790,19 +834,19 @@ def test_autodoc_descriptor(app): '.. py:class:: Class', ' :module: target.descriptor', '', - ' ', + '', ' .. py:attribute:: Class.descr', ' :module: target.descriptor', - ' ', + '', ' Descriptor instance docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: Class.prop', ' :module: target.descriptor', ' :property:', - ' ', + '', ' Property.', - ' ' + '' ] @@ -817,7 +861,7 @@ def test_autodoc_c_module(app): " Convert a time tuple to a string, e.g. 'Sat Jun 06 16:26:11 1998'.", ' When the time tuple is not present, current time as returned by localtime()', ' is used.', - ' ' + '', ] @@ -909,7 +953,7 @@ def test_autodoc_module_scope(app): ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', - ' ' + '', ] @@ -925,7 +969,7 @@ def test_autodoc_class_scope(app): ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', - ' ' + '', ] @@ -939,16 +983,16 @@ def test_class_attributes(app): '.. py:class:: AttCls', ' :module: target', '', - ' ', + '', ' .. py:attribute:: AttCls.a1', ' :module: target', ' :value: hello world', - ' ', - ' ', + '', + '', ' .. py:attribute:: AttCls.a2', ' :module: target', ' :value: None', - ' ' + '' ] @@ -962,43 +1006,43 @@ def test_instance_attributes(app): ' :module: target', '', ' Class with documented class and instance attributes.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :value: 'a'", - ' ', + '', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca2', ' :module: target', " :value: 'b'", - ' ', + '', ' Doc comment for InstAttCls.ca2. One line only.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca3', ' :module: target', " :value: 'c'", - ' ', + '', ' Docstring for class attribute InstAttCls.ca3.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :value: None', - ' ', + '', ' Doc comment for instance attribute InstAttCls.ia1', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia2', ' :module: target', ' :value: None', - ' ', + '', ' Docstring for instance attribute InstAttCls.ia2.', - ' ' + '' ] # pick up arbitrary attributes @@ -1010,22 +1054,22 @@ def test_instance_attributes(app): ' :module: target', '', ' Class with documented class and instance attributes.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', " :value: 'a'", - ' ', + '', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', - ' ', - ' ', + '', + '', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', ' :value: None', - ' ', + '', ' Doc comment for instance attribute InstAttCls.ia1', - ' ' + '' ] @@ -1042,30 +1086,30 @@ def test_slots(app): '.. py:class:: Bar()', ' :module: target.slots', '', - ' ', + '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', - ' ', + '', ' docstring of attr1', - ' ', - ' ', + '', + '', ' .. py:attribute:: Bar.attr2', ' :module: target.slots', - ' ', + '', ' docstring of instance attr2', - ' ', - ' ', + '', + '', ' .. py:attribute:: Bar.attr3', ' :module: target.slots', - ' ', + '', '', '.. py:class:: Foo', ' :module: target.slots', '', - ' ', + '', ' .. py:attribute:: Foo.attr', ' :module: target.slots', - ' ', + '', ] @@ -1080,39 +1124,39 @@ def test_enum_class(app): ' :module: target.enum', '', ' this is enum class', - ' ', - ' ', + '', + '', ' .. py:method:: EnumCls.say_hello()', ' :module: target.enum', - ' ', + '', ' a method says hello to you.', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val1', ' :module: target.enum', ' :value: 12', - ' ', + '', ' doc for val1', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val2', ' :module: target.enum', ' :value: 23', - ' ', + '', ' doc for val2', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val3', ' :module: target.enum', ' :value: 34', - ' ', + '', ' doc for val3', - ' ', - ' ', + '', + '', ' .. py:attribute:: EnumCls.val4', ' :module: target.enum', ' :value: 34', - ' ' + '' ] # checks for an attribute of EnumClass @@ -1124,7 +1168,7 @@ def test_enum_class(app): ' :value: 12', '', ' doc for val1', - ' ' + '' ] @@ -1141,19 +1185,19 @@ def test_descriptor_class(app): ' :module: target.descriptor', '', ' Descriptor class docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: CustomDataDescriptor.meth()', ' :module: target.descriptor', - ' ', + '', ' Function.', - ' ', + '', '', '.. py:class:: CustomDataDescriptor2(doc)', ' :module: target.descriptor', '', ' Descriptor class with custom metaclass docstring.', - ' ' + '', ] @@ -1166,7 +1210,7 @@ def test_autofunction_for_callable(app): ' :module: target.callable', '', ' A callable object that behaves like a function.', - ' ' + '', ] @@ -1179,7 +1223,7 @@ def test_autofunction_for_method(app): ' :module: target.callable', '', ' docstring of Callable.method().', - ' ' + '', ] @@ -1196,39 +1240,39 @@ def test_abstractmethods(): '.. py:class:: Base', ' :module: target.abstractmethods', '', - ' ', + '', ' .. py:method:: Base.abstractmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.classmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :classmethod:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.coroutinemeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :async:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.meth()', ' :module: target.abstractmethods', - ' ', - ' ', + '', + '', ' .. py:method:: Base.prop', ' :module: target.abstractmethods', ' :abstractmethod:', ' :property:', - ' ', - ' ', + '', + '', ' .. py:method:: Base.staticmeth()', ' :module: target.abstractmethods', ' :abstractmethod:', ' :staticmethod:', - ' ' + '', ] @@ -1245,25 +1289,25 @@ def test_partialfunction(): ' :module: target.partialfunction', '', ' docstring of func1', - ' ', + '', '', '.. py:function:: func2(b, c)', ' :module: target.partialfunction', '', ' docstring of func1', - ' ', + '', '', '.. py:function:: func3(c)', ' :module: target.partialfunction', '', ' docstring of func3', - ' ', + '', '', '.. py:function:: func4()', ' :module: target.partialfunction', '', ' docstring of func3', - ' ' + '', ] @@ -1291,7 +1335,7 @@ def test_bound_method(): ' :module: target.bound_method', '', ' Method docstring', - ' ', + '', ] @@ -1313,29 +1357,29 @@ def test_coroutine(): '.. py:class:: AsyncClass', ' :module: target.coroutine', '', - ' ', + '', ' .. py:method:: AsyncClass.do_coroutine()', ' :module: target.coroutine', ' :async:', - ' ', + '', ' A documented coroutine function', - ' ', - ' ', + '', + '', ' .. py:method:: AsyncClass.do_coroutine2()', ' :module: target.coroutine', ' :async:', ' :classmethod:', - ' ', + '', ' A documented coroutine classmethod', - ' ', - ' ', + '', + '', ' .. py:method:: AsyncClass.do_coroutine3()', ' :module: target.coroutine', ' :async:', ' :staticmethod:', - ' ', + '', ' A documented coroutine staticmethod', - ' ', + '', ] @@ -1347,21 +1391,21 @@ def test_partialmethod(app): ' :module: target.partialmethod', '', ' An example for partialmethod.', - ' ', + '', ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', - ' ', + '', ' Make a cell alive.', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', - ' ', + '', ' Update state of cell to *state*.', - ' ', + '', ] options = {"members": None} @@ -1377,25 +1421,25 @@ def test_partialmethod_undoc_members(app): ' :module: target.partialmethod', '', ' An example for partialmethod.', - ' ', + '', ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', - ' ', + '', ' Make a cell alive.', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_dead()', ' :module: target.partialmethod', - ' ', - ' ', + '', + '', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', - ' ', + '', ' Update state of cell to *state*.', - ' ', + '', ] options = {"members": None, @@ -1418,48 +1462,48 @@ def test_autodoc_typed_instance_variables(app): '.. py:class:: Class()', ' :module: target.typed_vars', '', - ' ', + '', ' .. py:attribute:: Class.attr1', ' :module: target.typed_vars', ' :type: int', ' :value: 0', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr2', ' :module: target.typed_vars', ' :type: int', ' :value: None', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr3', ' :module: target.typed_vars', ' :type: int', ' :value: 0', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr4', ' :module: target.typed_vars', ' :type: int', ' :value: None', - ' ', + '', ' attr4', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr5', ' :module: target.typed_vars', ' :type: int', ' :value: None', - ' ', + '', ' attr5', - ' ', - ' ', + '', + '', ' .. py:attribute:: Class.attr6', ' :module: target.typed_vars', ' :type: int', ' :value: None', - ' ', + '', ' attr6', - ' ', + '', '', '.. py:data:: attr1', ' :module: target.typed_vars', @@ -1467,7 +1511,7 @@ def test_autodoc_typed_instance_variables(app): " :value: ''", '', ' attr1', - ' ', + '', '', '.. py:data:: attr2', ' :module: target.typed_vars', @@ -1475,7 +1519,7 @@ def test_autodoc_typed_instance_variables(app): ' :value: None', '', ' attr2', - ' ', + '', '', '.. py:data:: attr3', ' :module: target.typed_vars', @@ -1483,7 +1527,25 @@ def test_autodoc_typed_instance_variables(app): " :value: ''", '', ' attr3', - ' ' + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='py39+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_Annotated(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.annotated', options) + assert list(actual) == [ + '', + '.. py:module:: target.annotated', + '', + '', + '.. py:function:: hello(name: str) -> None', + ' :module: target.annotated', + '', + ' docstring', + '', ] @@ -1502,9 +1564,86 @@ def test_autodoc_for_egged_code(app): ' :value: 1', '', ' constant on sample.py', - ' ', + '', '', '.. py:function:: hello(s)', ' :module: sample', '' ] + + +@pytest.mark.usefixtures('setup_test') +def test_singledispatch(): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatch', options) + assert list(actual) == [ + '', + '.. py:module:: target.singledispatch', + '', + '', + '.. py:function:: func(arg, kwarg=None)', + ' func(arg: int, kwarg=None)', + ' func(arg: str, kwarg=None)', + ' :module: target.singledispatch', + '', + ' A function for general use.', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 8), + reason='singledispatchmethod is available since python3.8') +@pytest.mark.usefixtures('setup_test') +def test_singledispatchmethod(): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatchmethod', options) + assert list(actual) == [ + '', + '.. py:module:: target.singledispatchmethod', + '', + '', + '.. py:class:: Foo', + ' :module: target.singledispatchmethod', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: int, kwarg=None)', + ' Foo.meth(arg: str, kwarg=None)', + ' :module: target.singledispatchmethod', + '', + ' A method for general use.', + '', + ] + + +@pytest.mark.usefixtures('setup_test') +@pytest.mark.skipif(pyximport is None, reason='cython is not installed') +def test_cython(): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.cython', options) + assert list(actual) == [ + '', + '.. py:module:: target.cython', + '', + '', + '.. py:class:: Class', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + ' .. py:method:: Class.meth(name: str, age: int = 0) -> None', + ' :module: target.cython', + '', + ' Docstring.', + '', + '', + '.. py:function:: foo(*args, **kwargs)', + ' :module: target.cython', + '', + ' Docstring.', + '', + ] diff --git a/tests/test_build.py b/tests/test_build.py index de73cb6d9..9dcf78165 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -45,7 +45,7 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): """)) master_doc = srcdir / 'index.txt' - master_doc.write_text(master_doc.text() + dedent(""" + master_doc.write_text(master_doc.read_text() + dedent(""" .. toctree:: %(test_name)s/%(test_name)s diff --git a/tests/test_build_changes.py b/tests/test_build_changes.py index 5adaa5777..3aedd03f5 100644 --- a/tests/test_build_changes.py +++ b/tests/test_build_changes.py @@ -17,7 +17,7 @@ def test_build(app): app.build() # TODO: Use better checking of html content - htmltext = (app.outdir / 'changes.html').text() + htmltext = (app.outdir / 'changes.html').read_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 @@ -28,7 +28,7 @@ def test_build(app): assert path_html in htmltext malloc_html = ( - '<b>Test_Malloc</b>: <i>changed:</i> Changed in version 0.6:' + '<b>void *Test_Malloc(size_t n)</b>: <i>changed:</i> Changed in version 0.6:' ' Can now be replaced with a different allocator.</a>') assert malloc_html in htmltext diff --git a/tests/test_build_dirhtml.py b/tests/test_build_dirhtml.py index 715db1146..e89e6888d 100644 --- a/tests/test_build_dirhtml.py +++ b/tests/test_build_dirhtml.py @@ -25,15 +25,16 @@ def test_dirhtml(app, status, warning): assert (app.outdir / 'foo/foo_2/index.html').exists() assert (app.outdir / 'bar/index.html').exists() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert 'href="foo/"' in content assert 'href="foo/foo_1/"' in content assert 'href="foo/foo_2/"' in content assert 'href="bar/"' in content # objects.inv (refs: #7095) - f = (app.outdir / 'objects.inv').open('rb') - invdata = InventoryFile.load(f, 'path/to', posixpath.join) + with (app.outdir / 'objects.inv').open('rb') as f: + invdata = InventoryFile.load(f, 'path/to', posixpath.join) + assert 'index' in invdata.get('std:doc') assert ('Python', '', 'path/to/', '-') == invdata['std:doc']['index'] diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index 9436ac020..ca6f09cdd 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -67,11 +67,11 @@ class EPUBElementTree: @pytest.mark.sphinx('epub', testroot='basic') def test_build_epub(app): app.build() - assert (app.outdir / 'mimetype').text() == 'application/epub+zip' + assert (app.outdir / 'mimetype').read_text() == 'application/epub+zip' assert (app.outdir / 'META-INF' / 'container.xml').exists() # toc.ncx - toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').text()) + toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_text()) assert toc.find("./ncx:docTitle/ncx:text").text == 'Python' # toc.ncx / head @@ -91,7 +91,7 @@ def test_build_epub(app): assert navlabel.text == 'The basic Sphinx documentation for testing' # content.opf - opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text()) + opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text()) # content.opf / metadata metadata = opf.find("./idpf:metadata") @@ -143,7 +143,7 @@ def test_build_epub(app): assert reference.get('href') == 'index.xhtml' # nav.xhtml - nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').text()) + nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_text()) assert nav.attrib == {'lang': 'en', '{http://www.w3.org/XML/1998/namespace}lang': 'en'} assert nav.find("./xhtml:head/xhtml:title").text == 'Table of Contents' @@ -163,7 +163,7 @@ def test_epub_cover(app): app.build() # content.opf / metadata - opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text()) + opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text()) cover_image = opf.find("./idpf:manifest/idpf:item[@href='%s']" % app.config.epub_cover[0]) cover = opf.find("./idpf:metadata/idpf:meta[@name='cover']") assert cover @@ -175,7 +175,7 @@ def test_nested_toc(app): app.build() # toc.ncx - toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').bytes()) + toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_bytes()) assert toc.find("./ncx:docTitle/ncx:text").text == 'Python' # toc.ncx / navPoint @@ -205,7 +205,7 @@ def test_nested_toc(app): anchor = elem.find("./xhtml:a") return (anchor.get('href'), anchor.text) - nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').bytes()) + nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_bytes()) toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li") assert len(toc) == 4 assert navinfo(toc[0]) == ('index.xhtml', @@ -230,7 +230,7 @@ def test_escaped_toc(app): app.build() # toc.ncx - toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').bytes()) + toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_bytes()) assert toc.find("./ncx:docTitle/ncx:text").text == 'need <b>"escaped"</b> project' # toc.ncx / navPoint @@ -260,7 +260,7 @@ def test_escaped_toc(app): anchor = elem.find("./xhtml:a") return (anchor.get('href'), anchor.text) - nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').bytes()) + nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_bytes()) toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li") assert len(toc) == 4 assert navinfo(toc[0]) == ('index.xhtml', @@ -286,7 +286,7 @@ def test_epub_writing_mode(app): app.build() # horizontal / page-progression-direction - opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text()) + opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text()) assert opf.find("./idpf:spine").get('page-progression-direction') == 'ltr' # horizontal / ibooks:scroll-axis @@ -294,7 +294,7 @@ def test_epub_writing_mode(app): assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'vertical' # horizontal / writing-mode (CSS) - css = (app.outdir / '_static' / 'epub.css').text() + css = (app.outdir / '_static' / 'epub.css').read_text() assert 'writing-mode: horizontal-tb;' in css # vertical @@ -303,7 +303,7 @@ def test_epub_writing_mode(app): app.build() # vertical / page-progression-direction - opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').text()) + opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text()) assert opf.find("./idpf:spine").get('page-progression-direction') == 'rtl' # vertical / ibooks:scroll-axis @@ -311,7 +311,7 @@ def test_epub_writing_mode(app): assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'horizontal' # vertical / writing-mode (CSS) - css = (app.outdir / '_static' / 'epub.css').text() + css = (app.outdir / '_static' / 'epub.css').read_text() assert 'writing-mode: vertical-rl;' in css @@ -319,10 +319,14 @@ def test_epub_writing_mode(app): def test_epub_anchor_id(app): app.build() - html = (app.outdir / 'index.xhtml').text() - assert '<p id="std-setting-STATICFILES_FINDERS">blah blah blah</p>' in html - assert '<span id="std-setting-STATICFILES_SECTION"></span><h1>blah blah blah</h1>' in html - assert 'see <a class="reference internal" href="#std-setting-STATICFILES_FINDERS">' in html + html = (app.outdir / 'index.xhtml').read_text() + assert ('<p id="std-setting-staticfiles_finders">' + '<span id="std-setting-STATICFILES_FINDERS"></span>' + 'blah blah blah</p>' in html) + assert ('<span id="std-setting-staticfiles_section"></span>' + '<span id="std-setting-STATICFILES_SECTION"></span>' + '<h1>blah blah blah</h1>' in html) + assert 'see <a class="reference internal" href="#std-setting-staticfiles_finders">' in html @pytest.mark.sphinx('epub', testroot='html_assets') @@ -330,7 +334,7 @@ def test_epub_assets(app): app.builder.build_all() # epub_sytlesheets (same as html_css_files) - content = (app.outdir / 'index.xhtml').text() + content = (app.outdir / 'index.xhtml').read_text() assert ('<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content) assert ('<link media="print" rel="stylesheet" title="title" type="text/css" ' @@ -343,7 +347,7 @@ def test_epub_css_files(app): app.builder.build_all() # epub_css_files - content = (app.outdir / 'index.xhtml').text() + content = (app.outdir / 'index.xhtml').read_text() assert '<link rel="stylesheet" type="text/css" href="_static/css/epub.css" />' in content # files in html_css_files are not outputed @@ -360,7 +364,7 @@ def test_html_download_role(app, status, warning): app.build() assert not (app.outdir / '_downloads' / 'dummy.dat').exists() - content = (app.outdir / 'index.xhtml').text() + content = (app.outdir / 'index.xhtml').read_text() assert ('<li><p><code class="xref download docutils literal notranslate">' '<span class="pre">dummy.dat</span></code></p></li>' in content) assert ('<li><p><code class="xref download docutils literal notranslate">' diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index a6107167a..1c86b8daa 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -31,7 +31,7 @@ def test_build_gettext(app): assert (app.outdir / 'subdir.pot').isfile() # regression test for issue #960 - catalog = (app.outdir / 'markup.pot').text() + catalog = (app.outdir / 'markup.pot').read_text() assert 'msgid "something, something else, something more"' in catalog @@ -84,7 +84,7 @@ def test_gettext_index_entries(app): return m.groups()[0] return None - pot = (app.outdir / 'index_entries.pot').text() + pot = (app.outdir / 'index_entries.pot').read_text() msgids = [_f for _f in map(msgid_getter, pot.splitlines()) if _f] expected_msgids = [ @@ -133,7 +133,7 @@ def test_gettext_disable_index_entries(app): return m.groups()[0] return None - pot = (app.outdir / 'index_entries.pot').text() + pot = (app.outdir / 'index_entries.pot').read_text() msgids = [_f for _f in map(msgid_getter, pot.splitlines()) if _f] expected_msgids = [ @@ -156,7 +156,7 @@ def test_gettext_template(app): app.builder.build_all() assert (app.outdir / 'sphinx.pot').isfile() - result = (app.outdir / 'sphinx.pot').text() + result = (app.outdir / 'sphinx.pot').read_text() assert "Welcome" in result assert "Sphinx %(version)s" in result @@ -166,7 +166,7 @@ def test_gettext_template_msgid_order_in_sphinxpot(app): app.builder.build_all() assert (app.outdir / 'sphinx.pot').isfile() - result = (app.outdir / 'sphinx.pot').text() + result = (app.outdir / 'sphinx.pot').read_text() assert re.search( ('msgid "Template 1".*' 'msgid "This is Template 1\\.".*' diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 63a5948af..2cf8dde20 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -176,8 +176,9 @@ def test_html4_output(app, status, warning): r'-| |-'), ], 'autodoc.html': [ - (".//dt[@id='autodoc_target.Class']", ''), - (".//dt[@id='autodoc_target.function']/em", r'\*\*kwds'), + (".//dl[@class='py class']/dt[@id='autodoc_target.class']", ''), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'\*\*'), + (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ @@ -218,11 +219,11 @@ def test_html4_output(app, status, warning): "[@class='rfc reference external']/strong", 'RFC 1'), (".//a[@href='https://tools.ietf.org/html/rfc1.html']" "[@class='rfc reference external']/strong", 'Request for Comments #1'), - (".//a[@href='objects.html#envvar-HOME']" + (".//a[@href='objects.html#envvar-home']" "[@class='reference internal']/code/span[@class='pre']", 'HOME'), (".//a[@href='#with']" "[@class='reference internal']/code/span[@class='pre']", '^with$'), - (".//a[@href='#grammar-token-try-stmt']" + (".//a[@href='#grammar-token-try_stmt']" "[@class='reference internal']/code/span", '^statement$'), (".//a[@href='#some-label'][@class='reference internal']/span", '^here$'), (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'), @@ -254,7 +255,7 @@ def test_html4_output(app, status, warning): (".//dl/dt[@id='term-boson']", 'boson'), # a production list (".//pre/strong", 'try_stmt'), - (".//pre/a[@href='#grammar-token-try1-stmt']/code/span", 'try1_stmt'), + (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), # tests for ``only`` directive (".//p", 'A global substitution.'), (".//p", 'In HTML.'), @@ -274,18 +275,18 @@ def test_html4_output(app, status, warning): (".//p", 'Il dit : « C’est “super” ! »'), ], 'objects.html': [ - (".//dt[@id='mod.Cls.meth1']", ''), - (".//dt[@id='errmod.Error']", ''), + (".//dt[@id='mod.cls.meth1']", ''), + (".//dt[@id='errmod.error']", ''), (".//dt/code", r'long\(parameter,\s* list\)'), (".//dt/code", 'another one'), - (".//a[@href='#mod.Cls'][@class='reference internal']", ''), - (".//dl[@class='userdesc']", ''), + (".//a[@href='#mod.cls'][@class='reference internal']", ''), + (".//dl[@class='std userdesc']", ''), (".//dt[@id='userdesc-myobj']", ''), (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), # docfields - (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'), - (".//a[@class='reference internal'][@href='#Time']", 'Time'), - (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), + (".//a[@class='reference internal'][@href='#timeint']/em", 'TimeInt'), + (".//a[@class='reference internal'][@href='#time']", 'Time'), + (".//a[@class='reference internal'][@href='#errmod.error']/strong", 'Error'), # C references (".//span[@class='pre']", 'CFunction()'), (".//a[@href='#c.Sphinx_DoSomething']", ''), @@ -324,7 +325,7 @@ def test_html4_output(app, status, warning): '\\+p'), (".//a[@class='reference internal'][@href='#cmdoption-perl-objc']/code/span", '--ObjC\\+\\+'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin-option']/code/span", + (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span", '--plugin.option'), (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']" "/code/span", @@ -426,7 +427,7 @@ def test_html_download(app): app.build() # subdir/includes.html - result = (app.outdir / 'subdir' / 'includes.html').text() + result = (app.outdir / 'subdir' / 'includes.html').read_text() pattern = ('<a class="reference download internal" download="" ' 'href="../(_downloads/.*/img.png)">') matched = re.search(pattern, result) @@ -435,7 +436,7 @@ def test_html_download(app): filename = matched.group(1) # includes.html - result = (app.outdir / 'includes.html').text() + result = (app.outdir / 'includes.html').read_text() pattern = ('<a class="reference download internal" download="" ' 'href="(_downloads/.*/img.png)">') matched = re.search(pattern, result) @@ -454,7 +455,7 @@ def test_html_download_role(app, status, warning): digest_another = md5(b'another/dummy.dat').hexdigest() assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert (('<li><p><a class="reference download internal" download="" ' 'href="_downloads/%s/dummy.dat">' '<code class="xref download docutils literal notranslate">' @@ -646,7 +647,7 @@ def test_numfig_disabled(app, cached_etree_parse, fname, expect): def test_numfig_without_numbered_toctree_warn(app, warning): app.build() # remove :numbered: option - index = (app.srcdir / 'index.rst').text() + index = (app.srcdir / 'index.rst').read_text() index = re.sub(':numbered:.*', '', index) (app.srcdir / 'index.rst').write_text(index) app.builder.build_all() @@ -746,7 +747,7 @@ def test_numfig_without_numbered_toctree_warn(app, warning): confoverrides={'numfig': True}) def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect): # remove :numbered: option - index = (app.srcdir / 'index.rst').text() + index = (app.srcdir / 'index.rst').read_text() index = re.sub(':numbered:.*', '', index) (app.srcdir / 'index.rst').write_text(index) @@ -1189,7 +1190,7 @@ def test_html_assets(app): assert not (app.outdir / '_static' / '.htaccess').exists() assert not (app.outdir / '_static' / '.htpasswd').exists() assert (app.outdir / '_static' / 'API.html').exists() - assert (app.outdir / '_static' / 'API.html').text() == 'Sphinx-1.4.4' + assert (app.outdir / '_static' / 'API.html').read_text() == 'Sphinx-1.4.4' assert (app.outdir / '_static' / 'css' / 'style.css').exists() assert (app.outdir / '_static' / 'js' / 'custom.js').exists() assert (app.outdir / '_static' / 'rimg.png').exists() @@ -1210,7 +1211,7 @@ def test_html_assets(app): assert not (app.outdir / 'subdir' / '.htpasswd').exists() # html_css_files - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content assert ('<link media="print" rel="stylesheet" title="title" type="text/css" ' 'href="https://example.com/custom.css" />' in content) @@ -1249,7 +1250,7 @@ def test_html_sourcelink_suffix_empty(app): def test_html_entity(app): app.builder.build_all() valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'} - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() for entity in re.findall(r'&([a-z]+);', content, re.M): assert entity not in valid_entities @@ -1261,11 +1262,18 @@ def test_html_inventory(app): with open(app.outdir / 'objects.inv', 'rb') as f: invdata = InventoryFile.load(f, 'https://www.google.com', os.path.join) assert set(invdata.keys()) == {'std:label', 'std:doc'} - assert set(invdata['std:label'].keys()) == {'modindex', 'genindex', 'search'} + assert set(invdata['std:label'].keys()) == {'modindex', + 'py-modindex', + 'genindex', + 'search'} assert invdata['std:label']['modindex'] == ('Python', '', 'https://www.google.com/py-modindex.html', 'Module Index') + assert invdata['std:label']['py-modindex'] == ('Python', + '', + 'https://www.google.com/py-modindex.html', + 'Python Module Index') assert invdata['std:label']['genindex'] == ('Python', '', 'https://www.google.com/genindex.html', @@ -1284,7 +1292,7 @@ def test_html_inventory(app): @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() + content = (app.outdir / 'index.html').read_text() assert ('<p class="caption"><span class="caption-text">The caption of pic</span>' '<a class="headerlink" href="#id1" title="Permalink to this image">¶</a></p>' in content) @@ -1293,7 +1301,7 @@ def test_html_anchor_for_figure(app): @pytest.mark.sphinx('html', testroot='directives-raw') def test_html_raw_directive(app, status, warning): app.builder.build_all() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() # standard case assert 'standalone raw directive (HTML)' in result @@ -1337,7 +1345,7 @@ def test_alternate_stylesheets(app, cached_etree_parse, fname, expect): @pytest.mark.sphinx('html', testroot='html_style') def test_html_style(app, status, warning): app.build() - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() assert '<link rel="stylesheet" href="_static/default.css" type="text/css" />' in result assert ('<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />' not in result) @@ -1347,7 +1355,7 @@ def test_html_style(app, status, warning): def test_html_remote_images(app, status, warning): app.builder.build_all() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert ('<img alt="https://www.python.org/static/img/python-logo.png" ' 'src="https://www.python.org/static/img/python-logo.png" />' in result) assert not (app.outdir / 'python-logo.png').exists() @@ -1359,7 +1367,7 @@ def test_html_sidebar(app, status, warning): # default for alabaster app.builder.build_all() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert ('<div class="sphinxsidebar" role="navigation" ' 'aria-label="main navigation">' in result) assert '<h1 class="logo"><a href="#">Python</a></h1>' in result @@ -1374,7 +1382,7 @@ def test_html_sidebar(app, status, warning): # only relations.html app.config.html_sidebars = {'**': ['relations.html']} app.builder.build_all() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert ('<div class="sphinxsidebar" role="navigation" ' 'aria-label="main navigation">' in result) assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result @@ -1388,7 +1396,7 @@ def test_html_sidebar(app, status, warning): # no sidebars app.config.html_sidebars = {'**': []} app.builder.build_all() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert ('<div class="sphinxsidebar" role="navigation" ' 'aria-label="main navigation">' not in result) assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result @@ -1419,10 +1427,10 @@ def test_html_manpage(app, cached_etree_parse, fname, expect): def test_html_baseurl(app, status, warning): app.build() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert '<link rel="canonical" href="https://example.com/index.html" />' in result - result = (app.outdir / 'qux' / 'index.html').text(encoding='utf8') + result = (app.outdir / 'qux' / 'index.html').read_text() assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result @@ -1432,10 +1440,10 @@ def test_html_baseurl(app, status, warning): def test_html_baseurl_and_html_file_suffix(app, status, warning): app.build() - result = (app.outdir / 'index.htm').text(encoding='utf8') + result = (app.outdir / 'index.htm').read_text() assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result - result = (app.outdir / 'qux' / 'index.htm').text(encoding='utf8') + result = (app.outdir / 'qux' / 'index.htm').read_text() assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result @@ -1518,6 +1526,11 @@ def test_html_pygments_for_classic_theme(app): assert style.__name__ == 'SphinxStyle' +@pytest.mark.sphinx('html', testroot='basic') +def test_html_dark_pygments_style_default(app): + assert app.builder.dark_highlighter is None + + @pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path') def test_validate_html_extra_path(app): (app.confdir / '_static').makedirs() @@ -1542,3 +1555,22 @@ def test_validate_html_static_path(app): ] validate_html_static_path(app, app.config) assert app.config.html_static_path == ['_static'] + + +@pytest.mark.sphinx(testroot='html_scaled_image_link') +def test_html_scaled_image_link(app): + app.build() + context = (app.outdir / 'index.html').read_text() + + # no scaled parameters + assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context) + + # scaled_image_link + assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">' + '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>', + context) + + # no-scaled-link class disables the feature + assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"' + ' src="_images/img.png" style="[^"]+" />', + context) diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 7a1a32507..61020b861 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -20,7 +20,7 @@ from test_build_html import ENV_WARNINGS from sphinx.builders.latex import default_latex_documents from sphinx.config import Config -from sphinx.errors import SphinxError +from sphinx.errors import SphinxError, ThemeError from sphinx.testing.util import strip_escseq from sphinx.util import docutils from sphinx.util.osutil import cd, ensuredir @@ -99,7 +99,7 @@ def skip_if_stylefiles_notfound(testfunc): def test_build_latex_doc(app, status, warning, engine, docclass): app.config.latex_engine = engine app.config.latex_documents = [app.config.latex_documents[0][:4] + (docclass,)] - app.builder.init_context() + app.builder.init() LaTeXTranslator.ignore_missing_images = True app.builder.build_all() @@ -113,7 +113,7 @@ def test_build_latex_doc(app, status, warning, engine, docclass): @pytest.mark.sphinx('latex') def test_writer(app, status, warning): app.builder.build_all() - result = (app.outdir / 'sphinxtests.tex').text(encoding='utf8') + result = (app.outdir / 'sphinxtests.tex').read_text() assert ('\\begin{sphinxfigure-in-table}\n\\centering\n\\capstart\n' '\\noindent\\sphinxincludegraphics{{img}.png}\n' @@ -156,7 +156,7 @@ def test_latex_warnings(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic') def test_latex_basic(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -165,10 +165,69 @@ def test_latex_basic(app, status, warning): assert r'\renewcommand{\releasename}{}' in result +@pytest.mark.sphinx('latex', testroot='basic', + confoverrides={ + 'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')] + }) +def test_latex_basic_manual(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'test.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{report}' in result + assert r'\documentclass[letterpaper,10pt,english]{sphinxmanual}' in result + + +@pytest.mark.sphinx('latex', testroot='basic', + confoverrides={ + 'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')] + }) +def test_latex_basic_howto(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'test.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{article}' in result + assert r'\documentclass[letterpaper,10pt,english]{sphinxhowto}' in result + + +@pytest.mark.sphinx('latex', testroot='basic', + confoverrides={ + 'language': 'ja', + 'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')] + }) +def test_latex_basic_manual_ja(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'test.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{jsbook}' in result + assert r'\documentclass[letterpaper,10pt,dvipdfmx]{sphinxmanual}' in result + + +@pytest.mark.sphinx('latex', testroot='basic', + confoverrides={ + 'language': 'ja', + 'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')] + }) +def test_latex_basic_howto_ja(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'test.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{jreport}' in result + assert r'\documentclass[letterpaper,10pt,dvipdfmx]{sphinxhowto}' in result + + +@pytest.mark.sphinx('latex', testroot='latex-theme') +def test_latex_theme(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'python.tex').read_text(encoding='utf8') + print(result) + assert r'\def\sphinxdocclass{book}' in result + assert r'\documentclass[letterpaper,10pt,english]{sphinxbook}' in result + + @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'zh'}) def test_latex_additional_settings_for_language_code(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -178,7 +237,7 @@ def test_latex_additional_settings_for_language_code(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'el'}) def test_latex_additional_settings_for_greek(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -189,7 +248,7 @@ def test_latex_additional_settings_for_greek(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-title') def test_latex_title_after_admonitions(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -200,7 +259,7 @@ def test_latex_title_after_admonitions(app, status, warning): confoverrides={'release': '1.0'}) def test_latex_release(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -212,7 +271,7 @@ def test_latex_release(app, status, warning): confoverrides={'numfig': True}) def test_numref(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -238,7 +297,7 @@ def test_numref(app, status, warning): '\\nameref{\\detokenize{foo:foo}}}') in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\addto\captionsenglish{\renewcommand{\figurename}{Fig.\@{} }}' in result assert r'\addto\captionsenglish{\renewcommand{\tablename}{Table }}' in result @@ -254,7 +313,7 @@ def test_numref(app, status, warning): 'section': 'SECTION-%s'}}) def test_numref_with_prefix1(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -286,7 +345,7 @@ def test_numref_with_prefix1(app, status, warning): '\\nameref{\\detokenize{foo:foo}}}') in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\addto\captionsenglish{\renewcommand{\figurename}{Figure:}}' in result assert r'\addto\captionsenglish{\renewcommand{\tablename}{Tab\_}}' in result @@ -302,7 +361,7 @@ def test_numref_with_prefix1(app, status, warning): 'section': 'SECTION_%s_'}}) def test_numref_with_prefix2(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -328,7 +387,7 @@ def test_numref_with_prefix2(app, status, warning): '\\nameref{\\detokenize{foo:foo}}}') in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\addto\captionsenglish{\renewcommand{\figurename}{Figure:}}' in result assert r'\def\fnum@figure{\figurename\thefigure{}.}' in result @@ -342,7 +401,7 @@ def test_numref_with_prefix2(app, status, warning): confoverrides={'numfig': True, 'language': 'ja'}) def test_numref_with_language_ja(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -368,7 +427,7 @@ def test_numref_with_language_ja(app, status, warning): '\\nameref{\\detokenize{foo:foo}}}') in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert '\\@iden{\\renewcommand{\\figurename}{図 }}' in result assert '\\@iden{\\renewcommand{\\tablename}{表 }}' in result @@ -379,10 +438,10 @@ def test_numref_with_language_ja(app, status, warning): def test_latex_obey_numfig_is_false(app, status, warning): app.builder.build_all() - result = (app.outdir / 'SphinxManual.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxManual.tex').read_text() assert '\\usepackage{sphinx}' in result - result = (app.outdir / 'SphinxHowTo.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxHowTo.tex').read_text() assert '\\usepackage{sphinx}' in result @@ -392,10 +451,10 @@ def test_latex_obey_numfig_is_false(app, status, warning): def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning): app.builder.build_all() - result = (app.outdir / 'SphinxManual.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxManual.tex').read_text() assert '\\usepackage[,nonumfigreset,mathnumfig]{sphinx}' in result - result = (app.outdir / 'SphinxHowTo.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxHowTo.tex').read_text() assert '\\usepackage[,nonumfigreset,mathnumfig]{sphinx}' in result @@ -405,10 +464,10 @@ def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning): def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning): app.builder.build_all() - result = (app.outdir / 'SphinxManual.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxManual.tex').read_text() assert '\\usepackage[,numfigreset=2,mathnumfig]{sphinx}' in result - result = (app.outdir / 'SphinxHowTo.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxHowTo.tex').read_text() assert '\\usepackage[,numfigreset=3,mathnumfig]{sphinx}' in result @@ -418,10 +477,10 @@ def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning): def test_latex_obey_numfig_but_math_numfig_false(app, status, warning): app.builder.build_all() - result = (app.outdir / 'SphinxManual.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxManual.tex').read_text() assert '\\usepackage[,numfigreset=1]{sphinx}' in result - result = (app.outdir / 'SphinxHowTo.tex').text(encoding='utf8') + result = (app.outdir / 'SphinxHowTo.tex').read_text() assert '\\usepackage[,numfigreset=2]{sphinx}' in result @@ -430,7 +489,7 @@ def test_latex_add_latex_package(app, status, warning): app.add_latex_package('foo') app.add_latex_package('bar', 'baz') app.builder.build_all() - result = (app.outdir / 'test.tex').text(encoding='utf8') + result = (app.outdir / 'test.tex').read_text() assert '\\usepackage{foo}' in result assert '\\usepackage[baz]{bar}' in result @@ -438,7 +497,7 @@ def test_latex_add_latex_package(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-babel') def test_babel_with_no_language_settings(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -451,7 +510,7 @@ def test_babel_with_no_language_settings(app, status, warning): assert '\\shorthandoff' not in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{page}' in result assert r'\addto\captionsenglish{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -463,7 +522,7 @@ def test_babel_with_no_language_settings(app, status, warning): confoverrides={'language': 'de'}) def test_babel_with_language_de(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -476,7 +535,7 @@ def test_babel_with_language_de(app, status, warning): assert '\\shorthandoff{"}' in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{Seite}' in result assert r'\addto\captionsngerman{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -488,7 +547,7 @@ def test_babel_with_language_de(app, status, warning): confoverrides={'language': 'ru'}) def test_babel_with_language_ru(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -501,7 +560,7 @@ def test_babel_with_language_ru(app, status, warning): assert '\\shorthandoff{"}' in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{страница}' in result assert r'\addto\captionsrussian{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -513,7 +572,7 @@ def test_babel_with_language_ru(app, status, warning): confoverrides={'language': 'tr'}) def test_babel_with_language_tr(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -526,7 +585,7 @@ def test_babel_with_language_tr(app, status, warning): assert '\\shorthandoff{=}' in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{sayfa}' in result assert r'\addto\captionsturkish{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -538,7 +597,7 @@ def test_babel_with_language_tr(app, status, warning): confoverrides={'language': 'ja'}) def test_babel_with_language_ja(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -550,7 +609,7 @@ def test_babel_with_language_ja(app, status, warning): assert '\\shorthandoff' not in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{ページ}' in result assert '\\@iden{\\renewcommand{\\figurename}{Fig.\\@{} }}' in result @@ -562,7 +621,7 @@ def test_babel_with_language_ja(app, status, warning): confoverrides={'language': 'unknown'}) def test_babel_with_unknown_language(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -577,7 +636,7 @@ def test_babel_with_unknown_language(app, status, warning): assert "WARNING: no Babel option known for language 'unknown'" in warning.getvalue() # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{page}' in result assert r'\addto\captionsenglish{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -589,7 +648,7 @@ def test_babel_with_unknown_language(app, status, warning): confoverrides={'language': 'de', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -603,7 +662,7 @@ def test_polyglossia_with_language_de(app, status, warning): assert '\\shorthandoff' not in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{Seite}' in result assert r'\addto\captionsgerman{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -615,7 +674,7 @@ def test_polyglossia_with_language_de(app, status, warning): confoverrides={'language': 'de-1901', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de_1901(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -629,7 +688,7 @@ def test_polyglossia_with_language_de_1901(app, status, warning): assert '\\shorthandoff' not in result # sphinxmessages.sty - result = (app.outdir / 'sphinxmessages.sty').text(encoding='utf8') + result = (app.outdir / 'sphinxmessages.sty').read_text() print(result) assert r'\def\pageautorefname{page}' in result assert r'\addto\captionsgerman{\renewcommand{\figurename}{Fig.\@{} }}' in result @@ -639,7 +698,7 @@ def test_polyglossia_with_language_de_1901(app, status, warning): @pytest.mark.sphinx('latex') def test_footnote(app, status, warning): app.builder.build_all() - result = (app.outdir / 'sphinxtests.tex').text(encoding='utf8') + result = (app.outdir / 'sphinxtests.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -666,7 +725,7 @@ def test_footnote(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -707,7 +766,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): confoverrides={'latex_show_urls': 'inline'}) def test_latex_show_urls_is_inline(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -752,7 +811,7 @@ def test_latex_show_urls_is_inline(app, status, warning): confoverrides={'latex_show_urls': 'footnote'}) def test_latex_show_urls_is_footnote(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -806,7 +865,7 @@ def test_latex_show_urls_is_footnote(app, status, warning): confoverrides={'latex_show_urls': 'no'}) def test_latex_show_urls_is_no(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -856,7 +915,7 @@ def test_latex_show_urls_footnote_and_substitutions(app, status, warning): @pytest.mark.sphinx('latex', testroot='image-in-section') def test_image_in_section(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -882,7 +941,7 @@ def test_latex_logo_if_not_found(app, status, warning): @pytest.mark.sphinx('latex', testroot='toctree-maxdepth') def test_toctree_maxdepth_manual(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -899,7 +958,7 @@ def test_toctree_maxdepth_manual(app, status, warning): ]}) def test_toctree_maxdepth_howto(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -913,7 +972,7 @@ def test_toctree_maxdepth_howto(app, status, warning): confoverrides={'master_doc': 'foo'}) def test_toctree_not_found(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -927,7 +986,7 @@ def test_toctree_not_found(app, status, warning): confoverrides={'master_doc': 'bar'}) def test_toctree_without_maxdepth(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -940,7 +999,7 @@ def test_toctree_without_maxdepth(app, status, warning): confoverrides={'master_doc': 'qux'}) def test_toctree_with_deeper_maxdepth(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -953,7 +1012,7 @@ def test_toctree_with_deeper_maxdepth(app, status, warning): confoverrides={'latex_toplevel_sectioning': None}) def test_latex_toplevel_sectioning_is_None(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -965,7 +1024,7 @@ def test_latex_toplevel_sectioning_is_None(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'part'}) def test_latex_toplevel_sectioning_is_part(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -983,7 +1042,7 @@ def test_latex_toplevel_sectioning_is_part(app, status, warning): ]}) def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -997,7 +1056,7 @@ def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'chapter'}) def test_latex_toplevel_sectioning_is_chapter(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1013,7 +1072,7 @@ def test_latex_toplevel_sectioning_is_chapter(app, status, warning): ]}) def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1025,7 +1084,7 @@ def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'section'}) def test_latex_toplevel_sectioning_is_section(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1036,7 +1095,7 @@ def test_latex_toplevel_sectioning_is_section(app, status, warning): @pytest.mark.sphinx('latex', testroot='maxlistdepth') def test_maxlistdepth_at_ten(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1049,14 +1108,14 @@ def test_maxlistdepth_at_ten(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_tabulars(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: sectname, content = chap.split('}', 1) tables[sectname] = content.strip() def get_expected(name): - return (app.srcdir / 'expects' / (name + '.tex')).text().strip() + return (app.srcdir / 'expects' / (name + '.tex')).read_text().strip() # simple_table actual = tables['simple table'] @@ -1120,14 +1179,14 @@ def test_latex_table_tabulars(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_longtable(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: sectname, content = chap.split('}', 1) tables[sectname] = content.strip() def get_expected(name): - return (app.srcdir / 'expects' / (name + '.tex')).text().strip() + return (app.srcdir / 'expects' / (name + '.tex')).read_text().strip() # longtable actual = tables['longtable'] @@ -1181,14 +1240,14 @@ def test_latex_table_longtable(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_complex_tables(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() tables = {} for chap in re.split(r'\\(?:section|renewcommand){', result)[1:]: sectname, content = chap.split('}', 1) tables[sectname] = content.strip() def get_expected(name): - return (app.srcdir / 'expects' / (name + '.tex')).text().strip() + return (app.srcdir / 'expects' / (name + '.tex')).read_text().strip() # grid table actual = tables['grid table'] @@ -1205,7 +1264,7 @@ def test_latex_table_complex_tables(app, status, warning): confoverrides={'templates_path': ['_mytemplates/latex']}) def test_latex_table_custom_template_caseA(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert 'SALUT LES COPAINS' in result @@ -1213,7 +1272,7 @@ def test_latex_table_custom_template_caseA(app, status, warning): confoverrides={'templates_path': ['_mytemplates']}) def test_latex_table_custom_template_caseB(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert 'SALUT LES COPAINS' not in result @@ -1221,14 +1280,14 @@ def test_latex_table_custom_template_caseB(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_custom_template_caseC(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert 'SALUT LES COPAINS' not in result @pytest.mark.sphinx('latex', testroot='directives-raw') def test_latex_raw_directive(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() # standard case assert 'standalone raw directive (HTML)' not in result @@ -1244,7 +1303,7 @@ def test_latex_raw_directive(app, status, warning): def test_latex_images(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() # images are copied assert '\\sphinxincludegraphics{{python-logo}.png}' in result @@ -1268,7 +1327,7 @@ def test_latex_images(app, status, warning): def test_latex_index(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert ('A \\index{famous@\\spxentry{famous}}famous ' '\\index{equation@\\spxentry{equation}}equation:\n' in result) assert ('\n\\index{Einstein@\\spxentry{Einstein}}' @@ -1282,8 +1341,8 @@ def test_latex_index(app, status, warning): def test_latex_equations(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') - expected = (app.srcdir / 'expects' / 'latex-equations.tex').text().strip() + result = (app.outdir / 'python.tex').read_text() + expected = (app.srcdir / 'expects' / 'latex-equations.tex').read_text().strip() assert expected in result @@ -1292,7 +1351,7 @@ def test_latex_equations(app, status, warning): def test_latex_image_in_parsed_literal(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert ('{\\sphinxunactivateextrasandspace \\raisebox{-0.5\\height}' '{\\sphinxincludegraphics[height=2.00000cm]{{pic}.png}}' '}AFTER') in result @@ -1302,7 +1361,7 @@ def test_latex_image_in_parsed_literal(app, status, warning): def test_latex_nested_enumerated_list(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n' '\\setcounter{enumi}{4}\n' in result) assert ('\\sphinxsetlistlabels{\\alph}{enumii}{enumiii}{}{.}%\n' @@ -1319,7 +1378,7 @@ def test_latex_nested_enumerated_list(app, status, warning): def test_latex_thebibliography(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(result) assert ('\\begin{sphinxthebibliography}{AuthorYe}\n' '\\bibitem[AuthorYear]{index:authoryear}\n' @@ -1332,7 +1391,7 @@ def test_latex_thebibliography(app, status, warning): def test_latex_glossary(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert ('\\item[{änhlich\\index{änhlich@\\spxentry{änhlich}|spxpagem}' r'\phantomsection' r'\label{\detokenize{index:term-anhlich}}}] \leavevmode' in result) @@ -1356,7 +1415,7 @@ def test_latex_glossary(app, status, warning): def test_latex_labels(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() # figures assert (r'\caption{labeled figure}' @@ -1403,7 +1462,7 @@ def test_latex_labels(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-figure-in-admonition') def test_latex_figure_in_admonition(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert(r'\begin{figure}[H]' in result) @@ -1415,6 +1474,7 @@ def test_default_latex_documents(): 'author': "Wolfgang Schäuble & G'Beckstein."}) config.init_values() config.add('latex_engine', None, True, None) + config.add('latex_theme', 'manual', True, None) expected = [('index', 'stasi.tex', 'STASI™ Documentation', r"Wolfgang Schäuble \& G\textquotesingle{}Beckstein.\@{}", 'manual')] assert default_latex_documents(config) == expected @@ -1433,7 +1493,7 @@ def test_includegraphics_oversized(app, status, warning): @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') + result = (app.outdir / 'python.tex').read_text() 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' @@ -1444,7 +1504,7 @@ def test_index_on_title(app, status, warning): confoverrides={'latex_engine': 'pdflatex'}) def test_texescape_for_non_unicode_supported_engine(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text() + result = (app.outdir / 'python.tex').read_text() print(result) assert 'script small e: e' in result assert 'double struck italic small i: i' in result @@ -1456,7 +1516,7 @@ def test_texescape_for_non_unicode_supported_engine(app, status, warning): confoverrides={'latex_engine': 'xelatex'}) def test_texescape_for_unicode_supported_engine(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text() + result = (app.outdir / 'python.tex').read_text() print(result) assert 'script small e: e' in result assert 'double struck italic small i: i' in result @@ -1468,7 +1528,7 @@ def test_texescape_for_unicode_supported_engine(app, status, warning): confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}}) def test_latex_elements_extrapackages(app, status, warning): app.builder.build_all() - result = (app.outdir / 'test.tex').text() + result = (app.outdir / 'test.tex').read_text() assert r'\usepackage{foo}' in result diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 22ed86e3e..54bde6b68 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -8,6 +8,8 @@ :license: BSD, see LICENSE for details. """ +import json +import re from unittest import mock import pytest @@ -17,10 +19,10 @@ def test_defaults(app, status, warning): app.builder.build_all() assert (app.outdir / 'output.txt').exists() - content = (app.outdir / 'output.txt').text() + content = (app.outdir / 'output.txt').read_text() print(content) - # looking for '#top' and 'does-not-exist' not found should fail + # looking for '#top' and '#does-not-exist' not found should fail assert "Anchor 'top' not found" in content assert "Anchor 'does-not-exist' not found" in content # looking for non-existent URL should fail @@ -31,6 +33,58 @@ def test_defaults(app, status, warning): assert len(content.splitlines()) == 5 +@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) +def test_defaults_json(app, status, warning): + app.builder.build_all() + + assert (app.outdir / 'output.json').exists() + content = (app.outdir / 'output.json').read_text() + print(content) + + rows = [json.loads(x) for x in content.splitlines()] + row = rows[0] + for attr in ["filename", "lineno", "status", "code", "uri", + "info"]: + assert attr in row + + assert len(content.splitlines()) == 8 + assert len(rows) == 8 + # the output order of the rows is not stable + # due to possible variance in network latency + rowsby = {row["uri"]:row for row in rows} + assert rowsby["https://www.google.com#!bar"] == { + 'filename': 'links.txt', + 'lineno': 10, + 'status': 'working', + 'code': 0, + 'uri': 'https://www.google.com#!bar', + 'info': '' + } + # looking for non-existent URL should fail + dnerow = rowsby['https://localhost:7777/doesnotexist'] + assert dnerow['filename'] == 'links.txt' + assert dnerow['lineno'] == 13 + assert dnerow['status'] == 'broken' + assert dnerow['code'] == 0 + assert dnerow['uri'] == 'https://localhost:7777/doesnotexist' + assert rowsby['https://www.google.com/image2.png'] == { + 'filename': 'links.txt', + 'lineno': 16, + 'status': 'broken', + 'code': 0, + 'uri': 'https://www.google.com/image2.png', + 'info': '404 Client Error: Not Found for url: https://www.google.com/image2.png' + } + # looking for '#top' and '#does-not-exist' not found should fail + assert "Anchor 'top' not found" == \ + rowsby["https://www.google.com/#top"]["info"] + assert "Anchor 'does-not-exist' not found" == \ + rowsby["http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist"]["info"] + # images should fail + assert "Not Found for url: https://www.google.com/image.png" in \ + rowsby["https://www.google.com/image.png"]["info"] + + @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck', freshenv=True, confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"], @@ -44,7 +98,7 @@ def test_anchors_ignored(app, status, warning): app.builder.build_all() assert (app.outdir / 'output.txt').exists() - content = (app.outdir / 'output.txt').text() + content = (app.outdir / 'output.txt').read_text() # expect all ok when excluding #top assert not content diff --git a/tests/test_build_manpage.py b/tests/test_build_manpage.py index 902c58131..c3c41a2ae 100644 --- a/tests/test_build_manpage.py +++ b/tests/test_build_manpage.py @@ -19,7 +19,7 @@ def test_all(app, status, warning): app.builder.build_all() assert (app.outdir / 'sphinxtests.1').exists() - content = (app.outdir / 'sphinxtests.1').text() + content = (app.outdir / 'sphinxtests.1').read_text() assert r'\fBprint \fP\fIi\fP\fB\en\fP' in content assert r'\fBmanpage\en\fP' in content @@ -33,7 +33,7 @@ def test_all(app, status, warning): @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() + content = (app.outdir / 'python.1').read_text() assert ('.sp\n' 'caption \\fItest\\fP rb\n' @@ -64,5 +64,5 @@ def test_default_man_pages(): @pytest.mark.sphinx('man', testroot='markup-rubric') def test_rubric(app, status, warning): app.build() - content = (app.outdir / 'python.1').text() + content = (app.outdir / 'python.1').read_text() assert 'This is a rubric\n' in content diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py index 247863bdb..378eaa192 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_build_texinfo.py @@ -49,7 +49,7 @@ def test_texinfo_warnings(app, status, warning): def test_texinfo(app, status, warning): TexinfoTranslator.ignore_missing_images = True app.builder.build_all() - result = (app.outdir / 'sphinxtests.texi').text(encoding='utf8') + result = (app.outdir / 'sphinxtests.texi').read_text() assert ('@anchor{markup doc}@anchor{11}' '@anchor{markup id1}@anchor{12}' '@anchor{markup testing-various-markup}@anchor{13}' in result) @@ -70,7 +70,7 @@ def test_texinfo(app, status, warning): def test_texinfo_rubric(app, status, warning): app.build() - output = (app.outdir / 'python.texi').text() + output = (app.outdir / 'python.texi').read_text() assert '@heading This is a rubric' in output assert '@heading This is a multiline rubric' in output @@ -79,7 +79,7 @@ def test_texinfo_rubric(app, status, warning): def test_texinfo_citation(app, status, warning): app.builder.build_all() - output = (app.outdir / 'python.texi').text() + output = (app.outdir / 'python.texi').read_text() assert 'This is a citation ref; @ref{1,,[CITE1]} and @ref{2,,[CITE2]}.' in output assert ('@anchor{index cite1}@anchor{1}@w{(CITE1)} \n' 'This is a citation\n') in output diff --git a/tests/test_build_text.py b/tests/test_build_text.py index 6dc155f80..8c00f5550 100644 --- a/tests/test_build_text.py +++ b/tests/test_build_text.py @@ -26,7 +26,7 @@ def with_text_app(*args, **kw): @with_text_app() def test_maxwitdh_with_prefix(app, status, warning): app.builder.build_update() - result = (app.outdir / 'maxwidth.txt').text() + result = (app.outdir / 'maxwidth.txt').read_text() lines = result.splitlines() line_widths = [column_width(line) for line in lines] @@ -49,7 +49,7 @@ def test_maxwitdh_with_prefix(app, status, warning): def test_lineblock(app, status, warning): # regression test for #1109: need empty line after line block app.builder.build_update() - result = (app.outdir / 'lineblock.txt').text() + result = (app.outdir / 'lineblock.txt').read_text() expect = ( "* one\n" "\n" @@ -64,7 +64,7 @@ def test_lineblock(app, status, warning): @with_text_app() def test_nonascii_title_line(app, status, warning): app.builder.build_update() - result = (app.outdir / 'nonascii_title.txt').text() + result = (app.outdir / 'nonascii_title.txt').read_text() expect_underline = '*********' result_underline = result.splitlines()[1].strip() assert expect_underline == result_underline @@ -73,7 +73,7 @@ def test_nonascii_title_line(app, status, warning): @with_text_app() def test_nonascii_table(app, status, warning): app.builder.build_update() - result = (app.outdir / 'nonascii_table.txt').text() + result = (app.outdir / 'nonascii_table.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] line_widths = [column_width(line) for line in lines] assert len(set(line_widths)) == 1 # same widths @@ -82,7 +82,7 @@ def test_nonascii_table(app, status, warning): @with_text_app() def test_nonascii_maxwidth(app, status, warning): app.builder.build_update() - result = (app.outdir / 'nonascii_maxwidth.txt').text() + result = (app.outdir / 'nonascii_maxwidth.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] line_widths = [column_width(line) for line in lines] assert max(line_widths) < MAXWIDTH @@ -126,7 +126,7 @@ def test_table_cell(): @with_text_app() def test_table_with_empty_cell(app, status, warning): app.builder.build_update() - result = (app.outdir / 'table.txt').text() + result = (app.outdir / 'table.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" assert lines[1] == "| XXX | XXX |" @@ -140,7 +140,7 @@ def test_table_with_empty_cell(app, status, warning): @with_text_app() def test_table_with_rowspan(app, status, warning): app.builder.build_update() - result = (app.outdir / 'table_rowspan.txt').text() + result = (app.outdir / 'table_rowspan.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" assert lines[1] == "| XXXXXXXXX |" @@ -154,7 +154,7 @@ def test_table_with_rowspan(app, status, warning): @with_text_app() def test_table_with_colspan(app, status, warning): app.builder.build_update() - result = (app.outdir / 'table_colspan.txt').text() + result = (app.outdir / 'table_colspan.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" assert lines[1] == "| XXX | XXX |" @@ -168,7 +168,7 @@ def test_table_with_colspan(app, status, warning): @with_text_app() def test_table_with_colspan_left(app, status, warning): app.builder.build_update() - result = (app.outdir / 'table_colspan_left.txt').text() + result = (app.outdir / 'table_colspan_left.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" assert lines[1] == "| XXX | XXX |" @@ -182,7 +182,7 @@ def test_table_with_colspan_left(app, status, warning): @with_text_app() def test_table_with_colspan_and_rowspan(app, status, warning): app.builder.build_update() - result = (app.outdir / 'table_colspan_and_rowspan.txt').text() + result = (app.outdir / 'table_colspan_and_rowspan.txt').read_text() lines = [line.strip() for line in result.splitlines() if line.strip()] assert result assert lines[0] == "+-------+-------+-------+" @@ -197,7 +197,7 @@ def test_table_with_colspan_and_rowspan(app, status, warning): @with_text_app() def test_list_items_in_admonition(app, status, warning): app.builder.build_update() - result = (app.outdir / 'listitems.txt').text() + result = (app.outdir / 'listitems.txt').read_text() lines = [line.rstrip() for line in result.splitlines()] assert lines[0] == "See also:" assert lines[1] == "" @@ -209,7 +209,7 @@ def test_list_items_in_admonition(app, status, warning): @with_text_app() def test_secnums(app, status, warning): app.builder.build_all() - index = (app.outdir / 'index.txt').text(encoding='utf8') + index = (app.outdir / 'index.txt').read_text() lines = index.splitlines() assert lines[0] == "* 1. Section A" assert lines[1] == "" @@ -218,7 +218,7 @@ def test_secnums(app, status, warning): assert lines[4] == " * 2.1. Sub Ba" assert lines[5] == "" assert lines[6] == " * 2.2. Sub Bb" - doc2 = (app.outdir / 'doc2.txt').text(encoding='utf8') + doc2 = (app.outdir / 'doc2.txt').read_text() expect = ( "2. Section B\n" "************\n" @@ -235,7 +235,7 @@ def test_secnums(app, status, warning): app.config.text_secnumber_suffix = " " app.builder.build_all() - index = (app.outdir / 'index.txt').text(encoding='utf8') + index = (app.outdir / 'index.txt').read_text() lines = index.splitlines() assert lines[0] == "* 1 Section A" assert lines[1] == "" @@ -244,7 +244,7 @@ def test_secnums(app, status, warning): assert lines[4] == " * 2.1 Sub Ba" assert lines[5] == "" assert lines[6] == " * 2.2 Sub Bb" - doc2 = (app.outdir / 'doc2.txt').text(encoding='utf8') + doc2 = (app.outdir / 'doc2.txt').read_text() expect = ( "2 Section B\n" "***********\n" @@ -261,7 +261,7 @@ def test_secnums(app, status, warning): app.config.text_add_secnumbers = False app.builder.build_all() - index = (app.outdir / 'index.txt').text(encoding='utf8') + index = (app.outdir / 'index.txt').read_text() lines = index.splitlines() assert lines[0] == "* Section A" assert lines[1] == "" @@ -270,7 +270,7 @@ def test_secnums(app, status, warning): assert lines[4] == " * Sub Ba" assert lines[5] == "" assert lines[6] == " * Sub Bb" - doc2 = (app.outdir / 'doc2.txt').text(encoding='utf8') + doc2 = (app.outdir / 'doc2.txt').read_text() expect = ( "Section B\n" "*********\n" diff --git a/tests/test_correct_year.py b/tests/test_correct_year.py index cbba75946..7dc3ea89d 100644 --- a/tests/test_correct_year.py +++ b/tests/test_correct_year.py @@ -32,5 +32,5 @@ def expect_date(request, monkeypatch): @pytest.mark.sphinx('html', testroot='correct-year') def test_correct_year(expect_date, app): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert expect_date in content diff --git a/tests/test_directive_code.py b/tests/test_directive_code.py index 7aeef81ec..eda331645 100644 --- a/tests/test_directive_code.py +++ b/tests/test_directive_code.py @@ -36,7 +36,7 @@ def test_LiteralIncludeReader(literal_inc_path): options = {'lineno-match': True} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) content, lines = reader.read() - assert content == literal_inc_path.text() + assert content == literal_inc_path.read_text() assert lines == 13 assert reader.lineno_start == 1 @@ -46,7 +46,7 @@ def test_LiteralIncludeReader_lineno_start(literal_inc_path): options = {'lineno-start': 4} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) content, lines = reader.read() - assert content == literal_inc_path.text() + assert content == literal_inc_path.read_text() assert lines == 13 assert reader.lineno_start == 4 @@ -324,7 +324,7 @@ def test_force_option(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_code_block_caption_html(app, status, warning): app.builder.build(['caption']) - html = (app.outdir / 'caption.html').text() + html = (app.outdir / 'caption.html').read_text() caption = ('<div class="code-block-caption">' '<span class="caption-number">Listing 1 </span>' '<span class="caption-text">caption <em>test</em> rb' @@ -336,7 +336,7 @@ def test_code_block_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_caption_latex(app, status, warning): app.builder.build_all() - latex = (app.outdir / 'python.tex').text() + latex = (app.outdir / 'python.tex').read_text() caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstyleemphasis{test} rb}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id1}}}' link = '\\hyperref[\\detokenize{caption:name-test-rb}]' \ @@ -349,7 +349,7 @@ def test_code_block_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_namedlink_latex(app, status, warning): app.builder.build_all() - latex = (app.outdir / 'python.tex').text() + latex = (app.outdir / 'python.tex').read_text() label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-rb}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-rb}]'\ '{\\sphinxcrossref{\\DUrole{std,std-ref}{Ruby}}' @@ -366,7 +366,7 @@ def test_code_block_namedlink_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_emphasize_latex(app, status, warning): app.builder.build(['emphasize']) - latex = (app.outdir / 'python.tex').text().replace('\r\n', '\n') + latex = (app.outdir / 'python.tex').read_text().replace('\r\n', '\n') includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n' assert includes in latex includes = '\\end{sphinxVerbatim}\n\\sphinxresetverbatimhllines\n' @@ -379,7 +379,7 @@ def test_literal_include(app, status, warning): et = etree_parse(app.outdir / 'index.xml') secs = et.findall('./section/section') literal_include = secs[1].findall('literal_block') - literal_src = (app.srcdir / 'literal.inc').text() + literal_src = (app.srcdir / 'literal.inc').read_text() assert len(literal_include) > 0 actual = literal_include[0].text assert actual == literal_src @@ -412,7 +412,7 @@ def test_literal_include_block_start_with_comment_or_brank(app, status, warning) @pytest.mark.sphinx('html', testroot='directive-code') def test_literal_include_linenos(app, status, warning): app.builder.build(['linenos']) - html = (app.outdir / 'linenos.html').text() + html = (app.outdir / 'linenos.html').read_text() # :linenos: assert ('<td class="linenos"><div class="linenodiv"><pre>' @@ -458,7 +458,7 @@ def test_literal_include_linenos(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_file_whole_of_emptyline(app, status, warning): app.builder.build_all() - latex = (app.outdir / 'python.tex').text().replace('\r\n', '\n') + latex = (app.outdir / 'python.tex').read_text().replace('\r\n', '\n') includes = ( '\\begin{sphinxVerbatim}' '[commandchars=\\\\\\{\\},numbers=left,firstnumber=1,stepnumber=1]\n' @@ -472,7 +472,7 @@ def test_literalinclude_file_whole_of_emptyline(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_literalinclude_caption_html(app, status, warning): app.builder.build('index') - html = (app.outdir / 'caption.html').text() + html = (app.outdir / 'caption.html').read_text() caption = ('<div class="code-block-caption">' '<span class="caption-number">Listing 2 </span>' '<span class="caption-text">caption <strong>test</strong> py' @@ -484,7 +484,7 @@ def test_literalinclude_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_caption_latex(app, status, warning): app.builder.build('index') - latex = (app.outdir / 'python.tex').text() + latex = (app.outdir / 'python.tex').read_text() caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstylestrong{test} py}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id2}}}' link = '\\hyperref[\\detokenize{caption:name-test-py}]' \ @@ -497,7 +497,7 @@ def test_literalinclude_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_namedlink_latex(app, status, warning): app.builder.build('index') - latex = (app.outdir / 'python.tex').text() + latex = (app.outdir / 'python.tex').read_text() label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-py}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-py}]'\ '{\\sphinxcrossref{\\DUrole{std,std-ref}{Python}}' @@ -584,7 +584,7 @@ def test_code_block_highlighted(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_linenothreshold(app, status, warning): app.builder.build(['linenothreshold']) - html = (app.outdir / 'linenothreshold.html').text() + html = (app.outdir / 'linenothreshold.html').read_text() lineos_head = '<td class="linenos"><div class="linenodiv"><pre>' lineos_tail = '</pre></div></td>' diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py new file mode 100644 index 000000000..6d4ba9cea --- /dev/null +++ b/tests/test_domain_c.py @@ -0,0 +1,497 @@ +""" + test_domain_c + ~~~~~~~~~~~~~ + + Tests the C Domain + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re + +import pytest + +from docutils import nodes +import sphinx.domains.c as cDomain +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, desc_type, + pending_xref +) +from sphinx.domains.c import DefinitionParser, DefinitionError +from sphinx.domains.c import _max_id, _id_prefix, Symbol +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node +from sphinx.util import docutils + + +def parse(name, string): + parser = DefinitionParser(string, location=None) + parser.allowFallbackExpressionParsing = False + ast = parser.parse_declaration(name, name) + parser.assert_end() + return ast + + +def check(name, input, idDict, output=None): + # first a simple check of the AST + if output is None: + output = input + ast = parse(name, input) + res = str(ast) + if res != output: + print("") + print("Input: ", input) + print("Result: ", res) + print("Expected: ", output) + raise DefinitionError("") + rootSymbol = Symbol(None, None, None, None) + symbol = rootSymbol.add_declaration(ast, docname="TestDoc") + parentNode = addnodes.desc() + signode = addnodes.desc_signature(input, '') + parentNode += signode + ast.describe_signature(signode, 'lastIsName', symbol, options={}) + + idExpected = [None] + for i in range(1, _max_id + 1): + if i in idDict: + idExpected.append(idDict[i]) + else: + idExpected.append(idExpected[i - 1]) + idActual = [None] + for i in range(1, _max_id + 1): + #try: + id = ast.get_id(version=i) + assert id is not None + idActual.append(id[len(_id_prefix[i]):]) + #except NoOldIdError: + # idActual.append(None) + + res = [True] + for i in range(1, _max_id + 1): + res.append(idExpected[i] == idActual[i]) + + if not all(res): + print("input: %s" % input.rjust(20)) + for i in range(1, _max_id + 1): + if res[i]: + continue + print("Error in id version %d." % i) + print("result: %s" % idActual[i]) + print("expected: %s" % idExpected[i]) + #print(rootSymbol.dump(0)) + raise DefinitionError("") + + +def test_expressions(): + def exprCheck(expr, output=None): + parser = DefinitionParser(expr, location=None) + parser.allowFallbackExpressionParsing = False + ast = parser.parse_expression() + parser.assert_end() + # first a simple check of the AST + if output is None: + output = expr + res = str(ast) + if res != output: + print("") + print("Input: ", input) + print("Result: ", res) + print("Expected: ", output) + raise DefinitionError("") + # type expressions + exprCheck('int*') + exprCheck('int *const*') + exprCheck('int *volatile*') + exprCheck('int *restrict*') + exprCheck('int *(*)(double)') + exprCheck('const int*') + exprCheck('__int64') + exprCheck('unsigned __int64') + + # actual expressions + + # primary + exprCheck('true') + exprCheck('false') + ints = ['5', '0', '075', '0x0123456789ABCDEF', '0XF', '0b1', '0B1'] + unsignedSuffix = ['', 'u', 'U'] + longSuffix = ['', 'l', 'L', 'll', 'LL'] + for i in ints: + for u in unsignedSuffix: + for l in longSuffix: + expr = i + u + l + exprCheck(expr) + expr = i + l + u + exprCheck(expr) + for suffix in ['', 'f', 'F', 'l', 'L']: + for e in [ + '5e42', '5e+42', '5e-42', + '5.', '5.e42', '5.e+42', '5.e-42', + '.5', '.5e42', '.5e+42', '.5e-42', + '5.0', '5.0e42', '5.0e+42', '5.0e-42']: + expr = e + suffix + exprCheck(expr) + for e in [ + 'ApF', 'Ap+F', 'Ap-F', + 'A.', 'A.pF', 'A.p+F', 'A.p-F', + '.A', '.ApF', '.Ap+F', '.Ap-F', + 'A.B', 'A.BpF', 'A.Bp+F', 'A.Bp-F']: + expr = "0x" + e + suffix + exprCheck(expr) + exprCheck('"abc\\"cba"') # string + # character literals + for p in ['', 'u8', 'u', 'U', 'L']: + exprCheck(p + "'a'") + exprCheck(p + "'\\n'") + exprCheck(p + "'\\012'") + exprCheck(p + "'\\0'") + exprCheck(p + "'\\x0a'") + exprCheck(p + "'\\x0A'") + exprCheck(p + "'\\u0a42'") + exprCheck(p + "'\\u0A42'") + exprCheck(p + "'\\U0001f34c'") + exprCheck(p + "'\\U0001F34C'") + + exprCheck('(5)') + exprCheck('C') + # postfix + exprCheck('A(2)') + exprCheck('A[2]') + exprCheck('a.b.c') + exprCheck('a->b->c') + exprCheck('i++') + exprCheck('i--') + # unary + exprCheck('++5') + exprCheck('--5') + exprCheck('*5') + exprCheck('&5') + exprCheck('+5') + exprCheck('-5') + exprCheck('!5') + exprCheck('~5') + exprCheck('sizeof(T)') + exprCheck('sizeof -42') + exprCheck('alignof(T)') + # cast + exprCheck('(int)2') + # binary op + exprCheck('5 || 42') + exprCheck('5 && 42') + exprCheck('5 | 42') + exprCheck('5 ^ 42') + exprCheck('5 & 42') + # ['==', '!='] + exprCheck('5 == 42') + exprCheck('5 != 42') + # ['<=', '>=', '<', '>'] + exprCheck('5 <= 42') + exprCheck('5 >= 42') + exprCheck('5 < 42') + exprCheck('5 > 42') + # ['<<', '>>'] + exprCheck('5 << 42') + exprCheck('5 >> 42') + # ['+', '-'] + exprCheck('5 + 42') + exprCheck('5 - 42') + # ['*', '/', '%'] + exprCheck('5 * 42') + exprCheck('5 / 42') + exprCheck('5 % 42') + # ['.*', '->*'] + # conditional + # TODO + # assignment + exprCheck('a = 5') + exprCheck('a *= 5') + exprCheck('a /= 5') + exprCheck('a %= 5') + exprCheck('a += 5') + exprCheck('a -= 5') + exprCheck('a >>= 5') + exprCheck('a <<= 5') + exprCheck('a &= 5') + exprCheck('a ^= 5') + exprCheck('a |= 5') + + +def test_type_definitions(): + check('type', "T", {1: "T"}) + + check('type', "bool *b", {1: 'b'}) + check('type', "bool *const b", {1: 'b'}) + check('type', "bool *const *b", {1: 'b'}) + check('type', "bool *volatile *b", {1: 'b'}) + check('type', "bool *restrict *b", {1: 'b'}) + check('type', "bool *volatile const b", {1: 'b'}) + check('type', "bool *volatile const b", {1: 'b'}) + check('type', "bool *volatile const *b", {1: 'b'}) + check('type', "bool b[]", {1: 'b'}) + check('type', "long long int foo", {1: 'foo'}) + # test decl specs on right + check('type', "bool const b", {1: 'b'}) + + # from breathe#267 (named function parameters for function pointers + check('type', 'void (*gpio_callback_t)(struct device *port, uint32_t pin)', + {1: 'gpio_callback_t'}) + + +def test_macro_definitions(): + check('macro', 'M', {1: 'M'}) + check('macro', 'M()', {1: 'M'}) + check('macro', 'M(arg)', {1: 'M'}) + check('macro', 'M(arg1, arg2)', {1: 'M'}) + check('macro', 'M(arg1, arg2, arg3)', {1: 'M'}) + check('macro', 'M(...)', {1: 'M'}) + check('macro', 'M(arg, ...)', {1: 'M'}) + check('macro', 'M(arg1, arg2, ...)', {1: 'M'}) + check('macro', 'M(arg1, arg2, arg3, ...)', {1: 'M'}) + + +def test_member_definitions(): + check('member', 'void a', {1: 'a'}) + check('member', '_Bool a', {1: 'a'}) + check('member', 'bool a', {1: 'a'}) + check('member', 'char a', {1: 'a'}) + check('member', 'int a', {1: 'a'}) + check('member', 'float a', {1: 'a'}) + check('member', 'double a', {1: 'a'}) + + check('member', 'unsigned long a', {1: 'a'}) + check('member', '__int64 a', {1: 'a'}) + check('member', 'unsigned __int64 a', {1: 'a'}) + + check('member', 'int .a', {1: 'a'}) + + check('member', 'int *a', {1: 'a'}) + check('member', 'int **a', {1: 'a'}) + check('member', 'const int a', {1: 'a'}) + check('member', 'volatile int a', {1: 'a'}) + check('member', 'restrict int a', {1: 'a'}) + check('member', 'volatile const int a', {1: 'a'}) + check('member', 'restrict const int a', {1: 'a'}) + check('member', 'restrict volatile int a', {1: 'a'}) + check('member', 'restrict volatile const int a', {1: 'a'}) + + check('member', 'T t', {1: 't'}) + + check('member', 'int a[]', {1: 'a'}) + + check('member', 'int (*p)[]', {1: 'p'}) + + check('member', 'int a[42]', {1: 'a'}) + check('member', 'int a = 42', {1: 'a'}) + check('member', 'T a = {}', {1: 'a'}) + check('member', 'T a = {1}', {1: 'a'}) + check('member', 'T a = {1, 2}', {1: 'a'}) + check('member', 'T a = {1, 2, 3}', {1: 'a'}) + + # test from issue #1539 + check('member', 'CK_UTF8CHAR model[16]', {1: 'model'}) + + check('member', 'auto int a', {1: 'a'}) + check('member', 'register int a', {1: 'a'}) + check('member', 'extern int a', {1: 'a'}) + check('member', 'static int a', {1: 'a'}) + + check('member', 'thread_local int a', {1: 'a'}) + check('member', '_Thread_local int a', {1: 'a'}) + check('member', 'extern thread_local int a', {1: 'a'}) + check('member', 'thread_local extern int a', {1: 'a'}, + 'extern thread_local int a') + check('member', 'static thread_local int a', {1: 'a'}) + check('member', 'thread_local static int a', {1: 'a'}, + 'static thread_local int a') + + check('member', 'int b : 3', {1: 'b'}) + + +def test_function_definitions(): + check('function', 'void f()', {1: 'f'}) + check('function', 'void f(int)', {1: 'f'}) + check('function', 'void f(int i)', {1: 'f'}) + check('function', 'void f(int i, int j)', {1: 'f'}) + check('function', 'void f(...)', {1: 'f'}) + check('function', 'void f(int i, ...)', {1: 'f'}) + check('function', 'void f(struct T)', {1: 'f'}) + check('function', 'void f(struct T t)', {1: 'f'}) + check('function', 'void f(union T)', {1: 'f'}) + check('function', 'void f(union T t)', {1: 'f'}) + check('function', 'void f(enum T)', {1: 'f'}) + check('function', 'void f(enum T t)', {1: 'f'}) + + # test from issue #1539 + check('function', 'void f(A x[])', {1: 'f'}) + + # test from issue #2377 + check('function', 'void (*signal(int sig, void (*func)(int)))(int)', {1: 'signal'}) + + check('function', 'extern void f()', {1: 'f'}) + check('function', 'static void f()', {1: 'f'}) + check('function', 'inline void f()', {1: 'f'}) + + # tests derived from issue #1753 (skip to keep sanity) + check('function', "void f(float *q(double))", {1: 'f'}) + check('function', "void f(float *(*q)(double))", {1: 'f'}) + check('function', "void f(float (*q)(double))", {1: 'f'}) + check('function', "int (*f(double d))(float)", {1: 'f'}) + check('function', "int (*f(bool b))[5]", {1: 'f'}) + check('function', "void f(int *const p)", {1: 'f'}) + check('function', "void f(int *volatile const p)", {1: 'f'}) + + # from breathe#223 + check('function', 'void f(struct E e)', {1: 'f'}) + check('function', 'void f(enum E e)', {1: 'f'}) + check('function', 'void f(union E e)', {1: 'f'}) + + +def test_union_definitions(): + check('struct', 'A', {1: 'A'}) + + +def test_union_definitions(): + check('union', 'A', {1: 'A'}) + + +def test_enum_definitions(): + check('enum', 'A', {1: 'A'}) + + check('enumerator', 'A', {1: 'A'}) + check('enumerator', 'A = 42', {1: 'A'}) + + +def test_anon_definitions(): + return # TODO + check('class', '@a', {3: "Ut1_a"}) + check('union', '@a', {3: "Ut1_a"}) + check('enum', '@a', {3: "Ut1_a"}) + check('class', '@1', {3: "Ut1_1"}) + check('class', '@a::A', {3: "NUt1_a1AE"}) + + +def test_initializers(): + idsMember = {1: 'v'} + idsFunction = {1: 'f'} + # no init + check('member', 'T v', idsMember) + check('function', 'void f(T v)', idsFunction) + # with '=', assignment-expression + check('member', 'T v = 42', idsMember) + check('function', 'void f(T v = 42)', idsFunction) + # with '=', braced-init + check('member', 'T v = {}', idsMember) + check('function', 'void f(T v = {})', idsFunction) + check('member', 'T v = {42, 42, 42}', idsMember) + check('function', 'void f(T v = {42, 42, 42})', idsFunction) + check('member', 'T v = {42, 42, 42,}', idsMember) + check('function', 'void f(T v = {42, 42, 42,})', idsFunction) + # TODO: designator-list + + +def test_attributes(): + return # TODO + # style: C++ + check('member', '[[]] int f', {1: 'f__i', 2: '1f'}) + check('member', '[ [ ] ] int f', {1: 'f__i', 2: '1f'}, + # this will fail when the proper grammar is implemented + output='[[ ]] int f') + check('member', '[[a]] int f', {1: 'f__i', 2: '1f'}) + # style: GNU + check('member', '__attribute__(()) int f', {1: 'f__i', 2: '1f'}) + check('member', '__attribute__((a)) int f', {1: 'f__i', 2: '1f'}) + check('member', '__attribute__((a, b)) int f', {1: 'f__i', 2: '1f'}) + # style: user-defined id + check('member', 'id_attr int f', {1: 'f__i', 2: '1f'}) + # style: user-defined paren + check('member', 'paren_attr() int f', {1: 'f__i', 2: '1f'}) + check('member', 'paren_attr(a) int f', {1: 'f__i', 2: '1f'}) + check('member', 'paren_attr("") int f', {1: 'f__i', 2: '1f'}) + check('member', 'paren_attr(()[{}][]{}) int f', {1: 'f__i', 2: '1f'}) + with pytest.raises(DefinitionError): + parse('member', 'paren_attr(() int f') + with pytest.raises(DefinitionError): + parse('member', 'paren_attr([) int f') + with pytest.raises(DefinitionError): + parse('member', 'paren_attr({) int f') + with pytest.raises(DefinitionError): + parse('member', 'paren_attr([)]) int f') + with pytest.raises(DefinitionError): + parse('member', 'paren_attr((])) int f') + with pytest.raises(DefinitionError): + parse('member', 'paren_attr({]}) int f') + + # position: decl specs + check('function', 'static inline __attribute__(()) void f()', + {1: 'f', 2: '1fv'}, + output='__attribute__(()) static inline void f()') + check('function', '[[attr1]] [[attr2]] void f()', + {1: 'f', 2: '1fv'}, + output='[[attr1]] [[attr2]] void f()') + # position: declarator + check('member', 'int *[[attr]] i', {1: 'i__iP', 2: '1i'}) + check('member', 'int *const [[attr]] volatile i', {1: 'i__iPVC', 2: '1i'}, + output='int *[[attr]] volatile const i') + check('member', 'int &[[attr]] i', {1: 'i__iR', 2: '1i'}) + check('member', 'int *[[attr]] *i', {1: 'i__iPP', 2: '1i'}) + + +# def test_print(): +# # used for getting all the ids out for checking +# for a in ids: +# print(a) +# raise DefinitionError("") + + +def filter_warnings(warning, file): + lines = warning.getvalue().split("\n"); + res = [l for l in lines if "domain-c" in l and "{}.rst".format(file) in l and + "WARNING: document isn't included in any toctree" not in l] + print("Filtered warnings for file '{}':".format(file)) + for w in res: + print(w) + return res + + +@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) +def test_build_domain_c(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "index") + assert len(ws) == 0 + + +def test_cfunction(app): + text = (".. c:function:: PyObject* " + "PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)") + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + + domain = app.env.get_domain('c') + entry = domain.objects.get('PyType_GenericAlloc') + assert entry == ('index', 'c.PyType_GenericAlloc', 'function') + + +def test_cmember(app): + text = ".. c:member:: PyObject* PyTypeObject.tp_bases" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1], addnodes.desc, desctype="member", + domain="c", objtype="member", noindex=False) + + domain = app.env.get_domain('c') + entry = domain.objects.get('PyTypeObject.tp_bases') + assert entry == ('index', 'c.PyTypeObject.tp_bases', 'member') + + +def test_cvar(app): + text = ".. c:var:: PyObject* PyClass_Type" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1], addnodes.desc, desctype="var", + domain="c", objtype="var", noindex=False) + + domain = app.env.get_domain('c') + entry = domain.objects.get('PyClass_Type') + assert entry == ('index', 'c.PyClass_Type', 'var') diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index b2cbf3035..aa8bb97b2 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -23,7 +23,8 @@ def parse(name, string): class Config: cpp_id_attributes = ["id_attr"] cpp_paren_attributes = ["paren_attr"] - parser = DefinitionParser(string, None, Config()) + parser = DefinitionParser(string, location=None, + config=Config()) parser.allowFallbackExpressionParsing = False ast = parser.parse_declaration(name, name) parser.assert_end() @@ -110,6 +111,21 @@ def test_expressions(): if id4 is not None: idDict[4] = ids % id4 check('class', 'template<> C<a[%s]>' % expr, idDict) + + class Config: + cpp_id_attributes = ["id_attr"] + cpp_paren_attributes = ["paren_attr"] + + parser = DefinitionParser(expr, location=None, + config=Config()) + parser.allowFallbackExpressionParsing = False + ast = parser.parse_expression() + res = str(ast) + if res != expr: + print("") + print("Input: ", expr) + print("Result: ", res) + raise DefinitionError("") # primary exprCheck('nullptr', 'LDnE') exprCheck('true', 'L1E') @@ -214,11 +230,14 @@ def test_expressions(): exprCheck('5 != 42', 'neL5EL42E') # ['<=', '>=', '<', '>'] exprCheck('5 <= 42', 'leL5EL42E') + exprCheck('A <= 42', 'le1AL42E') exprCheck('5 >= 42', 'geL5EL42E') exprCheck('5 < 42', 'ltL5EL42E') + exprCheck('A < 42', 'lt1AL42E') exprCheck('5 > 42', 'gtL5EL42E') # ['<<', '>>'] exprCheck('5 << 42', 'lsL5EL42E') + exprCheck('A << 42', 'ls1AL42E') exprCheck('5 >> 42', 'rsL5EL42E') # ['+', '-'] exprCheck('5 + 42', 'plL5EL42E') @@ -442,6 +461,13 @@ def test_function_definitions(): with pytest.raises(DefinitionError): parse('function', 'int foo(D d=x(a') check('function', 'int foo(const A&... a)', {1: "foo__ACRDp", 2: "3fooDpRK1A"}) + check('function', 'int foo(const A&...)', {1: "foo__ACRDp", 2: "3fooDpRK1A"}) + check('function', 'int foo(const A*... a)', {1: "foo__ACPDp", 2: "3fooDpPK1A"}) + check('function', 'int foo(const A*...)', {1: "foo__ACPDp", 2: "3fooDpPK1A"}) + check('function', 'int foo(const int A::*... a)', {2: "3fooDpM1AKi"}) + check('function', 'int foo(const int A::*...)', {2: "3fooDpM1AKi"}) + #check('function', 'int foo(int (*a)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) + #check('function', 'int foo(int (*)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) check('function', 'virtual void f()', {1: "f", 2: "1fv"}) # test for ::nestedName, from issue 1738 check("function", "result(int val, ::std::error_category const &cat)", @@ -483,18 +509,21 @@ def test_function_definitions(): check('function', 'void f(int C::*)', {2: '1fM1Ci'}) check('function', 'void f(int C::* p)', {2: '1fM1Ci'}) check('function', 'void f(int ::C::* p)', {2: '1fM1Ci'}) - check('function', 'void f(int C::* const)', {2: '1fKM1Ci'}) - check('function', 'void f(int C::* const&)', {2: '1fRKM1Ci'}) - check('function', 'void f(int C::* volatile)', {2: '1fVM1Ci'}) - check('function', 'void f(int C::* const volatile)', {2: '1fVKM1Ci'}, - output='void f(int C::* volatile const)') - check('function', 'void f(int C::* volatile const)', {2: '1fVKM1Ci'}) + check('function', 'void f(int C::*const)', {2: '1fKM1Ci'}) + check('function', 'void f(int C::*const&)', {2: '1fRKM1Ci'}) + check('function', 'void f(int C::*volatile)', {2: '1fVM1Ci'}) + check('function', 'void f(int C::*const volatile)', {2: '1fVKM1Ci'}, + output='void f(int C::*volatile const)') + check('function', 'void f(int C::*volatile const)', {2: '1fVKM1Ci'}) check('function', 'void f(int (C::*)(float, double))', {2: '1fM1CFifdE'}) check('function', 'void f(int (C::* p)(float, double))', {2: '1fM1CFifdE'}) check('function', 'void f(int (::C::* p)(float, double))', {2: '1fM1CFifdE'}) check('function', 'void f(void (C::*)() const &)', {2: '1fM1CKRFvvE'}) check('function', 'int C::* f(int, double)', {2: '1fid'}) - check('function', 'void f(int C::* *)', {2: '1fPM1Ci'}) + check('function', 'void f(int C::* *p)', {2: '1fPM1Ci'}) + check('function', 'void f(int C::**)', {2: '1fPM1Ci'}) + check('function', 'void f(int C::*const *p)', {2: '1fPKM1Ci'}) + check('function', 'void f(int C::*const*)', {2: '1fPKM1Ci'}) # exceptions from return type mangling check('function', 'template<typename T> C()', {2: 'I0E1Cv'}) @@ -770,7 +799,8 @@ def test_xref_parsing(): class Config: cpp_id_attributes = ["id_attr"] cpp_paren_attributes = ["paren_attr"] - parser = DefinitionParser(target, None, Config()) + parser = DefinitionParser(target, location=None, + config=Config()) ast, isShorthand = parser.parse_xref_object() parser.assert_end() check('f') @@ -796,6 +826,41 @@ def filter_warnings(warning, file): return res +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_build_domain_cpp_multi_decl_lookup(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "lookup-key-overload") + assert len(ws) == 0 + + ws = filter_warnings(warning, "multi-decl-lookup") + assert len(ws) == 0 + + +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_build_domain_cpp_warn_template_param_qualified_name(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "warn-template-param-qualified-name") + assert len(ws) == 2 + assert "WARNING: cpp:type reference target not found: T::typeWarn" in ws[0] + assert "WARNING: cpp:type reference target not found: T::U::typeWarn" in ws[1] + + +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_build_domain_cpp_backslash_ok(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "backslash") + assert len(ws) == 0 + + +@pytest.mark.sphinx(testroot='domain-cpp', + confoverrides={'nitpicky': True, 'strip_signature_backslash': True}) +def test_build_domain_cpp_backslash_ok(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "backslash") + assert len(ws) == 1 + assert "WARNING: Parsing of expression failed. Using fallback parser." in ws[0] + + @pytest.mark.sphinx(testroot='domain-cpp') def test_build_domain_cpp_misuse_of_roles(app, status, warning): app.builder.build_all() @@ -872,14 +937,14 @@ def test_build_domain_cpp_with_add_function_parentheses_is_True(app, status, war ] f = 'roles.html' - t = (app.outdir / f).text() + t = (app.outdir / f).read_text() for s in rolePatterns: check(s, t, f) for s in parenPatterns: check(s, t, f) f = 'any-role.html' - t = (app.outdir / f).text() + t = (app.outdir / f).read_text() for s in parenPatterns: check(s, t, f) @@ -915,14 +980,14 @@ def test_build_domain_cpp_with_add_function_parentheses_is_False(app, status, wa ] f = 'roles.html' - t = (app.outdir / f).text() + t = (app.outdir / f).read_text() for s in rolePatterns: check(s, t, f) for s in parenPatterns: check(s, t, f) f = 'any-role.html' - t = (app.outdir / f).text() + t = (app.outdir / f).read_text() for s in parenPatterns: check(s, t, f) @@ -932,7 +997,7 @@ def test_xref_consistency(app, status, warning): app.builder.build_all() test = 'xref_consistency.html' - output = (app.outdir / test).text() + output = (app.outdir / test).read_text() def classes(role, tag): pattern = (r'{role}-role:.*?' diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 581856502..f7bacb90e 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -14,7 +14,12 @@ import pytest from docutils import nodes from sphinx import addnodes +from sphinx.addnodes import ( + desc, desc_annotation, desc_content, desc_name, + desc_parameter, desc_parameterlist, desc_signature +) from sphinx.domains.javascript import JavaScriptDomain +from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -86,22 +91,22 @@ def test_domain_js_objects(app, status, warning): assert 'module_b.submodule' in modules assert 'module_b.submodule' in objects - assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class') - assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method') - assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method') - assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class') + assert objects['module_a.submodule.ModTopLevel'][2] == 'class' + assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method' + assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method' + assert objects['module_b.submodule.ModTopLevel'][2] == 'class' - assert objects['TopLevel'] == ('roles', 'class') - assert objects['top_level'] == ('roles', 'function') - assert objects['NestedParentA'] == ('roles', 'class') - assert objects['NestedParentA.child_1'] == ('roles', 'function') - assert objects['NestedParentA.any_child'] == ('roles', 'function') - assert objects['NestedParentA.NestedChildA'] == ('roles', 'class') - assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'function') - assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'function') - assert objects['NestedParentA.child_2'] == ('roles', 'function') - assert objects['NestedParentB'] == ('roles', 'class') - assert objects['NestedParentB.child_1'] == ('roles', 'function') + assert objects['TopLevel'][2] == 'class' + assert objects['top_level'][2] == 'function' + assert objects['NestedParentA'][2] == 'class' + assert objects['NestedParentA.child_1'][2] == 'function' + assert objects['NestedParentA.any_child'][2] == 'function' + assert objects['NestedParentA.NestedChildA'][2] == 'class' + assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'function' + assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'function' + assert objects['NestedParentA.child_2'][2] == 'function' + assert objects['NestedParentB'][2] == 'class' + assert objects['NestedParentB.child_1'][2] == 'function' @pytest.mark.sphinx('dummy', testroot='domain-js') @@ -115,21 +120,28 @@ def test_domain_js_find_obj(app, status, warning): assert (find_obj(None, None, 'NONEXISTANT', 'class') == (None, None)) assert (find_obj(None, None, 'NestedParentA', 'class') == - ('NestedParentA', ('roles', 'class'))) + ('NestedParentA', ('roles', 'nestedparenta', 'class'))) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == - ('NestedParentA.NestedChildA', ('roles', 'class'))) + ('NestedParentA.NestedChildA', + ('roles', 'nestedparenta.nestedchilda', 'class'))) assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == - ('NestedParentA.NestedChildA', ('roles', 'class'))) + ('NestedParentA.NestedChildA', + ('roles', 'nestedparenta.nestedchilda', 'class'))) assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'func') == - ('NestedParentA.NestedChildA.subchild_1', ('roles', 'function'))) + ('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'func') == - ('NestedParentA.NestedChildA.subchild_1', ('roles', 'function'))) + ('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'func') == - ('NestedParentA.NestedChildA.subchild_1', ('roles', 'function'))) + ('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'function'))) assert (find_obj('module_a.submodule', 'ModTopLevel', 'mod_child_2', 'meth') == - ('module_a.submodule.ModTopLevel.mod_child_2', ('module', 'method'))) + ('module_a.submodule.ModTopLevel.mod_child_2', + ('module', 'module_a.submodule.modtoplevel.mod_child_2', 'method'))) assert (find_obj('module_b.submodule', 'ModTopLevel', 'module_a.submodule', 'mod') == - ('module_a.submodule', ('module', 'module'))) + ('module_a.submodule', + ('module', 'module-module_a.submodule', 'module'))) def test_get_full_qualified_name(): @@ -158,3 +170,51 @@ def test_get_full_qualified_name(): kwargs = {'js:module': 'module1', 'js:object': 'Class'} node = nodes.reference(reftarget='func', **kwargs) assert domain.get_full_qualified_name(node) == 'module1.Class.func' + + +def test_js_module(app): + text = ".. js:module:: sphinx" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (nodes.target, + addnodes.index)) + assert_node(doctree[0], nodes.target, ids=["module-sphinx"]) + assert_node(doctree[1], addnodes.index, + entries=[("single", "sphinx (module)", "module-sphinx", "", None)]) + + +def test_js_function(app): + text = ".. js:function:: sum(a, b)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "sum"], + desc_parameterlist)], + [desc_content, ()])])) + assert_node(doctree[1][0][1], [desc_parameterlist, ([desc_parameter, "a"], + [desc_parameter, "b"])]) + assert_node(doctree[0], addnodes.index, + entries=[("single", "sum() (built-in function)", "sum", "", None)]) + assert_node(doctree[1], addnodes.desc, domain="js", objtype="function", noindex=False) + + +def test_js_class(app): + text = ".. js:class:: Application" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Application"], + [desc_parameterlist, ()])], + [desc_content, ()])])) + assert_node(doctree[0], addnodes.index, + entries=[("single", "Application() (class)", "application", "", None)]) + assert_node(doctree[1], addnodes.desc, domain="js", objtype="class", noindex=False) + + +def test_js_data(app): + text = ".. js:data:: name" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, desc_name, "name"], + [desc_content, ()])])) + assert_node(doctree[0], addnodes.index, + entries=[("single", "name (global variable or constant)", "name", "", None)]) + assert_node(doctree[1], addnodes.desc, domain="js", objtype="data", noindex=False) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index f78c1e9d8..e4bc17004 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -8,6 +8,7 @@ :license: BSD, see LICENSE for details. """ +import sys from unittest.mock import Mock import pytest @@ -16,11 +17,12 @@ from docutils import nodes 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 + desc_parameter, desc_parameterlist, desc_returns, desc_signature, + desc_sig_name, desc_sig_operator, desc_sig_punctuation, pending_xref, ) from sphinx.domains import IndexEntry from sphinx.domains.python import ( - py_sig_re, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex + py_sig_re, _parse_annotation, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex ) from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -76,7 +78,7 @@ def test_domain_py_xrefs(app, status, warning): assert_node(node, **attributes) doctree = app.env.get_doctree('roles') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) assert_refnode(refnodes[0], None, None, 'TopLevel', 'class') assert_refnode(refnodes[1], None, None, 'top_level', 'meth') assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth') @@ -94,7 +96,7 @@ def test_domain_py_xrefs(app, status, warning): assert len(refnodes) == 13 doctree = app.env.get_doctree('module') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) assert_refnode(refnodes[0], 'module_a.submodule', None, 'ModTopLevel', 'class') assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel', @@ -123,7 +125,7 @@ def test_domain_py_xrefs(app, status, warning): assert len(refnodes) == 16 doctree = app.env.get_doctree('module_option') - refnodes = list(doctree.traverse(addnodes.pending_xref)) + refnodes = list(doctree.traverse(pending_xref)) print(refnodes) print(refnodes[0]) print(refnodes[1]) @@ -144,36 +146,36 @@ def test_domain_py_objects(app, status, warning): assert 'module_b.submodule' in modules assert 'module_b.submodule' in objects - assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class') - assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method') - assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method') + assert objects['module_a.submodule.ModTopLevel'][2] == 'class' + assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method' + assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method' assert 'ModTopLevel.ModNoModule' not in objects - assert objects['ModNoModule'] == ('module', 'class') - assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class') - - assert objects['TopLevel'] == ('roles', 'class') - assert objects['top_level'] == ('roles', 'method') - assert objects['NestedParentA'] == ('roles', 'class') - assert objects['NestedParentA.child_1'] == ('roles', 'method') - assert objects['NestedParentA.any_child'] == ('roles', 'method') - assert objects['NestedParentA.NestedChildA'] == ('roles', 'class') - assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'method') - assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'method') - assert objects['NestedParentA.child_2'] == ('roles', 'method') - assert objects['NestedParentB'] == ('roles', 'class') - assert objects['NestedParentB.child_1'] == ('roles', 'method') + assert objects['ModNoModule'][2] == 'class' + assert objects['module_b.submodule.ModTopLevel'][2] == 'class' + + assert objects['TopLevel'][2] == 'class' + assert objects['top_level'][2] == 'method' + assert objects['NestedParentA'][2] == 'class' + assert objects['NestedParentA.child_1'][2] == 'method' + assert objects['NestedParentA.any_child'][2] == 'method' + assert objects['NestedParentA.NestedChildA'][2] == 'class' + assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method' + assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method' + assert objects['NestedParentA.child_2'][2] == 'method' + assert objects['NestedParentB'][2] == 'class' + assert objects['NestedParentB.child_1'][2] == 'method' @pytest.mark.sphinx('html', testroot='domain-py') def test_resolve_xref_for_properties(app, status, warning): app.builder.build_all() - content = (app.outdir / 'module.html').text() - assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + content = (app.outdir / 'module.html').read_text() + assert ('Link to <a class="reference internal" href="#module_a.submodule.modtoplevel.prop"' ' title="module_a.submodule.ModTopLevel.prop">' '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' 'prop</span> <span class="pre">attribute</span></code></a>' in content) - assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + assert ('Link to <a class="reference internal" href="#module_a.submodule.modtoplevel.prop"' ' title="module_a.submodule.ModTopLevel.prop">' '<code class="xref py py-meth docutils literal notranslate"><span class="pre">' 'prop</span> <span class="pre">method</span></code></a>' in content) @@ -190,17 +192,20 @@ def test_domain_py_find_obj(app, status, warning): assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) assert (find_obj(None, None, 'NestedParentA', 'class') == - [('NestedParentA', ('roles', 'class'))]) + [('NestedParentA', ('roles', 'nestedparenta', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))]) def test_get_full_qualified_name(): @@ -231,17 +236,133 @@ def test_get_full_qualified_name(): assert domain.get_full_qualified_name(node) == 'module1.Class.func' +def test_parse_annotation(): + doctree = _parse_annotation("int") + assert_node(doctree, ([pending_xref, "int"],)) + + doctree = _parse_annotation("List[int]") + assert_node(doctree, ([pending_xref, "List"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Tuple[int, int]") + assert_node(doctree, ([pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Callable[[int, int], int]") + assert_node(doctree, ([pending_xref, "Callable"], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"], + [desc_sig_punctuation, ", "], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + 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_returns, pending_xref, "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"]) + assert_node(doctree[1][0][1], + [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + " ", + [nodes.inline, pending_xref, "str"])]) + + +def test_pyfunction_signature_full(app): + text = (".. py:function:: hello(a: str, b = 1, *args: str, " + "c: bool = True, **kwargs: str) -> str") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ([desc_sig_name, "a"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, pending_xref, "str"])], + [desc_parameter, ([desc_sig_name, "b"], + [desc_sig_operator, "="], + [nodes.inline, "1"])], + [desc_parameter, ([desc_sig_operator, "*"], + [desc_sig_name, "args"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, pending_xref, "str"])], + [desc_parameter, ([desc_sig_name, "c"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, pending_xref, "bool"], + " ", + [desc_sig_operator, "="], + " ", + [nodes.inline, "True"])], + [desc_parameter, ([desc_sig_operator, "**"], + [desc_sig_name, "kwargs"], + [desc_sig_punctuation, ":"], + " ", + [desc_sig_name, pending_xref, "str"])])]) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +def test_pyfunction_signature_full_py38(app): + # case: separator at head + text = ".. py:function:: hello(*, a)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, nodes.inline, "*"], + [desc_parameter, ([desc_sig_name, "a"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) + + # case: separator in the middle + text = ".. py:function:: hello(a, /, b, *, c)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_name, "b"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, ([desc_sig_name, "c"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) + + # case: separator in the middle (2) + text = ".. py:function:: hello(a, /, *, b)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, ([desc_sig_name, "b"], + [desc_sig_operator, "="], + [nodes.inline, "None"])])]) + + # case: separator at tail + text = ".. py:function:: hello(a, /)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"])]) def test_optional_pyfunction_signature(app): @@ -250,7 +371,7 @@ def test_optional_pyfunction_signature(app): assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "compile"], desc_parameterlist, - [desc_returns, "ast object"])], + [desc_returns, pending_xref, "ast object"])], desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) @@ -335,7 +456,7 @@ def test_pydata(app): [desc, ([desc_signature, desc_name, "var"], [desc_content, ()])])) assert 'var' in domain.objects - assert domain.objects['var'] == ('index', 'data') + assert domain.objects['var'] == ('index', 'var', 'data') def test_pyfunction(app): @@ -354,9 +475,9 @@ def test_pyfunction(app): [desc_parameterlist, ()])], [desc_content, ()])])) assert 'func1' in domain.objects - assert domain.objects['func1'] == ('index', 'function') + assert domain.objects['func1'] == ('index', 'func1', 'function') assert 'func2' in domain.objects - assert domain.objects['func2'] == ('index', 'function') + assert domain.objects['func2'] == ('index', 'func2', 'function') def test_pymethod_options(app): @@ -393,61 +514,61 @@ def test_pymethod_options(app): # method assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)]) + entries=[('single', 'meth1() (Class method)', 'class.meth1', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth1' in domain.objects - assert domain.objects['Class.meth1'] == ('index', 'method') + assert domain.objects['Class.meth1'] == ('index', 'class.meth1', 'method') # :classmethod: assert_node(doctree[1][1][2], addnodes.index, - entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)]) + entries=[('single', 'meth2() (Class class method)', 'class.meth2', '', None)]) assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth2"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth2' in domain.objects - assert domain.objects['Class.meth2'] == ('index', 'method') + assert domain.objects['Class.meth2'] == ('index', 'class.meth2', 'method') # :staticmethod: assert_node(doctree[1][1][4], addnodes.index, - entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)]) + entries=[('single', 'meth3() (Class static method)', 'class.meth3', '', None)]) assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth3"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth3' in domain.objects - assert domain.objects['Class.meth3'] == ('index', 'method') + assert domain.objects['Class.meth3'] == ('index', 'class.meth3', 'method') # :async: assert_node(doctree[1][1][6], addnodes.index, - entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)]) + entries=[('single', 'meth4() (Class method)', 'class.meth4', '', None)]) assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "], [desc_name, "meth4"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth4' in domain.objects - assert domain.objects['Class.meth4'] == ('index', 'method') + assert domain.objects['Class.meth4'] == ('index', 'class.meth4', 'method') # :property: assert_node(doctree[1][1][8], addnodes.index, - entries=[('single', 'meth5() (Class property)', 'Class.meth5', '', None)]) + entries=[('single', 'meth5() (Class property)', 'class.meth5', '', None)]) assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "], [desc_name, "meth5"])], [desc_content, ()])) assert 'Class.meth5' in domain.objects - assert domain.objects['Class.meth5'] == ('index', 'method') + assert domain.objects['Class.meth5'] == ('index', 'class.meth5', 'method') # :abstractmethod: assert_node(doctree[1][1][10], addnodes.index, - entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)]) + entries=[('single', 'meth6() (Class method)', 'class.meth6', '', None)]) assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, "abstract "], [desc_name, "meth6"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth6' in domain.objects - assert domain.objects['Class.meth6'] == ('index', 'method') + assert domain.objects['Class.meth6'] == ('index', 'class.meth6', 'method') def test_pyclassmethod(app): @@ -462,13 +583,13 @@ def test_pyclassmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)]) + entries=[('single', 'meth() (Class class method)', 'class.meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'method') + assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method') def test_pystaticmethod(app): @@ -483,13 +604,13 @@ def test_pystaticmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)]) + entries=[('single', 'meth() (Class static method)', 'class.meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'method') + assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method') def test_pyattribute(app): @@ -506,13 +627,43 @@ def test_pyattribute(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) + entries=[('single', 'attr (Class attribute)', 'class.attr', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], [desc_annotation, ": str"], [desc_annotation, " = ''"])], [desc_content, ()])) assert 'Class.attr' in domain.objects - assert domain.objects['Class.attr'] == ('index', 'attribute') + assert domain.objects['Class.attr'] == ('index', 'class.attr', 'attribute') + + +def test_pydecorator_signature(app): + text = ".. py:decorator:: deco" + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_addname, "@"], + [desc_name, "deco"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", noindex=False) + + assert 'deco' in domain.objects + assert domain.objects['deco'] == ('index', 'deco', 'function') + + +def test_pydecoratormethod_signature(app): + text = ".. py:decoratormethod:: deco" + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_addname, "@"], + [desc_name, "deco"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="method", + domain="py", objtype="method", noindex=False) + + assert 'deco' in domain.objects + assert domain.objects['deco'] == ('index', 'deco', 'method') @pytest.mark.sphinx(freshenv=True) @@ -559,3 +710,26 @@ def test_module_index_not_collapsed(app): ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')])], True ) + + +@pytest.mark.sphinx(freshenv=True, confoverrides={'modindex_common_prefix': ['sphinx.']}) +def test_modindex_common_prefix(app): + text = (".. py:module:: docutils\n" + ".. py:module:: sphinx\n" + ".. py:module:: sphinx.config\n" + ".. py:module:: sphinx.builders\n" + ".. py:module:: sphinx.builders.html\n" + ".. py:module:: sphinx_intl\n") + restructuredtext.parse(app, text) + index = PythonModuleIndex(app.env.get_domain('py')) + assert index.generate() == ( + [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''), # NOQA + IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]), # NOQA + ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]), + ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), + ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''), + IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], + True + ) + + diff --git a/tests/test_domain_rst.py b/tests/test_domain_rst.py index 50d9d154f..86fe7ef3f 100644 --- a/tests/test_domain_rst.py +++ b/tests/test_domain_rst.py @@ -76,7 +76,7 @@ def test_rst_directive_option(app): [desc_content, ()])])) assert_node(doctree[0], entries=[("single", ":foo: (directive option)", - "directive:option--foo", "", "F")]) + "directive-option-foo", "", "F")]) assert_node(doctree[1], addnodes.desc, desctype="directive:option", domain="rst", objtype="directive:option", noindex=False) @@ -90,7 +90,7 @@ def test_rst_directive_option_with_argument(app): [desc_content, ()])])) assert_node(doctree[0], entries=[("single", ":foo: (directive option)", - "directive:option--foo", "", "F")]) + "directive-option-foo", "", "F")]) assert_node(doctree[1], addnodes.desc, desctype="directive:option", domain="rst", objtype="directive:option", noindex=False) @@ -105,7 +105,7 @@ def test_rst_directive_option_type(app): [desc_content, ()])])) assert_node(doctree[0], entries=[("single", ":foo: (directive option)", - "directive:option--foo", "", "F")]) + "directive-option-foo", "", "F")]) assert_node(doctree[1], addnodes.desc, desctype="directive:option", domain="rst", objtype="directive:option", noindex=False) @@ -121,7 +121,7 @@ def test_rst_directive_and_directive_option(app): desc)])])) assert_node(doctree[1][1][0], entries=[("pair", "foo (directive); :bar: (directive option)", - "directive:option-foo-bar", "", "B")]) + "directive-option-foo-bar", "", "B")]) assert_node(doctree[1][1][1], ([desc_signature, desc_name, ":bar:"], [desc_content, ()])) assert_node(doctree[1][1][1], addnodes.desc, desctype="directive:option", diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 56c2d968c..1f0024efc 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -8,11 +8,15 @@ :license: BSD, see LICENSE for details. """ +import pytest + from unittest import mock from docutils import nodes from docutils.nodes import definition, definition_list, definition_list_item, term +from html5lib import HTMLParser + from sphinx import addnodes from sphinx.addnodes import ( desc, desc_addname, desc_content, desc_name, desc_signature, glossary, index @@ -20,6 +24,7 @@ from sphinx.addnodes import ( from sphinx.domains.std import StandardDomain from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.util import docutils def test_process_doc_handle_figure_caption(): @@ -172,6 +177,15 @@ def test_glossary_warning(app, status, warning): assert ("case3.rst:4: WARNING: glossary term must be preceded by empty line" in warning.getvalue()) + # duplicated terms + text = (".. glossary::\n" + "\n" + " term-case4\n" + " term-case4\n") + restructuredtext.parse(app, text, "case4") + assert ("case4.rst:3: WARNING: duplicate term description of term-case4, " + "other instance in case4" in warning.getvalue()) + def test_glossary_comment(app): text = (".. glossary::\n" @@ -303,3 +317,59 @@ def test_multiple_cmdoptions(app): assert ('cmd', '--output') in domain.progoptions assert domain.progoptions[('cmd', '-o')] == ('index', 'cmdoption-cmd-o') assert domain.progoptions[('cmd', '--output')] == ('index', 'cmdoption-cmd-o') + + +@pytest.mark.skipif(docutils.__version_info__ < (0, 13), + reason='docutils-0.13 or above is required') +@pytest.mark.sphinx(testroot='productionlist') +def test_productionlist(app, status, warning): + app.builder.build_all() + + warnings = warning.getvalue().split("\n"); + assert len(warnings) == 2 + assert warnings[-1] == '' + assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0] + + with (app.outdir / 'index.html').open('rb') as f: + etree = HTMLParser(namespaceHTMLElements=False).parse(f) + ul = list(etree.iter('ul'))[1] + cases = [] + for li in list(ul): + assert len(list(li)) == 1 + p = list(li)[0] + assert p.tag == 'p' + text = str(p.text).strip(' :') + assert len(list(p)) == 1 + a = list(p)[0] + assert a.tag == 'a' + link = a.get('href') + assert len(list(a)) == 1 + code = list(a)[0] + assert code.tag == 'code' + assert len(list(code)) == 1 + span = list(code)[0] + assert span.tag == 'span' + linkText = span.text.strip() + cases.append((text, link, linkText)) + assert cases == [ + ('A', 'Bare.html#grammar-token-a', 'A'), + ('B', 'Bare.html#grammar-token-b', 'B'), + ('P1:A', 'P1.html#grammar-token-p1-a', 'P1:A'), + ('P1:B', 'P1.html#grammar-token-p1-b', 'P1:B'), + ('P2:A', 'P1.html#grammar-token-p1-a', 'P1:A'), + ('P2:B', 'P2.html#grammar-token-p2-b', 'P2:B'), + ('Explicit title A, plain', 'Bare.html#grammar-token-a', 'MyTitle'), + ('Explicit title A, colon', 'Bare.html#grammar-token-a', 'My:Title'), + ('Explicit title P1:A, plain', 'P1.html#grammar-token-p1-a', 'MyTitle'), + ('Explicit title P1:A, colon', 'P1.html#grammar-token-p1-a', 'My:Title'), + ('Tilde A', 'Bare.html#grammar-token-a', 'A'), + ('Tilde P1:A', 'P1.html#grammar-token-p1-a', 'A'), + ('Tilde explicit title P1:A', 'P1.html#grammar-token-p1-a', '~MyTitle'), + ('Tilde, explicit title P1:A', 'P1.html#grammar-token-p1-a', 'MyTitle'), + ('Dup', 'Dup2.html#grammar-token-dup', 'Dup'), + ('FirstLine', 'firstLineRule.html#grammar-token-firstline', 'FirstLine'), + ('SecondLine', 'firstLineRule.html#grammar-token-secondline', 'SecondLine'), + ] + + text = (app.outdir / 'LineContinuation.html').read_text() + assert "A</strong> ::= B C D E F G" in text diff --git a/tests/test_environment.py b/tests/test_environment.py index 12fe86176..7290eb6a0 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -84,7 +84,7 @@ def test_object_inventory(app): refs = app.env.domaindata['py']['objects'] assert 'func_without_module' in refs - assert refs['func_without_module'] == ('objects', 'function') + assert refs['func_without_module'] == ('objects', 'func_without_module', 'function') assert 'func_without_module2' in refs assert 'mod.func_in_module' in refs assert 'mod.Cls' in refs @@ -99,7 +99,7 @@ def test_object_inventory(app): assert 'func_noindex' not in refs assert app.env.domaindata['py']['modules']['mod'] == \ - ('objects', 'Module synopsis.', 'UNIX', False) + ('objects', 'module-mod', 'Module synopsis.', 'UNIX', False) assert app.env.domains['py'].data is app.env.domaindata['py'] assert app.env.domains['c'].data is app.env.domaindata['c'] diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index ad7559ff2..97882d44b 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -113,6 +113,43 @@ def test_create_seealso_index(app): @pytest.mark.sphinx('dummy', freshenv=True) +def test_create_main_index(app): + text = (".. index:: !docutils\n" + ".. index:: docutils\n" + ".. index:: pip; install\n" + ".. index:: !pip; install\n") + restructuredtext.parse(app, text) + index = IndexEntries(app.env).create_index(app.builder) + assert len(index) == 2 + assert index[0] == ('D', [('docutils', [[('main', '#index-0'), + ('', '#index-1')], [], None])]) + assert index[1] == ('P', [('pip', [[], [('install', [('main', '#index-3'), + ('', '#index-2')])], None])]) + + +@pytest.mark.sphinx('dummy', freshenv=True) +def test_create_index_with_name(app): + text = (".. index:: single: docutils\n" + " :name: ref1\n" + ".. index:: single: Python\n" + " :name: ref2\n" + ".. index:: Sphinx\n") + restructuredtext.parse(app, text) + index = IndexEntries(app.env).create_index(app.builder) + + # check index is created correctly + assert len(index) == 3 + assert index[0] == ('D', [('docutils', [[('', '#ref1')], [], None])]) + assert index[1] == ('P', [('Python', [[('', '#ref2')], [], None])]) + assert index[2] == ('S', [('Sphinx', [[('', '#index-0')], [], None])]) + + # check the reference labels are created correctly + std = app.env.get_domain('std') + assert std.anonlabels['ref1'] == ('index', 'ref1') + assert std.anonlabels['ref2'] == ('index', 'ref2') + + +@pytest.mark.sphinx('dummy', freshenv=True) def test_create_index_by_key(app): # At present, only glossary directive is able to create index key text = (".. glossary::\n" diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 000000000..4881588a4 --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,24 @@ +""" + test_events + ~~~~~~~~~~~ + + Test the EventManager class. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.events import EventManager + + +def test_event_priority(): + result = [] + events = EventManager(object()) # pass an dummy object as an app + events.connect('builder-inited', lambda app: result.append(1), priority = 500) + events.connect('builder-inited', lambda app: result.append(2), priority = 500) + events.connect('builder-inited', lambda app: result.append(3), priority = 200) # eariler + events.connect('builder-inited', lambda app: result.append(4), priority = 700) # later + events.connect('builder-inited', lambda app: result.append(5), priority = 500) + + events.emit('builder-inited') + assert result == [3, 1, 2, 5, 4] diff --git a/tests/test_ext_apidoc.py b/tests/test_ext_apidoc.py index 3033cb450..e8d923b71 100644 --- a/tests/test_ext_apidoc.py +++ b/tests/test_ext_apidoc.py @@ -121,15 +121,16 @@ def test_pep_0420_enabled_separate(make_app, apidoc): with open(outdir / 'a.b.c.rst') as f: rst = f.read() - assert ".. toctree::\n\n a.b.c.d\n" in rst + + assert ".. toctree::\n :maxdepth: 4\n\n a.b.c.d\n" in rst with open(outdir / 'a.b.e.rst') as f: rst = f.read() - assert ".. toctree::\n\n a.b.e.f\n" in rst + assert ".. toctree::\n :maxdepth: 4\n\n a.b.e.f\n" in rst with open(outdir / 'a.b.x.rst') as f: rst = f.read() - assert ".. toctree::\n\n a.b.x.y\n" in rst + assert ".. toctree::\n :maxdepth: 4\n\n a.b.x.y\n" in rst app = make_app('text', srcdir=outdir) app.build() @@ -275,7 +276,7 @@ def test_multibyte_parameters(make_app, apidoc): assert (outdir / 'conf.py').isfile() assert (outdir / 'index.rst').isfile() - conf_py = (outdir / 'conf.py').text() + conf_py = (outdir / 'conf.py').read_text() assert "project = 'プロジェクト名'" in conf_py assert "author = '著者名'" in conf_py assert "version = 'バージョン'" in conf_py @@ -408,13 +409,13 @@ def test_private(tempdir): # without --private option apidoc_main(['-o', tempdir, tempdir]) assert (tempdir / 'hello.rst').exists() - assert ':private-members:' not in (tempdir / 'hello.rst').text() + assert ':private-members:' not in (tempdir / 'hello.rst').read_text() assert not (tempdir / '_world.rst').exists() # with --private option apidoc_main(['--private', '-f', '-o', tempdir, tempdir]) assert (tempdir / 'hello.rst').exists() - assert ':private-members:' in (tempdir / 'hello.rst').text() + assert ':private-members:' in (tempdir / 'hello.rst').read_text() assert (tempdir / '_world.rst').exists() @@ -426,7 +427,7 @@ def test_toc_file(tempdir): apidoc_main(['-o', tempdir, tempdir]) assert (outdir / 'modules.rst').exists() - content = (outdir / 'modules.rst').text() + content = (outdir / 'modules.rst').read_text() assert content == ("test_toc_file0\n" "==============\n" "\n" @@ -442,7 +443,7 @@ def test_module_file(tempdir): apidoc_main(['-o', tempdir, tempdir]) assert (outdir / 'example.rst').exists() - content = (outdir / 'example.rst').text() + content = (outdir / 'example.rst').read_text() assert content == ("example module\n" "==============\n" "\n" @@ -458,7 +459,7 @@ def test_module_file_noheadings(tempdir): apidoc_main(['--no-headings', '-o', tempdir, tempdir]) assert (outdir / 'example.rst').exists() - content = (outdir / 'example.rst').text() + content = (outdir / 'example.rst').read_text() assert content == (".. automodule:: example\n" " :members:\n" " :undoc-members:\n" @@ -477,7 +478,7 @@ def test_package_file(tempdir): assert (outdir / 'testpkg.rst').exists() assert (outdir / 'testpkg.subpkg.rst').exists() - content = (outdir / 'testpkg.rst').text() + content = (outdir / 'testpkg.rst').read_text() assert content == ("testpkg package\n" "===============\n" "\n" @@ -485,6 +486,7 @@ def test_package_file(tempdir): "-----------\n" "\n" ".. toctree::\n" + " :maxdepth: 4\n" "\n" " testpkg.subpkg\n" "\n" @@ -516,7 +518,7 @@ def test_package_file(tempdir): " :undoc-members:\n" " :show-inheritance:\n") - content = (outdir / 'testpkg.subpkg.rst').text() + content = (outdir / 'testpkg.subpkg.rst').read_text() assert content == ("testpkg.subpkg package\n" "======================\n" "\n" @@ -538,7 +540,7 @@ def test_package_file_separate(tempdir): assert (outdir / 'testpkg.rst').exists() assert (outdir / 'testpkg.example.rst').exists() - content = (outdir / 'testpkg.rst').text() + content = (outdir / 'testpkg.rst').read_text() assert content == ("testpkg package\n" "===============\n" "\n" @@ -546,6 +548,7 @@ def test_package_file_separate(tempdir): "----------\n" "\n" ".. toctree::\n" + " :maxdepth: 4\n" "\n" " testpkg.example\n" "\n" @@ -557,7 +560,7 @@ def test_package_file_separate(tempdir): " :undoc-members:\n" " :show-inheritance:\n") - content = (outdir / 'testpkg.example.rst').text() + content = (outdir / 'testpkg.example.rst').read_text() assert content == ("testpkg.example module\n" "======================\n" "\n" @@ -574,7 +577,7 @@ def test_package_file_module_first(tempdir): (outdir / 'testpkg' / 'example.py').write_text('') apidoc_main(['--module-first', '-o', tempdir, tempdir]) - content = (outdir / 'testpkg.rst').text() + content = (outdir / 'testpkg.rst').read_text() assert content == ("testpkg package\n" "===============\n" "\n" @@ -603,7 +606,7 @@ def test_package_file_without_submodules(tempdir): apidoc_main(['-o', tempdir, tempdir / 'testpkg']) assert (outdir / 'testpkg.rst').exists() - content = (outdir / 'testpkg.rst').text() + content = (outdir / 'testpkg.rst').read_text() assert content == ("testpkg package\n" "===============\n" "\n" @@ -623,7 +626,7 @@ def test_namespace_package_file(tempdir): apidoc_main(['--implicit-namespace', '-o', tempdir, tempdir / 'testpkg']) assert (outdir / 'testpkg.rst').exists() - content = (outdir / 'testpkg.rst').text() + content = (outdir / 'testpkg.rst').read_text() assert content == ("testpkg namespace\n" "=================\n" "\n" diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index f21431238..f351d0e4b 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -12,7 +12,6 @@ import platform import pytest -from sphinx.ext.autodoc import merge_autodoc_default_flags from test_autodoc import do_autodoc IS_PYPY = platform.python_implementation() == 'PyPy' @@ -32,49 +31,49 @@ def test_autoclass_content_class(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' A class having __init__, no __new__', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' A class having both __init__ and __new__', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' A class inherits __init__ without docstring.', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' A class inherits __new__ without docstring.', - ' ' + '', ] @@ -92,49 +91,49 @@ def test_autoclass_content_init(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' __new__ docstring', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' __new__ docstring', - ' ' + '', ] @@ -152,59 +151,59 @@ def test_autoclass_content_both(app): ' :module: target.autoclass_content', '', ' A class having no __init__, no __new__', - ' ', + '', '', '.. py:class:: B()', ' :module: target.autoclass_content', '', ' A class having __init__(no docstring), no __new__', - ' ', + '', '', '.. py:class:: C()', ' :module: target.autoclass_content', '', ' A class having __init__, no __new__', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: D', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__(no docstring)', - ' ', + '', '', '.. py:class:: E', ' :module: target.autoclass_content', '', ' A class having no __init__, __new__', - ' ', + '', ' __new__ docstring', - ' ', + '', '', '.. py:class:: F()', ' :module: target.autoclass_content', '', ' A class having both __init__ and __new__', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: G()', ' :module: target.autoclass_content', '', ' A class inherits __init__ without docstring.', - ' ', + '', ' __init__ docstring', - ' ', + '', '', '.. py:class:: H()', ' :module: target.autoclass_content', '', ' A class inherits __new__ without docstring.', - ' ', + '', ' __new__ docstring', - ' ' + '', ] @@ -218,7 +217,7 @@ def test_autodoc_inherit_docstrings(app): ' :module: target.inheritance', '', ' Inherited function.', - ' ' + '', ] # disable autodoc_inherit_docstrings @@ -241,38 +240,38 @@ def test_autodoc_docstring_signature(app): '.. py:class:: DocstringSig', ' :module: target', '', - ' ', + '', ' .. py:method:: DocstringSig.meth(FOO, BAR=1) -> BAZ', ' :module: target', - ' ', + '', ' First line of docstring', - ' ', + '', ' rest of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.meth2()', ' :module: target', - ' ', + '', ' First line, no signature', ' Second line followed by indentation::', - ' ', + '', ' indented line', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop1', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop2', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', ' Second line of docstring', - ' ' + '', ] # disable autodoc_docstring_signature @@ -283,41 +282,41 @@ def test_autodoc_docstring_signature(app): '.. py:class:: DocstringSig', ' :module: target', '', - ' ', + '', ' .. py:method:: DocstringSig.meth()', ' :module: target', - ' ', + '', ' meth(FOO, BAR=1) -> BAZ', ' First line of docstring', - ' ', + '', ' rest of docstring', - ' ', - ' ', - ' ', + '', + '', + '', ' .. py:method:: DocstringSig.meth2()', ' :module: target', - ' ', + '', ' First line, no signature', ' Second line followed by indentation::', - ' ', + '', ' indented line', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop1', ' :module: target', ' :property:', - ' ', + '', ' DocstringSig.prop1(self)', ' First line of docstring', - ' ', - ' ', + '', + '', ' .. py:method:: DocstringSig.prop2', ' :module: target', ' :property:', - ' ', + '', ' First line of docstring', ' Second line of docstring', - ' ' + '', ] @@ -398,13 +397,13 @@ def test_autoclass_content_and_docstring_signature_both(app): ' :module: target.docstring_signature', '', ' B(foo, bar, baz)', - ' ', + '', '', '.. py:class:: C(foo, bar)', ' :module: target.docstring_signature', '', ' C(foo, bar, baz)', - ' ', + '', '', '.. py:class:: D(foo, bar, baz)', ' :module: target.docstring_signature', @@ -440,33 +439,32 @@ def test_mocked_module_imports(app, warning): ' :module: target.need_mocks', '', ' TestAutodoc docstring.', - ' ', - ' ', + '', + '', ' .. py:method:: TestAutodoc.decoratedMethod()', ' :module: target.need_mocks', - ' ', + '', ' TestAutodoc::decoratedMethod docstring', - ' ', + '', '', '.. py:function:: decoratedFunction()', ' :module: target.need_mocks', '', ' decoratedFunction docstring', - ' ', + '', '', '.. py:function:: func(arg: missing_module.Class)', ' :module: target.need_mocks', '', ' a function takes mocked object as an argument', - ' ' + '', ] assert warning.getvalue() == '' -@pytest.mark.sphinx('html', testroot='ext-autodoc') +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "signature"}) def test_autodoc_typehints_signature(app): - app.config.autodoc_typehints = "signature" - options = {"members": None, "undoc-members": True} actual = do_autodoc(app, 'module', 'target.typehints', options) @@ -478,22 +476,22 @@ def test_autodoc_typehints_signature(app): '.. py:class:: Math(s: str, o: object = None)', ' :module: target.typehints', '', - ' ', + '', ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.horse(a: str, b: int) -> None', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.nothing() -> None', ' :module: target.typehints', - ' ', + '', '', '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', @@ -514,10 +512,9 @@ def test_autodoc_typehints_signature(app): ] -@pytest.mark.sphinx('html', testroot='ext-autodoc') +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "none"}) def test_autodoc_typehints_none(app): - app.config.autodoc_typehints = "none" - options = {"members": None, "undoc-members": True} actual = do_autodoc(app, 'module', 'target.typehints', options) @@ -529,22 +526,22 @@ def test_autodoc_typehints_none(app): '.. py:class:: Math(s, o=None)', ' :module: target.typehints', '', - ' ', + '', ' .. py:method:: Math.decr(a, b=1)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.horse(a, b)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.incr(a, b=1)', ' :module: target.typehints', - ' ', - ' ', + '', + '', ' .. py:method:: Math.nothing()', ' :module: target.typehints', - ' ', + '', '', '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', ' :module: target.typehints', @@ -565,11 +562,10 @@ def test_autodoc_typehints_none(app): @pytest.mark.sphinx('text', testroot='ext-autodoc', - confoverrides={'extensions': ['sphinx.ext.autodoc.typehints'], - 'autodoc_typehints': 'description'}) + confoverrides={'autodoc_typehints': "description"}) def test_autodoc_typehints_description(app): app.build() - context = (app.outdir / 'index.txt').text() + context = (app.outdir / 'index.txt').read_text() assert ('target.typehints.incr(a, b=1)\n' '\n' ' Parameters:\n' @@ -583,27 +579,6 @@ def test_autodoc_typehints_description(app): @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 actual = do_autodoc(app, 'class', 'target.enum.EnumCls') @@ -612,7 +587,7 @@ def test_autodoc_default_options(app): actual = do_autodoc(app, 'class', 'target.CustomIter') assert ' .. py:method:: target.CustomIter' not in actual actual = do_autodoc(app, 'module', 'target') - assert '.. py:function:: save_traceback(app: Sphinx) -> str' not in actual + assert '.. py:function:: save_traceback(app)' not in actual # with :members: app.config.autodoc_default_options = {'members': None} @@ -676,16 +651,6 @@ def test_autodoc_default_options(app): assert ' .. py:method:: CustomIter.snafucate()' in actual assert ' Makes this snafucated.' in actual - # with :imported-members: - app.config.autodoc_default_options = { - 'members': None, - 'imported-members': None, - 'ignore-module-all': None, - } - actual = do_autodoc(app, 'module', 'target') - print('\n'.join(actual)) - assert '.. py:function:: save_traceback(app: Sphinx) -> str' in actual - @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options_with_values(app): diff --git a/tests/test_ext_autodoc_events.py b/tests/test_ext_autodoc_events.py index 91fc19630..106c5793a 100644 --- a/tests/test_ext_autodoc_events.py +++ b/tests/test_ext_autodoc_events.py @@ -44,7 +44,7 @@ def test_cut_lines(app): ' :module: target.process_docstring', '', ' second line', - ' ' + '', ] @@ -60,7 +60,7 @@ def test_between(app): ' :module: target.process_docstring', '', ' second line', - ' ' + '', ] @@ -77,5 +77,5 @@ def test_between_exclude(app): '', ' first line', ' third line', - ' ' + '', ] diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_ext_autodoc_mock.py index 35084c23e..4760493cf 100644 --- a/tests/test_ext_autodoc_mock.py +++ b/tests/test_ext_autodoc_mock.py @@ -18,7 +18,7 @@ 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_autodoc_private_members.py b/tests/test_ext_autodoc_private_members.py new file mode 100644 index 000000000..2d9208b41 --- /dev/null +++ b/tests/test_ext_autodoc_private_members.py @@ -0,0 +1,46 @@ +""" + test_ext_autodoc_private_members + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly for private-members option. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from test_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field(app): + app.config.autoclass_content = 'class' + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_private_field_and_private_members(app): + app.config.autoclass_content = 'class' + options = {"members": None, + "private-members": None} + actual = do_autodoc(app, 'module', 'target.private', options) + assert list(actual) == [ + '', + '.. py:module:: target.private', + '', + '', + '.. py:function:: private_function(name)', + ' :module: target.private', + '', + ' private_function is a docstring().', + '', + ' :meta private:', + '', + ] diff --git a/tests/test_ext_autosectionlabel.py b/tests/test_ext_autosectionlabel.py index 947be0e11..310435d8e 100644 --- a/tests/test_ext_autosectionlabel.py +++ b/tests/test_ext_autosectionlabel.py @@ -21,7 +21,7 @@ from sphinx.util import docutils def test_autosectionlabel_html(app, status, warning, skipped_labels=False): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = ('<li><p><a class="reference internal" href="#introduce-of-sphinx">' '<span class=".*?">Introduce of Sphinx</span></a></p></li>') assert re.search(html, content, re.S) @@ -69,7 +69,7 @@ def test_autosectionlabel_prefix_document_html(app, status, warning): def test_autosectionlabel_maxdepth(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() # depth: 1 html = ('<li><p><a class="reference internal" href="#test-ext-autosectionlabel">' diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 628c2524b..cc6b0fa38 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -19,7 +19,7 @@ from sphinx import addnodes from sphinx.ext.autosummary import ( autosummary_table, autosummary_toc, mangle_signature, import_by_name, extract_summary ) -from sphinx.ext.autosummary.generate import generate_autosummary_docs +from sphinx.ext.autosummary.generate import AutosummaryEntry, generate_autosummary_docs from sphinx.testing.util import assert_node, etree_parse from sphinx.util.docutils import new_document @@ -31,6 +31,7 @@ default_kw = { 'confoverrides': { 'extensions': ['sphinx.ext.autosummary'], 'autosummary_generate': True, + 'autosummary_generate_overwrite': False, 'source_suffix': '.rst' } } @@ -209,13 +210,13 @@ def test_autosummary_generate(app, status, warning): assert doctree[3][0][0][2][2].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n' assert doctree[3][0][0][2][3].astext() == 'autosummary_importfail\n\n' - module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').text() + module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() assert (' .. autosummary::\n' ' \n' ' Foo\n' ' \n' in module) - Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').text() + Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').read_text() assert '.. automethod:: __init__' in Foo assert (' .. autosummary::\n' ' \n' @@ -228,10 +229,40 @@ def test_autosummary_generate(app, status, warning): ' \n' in Foo) +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate_overwrite': False}) +def test_autosummary_generate_overwrite1(app_params, make_app): + args, kwargs = app_params + srcdir = kwargs.get('srcdir') + + (srcdir / 'generated').makedirs(exist_ok=True) + (srcdir / 'generated' / 'autosummary_dummy_module.rst').write_text('') + + app = make_app(*args, **kwargs) + content = (srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() + assert content == '' + assert 'autosummary_dummy_module.rst' not in app._warning.getvalue() + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary', + confoverrides={'autosummary_generate_overwrite': True}) +def test_autosummary_generate_overwrite2(app_params, make_app): + args, kwargs = app_params + srcdir = kwargs.get('srcdir') + + (srcdir / 'generated').makedirs(exist_ok=True) + (srcdir / 'generated' / 'autosummary_dummy_module.rst').write_text('') + + app = make_app(*args, **kwargs) + content = (srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() + assert content != '' + assert 'autosummary_dummy_module.rst' not in app._warning.getvalue() + + @pytest.mark.sphinx('latex', **default_kw) def test_autosummary_latex_table_colspec(app, status, warning): app.builder.build_all() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() print(status.getvalue()) print(warning.getvalue()) assert r'\begin{longtable}[c]{\X{1}{2}\X{1}{2}}' in result @@ -281,7 +312,7 @@ def test_autosummary_imported_members(app, status, warning): # generated/foo is generated successfully assert app.env.get_doctree('generated/autosummary_dummy_package') - module = (app.srcdir / 'generated' / 'autosummary_dummy_package.rst').text() + module = (app.srcdir / 'generated' / 'autosummary_dummy_package.rst').read_text() assert (' .. autosummary::\n' ' \n' ' Bar\n' @@ -297,10 +328,10 @@ def test_autosummary_imported_members(app, status, warning): @pytest.mark.sphinx(testroot='ext-autodoc') def test_generate_autosummary_docs_property(app): with patch('sphinx.ext.autosummary.generate.find_autosummary_in_files') as mock: - mock.return_value = [('target.methods.Base.prop', 'prop', None)] + mock.return_value = [AutosummaryEntry('target.methods.Base.prop', 'prop', None)] generate_autosummary_docs([], output_dir=app.srcdir, builder=app.builder, app=app) - content = (app.srcdir / 'target.methods.Base.prop.rst').text() + content = (app.srcdir / 'target.methods.Base.prop.rst').read_text() assert content == ("target.methods.Base.prop\n" "========================\n" "\n" @@ -313,7 +344,7 @@ def test_generate_autosummary_docs_property(app): def test_autosummary_skip_member(app): app.build() - content = (app.srcdir / 'generate' / 'target.Foo.rst').text() + content = (app.srcdir / 'generate' / 'target.Foo.rst').read_text() assert 'Foo.skipmeth' not in content assert 'Foo._privatemeth' in content diff --git a/tests/test_ext_coverage.py b/tests/test_ext_coverage.py index 87e5e4931..16f53112b 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_ext_coverage.py @@ -17,7 +17,7 @@ import pytest def test_build(app, status, warning): app.builder.build_all() - py_undoc = (app.outdir / 'python.txt').text() + py_undoc = (app.outdir / 'python.txt').read_text() assert py_undoc.startswith('Undocumented Python objects\n' '===========================\n') assert 'autodoc_target\n--------------\n' in py_undoc @@ -28,13 +28,13 @@ def test_build(app, status, warning): assert ' * mod -- No module named mod' # in the "failed import" section - c_undoc = (app.outdir / 'c.txt').text() + c_undoc = (app.outdir / 'c.txt').read_text() assert c_undoc.startswith('Undocumented C API elements\n' '===========================\n') assert 'api.h' in c_undoc assert ' * Py_SphinxTest' in c_undoc - undoc_py, undoc_c = pickle.loads((app.outdir / 'undoc.pickle').bytes()) + undoc_py, undoc_c = pickle.loads((app.outdir / 'undoc.pickle').read_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] == {('function', 'Py_SphinxTest')} @@ -50,7 +50,7 @@ def test_build(app, status, warning): @pytest.mark.sphinx('coverage', testroot='ext-coverage') def test_coverage_ignore_pyobjects(app, status, warning): app.builder.build_all() - actual = (app.outdir / 'python.txt').text() + actual = (app.outdir / 'python.txt').read_text() expected = '''Undocumented Python objects =========================== coverage_not_ignored diff --git a/tests/test_ext_githubpages.py b/tests/test_ext_githubpages.py index 8a8004347..41dbe4f60 100644 --- a/tests/test_ext_githubpages.py +++ b/tests/test_ext_githubpages.py @@ -31,4 +31,4 @@ def test_no_cname_for_github_io_domain(app, status, warning): def test_cname_for_custom_domain(app, status, warning): app.builder.build_all() assert (app.outdir / '.nojekyll').exists() - assert (app.outdir / 'CNAME').text() == 'sphinx-doc.org' + assert (app.outdir / 'CNAME').read_text() == 'sphinx-doc.org' diff --git a/tests/test_ext_graphviz.py b/tests/test_ext_graphviz.py index f89a96860..a8de950ce 100644 --- a/tests/test_ext_graphviz.py +++ b/tests/test_ext_graphviz.py @@ -20,7 +20,7 @@ from sphinx.ext.graphviz import ClickableMapDefinition def test_graphviz_png_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class="figure align-default" .*?>\s*' r'<div class="graphviz"><img .*?/></div>\s*<p class="caption">' r'<span class="caption-text">caption of graph</span>.*</p>\s*</div>') @@ -51,7 +51,7 @@ def test_graphviz_png_html(app, status, warning): def test_graphviz_svg_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class=\"figure align-default\" .*?>\n' r'<div class="graphviz"><object data=\".*\.svg\".*>\n' @@ -91,7 +91,7 @@ def test_graphviz_svg_html(app, status, warning): def test_graphviz_latex(app, status, warning): app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() macro = ('\\\\begin{figure}\\[htbp\\]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' '\\\\caption{caption of graph}\\\\label{.*}\\\\end{figure}') @@ -117,7 +117,7 @@ def test_graphviz_latex(app, status, warning): def test_graphviz_i18n(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = '<img src=".*?" alt="digraph {\n BAR -> BAZ\n}" class="graphviz" />' assert re.search(html, content, re.M) diff --git a/tests/test_ext_ifconfig.py b/tests/test_ext_ifconfig.py index abce1d025..232ddf0d8 100644 --- a/tests/test_ext_ifconfig.py +++ b/tests/test_ext_ifconfig.py @@ -14,6 +14,6 @@ import pytest @pytest.mark.sphinx('text', testroot='ext-ifconfig') def test_ifconfig(app, status, warning): app.builder.build_all() - result = (app.outdir / 'index.txt').text() + result = (app.outdir / 'index.txt').read_text() assert 'spam' in result assert 'ham' not in result diff --git a/tests/test_ext_imgconverter.py b/tests/test_ext_imgconverter.py index 232808270..075446b0a 100644 --- a/tests/test_ext_imgconverter.py +++ b/tests/test_ext_imgconverter.py @@ -18,7 +18,7 @@ import pytest def test_ext_imgconverter(app, status, warning): app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() assert '\\sphinxincludegraphics{{svgimg}.png}' in content assert not (app.outdir / 'svgimg.svg').exists() assert (app.outdir / 'svgimg.png').exists() diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index c1cf4524d..3125f2c6e 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -132,13 +132,21 @@ def test_inheritance_diagram(app, status, warning): ('dummy.test.A', 'dummy.test.A', [], None), ] + # inheritance diagram involving a base class nested within another class + for cls in graphs['diagram_w_nested_classes'].class_info: + assert cls in [ + ('dummy.test_nested.A', 'dummy.test_nested.A', [], None), + ('dummy.test_nested.C', 'dummy.test_nested.C', ['dummy.test_nested.A.B'], None), + ('dummy.test_nested.A.B', 'dummy.test_nested.A.B', [], None) + ] + @pytest.mark.sphinx('html', testroot='ext-inheritance_diagram') @pytest.mark.usefixtures('if_graphviz_found') def test_inheritance_diagram_png_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() pattern = ('<div class="figure align-default" id="id1">\n' '<div class="graphviz">' @@ -155,7 +163,7 @@ def test_inheritance_diagram_png_html(app, status, warning): def test_inheritance_diagram_svg_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() pattern = ('<div class="figure align-default" id="id1">\n' '<div class="graphviz">' @@ -173,7 +181,7 @@ def test_inheritance_diagram_svg_html(app, status, warning): def test_inheritance_diagram_latex(app, status, warning): app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() pattern = ('\\\\begin{figure}\\[htbp]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{inheritance-\\w+.pdf}\n' @@ -195,7 +203,7 @@ def test_inheritance_diagram_latex_alias(app, status, warning): assert ('test.Bar', 'test.Bar', ['alias.Foo'], None) in aliased_graph assert ('alias.Foo', 'alias.Foo', [], None) in aliased_graph - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() pattern = ('<div class="figure align-default" id="id1">\n' '<div class="graphviz">' diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index eac2394c8..53faa7a37 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -242,7 +242,7 @@ def test_missing_reference_cppdomain(tempdir, app, status, warning): load_mappings(app) app.build() - html = (app.outdir / 'index.html').text() + html = (app.outdir / 'index.html').read_text() assert ('<a class="reference external"' ' href="https://docs.python.org/index.html#cpp_foo_bar"' ' title="(in foo v2.0)">' diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py index 256236489..b5de6cc1c 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -39,7 +39,7 @@ def test_imgmath_png(app, status, warning): if "dvipng command 'dvipng' cannot be run" in warning.getvalue(): raise pytest.skip.Exception('dvipng command "dvipng" is not available') - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.png"' r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') assert re.search(html, content, re.S) @@ -57,7 +57,7 @@ def test_imgmath_svg(app, status, warning): if "dvisvgm command 'dvisvgm' cannot be run" in warning.getvalue(): raise pytest.skip.Exception('dvisvgm command "dvisvgm" is not available') - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.svg"' r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') assert re.search(html, content, re.S) @@ -69,7 +69,7 @@ def test_imgmath_svg(app, status, warning): def test_mathjax_options(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert ('<script async="async" integrity="sha384-0123456789" ' 'src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?' 'config=TeX-AMS-MML_HTMLorMML"></script>' in content) @@ -80,7 +80,7 @@ def test_mathjax_options(app, status, warning): def test_mathjax_align(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class="math notranslate nohighlight">\s*' r'\\\[ \\begin\{align\}\\begin\{aligned\}S \&= \\pi r\^2\\\\' r'V \&= \\frac\{4\}\{3\} \\pi r\^3\\end\{aligned\}\\end\{align\} \\\]</div>') @@ -93,7 +93,7 @@ def test_mathjax_align(app, status, warning): def test_math_number_all_mathjax(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() html = (r'<div class="math notranslate nohighlight" id="equation-index-0">\s*' r'<span class="eqno">\(1\)<a .*>\xb6</a></span>\\\[a\^2\+b\^2=c\^2\\\]</div>') assert re.search(html, content, re.S) @@ -104,7 +104,7 @@ def test_math_number_all_mathjax(app, status, warning): def test_math_number_all_latex(app, status, warning): app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() macro = (r'\\begin{equation\*}\s*' r'\\begin{split}a\^2\+b\^2=c\^2\\end{split}\s*' r'\\end{equation\*}') @@ -134,7 +134,7 @@ def test_math_number_all_latex(app, status, warning): def test_math_eqref_format_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'math.html').text() + content = (app.outdir / 'math.html').read_text() html = ('<p>Referencing equation <a class="reference internal" ' 'href="#equation-foo">Eq.1</a> and <a class="reference internal" ' 'href="#equation-foo">Eq.1</a>.</p>') @@ -147,7 +147,7 @@ def test_math_eqref_format_html(app, status, warning): def test_math_eqref_format_latex(app, status, warning): app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() macro = (r'Referencing equation Eq.\\ref{equation:math:foo} and ' r'Eq.\\ref{equation:math:foo}.') assert re.search(macro, content, re.S) @@ -160,7 +160,7 @@ def test_math_eqref_format_latex(app, status, warning): def test_mathjax_numfig_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'math.html').text() + content = (app.outdir / 'math.html').read_text() html = ('<div class="math notranslate nohighlight" id="equation-math-0">\n' '<span class="eqno">(1.2)') assert html in content @@ -178,7 +178,7 @@ def test_mathjax_numfig_html(app, status, warning): def test_imgmath_numfig_html(app, status, warning): app.builder.build_all() - content = (app.outdir / 'page.html').text() + content = (app.outdir / 'page.html').read_text() html = '<span class="eqno">(3)<a class="headerlink" href="#equation-bar"' assert html in content html = ('<p>Referencing equations <a class="reference internal" ' @@ -218,7 +218,7 @@ def test_math_compat(app, status, warning): def test_mathjax_config(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert ('<script type="text/x-mathjax-config">' 'MathJax.Hub.Config({"extensions": ["tex2jax.js"]})' '</script>' in content) @@ -229,5 +229,5 @@ def test_mathjax_config(app, status, warning): def test_mathjax_is_not_installed_if_no_equations(app, status, warning): app.builder.build_all() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert 'MathJax.js' not in content diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 2ce754eff..160079a50 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -1020,6 +1020,34 @@ Sooper Warning: actual = str(GoogleDocstring(docstring, testConfig)) self.assertEqual(expected, actual) + def test_noindex(self): + docstring = """ +Attributes: + arg + description + +Methods: + func(i, j) + description +""" + + expected = """ +.. attribute:: arg + :noindex: + + description + +.. method:: func(i, j) + :noindex: + + + description +""" + config = Config() + actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', + options={'noindex': True})) + self.assertEqual(expected, actual) + class NumpyDocstringTest(BaseDocstringTest): docstrings = [( diff --git a/tests/test_ext_todo.py b/tests/test_ext_todo.py index 8c00ef088..7b4fdeabe 100644 --- a/tests/test_ext_todo.py +++ b/tests/test_ext_todo.py @@ -29,7 +29,7 @@ def test_todo(app, status, warning): app.builder.build_all() # check todolist - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert ('<p class="admonition-title">Todo</p>\n' '<p>todo in foo</p>') in content @@ -37,7 +37,7 @@ def test_todo(app, status, warning): '<p>todo in bar</p>') in content # check todo - content = (app.outdir / 'foo.html').text() + content = (app.outdir / 'foo.html').read_text() assert ('<p class="admonition-title">Todo</p>\n' '<p>todo in foo</p>') in content @@ -67,7 +67,7 @@ def test_todo_not_included(app, status, warning): app.builder.build_all() # check todolist - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert ('<p class="admonition-title">Todo</p>\n' '<p>todo in foo</p>') not in content @@ -75,7 +75,7 @@ def test_todo_not_included(app, status, warning): '<p>todo in bar</p>') not in content # check todo - content = (app.outdir / 'foo.html').text() + content = (app.outdir / 'foo.html').read_text() assert ('<p class="admonition-title">Todo</p>\n' '<p>todo in foo</p>') not in content @@ -102,7 +102,7 @@ def test_todo_valid_link(app, status, warning): # Ensure the LaTeX output is built. app.builder.build_all() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() # 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 diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py index 2540d09ea..3d9ea27d7 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_ext_viewcode.py @@ -24,7 +24,7 @@ def test_viewcode(app, status, warning): warnings ) - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() assert result.count('href="_modules/spam/mod1.html#func1"') == 2 assert result.count('href="_modules/spam/mod2.html#func2"') == 2 assert result.count('href="_modules/spam/mod1.html#Class1"') == 2 @@ -37,7 +37,7 @@ def test_viewcode(app, status, warning): # the next assert fails, until the autodoc bug gets fixed assert result.count('this is the class attribute class_attr') == 2 - result = (app.outdir / '_modules/spam/mod1.html').text() + result = (app.outdir / '_modules/spam/mod1.html').read_text() result = re.sub('<span class=".*?">', '<span>', result) # filter pygments classes assert ('<div class="viewcode-block" id="Class1"><a class="viewcode-back" ' 'href="../../index.html#spam.Class1">[docs]</a>' @@ -53,7 +53,7 @@ def test_viewcode(app, status, warning): def test_linkcode(app, status, warning): app.builder.build(['objects']) - stuff = (app.outdir / 'objects.html').text() + stuff = (app.outdir / 'objects.html').read_text() assert 'http://foobar/source/foolib.py' in stuff assert 'http://foobar/js/' in stuff @@ -65,7 +65,7 @@ def test_linkcode(app, status, warning): def test_local_source_files(app, status, warning): def find_source(app, modname): if modname == 'not_a_package': - source = (app.srcdir / 'not_a_package/__init__.py').text() + source = (app.srcdir / 'not_a_package/__init__.py').read_text() tags = { 'func1': ('def', 1, 1), 'Class1': ('class', 1, 1), @@ -73,7 +73,7 @@ def test_local_source_files(app, status, warning): 'not_a_package.submodule.Class1': ('class', 1, 1), } else: - source = (app.srcdir / 'not_a_package/submodule.py').text() + source = (app.srcdir / 'not_a_package/submodule.py').read_text() tags = { 'not_a_package.submodule.func1': ('def', 11, 15), 'Class1': ('class', 19, 22), @@ -93,7 +93,7 @@ def test_local_source_files(app, status, warning): warnings ) - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() assert result.count('href="_modules/not_a_package.html#func1"') == 1 assert result.count('href="_modules/not_a_package.html#not_a_package.submodule.func1"') == 1 assert result.count('href="_modules/not_a_package/submodule.html#Class1"') == 1 diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py index 525af2547..c2b470a6b 100644 --- a/tests/test_highlighting.py +++ b/tests/test_highlighting.py @@ -40,7 +40,7 @@ class ComplainOnUnhighlighted(PygmentsBridge): def test_add_lexer(app, status, warning): - app.add_lexer('test', MyLexer()) + app.add_lexer('test', MyLexer) bridge = PygmentsBridge('html') ret = bridge.highlight_block('ab', 'test') diff --git a/tests/test_intl.py b/tests/test_intl.py index f038c2b28..0e7dd4f62 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -95,7 +95,7 @@ def assert_count(expected_expr, result, count): @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_toctree(app): app.build() - result = (app.outdir / 'index.txt').text() + result = (app.outdir / 'index.txt').read_text() assert_startswith(result, "CONTENTS\n********\n\nTABLE OF CONTENTS\n") @@ -117,7 +117,7 @@ def test_text_emit_warnings(app, warning): def test_text_warning_node(app): app.build() # test warnings in translation - result = (app.outdir / 'warnings.txt').text() + result = (app.outdir / 'warnings.txt').read_text() expect = ("3. I18N WITH REST WARNINGS" "\n**************************\n" "\nLINE OF >>``<<BROKEN LITERAL MARKUP.\n") @@ -131,7 +131,7 @@ def test_text_warning_node(app): def test_text_title_underline(app): app.build() # --- simple translation; check title underlines - result = (app.outdir / 'bom.txt').text() + result = (app.outdir / 'bom.txt').read_text() expect = ("2. Datei mit UTF-8" "\n******************\n" # underline matches new translation "\nThis file has umlauts: äöü.\n") @@ -144,7 +144,7 @@ def test_text_title_underline(app): def test_text_subdirs(app): app.build() # --- check translation in subdirs - result = (app.outdir / 'subdir' / 'index.txt').text() + result = (app.outdir / 'subdir' / 'index.txt').read_text() assert_startswith(result, "1. subdir contents\n******************\n") @@ -154,7 +154,7 @@ def test_text_subdirs(app): def test_text_inconsistency_warnings(app, warning): app.build() # --- check warnings for inconsistency in number of references - result = (app.outdir / 'refs_inconsistency.txt').text() + result = (app.outdir / 'refs_inconsistency.txt').read_text() expect = ("8. I18N WITH REFS INCONSISTENCY" "\n*******************************\n" "\n* FOR CITATION [ref3].\n" @@ -204,7 +204,7 @@ def test_text_inconsistency_warnings(app, warning): def test_text_literalblock_warnings(app, warning): app.build() # --- check warning for literal block - result = (app.outdir / 'literalblock.txt').text() + result = (app.outdir / 'literalblock.txt').read_text() expect = ("9. I18N WITH LITERAL BLOCK" "\n**************************\n" "\nCORRECT LITERAL BLOCK:\n" @@ -226,7 +226,7 @@ def test_text_literalblock_warnings(app, warning): def test_text_definition_terms(app): app.build() # --- definition terms: regression test for #975, #2198, #2205 - result = (app.outdir / 'definition_terms.txt').text() + result = (app.outdir / 'definition_terms.txt').read_text() expect = ("13. I18N WITH DEFINITION TERMS" "\n******************************\n" "\nSOME TERM" @@ -246,7 +246,7 @@ def test_text_definition_terms(app): def test_text_glossary_term(app, warning): app.build() # --- glossary terms: regression test for #1090 - result = (app.outdir / 'glossary_terms.txt').text() + result = (app.outdir / 'glossary_terms.txt').read_text() expect = ("18. I18N WITH GLOSSARY TERMS" "\n****************************\n" "\nSOME NEW TERM" @@ -265,7 +265,7 @@ def test_text_glossary_term(app, warning): def test_text_glossary_term_inconsistencies(app, warning): app.build() # --- glossary term inconsistencies: regression test for #1090 - result = (app.outdir / 'glossary_terms_inconsistency.txt').text() + result = (app.outdir / 'glossary_terms_inconsistency.txt').read_text() expect = ("19. I18N WITH GLOSSARY TERMS INCONSISTENCY" "\n******************************************\n" "\n1. LINK TO *SOME NEW TERM*.\n") @@ -298,7 +298,7 @@ def test_gettext_section(app): def test_text_section(app): app.build() # --- section - result = (app.outdir / 'section.txt').text() + result = (app.outdir / 'section.txt').read_text() 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 @@ -310,7 +310,7 @@ def test_text_section(app): def test_text_seealso(app): app.build() # --- seealso - result = (app.outdir / 'seealso.txt').text() + result = (app.outdir / 'seealso.txt').read_text() expect = ("12. I18N WITH SEEALSO" "\n*********************\n" "\nSee also: SHORT TEXT 1\n" @@ -327,7 +327,7 @@ def test_text_seealso(app): def test_text_figure_captions(app): app.build() # --- figure captions: regression test for #940 - result = (app.outdir / 'figure.txt').text() + result = (app.outdir / 'figure.txt').read_text() expect = ("14. I18N WITH FIGURE CAPTION" "\n****************************\n" "\n [image]MY CAPTION OF THE FIGURE\n" @@ -371,7 +371,7 @@ def test_text_figure_captions(app): def test_text_rubric(app): app.build() # --- rubric: regression test for pull request #190 - result = (app.outdir / 'rubric.txt').text() + result = (app.outdir / 'rubric.txt').read_text() expect = ("I18N WITH RUBRIC" "\n****************\n" "\n-[ RUBRIC TITLE ]-\n" @@ -389,7 +389,7 @@ def test_text_rubric(app): def test_text_docfields(app): app.build() # --- docfields - result = (app.outdir / 'docfields.txt').text() + result = (app.outdir / 'docfields.txt').read_text() expect = ("21. I18N WITH DOCFIELDS" "\n***********************\n" "\nclass Cls1\n" @@ -420,7 +420,7 @@ def test_text_admonitions(app): # --- admonitions # #1206: gettext did not translate admonition directive's title # seealso: http://docutils.sourceforge.net/docs/ref/rst/directives.html#admonitions - result = (app.outdir / 'admonitions.txt').text() + result = (app.outdir / 'admonitions.txt').read_text() directives = ( "attention", "caution", "danger", "error", "hint", "important", "note", "tip", "warning", "admonition") @@ -462,7 +462,7 @@ def test_gettext_table(app): def test_text_table(app): app.build() # --- toctree - result = (app.outdir / 'table.txt').text() + result = (app.outdir / 'table.txt').read_text() 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 @@ -486,7 +486,7 @@ def test_gettext_toctree(app): def test_text_toctree(app): app.build() # --- toctree - result = (app.outdir / 'toctree.txt').text() + result = (app.outdir / 'toctree.txt').read_text() expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -510,7 +510,7 @@ def test_gettext_topic(app): def test_text_topic(app): app.build() # --- topic - result = (app.outdir / 'topic.txt').text() + result = (app.outdir / 'topic.txt').read_text() 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 @@ -628,7 +628,7 @@ def test_gettext_dont_rebuild_mo(make_app, app_params): def test_html_meta(app): app.build() # --- test for meta - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() expected_expr = '<meta content="TESTDATA FOR I18N" name="description" />' assert expected_expr in result expected_expr = '<meta content="I18N, SPHINX, MARKUP" name="keywords" />' @@ -644,7 +644,7 @@ def test_html_footnotes(app): app.build() # --- test for #955 cant-build-html-with-footnotes-when-using # expect no error by build - (app.outdir / 'footnote.html').text() + (app.outdir / 'footnote.html').read_text() @sphinx_intl @@ -653,7 +653,7 @@ def test_html_footnotes(app): def test_html_undefined_refs(app): app.build() # --- links to undefined reference - result = (app.outdir / 'refs_inconsistency.html').text() + result = (app.outdir / 'refs_inconsistency.html').read_text() expected_expr = ('<a class="reference external" ' 'href="http://www.example.com">reference</a>') @@ -675,7 +675,7 @@ def test_html_undefined_refs(app): def test_html_index_entries(app): app.build() # --- index entries: regression test for #976 - result = (app.outdir / 'genindex.html').text() + result = (app.outdir / 'genindex.html').read_text() def wrap(tag, keyword): start_tag = "<%s[^>]*>" % tag @@ -713,7 +713,7 @@ def test_html_index_entries(app): def test_html_versionchanges(app): app.build() # --- versionchanges - result = (app.outdir / 'versionchange.html').text() + result = (app.outdir / 'versionchange.html').read_text() def get_content(result, name): matched = re.search(r'<div class="%s">\n*(.*?)</div>' % name, @@ -750,7 +750,7 @@ def test_html_docfields(app): app.build() # --- docfields # expect no error by build - (app.outdir / 'docfields.html').text() + (app.outdir / 'docfields.html').read_text() @sphinx_intl @@ -759,7 +759,7 @@ def test_html_docfields(app): def test_html_template(app): app.build() # --- gettext template - result = (app.outdir / 'contents.html').text() + result = (app.outdir / 'contents.html').read_text() assert "WELCOME" in result assert "SPHINX 2013.120" in result @@ -1056,7 +1056,7 @@ def test_xml_label_targets(app): def test_additional_targets_should_not_be_translated(app): app.build() # [literalblock.txt] - result = (app.outdir / 'literalblock.html').text() + result = (app.outdir / 'literalblock.html').read_text() # title should be translated expected_expr = 'CODE-BLOCKS' @@ -1092,7 +1092,7 @@ def test_additional_targets_should_not_be_translated(app): # [raw.txt] - result = (app.outdir / 'raw.html').text() + result = (app.outdir / 'raw.html').read_text() # raw block should not be translated expected_expr = """<iframe src="http://sphinx-doc.org"></iframe></div>""" @@ -1100,7 +1100,7 @@ def test_additional_targets_should_not_be_translated(app): # [figure.txt] - result = (app.outdir / 'figure.html').text() + result = (app.outdir / 'figure.html').read_text() # alt and src for image block should not be translated expected_expr = """<img alt="i18n" src="_images/i18n.png" />""" @@ -1130,7 +1130,7 @@ def test_additional_targets_should_not_be_translated(app): def test_additional_targets_should_be_translated(app): app.build() # [literalblock.txt] - result = (app.outdir / 'literalblock.html').text() + result = (app.outdir / 'literalblock.html').read_text() # title should be translated expected_expr = 'CODE-BLOCKS' @@ -1166,7 +1166,7 @@ def test_additional_targets_should_be_translated(app): # [raw.txt] - result = (app.outdir / 'raw.html').text() + result = (app.outdir / 'raw.html').read_text() # raw block should be translated expected_expr = """<iframe src="HTTP://SPHINX-DOC.ORG"></iframe></div>""" @@ -1174,7 +1174,7 @@ def test_additional_targets_should_be_translated(app): # [figure.txt] - result = (app.outdir / 'figure.html').text() + result = (app.outdir / 'figure.html').read_text() # alt and src for image block should be translated expected_expr = """<img alt="I18N -> IMG" src="_images/img.png" />""" diff --git a/tests/test_markup.py b/tests/test_markup.py index edf4d7379..b6d99db90 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -231,6 +231,13 @@ def get_verifier(verify, verify_re): r'\sphinxguilabel{Foo}', ), ( + # kbd role + 'verify', + ':kbd:`space`', + '<p><kbd class="kbd docutils literal notranslate">space</kbd></p>', + '\\sphinxkeyboard{\\sphinxupquote{space}}', + ), + ( # non-interpolation of dashes in option role 'verify_re', ':option:`--with-option`', diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index af7e34a86..117feb8f7 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -8,27 +8,51 @@ :license: BSD, see LICENSE for details. """ +import sys + import pytest from sphinx.pycode import ast @pytest.mark.parametrize('source,expected', [ + ("a + b", "a + b"), # Add + ("a and b", "a and b"), # And ("os.path", "os.path"), # Attribute + ("1 * 2", "1 * 2"), # BinOp + ("a & b", "a & b"), # BitAnd + ("a | b", "a | b"), # BitOr + ("a ^ b", "a ^ b"), # BitXor + ("a and b and c", "a and b and c"), # BoolOp ("b'bytes'", "b'bytes'"), # Bytes ("object()", "object()"), # Call ("1234", "1234"), # Constant ("{'key1': 'value1', 'key2': 'value2'}", "{'key1': 'value1', 'key2': 'value2'}"), # Dict + ("a / b", "a / b"), # Div ("...", "..."), # Ellipsis + ("a // b", "a // b"), # FloorDiv ("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript + ("~ 1", "~ 1"), # Invert ("lambda x, y: x + y", - "<function <lambda>>"), # Lambda + "lambda x, y: ..."), # Lambda ("[1, 2, 3]", "[1, 2, 3]"), # List + ("a << b", "a << b"), # LShift + ("a @ b", "a @ b"), # MatMult + ("a % b", "a % b"), # Mod + ("a * b", "a * b"), # Mult ("sys", "sys"), # Name, NameConstant ("1234", "1234"), # Num + ("not a", "not a"), # Not + ("a or b", "a or b"), # Or + ("a ** b", "a ** b"), # Pow + ("a >> b", "a >> b"), # RShift ("{1, 2, 3}", "{1, 2, 3}"), # Set + ("a - b", "a - b"), # Sub ("'str'", "'str'"), # Str + ("+ a", "+ a"), # UAdd + ("- 1", "- 1"), # UnaryOp + ("- a", "- a"), # USub ("(1, 2, 3)", "1, 2, 3"), # Tuple ]) def test_unparse(source, expected): @@ -38,3 +62,11 @@ def test_unparse(source, expected): def test_unparse_None(): assert ast.unparse(None) is None + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +def test_unparse_py38(): + source = "lambda x=0, /, y=1, *args, z, **kwargs: x + y + z" + expected = "lambda x=0, /, y=1, *args, z, **kwargs: ..." + module = ast.parse(source) + assert ast.unparse(module.body[0].value) == expected diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 70a6f030e..bdd7073d1 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -193,7 +193,7 @@ def test_generated_files_eol(tempdir): qs.generate(d) def assert_eol(filename, eol): - content = filename.bytes().decode() + content = filename.read_bytes().decode() assert all([l[-len(eol):] == eol for l in content.splitlines(True)]) assert_eol(tempdir / 'make.bat', '\r\n') diff --git a/tests/test_search.py b/tests/test_search.py index 802602d5f..a4cefbc67 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -41,7 +41,7 @@ def setup_module(): def jsload(path): - searchindex = path.text() + searchindex = path.read_text() assert searchindex.startswith('Search.setIndex(') assert searchindex.endswith(')') @@ -98,7 +98,7 @@ def test_meta_keys_are_handled_for_language_de(app, status, warning): @pytest.mark.sphinx(testroot='search') def test_stemmer_does_not_remove_short_words(app, status, warning): app.builder.build_all() - searchindex = (app.outdir / 'searchindex.js').text() + searchindex = (app.outdir / 'searchindex.js').read_text() assert 'zfs' in searchindex @@ -112,11 +112,11 @@ def test_stemmer(app, status, warning): @pytest.mark.sphinx(testroot='search') def test_term_in_heading_and_section(app, status, warning): - searchindex = (app.outdir / 'searchindex.js').text() + searchindex = (app.outdir / 'searchindex.js').read_text() # if search term is in the title of one doc and in the text of another # both documents should be a hit in the search index as a title, # respectively text hit - assert 'textinhead:1' in searchindex + assert 'textinhead:2' in searchindex assert 'textinhead:0' in searchindex @@ -247,8 +247,18 @@ def test_IndexBuilder_lookup(): def test_search_index_gen_zh(app, status, warning): app.builder.build_all() # jsdump fails if search language is 'zh'; hence we just get the text: - searchindex = (app.outdir / 'searchindex.js').text() + searchindex = (app.outdir / 'searchindex.js').read_text() assert 'chinesetest ' not in searchindex assert 'chinesetest' in searchindex assert 'chinesetesttwo' in searchindex assert 'cas' in searchindex + + +@pytest.mark.sphinx(testroot='search') +def test_nosearch(app): + app.build() + index = jsload(app.outdir / 'searchindex.js') + assert index['docnames'] == ['index', 'nosearch', 'tocitem'] + assert 'latex' not in index['terms'] + assert 'zfs' in index['terms'] + assert index['terms']['zfs'] == 0 # zfs on nosearch.rst is not registered to index diff --git a/tests/test_setup_command.py b/tests/test_setup_command.py index 9bfc08d06..14a687ada 100644 --- a/tests/test_setup_command.py +++ b/tests/test_setup_command.py @@ -94,7 +94,7 @@ def nonascii_srcdir(request, setup_command): """)) master_doc = srcdir / 'index.txt' - master_doc.write_bytes((master_doc.text() + dedent(""" + master_doc.write_bytes((master_doc.read_text() + dedent(""" .. toctree:: %(mb_name)s/%(mb_name)s diff --git a/tests/test_smartquotes.py b/tests/test_smartquotes.py index 50a1cf52e..6276e6a74 100644 --- a/tests/test_smartquotes.py +++ b/tests/test_smartquotes.py @@ -17,7 +17,7 @@ from sphinx.util import docutils def test_basic(app, status, warning): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<p>– “Sphinx” is a tool that makes it easy …</p>' in content @@ -25,7 +25,7 @@ def test_basic(app, status, warning): def test_text_builder(app, status, warning): app.build() - content = (app.outdir / 'index.txt').text() + content = (app.outdir / 'index.txt').read_text() assert '-- "Sphinx" is a tool that makes it easy ...' in content @@ -33,7 +33,7 @@ def test_text_builder(app, status, warning): def test_man_builder(app, status, warning): app.build() - content = (app.outdir / 'python.1').text() + content = (app.outdir / 'python.1').read_text() assert '\\-\\- "Sphinx" is a tool that makes it easy ...' in content @@ -41,7 +41,7 @@ def test_man_builder(app, status, warning): def test_latex_builder(app, status, warning): app.build() - content = (app.outdir / 'python.tex').text() + content = (app.outdir / 'python.tex').read_text() assert '\\textendash{} “Sphinx” is a tool that makes it easy …' in content @@ -50,7 +50,7 @@ def test_latex_builder(app, status, warning): def test_ja_html_builder(app, status, warning): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<p>-- "Sphinx" is a tool that makes it easy ...</p>' in content @@ -59,7 +59,7 @@ def test_ja_html_builder(app, status, warning): def test_smartquotes_disabled(app, status, warning): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<p>-- "Sphinx" is a tool that makes it easy ...</p>' in content @@ -70,7 +70,7 @@ def test_smartquotes_disabled(app, status, warning): def test_smartquotes_action(app, status, warning): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<p>-- “Sphinx” is a tool that makes it easy ...</p>' in content @@ -79,7 +79,7 @@ def test_smartquotes_action(app, status, warning): def test_smartquotes_excludes_language(app, status, warning): app.build() - content = (app.outdir / 'index.html').text() + content = (app.outdir / 'index.html').read_text() assert '<p>– 「Sphinx」 is a tool that makes it easy …</p>' in content @@ -88,5 +88,5 @@ def test_smartquotes_excludes_language(app, status, warning): def test_smartquotes_excludes_builders(app, status, warning): app.build() - content = (app.outdir / 'python.1').text() + content = (app.outdir / 'python.1').read_text() assert '– “Sphinx” is a tool that makes it easy …' in content diff --git a/tests/test_templating.py b/tests/test_templating.py index 0be960b88..becacda0d 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -20,7 +20,7 @@ def test_layout_overloading(make_app, app_params): setup_documenters(app) app.builder.build_update() - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() assert '<!-- layout overloading -->' in result @@ -31,5 +31,5 @@ def test_autosummary_class_template_overloading(make_app, app_params): setup_documenters(app) app.builder.build_update() - result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').text() + result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text() assert 'autosummary/class.rst method block overloading' in result diff --git a/tests/test_theming.py b/tests/test_theming.py index d154d8a4c..affaf5408 100644 --- a/tests/test_theming.py +++ b/tests/test_theming.py @@ -11,8 +11,8 @@ import os import alabaster -import pytest +import pytest from sphinx.theming import ThemeError @@ -76,16 +76,16 @@ def test_js_source(app, status, warning): v = '3.4.1' msg = 'jquery.js version does not match to {v}'.format(v=v) - jquery_min = (app.outdir / '_static' / 'jquery.js').text() + jquery_min = (app.outdir / '_static' / 'jquery.js').read_text() assert 'jQuery v{v}'.format(v=v) in jquery_min, msg - jquery_src = (app.outdir / '_static' / 'jquery-{v}.js'.format(v=v)).text() + jquery_src = (app.outdir / '_static' / 'jquery-{v}.js'.format(v=v)).read_text() assert 'jQuery JavaScript Library v{v}'.format(v=v) in jquery_src, msg v = '1.3.1' msg = 'underscore.js version does not match to {v}'.format(v=v) - underscore_min = (app.outdir / '_static' / 'underscore.js').text() + underscore_min = (app.outdir / '_static' / 'underscore.js').read_text() assert 'Underscore.js {v}'.format(v=v) in underscore_min, msg - underscore_src = (app.outdir / '_static' / 'underscore-{v}.js'.format(v=v)).text() + underscore_src = (app.outdir / '_static' / 'underscore-{v}.js'.format(v=v)).read_text() assert 'Underscore.js {v}'.format(v=v) in underscore_src, msg @@ -108,21 +108,37 @@ def test_staticfiles(app, status, warning): app.build() assert (app.outdir / '_static' / 'staticimg.png').exists() assert (app.outdir / '_static' / 'statictmpl.html').exists() - assert (app.outdir / '_static' / 'statictmpl.html').text() == ( + assert (app.outdir / '_static' / 'statictmpl.html').read_text() == ( '<!-- testing static templates -->\n' '<html><project>Python</project></html>' ) - result = (app.outdir / 'index.html').text() + result = (app.outdir / 'index.html').read_text() assert '<meta name="testopt" content="optdefault" />' in result +@pytest.mark.sphinx(testroot='theming', + confoverrides={'html_theme': 'test-theme'}) +def test_dark_style(app, status, warning): + style = app.builder.dark_highlighter.formatter_args.get('style') + assert style.__name__ == 'MonokaiStyle' + + app.build() + assert (app.outdir / '_static' / 'pygments_dark.css').exists() + + result = (app.outdir / 'index.html').read_text() + assert '<link rel="stylesheet" href="_static/pygments.css" type="text/css" />' in result + assert ('<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" ' + 'rel="stylesheet" type="text/css" ' + 'href="_static/pygments_dark.css" />') in result + + @pytest.mark.sphinx(testroot='theming') def test_theme_sidebars(app, status, warning): app.build() # test-theme specifies globaltoc and searchbox as default sidebars - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert '<h3><a href="#">Table of Contents</a></h3>' in result assert '<h3>Related Topics</h3>' not in result assert '<h3>This Page</h3>' not in result diff --git a/tests/test_toctree.py b/tests/test_toctree.py index c4d0c9b7b..38886769a 100644 --- a/tests/test_toctree.py +++ b/tests/test_toctree.py @@ -41,7 +41,7 @@ def test_singlehtml_toctree(app, status, warning): @pytest.mark.sphinx(testroot='toctree', srcdir="numbered-toctree") def test_numbered_toctree(app, status, warning): # give argument to :numbered: option - index = (app.srcdir / 'index.rst').text() + index = (app.srcdir / 'index.rst').read_text() index = re.sub(':numbered:.*', ':numbered: 1', index) (app.srcdir / 'index.rst').write_text(index) app.builder.build_all() diff --git a/tests/test_transforms_post_transforms_code.py b/tests/test_transforms_post_transforms_code.py index bd66a4c91..880f4711b 100644 --- a/tests/test_transforms_post_transforms_code.py +++ b/tests/test_transforms_post_transforms_code.py @@ -13,7 +13,7 @@ import pytest def test_trim_doctest_flags_html(app, status, warning): app.build() - result = (app.outdir / 'index.html').text(encoding='utf8') + result = (app.outdir / 'index.html').read_text() assert 'FOO' not in result assert 'BAR' in result assert 'BAZ' not in result @@ -25,7 +25,7 @@ def test_trim_doctest_flags_html(app, status, warning): def test_trim_doctest_flags_latex(app, status, warning): app.build() - result = (app.outdir / 'python.tex').text(encoding='utf8') + result = (app.outdir / 'python.tex').read_text() assert 'FOO' not in result assert 'BAR' in result assert 'BAZ' not in result diff --git a/tests/test_util.py b/tests/test_util.py index f794c4f74..434d96d3a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -18,7 +18,7 @@ import sphinx 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, + SkipProgressMessage, display_chunk, encode_uri, ensuredir, import_object, parselinenos, progress_message, status_iterator, xmlname_checker ) from sphinx.util import logging @@ -61,16 +61,6 @@ def test_display_chunk(): assert display_chunk(('hello', 'sphinx', 'world')) == 'hello .. world' -def test_get_module_source(): - assert get_module_source('sphinx') == ('file', sphinx.__file__) - - # failed to obtain source information from builtin modules - with pytest.raises(PycodeError): - get_module_source('builtins') - with pytest.raises(PycodeError): - get_module_source('itertools') - - def test_import_object(): module = import_object('sphinx') assert module.__name__ == 'sphinx' diff --git a/tests/test_util_docstrings.py b/tests/test_util_docstrings.py index bfd5b58b4..2f0901d06 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util_docstrings.py @@ -8,7 +8,34 @@ :license: BSD, see LICENSE for details. """ -from sphinx.util.docstrings import prepare_docstring, prepare_commentdoc +from sphinx.util.docstrings import ( + extract_metadata, prepare_docstring, prepare_commentdoc +) + + +def test_extract_metadata(): + metadata = extract_metadata(":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar', 'baz': ''} + + # field_list like text following just after paragaph is not a field_list + metadata = extract_metadata("blah blah blah\n" + ":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {} + + # field_list like text following after blank line is a field_list + metadata = extract_metadata("blah blah blah\n" + "\n" + ":meta foo: bar\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar', 'baz': ''} + + # non field_list item breaks field_list + metadata = extract_metadata(":meta foo: bar\n" + "blah blah blah\n" + ":meta baz:\n") + assert metadata == {'foo': 'bar'} def test_prepare_docstring(): diff --git a/tests/test_util_fileutil.py b/tests/test_util_fileutil.py index 02d1acada..e9c80d5b6 100644 --- a/tests/test_util_fileutil.py +++ b/tests/test_util_fileutil.py @@ -33,7 +33,7 @@ def test_copy_asset_file(tempdir): copy_asset_file(src, dest) assert dest.exists() - assert src.text() == dest.text() + assert src.read_text() == dest.read_text() # copy template file src = (tempdir / 'asset.txt_t') @@ -43,7 +43,7 @@ def test_copy_asset_file(tempdir): copy_asset_file(src, dest, {'var1': 'template'}, renderer) assert not dest.exists() assert (tempdir / 'output.txt').exists() - assert (tempdir / 'output.txt').text() == '# template data' + assert (tempdir / 'output.txt').read_text() == '# template data' # copy template file to subdir src = (tempdir / 'asset.txt_t') @@ -53,7 +53,7 @@ def test_copy_asset_file(tempdir): copy_asset_file(src, subdir1, {'var1': 'template'}, renderer) assert (subdir1 / 'asset.txt').exists() - assert (subdir1 / 'asset.txt').text() == '# template data' + assert (subdir1 / 'asset.txt').read_text() == '# template data' # copy template file without context src = (tempdir / 'asset.txt_t') @@ -63,7 +63,7 @@ def test_copy_asset_file(tempdir): copy_asset_file(src, subdir2) assert not (subdir2 / 'asset.txt').exists() assert (subdir2 / 'asset.txt_t').exists() - assert (subdir2 / 'asset.txt_t').text() == '# {{var1}} data' + assert (subdir2 / 'asset.txt_t').read_text() == '# {{var1}} data' def test_copy_asset(tempdir): @@ -91,11 +91,11 @@ def test_copy_asset(tempdir): copy_asset(source, destdir, context=dict(var1='bar', var2='baz'), renderer=renderer) assert (destdir / 'index.rst').exists() assert (destdir / 'foo.rst').exists() - assert (destdir / 'foo.rst').text() == 'bar.rst' + assert (destdir / 'foo.rst').read_text() == 'bar.rst' assert (destdir / '_static' / 'basic.css').exists() assert (destdir / '_templates' / 'layout.html').exists() assert (destdir / '_templates' / 'sidebar.html').exists() - assert (destdir / '_templates' / 'sidebar.html').text() == 'sidebar: baz' + assert (destdir / '_templates' / 'sidebar.html').read_text() == 'sidebar: baz' # copy with exclusion def excluded(path): diff --git a/tests/test_util_images.py b/tests/test_util_images.py index da492fd05..2a256577f 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 a8019a9c3..627d20f54 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -13,6 +13,7 @@ import datetime import functools import sys import types +from inspect import Parameter import pytest @@ -20,76 +21,6 @@ from sphinx.util import inspect from sphinx.util.inspect import stringify_signature -def test_getargspec(): - def func(a, b, c=1, d=2, *e, **f): - pass - - spec = inspect.getargspec(func) - assert spec.args == ['a', 'b', 'c', 'd'] - assert spec.varargs == 'e' - assert spec.varkw == 'f' - assert spec.defaults == (1, 2) - assert spec.kwonlyargs == [] - assert spec.kwonlydefaults is None - assert spec.annotations == {} - - -def test_getargspec_partial(): - def func1(a, b, c=1, d=2, *e, **f): - pass - - partial = functools.partial(func1, 10, c=11) - spec = inspect.getargspec(partial) - assert spec.args == ['b'] - assert spec.varargs is None - assert spec.varkw == 'f' - assert spec.defaults is None - assert spec.kwonlyargs == ['c', 'd'] - assert spec.kwonlydefaults == {'c': 11, 'd': 2} - assert spec.annotations == {} - - -def test_getargspec_partial2(): - def fun(a, b, c=1, d=2): - pass - p = functools.partial(fun, 10, c=11) - - def f_expected(b, *, c=11, d=2): - pass - expected = inspect.getargspec(f_expected) - - assert expected == inspect.getargspec(p) - - -def test_getargspec_builtin_type(): - with pytest.raises(TypeError): - inspect.getargspec(int) - - -def test_getargspec_bound_methods(): - def f_expected_unbound(self, arg1, **kwargs): - pass - expected_unbound = inspect.getargspec(f_expected_unbound) - - def f_expected_bound(arg1, **kwargs): - pass - expected_bound = inspect.getargspec(f_expected_bound) - - class Foo: - def method(self, arg1, **kwargs): - pass - - bound_method = Foo().method - - @functools.wraps(bound_method) - def wrapped_bound_method(*args, **kwargs): - pass - - assert expected_unbound == inspect.getargspec(Foo.method) - assert expected_bound == inspect.getargspec(bound_method) - assert expected_bound == inspect.getargspec(wrapped_bound_method) - - def test_signature(): # literals with pytest.raises(TypeError): @@ -316,6 +247,90 @@ def test_signature_annotations_py38(app): assert stringify_signature(sig) == '(a, b, /)' +def test_signature_from_str_basic(): + signature = '(a, b, *args, c=0, d="blah", **kwargs)' + sig = inspect.signature_from_str(signature) + assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs'] + assert sig.parameters['a'].name == 'a' + assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD + assert sig.parameters['a'].default == Parameter.empty + assert sig.parameters['a'].annotation == Parameter.empty + assert sig.parameters['b'].name == 'b' + assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD + assert sig.parameters['b'].default == Parameter.empty + assert sig.parameters['b'].annotation == Parameter.empty + assert sig.parameters['args'].name == 'args' + assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL + assert sig.parameters['args'].default == Parameter.empty + assert sig.parameters['args'].annotation == Parameter.empty + assert sig.parameters['c'].name == 'c' + assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY + assert sig.parameters['c'].default == '0' + assert sig.parameters['c'].annotation == Parameter.empty + assert sig.parameters['d'].name == 'd' + assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY + assert sig.parameters['d'].default == "'blah'" + assert sig.parameters['d'].annotation == Parameter.empty + assert sig.parameters['kwargs'].name == 'kwargs' + assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD + assert sig.parameters['kwargs'].default == Parameter.empty + assert sig.parameters['kwargs'].annotation == Parameter.empty + assert sig.return_annotation == Parameter.empty + + +def test_signature_from_str_default_values(): + signature = ('(a=0, b=0.0, c="str", d=b"bytes", e=..., f=True, ' + 'g=[1, 2, 3], h={"a": 1}, i={1, 2, 3}, ' + 'j=lambda x, y: None, k=None, l=object(), m=foo.bar.CONSTANT)') + sig = inspect.signature_from_str(signature) + assert sig.parameters['a'].default == '0' + assert sig.parameters['b'].default == '0.0' + assert sig.parameters['c'].default == "'str'" + assert sig.parameters['d'].default == "b'bytes'" + assert sig.parameters['e'].default == '...' + assert sig.parameters['f'].default == 'True' + assert sig.parameters['g'].default == '[1, 2, 3]' + assert sig.parameters['h'].default == "{'a': 1}" + assert sig.parameters['i'].default == '{1, 2, 3}' + assert sig.parameters['j'].default == 'lambda x, y: ...' + assert sig.parameters['k'].default == 'None' + assert sig.parameters['l'].default == 'object()' + assert sig.parameters['m'].default == 'foo.bar.CONSTANT' + + +def test_signature_from_str_annotations(): + signature = '(a: int, *args: bytes, b: str = "blah", **kwargs: float) -> None' + sig = inspect.signature_from_str(signature) + assert list(sig.parameters.keys()) == ['a', 'args', 'b', 'kwargs'] + assert sig.parameters['a'].annotation == "int" + assert sig.parameters['args'].annotation == "bytes" + assert sig.parameters['b'].annotation == "str" + assert sig.parameters['kwargs'].annotation == "float" + assert sig.return_annotation == 'None' + + +def test_signature_from_str_complex_annotations(): + sig = inspect.signature_from_str('() -> Tuple[str, int, ...]') + assert sig.return_annotation == 'Tuple[str, int, ...]' + + sig = inspect.signature_from_str('() -> Callable[[int, int], int]') + assert sig.return_annotation == 'Callable[[int, int], int]' + + +@pytest.mark.skipif(sys.version_info < (3, 8), + reason='python-3.8 or above is required') +def test_signature_from_str_positionaly_only_args(): + sig = inspect.signature_from_str('(a, /, b)') + assert list(sig.parameters.keys()) == ['a', 'b'] + assert sig.parameters['a'].kind == Parameter.POSITIONAL_ONLY + assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD + + +def test_signature_from_str_invalid(): + with pytest.raises(SyntaxError): + inspect.signature_from_str('') + + def test_safe_getattr_with_default(): class Foo: def __getattr__(self, item): diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index 1581275ee..337782d87 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -103,6 +103,17 @@ def test_nonl_info_log(app, status, warning): assert 'message1message2\nmessage3' in status.getvalue() +def test_once_warning_log(app, status, warning): + logging.setup(app, status, warning) + logger = logging.getLogger(__name__) + + logger.warning('message: %d', 1, once=True) + logger.warning('message: %d', 1, once=True) + logger.warning('message: %d', 2, once=True) + + assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_escseq(warning.getvalue()) + + def test_is_suppressed_warning(): suppress_warnings = ["ref", "files.*", "rest.duplicated_labels"] @@ -233,6 +244,20 @@ def test_warning_location(app, status, warning): assert colorize('red', 'WARNING: message7') in warning.getvalue() +def test_suppress_logging(app, status, warning): + logging.setup(app, status, warning) + logger = logging.getLogger(__name__) + + logger.warning('message1') + with logging.suppress_logging(): + logger.warning('message2') + assert 'WARNING: message1' in warning.getvalue() + assert 'WARNING: message2' not in warning.getvalue() + + assert 'WARNING: message1' in warning.getvalue() + assert 'WARNING: message2' not in warning.getvalue() + + def test_pending_warnings(app, status, warning): logging.setup(app, status, warning) logger = logging.getLogger(__name__) diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index 76ba51800..01c5d2e3f 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -183,18 +183,37 @@ def test_clean_astext(): assert 'hello world' == clean_astext(node) -def test_make_id(app): +@pytest.mark.parametrize( + 'prefix, term, expected', + [ + ('', '', 'id0'), + ('term', '', 'term-0'), + ('term', 'Sphinx', 'term-sphinx'), + ('', 'io.StringIO', 'io.stringio'), # contains a dot + ('', 'sphinx.setup_command', 'sphinx.setup_command'), # contains a dot & underscore + ('', '_io.StringIO', 'io.stringio'), # starts with underscore + ('', 'sphinx', 'sphinx'), # alphabets in unicode fullwidth characters + ('', '悠好', 'id0'), # multibytes text (in Chinese) + ('', 'Hello=悠好=こんにちは', 'hello'), # alphabets and multibytes text + ('', 'fünf', 'funf'), # latin1 (umlaut) + ('', '0sphinx', 'sphinx'), # starts with number + ('', 'sphinx-', 'sphinx'), # ends with hyphen + ]) +def test_make_id(app, prefix, term, expected): + document = create_new_document() + assert make_id(app.env, document, prefix, term) == expected + + +def test_make_id_already_registered(app): document = create_new_document() - assert make_id(app.env, document) == 'id0' - assert make_id(app.env, document, 'term') == 'term-0' - assert make_id(app.env, document, 'term', 'Sphinx') == 'term-sphinx' + document.ids['term-sphinx'] = True # register "term-sphinx" manually + assert make_id(app.env, document, 'term', 'Sphinx') == 'term-0' - # when same ID is already registered - document.ids['term-sphinx'] = True - assert make_id(app.env, document, 'term', 'Sphinx') == 'term-1' - document.ids['term-2'] = True - assert make_id(app.env, document, 'term') == 'term-3' +def test_make_id_sequential(app): + document = create_new_document() + document.ids['term-0'] = True + assert make_id(app.env, document, 'term') == 'term-1' @pytest.mark.parametrize( diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 9a225f0f1..f6fd35fb0 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -12,6 +12,8 @@ import sys from numbers import Integral from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional +import pytest + from sphinx.util.typing import stringify @@ -42,6 +44,12 @@ def test_stringify_type_hints_containers(): assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" +@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') +def test_stringify_Annotated(): + from typing import Annotated + assert stringify(Annotated[str, "foo", "bar"]) == "str" + + def test_stringify_type_hints_string(): assert stringify("int") == "int" assert stringify("str") == "str" @@ -5,12 +5,19 @@ envlist = docs,flake8,mypy,coverage,py{35,36,37,38,39},du{12,13,14,15} [testenv] usedevelop = True passenv = - https_proxy http_proxy no_proxy PERL PERL5LIB PYTEST_ADDOPTS EPUBCHECK_PATH + https_proxy + http_proxy + no_proxy + PERL + PERL5LIB + PYTEST_ADDOPTS + EPUBCHECK_PATH + TERM description = py{35,36,37,38,39}: Run unit tests against {envname}. du{12,13,14}: Run unit tests with the given version of docutils. deps = - git+https://github.com/html5lib/html5lib-python ; python_version >= "3.9" # refs: https://github.com/html5lib/html5lib-python/issues/419 + git+https://github.com/html5lib/html5lib-python # refs: https://github.com/html5lib/html5lib-python/issues/419 du12: docutils==0.12 du13: docutils==0.13.1 du14: docutils==0.14 @@ -19,7 +26,7 @@ deps = extras = test setenv = - PYTHONWARNINGS = all,ignore::ImportWarning:pkgutil,ignore::ImportWarning:importlib._bootstrap,ignore::ImportWarning:importlib._bootstrap_external,ignore::ImportWarning:pytest_cov.plugin,ignore::DeprecationWarning:site,ignore::DeprecationWarning:_pytest.assertion.rewrite,ignore::DeprecationWarning:_pytest.fixtures,ignore::DeprecationWarning:distutils + PYTHONWARNINGS = all,ignore::ImportWarning:importlib._bootstrap_external,ignore::DeprecationWarning:site,ignore::DeprecationWarning:distutils commands= pytest --durations 25 {posargs} @@ -27,8 +34,12 @@ commands= basepython = python3 description = Run style checks. -commands = +whitelist_externals = flake8 +extras = + lint +commands = + flake8 {posargs} [testenv:pylint] basepython = python3 @@ -54,9 +65,8 @@ commands = basepython = python3 description = Run type checks. -deps = - mypy - docutils-stubs +extras = + lint commands= mypy sphinx/ diff --git a/utils/jssplitter_generator.py b/utils/jssplitter_generator.py index 360ce7d15..7806d64d2 100644 --- a/utils/jssplitter_generator.py +++ b/utils/jssplitter_generator.py @@ -133,7 +133,7 @@ with open('../sphinx/search/jssplitter.py', 'w') as f: f.write(python_src) with open('./regression_test.js', 'w') as f: - f.write(js_test_src.encode()) + f.write(js_test_src) print("starting test...") result = subprocess.call(['node', './regression_test.js']) |