pax_global_header00006660000000000000000000000064141670372530014522gustar00rootroot0000000000000052 comment=f00d509a20183cbf45d128d730e28399286be669 pydoctor-21.12.1/000077500000000000000000000000001416703725300135315ustar00rootroot00000000000000pydoctor-21.12.1/.codecov.yml000066400000000000000000000002021416703725300157460ustar00rootroot00000000000000coverage: status: project: default: informational: true patch: default: informational: true pydoctor-21.12.1/.coveragerc000066400000000000000000000007531416703725300156570ustar00rootroot00000000000000[run] branch = True omit = pydoctor/sphinx_ext/* pydoctor/test/* pydoctor/epydoc/sre_parse36.py source = pydoctor [report] exclude_lines = # Manually marked: pragma: no cover # Intended to be unreachable: raise NotImplementedError$ raise NotImplementedError\( raise AssertionError$ raise AssertionError\( assert False$ assert False, # Debug-only code: def __repr__\( # Exclusive to mypy: if TYPE_CHECKING:$ \.\.\.$ pydoctor-21.12.1/.github/000077500000000000000000000000001416703725300150715ustar00rootroot00000000000000pydoctor-21.12.1/.github/workflows/000077500000000000000000000000001416703725300171265ustar00rootroot00000000000000pydoctor-21.12.1/.github/workflows/static.yaml000066400000000000000000000017731416703725300213110ustar00rootroot00000000000000name: Static code checks on: push: branches: [ master ] pull_request: branches: [ master ] jobs: static_checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.8' - name: Install tox run: | python -m pip install --upgrade pip tox - name: Log system information run: | test -r /etc/os-release && sh -c '. /etc/os-release && echo "OS: $PRETTY_NAME"' python --version python -c "print('\nENVIRONMENT VARIABLES\n=====================\n')" python -c "import os; [print(f'{k}={v}') for k, v in os.environ.items()]" - name: Run mypy run: | tox -e mypy - name: Run pyflakes run: | tox -e pyflakes - name: Run pydoctor on its own source and fail on docstring errors run: | tox -e apidocs - name: Run docs and check extensions run: | tox -e testdocs pydoctor-21.12.1/.github/workflows/system.yaml000066400000000000000000000015501416703725300213370ustar00rootroot00000000000000name: System tests on: push: branches: [ master ] pull_request: branches: [ master ] jobs: system_tests: runs-on: ubuntu-latest strategy: matrix: tox_target: [twisted-apidoc, cpython-summary] steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.8' - name: Install tox run: | python -m pip install --upgrade pip tox - name: Log system information run: | test -r /etc/os-release && sh -c '. /etc/os-release && echo "OS: $PRETTY_NAME"' python --version python -c "print('\nENVIRONMENT VARIABLES\n=====================\n')" python -c "import os; [print(f'{k}={v}') for k, v in os.environ.items()]" - name: Generate API docs run: | tox -e ${{ matrix.tox_target }} pydoctor-21.12.1/.github/workflows/unit.yaml000066400000000000000000000042551416703725300207770ustar00rootroot00000000000000name: Unit tests and release on: push: branches: [ master ] tags: - '*' pull_request: branches: [ master ] jobs: unit_tests: runs-on: ${{ matrix.os }} strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0, pypy-3.6] os: [ubuntu-20.04] include: - os: windows-latest python-version: 3.6 - os: macos-latest python-version: 3.6 steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install tox run: | python -m pip install --upgrade pip tox - name: Log system information run: | test -r /etc/os-release && sh -c '. /etc/os-release && echo "OS: $PRETTY_NAME"' python --version python -c "print('\nENVIRONMENT VARIABLES\n=====================\n')" python -c "import os; [print(f'{k}={v}') for k, v in os.environ.items()]" - name: Run unit tests run: | tox -e test - name: Publish code coverage continue-on-error: true run: | tox -e codecov release: needs: [unit_tests] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Log system information run: | test -r /etc/os-release && sh -c '. /etc/os-release && echo "OS: $PRETTY_NAME"' python --version python -c "print('\nENVIRONMENT VARIABLES\n=====================\n')" python -c "import os; [print(f'{k}={v}') for k, v in os.environ.items()]" - name: Install deps run: | python -m pip install --upgrade pip setuptools wheel - name: Build pydoctor run: | python setup.py --quiet build check sdist bdist_wheel ls -alh ./dist/ - name: Publish pydoctor to PyPI on tags if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} pydoctor-21.12.1/.gitignore000066400000000000000000000001361416703725300155210ustar00rootroot00000000000000build MANIFEST dist *.pyc .tox/ .coverage .DS_Store *~ _trial_temp/ apidocs/ *.egg-info .eggs pydoctor-21.12.1/CONTRIBUTING.rst000066400000000000000000000000411416703725300161650ustar00rootroot00000000000000See ``_ pydoctor-21.12.1/LICENSE.txt000066400000000000000000000041321416703725300153540ustar00rootroot00000000000000 Copyright 2006-2008 Michael Hudson Copyright 2006-2020 Various contributors (see Git history) All Rights Reserved Permission to use, copy, modify, and distribute this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Support for epytext and reStructuredText was imported from epydoc. This code can be found under pydoctor/epydoc/ and is licensed as follows: Copyright 2001-2009 Edward Loper Permission is hereby granted, free of charge, to any person obtaining a copy of this software and any associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software. pydoctor-21.12.1/MANIFEST.in000066400000000000000000000001751416703725300152720ustar00rootroot00000000000000include setup.cfg graft pydoctor/themes graft docs include *.txt *.tac include MANIFEST.in include *.cfg graft pydoctor/test pydoctor-21.12.1/README.rst000066400000000000000000000263171416703725300152310ustar00rootroot00000000000000pydoctor -------- .. image:: https://travis-ci.org/twisted/pydoctor.svg?branch=tox-travis-2 :target: https://travis-ci.org/twisted/pydoctor .. image:: https://codecov.io/gh/twisted/pydoctor/branch/master/graph/badge.svg :target: https://codecov.io/gh/twisted/pydoctor .. image:: https://img.shields.io/badge/-documentation-blue :target: https://pydoctor.readthedocs.io/ This is *pydoctor*, an API documentation generator that works by static analysis. It was written primarily to replace ``epydoc`` for the purposes of the Twisted project as ``epydoc`` has difficulties with ``zope.interface``. If you are looking for a successor to ``epydoc`` after moving to Python 3, ``pydoctor`` might be the right tool for your project as well. ``pydoctor`` puts a fair bit of effort into resolving imports and computing inheritance hierarchies and, as it aims at documenting Twisted, knows about ``zope.interface``'s declaration API and can present information about which classes implement which interface, and vice versa. .. contents:: Contents: Simple Usage ~~~~~~~~~~~~ You can run pydoctor on your project like this:: $ pydoctor --make-html --html-output=docs/api src/mylib For more info, `Read The Docs `_. Markup ~~~~~~ pydoctor currently supports the following markup languages in docstrings: `epytext`__ (default) The markup language of epydoc. Simple and compact. `restructuredtext`__ The markup language used by Sphinx. More expressive than epytext, but also slightly more complex and verbose. `google`__ Docstrings formatted as specified by the Google Python Style Guide. (compatible with reStructuredText markup) `numpy`__ Docstrings formatted as specified by the Numpy Docstring Standard. (compatible with reStructuredText markup) ``plaintext`` Text without any markup. __ http://epydoc.sourceforge.net/manual-epytext.html __ https://docutils.sourceforge.io/rst.html __ https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings __ https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard You can select a different format using the ``--docformat`` option or the ``__docformat__`` module variable. What's New? ~~~~~~~~~~~ pydoctor 21.12.1 ^^^^^^^^^^^^^^^^ * Include module ``sre_parse36.py`` within ``pydoctor.epydoc`` to avoid an extra PyPi dependency. pydoctor 21.12.0 ^^^^^^^^^^^^^^^^ * Add support for reStructuredText directives ``.. deprecated::``, ``.. versionchanged::`` and ``.. versionadded::``. * Add syntax highlight for constant values, decorators and parameter defaults. * Embedded documentation links inside the value of constants, decorators and parameter defaults. * Provide option ``--pyval-repr-maxlines`` and ``--pyval-repr-linelen`` to control the size of a constant value representation. * Provide option ``--process-types`` to automatically link types in docstring fields (`more info `_). * Forked Napoleon Sphinx extension to provide google-style and numpy-style docstring parsing. * Introduced fields ``warns``, ``yields`` and ``yieldtype``. * Following google style guide, ``*args`` and ``**kwargs`` are now rendered with asterisks in the parameters table. * Mark variables as constants when their names is all caps or if using `Final` annotation. pydoctor 21.9.2 ^^^^^^^^^^^^^^^ * Fix ``AttributeError`` raised when parsing reStructuredText consolidated fields, caused by a change in ``docutils`` 0.18. * Fix ``DeprecationWarning``, use newer APIs of ``importlib_resources`` module. pydoctor 21.9.1 ^^^^^^^^^^^^^^^ * Fix deprecation warning and officially support Python 3.10. * Fix the literals style (use same style as before). pydoctor 21.9.0 ^^^^^^^^^^^^^^^ * Add support for multiple themes, selectable with ``--theme`` option. * Support selecting a different docstring format for a module using the ``__docformat__`` variable. * HTML templates are now customizable with ``--template-dir`` option. * Change the fields layout to display the arguments type right after their name. Same goes for variables. pydoctor 21.2.2 ^^^^^^^^^^^^^^^ * Fix positioning of anchors, such that following a link to a member of a module or class will scroll its documentation to a visible spot at the top of the page. pydoctor 21.2.1 ^^^^^^^^^^^^^^^ * Fix presentation of the project name and URL in the navigation bars, such that it works as expected on all generated HTML pages. pydoctor 21.2.0 ^^^^^^^^^^^^^^^ * Removed the ``--html-write-function-pages`` option. As a replacement, you can use the generated Intersphinx inventory (``objects.inv``) for deep-linking your documentation. * Fixed project version in the generated Intersphinx inventory. This used to be hardcoded to 2.0 (we mistook it for a format version), now it is unversioned by default and a version can be specified using the new ``--project-version`` option. * Fixed multiple bugs in Python name resolution, which could lead to for example missing "implemented by" links. * Fixed bug where class docstring fields such as ``cvar`` and ``ivar`` are ignored when they override inherited attribute docstrings. * Property decorators containing one or more dots (such as ``@abc.abstractproperty``) are now recognized by the custom properties support. * Improvements to `attrs`__ support: - Attributes are now marked as instance variables. - Type comments are given precedence over types inferred from ``attr.ib``. - Support positional arguments in ``attr.ib`` definitions. Please use keyword arguments instead though, both for clarity and to be compatible with future ``attrs`` releases. * Improvements in the treatment of the ``__all__`` module variable: - Assigning an empty sequence is interpreted as exporting nothing instead of being ignored. - Better error reporting when the value assigned is either invalid or pydoctor cannot make sense of it. * Added ``except`` field as a synonym of ``raises``, to be compatible with epydoc and to fix handling of the ``:Exceptions:`` consolidated field in reStructuredText. * Exception types and external base classes are hyperlinked to their class documentation. * Formatting of ``def func():`` and ``class Class:`` lines was made consistent with code blocks. * Changes to the "Show/hide Private API" button: - The button was moved to the right hand side of the navigation bar, to avoid overlapping the content on narrow displays. - The show/hide state is now synced with a query argument in the location bar. This way, if you bookmark the page or send a link to someone else, the show/hide state will be preserved. - A deep link to a private API item will now automatically enable "show private API" mode. * Improvements to the ``build_apidocs`` Sphinx extension: - API docs are now built before Sphinx docs, such that the rest of the documentation can link to it via Intersphinx. - New configuration variable ``pydoctor_url_path`` that will automatically update the ``intersphinx_mapping`` variable so that it uses the latest API inventory. - The extension can be configured to build API docs for more than one package. * ``pydoctor.__version__`` is now a plain ``str`` instead of an ``incremental.Version`` object. __ https://www.attrs.org/ pydoctor 20.12.1 ^^^^^^^^^^^^^^^^ * Reject source directories outside the project base directory (if given), instead of crashing. * Fixed bug where source directories containing symbolic links could appear to be outside of the project base directory, leading to a crash. * Bring back source link on package pages. pydoctor 20.12.0 ^^^^^^^^^^^^^^^^ * Python 3.6 or higher is required. * There is now a user manual that can be built with Sphinx or read online on `Read the Docs`__. This is a work in progress and the online version will be updated between releases. * Added support for Python language features: - Type annotations of function parameters and return value are used when the docstring does not document a type. - Functions decorated with ``@property`` or any other decorator with a name ending in "property" are now formatted similar to variables. - Coroutine functions (``async def``) are included in the output. - Keyword-only and position-only parameters are included in the output. * Output improvements: - Type names in annotations are hyperlinked to the corresponding documentation. - Styling changes to make the generated documentation easier to read and navigate. - Private API is now hidden by default on the Module Index, Class Hierarchy and Index of Names pages. - The pydoctor version is included in the "generated by" line in the footer. * All parents of the HTML output directory are now created by pydoctor; previously it would create only the deepest directory. * The ``--add-package`` and ``--add-module`` options have been deprecated; pass the source paths as positional arguments instead. * New option ``-W``/``--warnings-as-errors`` to fail your build on documentation errors. * Linking to the standard library documentation is more accurate now, but does require the use of an Intersphinx inventory (``--intersphinx=https://docs.python.org/3/objects.inv``). * Caching of Intersphinx inventories is now enabled by default. * Added a `Sphinx extension`__ for embedding pydoctor's output in a project's Sphinx documentation. * Added an extra named ``rst`` for the dependencies needed to process reStructuredText (``pip install -U pydoctor[rst]``). * Improved error reporting: - More accurate source locations (file + line number) in error messages. - Warnings were added for common mistakes when documenting parameters. - Clearer error message when a link target is not found. * Increased reliability: - Fixed crash when analyzing ``from package import *``. - Fixed crash when the line number for a docstring error is unknown. - Better unit test coverage, more system tests, started adding type annotations to the code. - Unit tests are also run on Windows. __ https://pydoctor.readthedocs.io/ __ https://pydoctor.readthedocs.io/en/latest/usage.html#building-pydoctor-together-with-sphinx-html-build pydoctor 20.7.2 ^^^^^^^^^^^^^^^ * Fix handling of external links in reStructuredText under Python 3. * Fix reporting of errors in reStructuredText under Python 3. * Restore syntax highlighting of Python code blocks. pydoctor 20.7.1 ^^^^^^^^^^^^^^^ * Fix cross-reference links to builtin types in standard library. * Fix and improve error message printed for unknown fields. pydoctor 20.7.0 ^^^^^^^^^^^^^^^ * Python 3 support. * Type annotations on attributes are supported when running on Python 3. * Type comments on attributes are supported when running on Python 3.8+. * Type annotations on function definitions are not supported yet. * Undocumented attributes are now included in the output. * Attribute docstrings: a module, class or instance variable can be documented by a following it up with a docstring. * Improved error reporting: more errors are reported, error messages include file name and line number. * Dropped support for implicit relative imports. * Explicit relative imports (using ``from``) no longer cause warnings. * Dropped support for index terms in epytext (``X{}``). This was never supported in any meaningful capacity, but now the tag is gone. This was the last major release to support Python 2.7 and 3.5. .. description-end pydoctor-21.12.1/docs/000077500000000000000000000000001416703725300144615ustar00rootroot00000000000000pydoctor-21.12.1/docs/Makefile000066400000000000000000000011761416703725300161260ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pydoctor-21.12.1/docs/epytext_demo/000077500000000000000000000000001416703725300171675ustar00rootroot00000000000000pydoctor-21.12.1/docs/epytext_demo/__init__.py000066400000000000000000000121111416703725300212740ustar00rootroot00000000000000""" General epytext formating markups are documented here. Epydoc code related formating are demonstrated in the L{demo_epytext_module}. Read the U{the epytext manual } for more documentation. Scope and Purpose ================= Sample package for describing and demonstrating C{pydoctor} HTML API rendering for B{Epytext} based documentation. Many examples are copied from U{the epytext manual }. Try to keep the example as condensed as possible. - Make it easy to review HTML rendering. - Cover all epytext markup. Like the usage of list with various indentation types. - Have it build as part of our continuous integration tests. To ensure we don't introduce regressions. Lists ===== Epytext supports both ordered and unordered lists. A list consists of one or more consecutive list items with the same indentation. Each list item is marked by a bullet. The bullet for unordered list items is a single dash character (C{-}). Bullets for ordered list items consist of a series of numbers followed by periods, such as C{12.} or C{1.2.8.}. Ordered list example: 1. This is an ordered list item. 2. This is a another ordered list item. 3. This is a third list item. Note that the paragraph may be indented more than the bullet. Example of unordered list: - This is an ordered list item. - This is a another ordered list item. Example of complex list: 1. This is a list item. - This is a sublist. - The sublist contains two items. - The second item of the sublist has its own sublist. 2. This list item contains two paragraphs and a doctest block. >>> print 'This is a doctest block' This is a doctest block This is the second paragraph. Literal Blocks ============== Literal blocks are used to represent "preformatted" text. Everything within a literal block should be displayed exactly as it appears in plaintext. - Spaces and newlines are preserved. - Text is shown in a monospaced font. - Inline markup is not detected. Literal blocks are introduced by paragraphs ending in the special sequence C{::}. Literal blocks end at the first line whose indentation is equal to or less than that of the paragraph that introduces them. The following is a literal block:: Literal / / X{Block} Doctest Blocks ============== - contain examples consisting of Python expressions and their output - can be used by the doctest module to test the documented object - begin with the special sequence C{>>>} - are delimited from surrounding blocks by blank lines - may not contain blank lines The following is a doctest block: >>> print (1+3, ... 3+5) (4, 8) >>> 'a-b-c-d-e'.split('-') ['a', 'b', 'c', 'd', 'e'] This is a paragraph following the doctest block. Basic Inline Markup =================== I{B{Inline markup} may be nested; and it may span} multiple lines. Epytext defines four types of inline markup that specify how text should be displayed: - I{Italicized text} - B{Bold-faced text} - C{Source code} - M{Math} Without the capital letter, matching braces are not interpreted as markup: C{my_dict={1:2, 3:4}}. URLs ==== The inline markup construct U{text} is used to create links to external URLs and URIs. 'text' is the text that should be displayed for the link, and 'url' is the target of the link. If you wish to use the URL as the text for the link, you can simply write "U{url}". Whitespace within URL targets is ignored. In particular, URL targets may be split over multiple lines. The following example illustrates how URLs can be used: - U{www.python.org} - U{http://www.python.org} - U{The epydoc homepage} - U{The B{Python} homepage } - U{Edward Loper} Symbols ======= Symbols are used to insert special characters in your documentation. A symbol has the form SE{lb}codeE{rb}, where code is a symbol code that specifies what character should be produced. Symbols can be used in equations: S{sum}S{alpha}/x S{<=} S{beta} S{<-} and S{larr} both give left arrows. Some other arrows are S{rarr}, S{uarr}, and S{darr}. Escaping ======== Escaping is used to write text that would otherwise be interpreted as epytext markup. Escaped text has the form EE{lb}codeE{rb}, where code is an escape code that specifies what character should be produced. If the escape code is a single character (other than '{' or '}'), then that character is produced. For example, to begin a paragraph with a dash (which would normally signal a list item), write 'E{-}'. In addition, two special escape codes are defined: 'E{lb}' produces a left curly brace ('{'); and 'E{rb}' produces a right curly brace ('}'). This paragraph ends with two colons, but does not introduce a literal blockE{:}E{:} E{-} This is not a list item. Escapes can be used to write unmatched curly braces: E{rb}E{lb} """ pydoctor-21.12.1/docs/epytext_demo/constants.py000066400000000000000000000054501416703725300215610ustar00rootroot00000000000000""" Module demonstrating the constant representations. """ import re from .demo_epytext_module import demo_fields_docstring_arguments, _PrivateClass A_DICT = {'1':33, '2':[1,2,3,{7:'oo'*20}], '3': demo_fields_docstring_arguments, '4': _PrivateClass.method_inside_private, '5': re.compile('^<(?P.*) at (?P0x[0-9a-f]+)>$') } """ The value of a constant is rendered with syntax highlighting. Internal and external links are generated to references of classes/functions used inside the constant """ A_STIRNG = "L'humour, c'est l'arme blanche des hommes désarmés; c'est une déclaration de supériorité de l'humain sur ce qui lui arrive 😀. Romain GARY." """ Strings are always rendered in single quotes, and appropriate escaping is added when required. Continuing lines are wrapped with symbol: "↵" after reaching the maximum number of caracters per line (defaults to 80), change this value with option --pyval-repr-linelen. Unicode is supported. """ A_MULTILINE_STRING = "Dieu se rit des hommes qui déplorent les effets dont ils cherrissent les causes.\n\nJacques-Bénigne BOSSUET." """ Multiline strings are always rendered in triple quotes. """ A_LIST = [1,2,[5,6,[(11,22,33),9],10],11]+[99,98,97,96,95] """ Nested objects are colorized. """ FUNCTION_CALL = list(range(100))+[99,98,97,96,95] """ Function calls are colorized. """ OPERATORS = 1 << (10 | 1) << 1 """Operators are colorized and parenthesis are added when syntactically required.""" UNSUPPORTED = lambda x: demo_fields_docstring_arguments(x, 0) // 2 """ A lot of objects can be colorized: function calls, strings, lists, dicts, sets, frozensets, operators, annotations, names, compiled regular expressions, etc. But when dealing with usupported constructs, like lamba functions, it will display the value without colorization. """ RE_STR = re.compile("(foo (?Pbar) | (?Pbaz))") """ Regular expressions have special colorizing that add syntax highlight to the regex components. """ RE_WITH_UNICODE = re.compile("abc 😀") """ Unicode is supported in regular expressions. """ RE_MULTILINE = re.compile(r''' # Source consists of a PS1 line followed by zero or more PS2 lines. (?P (?:^(?P [ ]*) >>> .*) # PS1 line (?:\n [ ]* \.\.\. .*)* # PS2 lines \n?) # Want consists of any non-blank lines that do not start with PS1. (?P (?:(?![ ]*$) # Not a blank line (?![ ]*>>>) # Not a line starting with PS1 .*$\n? # But any other line )*) ''', re.MULTILINE | re.VERBOSE) """ Multiline regex patterns are rendered as string. "..." is added when reaching the maximum number of lines for constant representation (defaults to 7), change this value with option --pyval-repr-maxlines. """ pydoctor-21.12.1/docs/epytext_demo/demo_epytext_module.py000066400000000000000000000103461416703725300236200ustar00rootroot00000000000000""" This is a module demonstrating epydoc code documentation features. Most part of this documentation is using Python type hinting. """ from abc import ABC from typing import AnyStr, Dict, Generator, List, Union, TYPE_CHECKING from somelib import SomeInterface import zope.interface import zope.schema from typing import Sequence, Optional if TYPE_CHECKING: from typing_extensions import Final LANG = 'Fr' """ This is a constant. See L{constants} for more examples. """ lang: 'Final[Sequence[str]]' = ['Fr', 'En'] """ This is also a constant, but annotated with typing.Final. """ def demo_fields_docstring_arguments(m, b): # type: ignore """ Fields are used to describe specific properties of a documented object. This function can be used in conjunction with L{demo_typing_arguments} to find an arbitrary function's zeros. @type m: number @param m: The slope of the line. @type b: number @param b: The y intercept of the line. @rtype: number @return: the x intercept of the line M{y=m*x+b}. """ return -b/m def demo_typing_arguments(name: str, size: Optional[bytes] = None) -> bool: """ Type documentation can be extracted from standard Python type hints. @param name: The human readable name for something. @param size: How big the name should be. Leave none if you don't care. @return: Always C{True}. """ return True def demo_long_function_and_parameter_names__this_indeed_very_long( this_is_a_very_long_parameter_name_aahh: str, what__another_super_super_long_name__ho_no: Generator[Union[List[AnyStr], Dict[str, AnyStr]], None, None]) -> bool: """ Long names and annotations should display on several lines when they don't fit in a single line. """ return True def demo_cross_reference() -> None: """ The inline markup construct C{LE{lb}textE{rb}} is used to create links to the documentation for other Python objects. 'text' is the text that should be displayed for the link, and 'object' is the name of the Python object that should be linked to. If you wish to use the name of the Python object as the text for the link, you can simply write C{LE{lb}objectE{rb}}. - L{demo_typing_arguments} - L{Custom name } """ class _PrivateClass: """ This is the docstring of a private class. """ def method_inside_private(self) -> bool: """ A public method inside a private class. @return: Something. """ return True def _private_inside_private(self) -> bool: """ A private method inside a private class. @return: Something. """ return True class DemoClass(ABC, SomeInterface, _PrivateClass): """ This is the docstring of this class. """ def __init__(self, one: str, two: bytes) -> None: """ Documentation for class initialization. @param one: Docs for first argument. @param two: Docs for second argument. """ @property def read_only(self) -> int: """ This is a read-only property. """ return 1 @property def read_and_write(self) -> int: """ This is a read-write property. """ return 1 @read_and_write.setter def read_and_write(self, value: int) -> None: """ This is a docstring for setter. """ @property def read_and_write_delete(self) -> int: """ This is a read-write-delete property. """ return 1 @read_and_write_delete.setter def read_and_write_delete(self, value: int) -> None: """ This is a docstring for setter. """ @read_and_write_delete.deleter def read_and_write_delete(self) -> None: """ This is a docstring for deleter. """ class IContact(zope.interface.Interface): """ Example of an interface with schemas. Provides access to basic contact information. """ first = zope.schema.TextLine(description="First name") email = zope.schema.TextLine(description="Electronic mail address") address = zope.schema.Text(description="Postal address") def send_email(text: str) -> None: pass pydoctor-21.12.1/docs/google_demo/000077500000000000000000000000001416703725300167415ustar00rootroot00000000000000pydoctor-21.12.1/docs/google_demo/__init__.py000066400000000000000000000221611416703725300210540ustar00rootroot00000000000000""" Pydoctor pre-process Google-style docstrings to convert them to reStructuredText. **All standard reStructuredText formatting will still works as expected**. Please see `restructuredtext_demo <../restructuredtext/restructuredtext_demo.html>`_ for general reStructuredText formmating exemple. Example Google style docstrings. This module demonstrates documentation as specified by the `Google Python Style Guide`_. Docstrings may extend over multiple lines. Sections are created with a section header and a colon followed by a block of indented text. Example: Examples can be given using either the ``Example`` or ``Examples`` sections. Sections support any reStructuredText formatting, including literal blocks:: $ python example_google.py Section breaks are created by resuming unindented text. Section breaks are also implicitly created anytime a new section starts. Attributes: module_level_variable1 (int): Module level variables may be documented in either the ``Attributes`` section of the module docstring, or in an inline docstring immediately following the variable. Either form is acceptable, but the two should not be mixed. Choose one convention to document module level variables and be consistent with it. .. _Google Python Style Guide: https://google.github.io/styleguide/pyguide.html """ from typing import List, Union # NOQA module_level_variable1 = 12345 module_level_variable2 = 98765 """int: Module level variable documented inline. The docstring may span multiple lines. The type may optionally be specified on the first line, separated by a colon. """ def function_with_types_in_docstring(param1, param2): """Example function with types documented in the docstring. `PEP 484`_ type annotations are supported. If attribute, parameter, and return types are annotated according to `PEP 484`_, they do not need to be included in the docstring: Args: param1 (int): The first parameter. param2 (str): The second parameter. Returns: bool: The return value. True for success, False otherwise. .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ """ def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: """Example function with PEP 484 type annotations. Args: param1: The first parameter. param2: The second parameter. Returns: The return value. True for success, False otherwise. """ def module_level_function(param1, param2=None, *args, **kwargs): """This is an example of a module level function. Function parameters should be documented in the ``Args`` section. The name of each parameter is required. The type and description of each parameter is optional, but should be included if not obvious. If ``*args`` or ``**kwargs`` are accepted, they should be listed as ``*args`` and ``**kwargs``. The format for a parameter is:: name (type): description The description may span multiple lines. Following lines should be indented. The "(type)" is optional. Multiple paragraphs are supported in parameter descriptions. Args: param1 (int): The first parameter. param2 (`str`, optional): The second parameter. Defaults to None. Second line of description should be indented. *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. Returns: bool: True if successful, False otherwise. The return type is optional and may be specified at the beginning of the ``Returns`` section followed by a colon. The ``Returns`` section may span multiple lines and paragraphs. Following lines should be indented to match the first line. The ``Returns`` section supports any reStructuredText formatting, including literal blocks:: { 'param1': param1, 'param2': param2 } Raises: AttributeError: The ``Raises`` section is a list of all exceptions that are relevant to the interface. ValueError: If ``param2`` is equal to ``param1``. """ if param1 == param2: raise ValueError('param1 may not be equal to param2') return True def example_generator(n): """Generators have a ``Yields`` section instead of a ``Returns`` section. Args: n (int): The upper limit of the range to generate, from 0 to ``n`` - 1. Yields: int: The next number in the range of 0 to ``n`` - 1. Examples: Examples should be written in doctest format, and should illustrate how to use the function. >>> print([i for i in example_generator(4)]) [0, 1, 2, 3] """ for i in range(n): yield i class ExampleError(Exception): """Exceptions are documented in the same way as classes. The __init__ method should be documented as a docstring on the __init__ method. Note: Do not include the ``self`` parameter in the ``Args`` section. Args: msg (str): Human readable string describing the exception. code (int, optional): Error code. Attributes: msg (str): Human readable string describing the exception. code (int): Exception error code. """ def __init__(self, msg, code): self.msg = msg self.code = code class ExampleClass: """The summary line for a class docstring should fit on one line. If the class has public attributes, they may be documented here in an ``Attributes`` section and follow the same formatting as a function's ``Args`` section. Alternatively, attributes may be documented inline with the attribute's declaration (see __init__ method below). Attributes: attr1 (str): Description of `attr1`. attr2 (List[Union[str, bytes, int]], optional): Description of `attr2`. Methods: example_method: Quick example __special__: Dunder methods are considered public __special_without_docstring__: *Undocumented* text will appear. Note: The "Methods" section is supported only as a "best effort" basis. See: Google style "See Also" section is just like any admonition. """ def __init__(self, param1, param2, param3): """Example of docstring on the __init__ method. The __init__ method should be documented as a docstring on the __init__ method. Note: Do not include the ``self`` parameter in the ``Args`` section. Args: param1 (str): Description of ``param1``. param2 (`int`, optional): Description of ``param2``. Multiple lines are supported. param3 (list(str)): Description of ``param3``. """ self.attr1 = param1 self.attr2 = param2 self.attr3 = param3 #: Doc comment *inline* with attribute #: list(str): Doc comment *before* attribute, with type specified self.attr4 = ['attr4'] self.attr5 = None """str: Docstring *after* attribute, with type specified.""" @property def readonly_property(self): """str: Properties should be documented in their getter method.""" return 'readonly_property' @property def readwrite_property(self): """list(str): Properties with both a getter and setter should only be documented in their getter method. If the setter method contains notable behavior, it should be mentioned here. """ return ['readwrite_property'] @readwrite_property.setter def readwrite_property(self, value): value def example_method(self, param1, param2): """Class methods are similar to regular functions. Note: Do not include the ``self`` parameter in the ``Args`` section. Args: param1: The first parameter. param2: The second parameter. Returns: tuple(str, str, int, tuple(str, str)): A complicated result. """ return tuple('string', 'foo', -1, tuple('cool', 'right')) def __special__(self): """Dunder methods are considered public and will be included in the output. """ pass def __special_without_docstring__(self): pass def _private(self): """ Private members are any methods or attributes that start with an underscore and are *not* special. By default they are hidden, they can be displayed with the "Show Private API" button. """ pass def _private_without_docstring(self): pass class ExamplePEP526Class: """The summary line for a class docstring should fit on one line. If the class has public attributes, they may be documented here in an ``Attributes`` section and follow the same formatting as a function's ``Args`` section. If ``napoleon_attr_annotations`` is True, types can be specified in the class body using ``PEP 526`` annotations. Attributes: attr1: Description of `attr1`. attr2: Description of `attr2`. """ attr1: str attr2: int pydoctor-21.12.1/docs/make.bat000066400000000000000000000014371416703725300160730ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pydoctor-21.12.1/docs/numpy_demo/000077500000000000000000000000001416703725300166355ustar00rootroot00000000000000pydoctor-21.12.1/docs/numpy_demo/__init__.py000066400000000000000000000224231416703725300207510ustar00rootroot00000000000000""" Pydoctor pre-process Numpy-style docstrings to convert them to reStructuredText. **All standard reStructuredText formatting will still works as expected**. Please see `restructuredtext_demo <../restructuredtext/restructuredtext_demo.html>`_ for general reStructuredText formmating exemple. Example NumPy style docstrings. This module demonstrates documentation as specified by the `NumPy Documentation HOWTO`_. Docstrings may extend over multiple lines. Sections are created with a section header followed by an underline of equal length. Example ------- Examples can be given using either the ``Example`` or ``Examples`` sections. Sections support any reStructuredText formatting, including literal blocks:: $ python example_numpy.py Section breaks are created with two blank lines. Section breaks are also implicitly created anytime a new section starts. Section bodies *may* be indented: Notes ----- This is an example of an indented section. It's like any other section, but the body is indented to help it stand out from surrounding text. If a section is indented, then a section break is created by resuming unindented text. Attributes ---------- module_level_variable1 : int Module level variables may be documented in either the ``Attributes`` section of the module docstring, or in an inline docstring immediately following the variable. Either form is acceptable, but the two should not be mixed. Choose one convention to document module level variables and be consistent with it. .. _NumPy Documentation HOWTO: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard """ from typing import List, Union module_level_variable1 = 12345 module_level_variable2 = 98765 """int: Module level variable documented inline. The docstring may span multiple lines. The type may optionally be specified on the first line, separated by a colon. """ def function_with_types_in_docstring(param1, param2): """Example function with types documented in the docstring. `PEP 484`_ type annotations are supported. If attribute, parameter, and return types are annotated according to `PEP 484`_, they do not need to be included in the docstring: Parameters ---------- param1 : int The first parameter. param2 : str The second parameter. Returns ------- bool True if successful, False otherwise. .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ """ def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: """Example function with PEP 484 type annotations. The return type must be duplicated in the docstring to comply with the NumPy docstring style. Parameters ---------- param1 The first parameter. param2 The second parameter. Returns ------- bool True if successful, False otherwise. """ def module_level_function(param1, param2=None, *args, **kwargs): """This is an example of a module level function. Function parameters should be documented in the ``Parameters`` section. The name of each parameter is required. The type and description of each parameter is optional, but should be included if not obvious. If ``*args`` or ``**kwargs`` are accepted, they should be listed as ``*args`` and ``**kwargs``. The format for a parameter is:: name : type description The description may span multiple lines. Following lines should be indented to match the first line of the description. The ": type" is optional. Multiple paragraphs are supported in parameter descriptions. Parameters ---------- param1 : int The first parameter. param2 : `str`, optional The second parameter. *args Variable length argument list. **kwargs Arbitrary keyword arguments. Returns ------- bool True if successful, False otherwise. The return type is not optional. The ``Returns`` section may span multiple lines and paragraphs. Following lines should be indented to match the first line of the description. The ``Returns`` section supports any reStructuredText formatting, including literal blocks:: { 'param1': param1, 'param2': param2 } Raises ------ AttributeError The ``Raises`` section is a list of all exceptions that are relevant to the interface. ValueError If ``param2`` is equal to ``param1``. """ if param1 == param2: raise ValueError('param1 may not be equal to param2') return True def example_generator(n): """Generators have a ``Yields`` section instead of a ``Returns`` section. Parameters ---------- n : int The upper limit of the range to generate, from 0 to ``n`` - 1. Yields ------ int The next number in the range of 0 to ``n`` - 1. Examples -------- Examples should be written in doctest format, and should illustrate how to use the function. >>> print([i for i in example_generator(4)]) [0, 1, 2, 3] """ for i in range(n): yield i class ExampleError(Exception): """Exceptions are documented in the same way as classes. The __init__ method should be documented as a docstring on the __init__ method. Note ---- Do not include the ``self`` parameter in the ``Parameters`` section. Parameters ---------- msg : str Human readable string describing the exception. code : `int`, optional Numeric error code. Attributes ---------- msg : str Human readable string describing the exception. code : int Numeric error code. """ def __init__(self, msg, code): self.msg = msg self.code = code class ExampleClass: """The summary line for a class docstring should fit on one line. If the class has public attributes, they may be documented here in an ``Attributes`` section and follow the same formatting as a function's ``Args`` section. Alternatively, attributes may be documented inline with the attribute's declaration (see __init__ method below). Attributes ---------- attr1 : str Description of `attr1`. attr2 : List[Union[str, bytes, int]], optional Description of `attr2`. Methods ------- example_method Quick example __special__ Dunder methods are considered public __special_without_docstring__ *Undocumented* text will appear. Note ---- The "Methods" section is supported only as a "best effort" basis. See Also -------- ``example_google`` The same document but for google-style. """ def __init__(self, param1, param2, param3): """Example of docstring on the __init__ method. The __init__ method should be documented as a docstring on the __init__ method. Note ---- Do not include the ``self`` parameter in the ``Parameters`` section. Parameters ---------- param1 : str Description of ``param1``. param2 : list(str) Description of ``param2``. Multiple lines are supported. param3 : `int`, optional Description of ``param3``. """ self.attr1 = param1 self.attr2 = param2 self.attr3 = param3 #: Doc comment *inline* with attribute #: list(str): Doc comment *before* attribute, with type specified self.attr4 = ["attr4"] self.attr5 = None """str: Docstring *after* attribute, with type specified.""" @property def readonly_property(self): """str: Properties should be documented in their getter method.""" return "readonly_property" @property def readwrite_property(self): """list(str): Properties with both a getter and setter should only be documented in their getter method. If the setter method contains notable behavior, it should be mentioned here. """ return ["readwrite_property"] @readwrite_property.setter def readwrite_property(self, value): value def example_method(self, param1, param2): """Class methods are similar to regular functions. Note ---- Do not include the ``self`` parameter in the ``Parameters`` section. Parameters ---------- param1 The first parameter. param2 The second parameter. Returns ------- tuple(str, str, int, tuple(str, str)) A complicated result. """ return tuple('string', 'foo', -1, tuple('cool', 'right')) def __special__(self): """Dunder methods are considered public and will be included in the output. """ pass def __special_without_docstring__(self): pass def _private(self): """ Private members are any methods or attributes that start with an underscore and are *not* special. By default they are hidden, they can be displayed with the "Show Private API" button. """ pass def _private_without_docstring(self): pass pydoctor-21.12.1/docs/restructuredtext_demo/000077500000000000000000000000001416703725300211255ustar00rootroot00000000000000pydoctor-21.12.1/docs/restructuredtext_demo/__init__.py000066400000000000000000000151451416703725300232440ustar00rootroot00000000000000r""" Few general reStructuredText formating markups are documented here. reStructuredText code related formating are demonstrated in the `demo_restructuredtext_module`. Many examples are copied from `the docutils quickref `_. Scope and Purpose ================= Sample package for describing and demonstrating ``pydoctor`` HTML API rendering for **reStructuredText** based documentation. Try to keep the example as condensed as possible. - Make it easy to review HTML rendering. - Cover all most common reStructuredText markup. Like the usage of list with various indentation types. - Have it build as part of our continuous integration tests. To ensure we don't introduce regressions. .. note:: Even if most of the structural (i.e. not inline) reST markup appears to ressemble Epytext markup, blank lines are often needed where Epytext allowed no blank line after parent element. Indentation is also much more important, lists content and child items must be correctly indented. Lists ===== reStructuredText supports both ordered and unordered lists. A list consists of one or more consecutive list items with the same indentation. Each list item is marked by a bullet. The bullet for unordered list items is a single dash character (``-``). Bullets for ordered list items consist of a series of numbers followed by periods, such as ``12.`` or ``1.2.8.``. Ordered list example: 1. This is an ordered list item. 2. This is a another ordered list item. 3. This is a third list item. Note that the paragraph may be indented more than the bullet. Example of unordered list: - This is an ordered list item. - This is a another ordered list item. Example of complex list: 1. This is a list item. - This is a sublist. - The sublist contains two items. - The second item of the sublist has its own sublist. 2. This list item contains two paragraphs and a doctest block. >>> print 'This is a doctest block' This is a doctest block This is the second paragraph. Literal Blocks ============== Literal blocks are used to represent "preformatted" text. Everything within a literal block should be displayed exactly as it appears in plaintext. - Spaces and newlines are preserved. - Text is shown in a monospaced font. - Inline markup is not detected. Literal blocks are introduced by paragraphs ending in the special sequence ``::``. Literal blocks end at the first line whose indentation is equal to or less than that of the paragraph that introduces them. The following is a literal block:: Literal / / **Block** Doctest Blocks ============== - contain examples consisting of Python expressions and their output - can be used by the doctest module to test the documented object - begin with the special sequence ``>>>`` - are delimited from surrounding blocks by blank lines - may not contain blank lines The following is a doctest block inside a block quote (automatically added because of indentation): >>> print (1+3, ... 3+5) (4, 8) >>> 'a-b-c-d-e'.split('-') ['a', 'b', 'c', 'd', 'e'] This is a paragraph following the doctest block. Python code Blocks ================== Using reStructuredText markup it is possible to specify Python code snippets in a ``.. python::`` directive . If the Python prompt gets in your way when you try to copy and paste and you are not interested in self-testing docstrings, the This will let you obtain a simple block of colorized text: .. python:: def fib(n): '''Print a Fibonacci series.''' a, b = 0, 1 while b < n: print b, a, b = b, a+b Inline Markup ============= reStructuredText defines a lot of inline markup, here's a few of the most common: - *Italicized text* - **Bold-faced text** - ``Source code`` - `subprocess.Popen` (Interpreted text: used for cross-referencing python objects) .. note:: Inline markup cannot be nested. A workaround is to use the ``.. replace::`` directive: I recommend you try |Python|_. .. |Python| replace:: **Python**, *the* best language around URLs ==== The inline markup construct ```text `_`` is used to create links to external URLs and URIs. 'text' is the text that should be displayed for the link, and 'url' is the target of the link. If you wish to use the URL as the text for the link, you can simply write the URL as is. The following example illustrates how URLs can be used: - http://www.python.org (A standalone hyperlink.) - `docutils quickref `_ - External hyperlinks with substitution, like Python_. .. _Python: http://www.python.org/ Admonitions =========== .. note:: This is just a info. .. tip:: This is good for you. .. hint:: This too. .. important:: Important information here. .. warning:: This should be taken seriouly. .. attention:: Beware. .. caution:: This should be taken very seriouly. .. danger:: This function is a security whole! .. error:: This is not right. .. raw:: html .. admonition:: Purple This needs additionnal CSS for the new "rst-admonition-purple" class. Include additional CSS by defining a raw block:: .. raw:: html .. note:: The ``! important`` is required to overrride ``apidocs.css``. Symbols ======= Any symbol can be rendered with the ``.. unicode::`` directive. Copyright |copy| 2021, |MojoInc (TM)| |---| all rights reserved. .. |copy| unicode:: 0xA9 .. copyright sign .. |MojoInc (TM)| unicode:: MojoInc U+2122 .. with trademark sign .. |---| unicode:: U+02014 .. em dash :trim: Comments ======== This is a commented warning:: .. .. warning:: This should not be used! .. .. warning:: This should not be used! Escaping ======== Escaping is used to write text that would otherwise be interpreted as reStructuredText markup. ReStructuredText handles escaping with the backslash character. thisis\ *one*\ word. .. note:: The docstring must be declared as a raw docstring: with the ``r`` prefix to prevent Python to interpret the backslashes. See more on escaping on `docutils documentation page `_ """ pydoctor-21.12.1/docs/restructuredtext_demo/constants.py000066400000000000000000000054611416703725300235210ustar00rootroot00000000000000""" Module demonstrating the constant representations. """ import re from .demo_restructuredtext_module import demo_fields_docstring_arguments, _PrivateClass A_DICT = {'1':33, '2':[1,2,3,{7:'oo'*20}], '3': demo_fields_docstring_arguments, '4': _PrivateClass.method_inside_private, '5': re.compile('^<(?P.*) at (?P0x[0-9a-f]+)>$') } """ The value of a constant is rendered with syntax highlighting. Internal and external links are generated to references of classes/functions used inside the constant """ A_STIRNG = "L'humour, c'est l'arme blanche des hommes désarmés; c'est une déclaration de supériorité de l'humain sur ce qui lui arrive 😀. Romain GARY." """ Strings are always rendered in single quotes, and appropriate escaping is added when required. Continuing lines are wrapped with symbol: "↵" after reaching the maximum number of caracters per line (defaults to 80), change this value with option --pyval-repr-linelen. Unicode is supported. """ A_MULTILINE_STRING = "Dieu se rit des hommes qui déplorent les effets dont ils cherrissent les causes.\n\nJacques-Bénigne BOSSUET." """ Multiline strings are always rendered in triple quotes. """ A_LIST = [1,2,[5,6,[(11,22,33),9],10],11]+[99,98,97,96,95] """ Nested objects are colorized. """ FUNCTION_CALL = list(range(100))+[99,98,97,96,95] """ Function calls are colorized. """ OPERATORS = 1 << (10 | 1) << 1 """Operators are colorized and parenthesis are added when syntactically required.""" UNSUPPORTED = lambda x: demo_fields_docstring_arguments(x, 0) // 2 """ A lot of objects can be colorized: function calls, strings, lists, dicts, sets, frozensets, operators, annotations, names, compiled regular expressions, etc. But when dealing with usupported constructs, like lamba functions, it will display the value without colorization. """ RE_STR = re.compile("(foo (?Pbar) | (?Pbaz))") """ Regular expressions have special colorizing that add syntax highlight to the regex components. """ RE_WITH_UNICODE = re.compile("abc 😀") """ Unicode is supported in regular expressions. """ RE_MULTILINE = re.compile(r''' # Source consists of a PS1 line followed by zero or more PS2 lines. (?P (?:^(?P [ ]*) >>> .*) # PS1 line (?:\n [ ]* \.\.\. .*)* # PS2 lines \n?) # Want consists of any non-blank lines that do not start with PS1. (?P (?:(?![ ]*$) # Not a blank line (?![ ]*>>>) # Not a line starting with PS1 .*$\n? # But any other line )*) ''', re.MULTILINE | re.VERBOSE) """ Multiline regex patterns are rendered as string. "..." is added when reaching the maximum number of lines for constant representation (defaults to 7), change this value with option --pyval-repr-maxlines. """ pydoctor-21.12.1/docs/restructuredtext_demo/demo_restructuredtext_module.py000066400000000000000000000112331416703725300275100ustar00rootroot00000000000000""" This is a module demonstrating reST code documentation features. Most part of this documentation is using Python type hinting. """ from abc import ABC import zope.interface import zope.schema from typing import Sequence, Optional, TYPE_CHECKING if TYPE_CHECKING: from typing_extensions import Final LANG = 'Fr' """ This is a constant. See `constants` for more examples. """ lang: 'Final[Sequence[str]]' = ['Fr', 'En'] """ This is also a constant, but annotated with typing.Final. """ from typing import AnyStr, Generator, Union, List, Dict def demo_fields_docstring_arguments(m, b = 0): # type: ignore """ Fields are used to describe specific properties of a documented object. This function's ":type:" tags are taking advantage of the --process-types. :type m: numbers.Number :param m: The slope of the line. :type b: numbers.Number, optional :param b: The y intercept of the line. :rtype: numbers.Number :return: the x intercept of the line M{y=m*x+b}. """ return -b/m def demo_consolidated_fields(a:float, b): # type: ignore """ Fields can be condensed into one "consolidated" field. Looks better in plain text. :Parameters: - `a`: The size of the fox (in meters) - `b`: The weight of the fox (in stones) :rtype: str :return: The number of foxes """ return -b/a def demo_typing_arguments(name: str, size: Optional[bytes] = None) -> bool: """ Type documentation can be extracted from standard Python type hints. :param name: The human readable name for something. :param size: How big the name should be. Leave none if you don't care. :return: Always `True`. """ return True def demo_long_function_and_parameter_names__this_indeed_very_long( this_is_a_very_long_parameter_name_aahh: str, what__another_super_super_long_name__ho_no: Generator[Union[List[AnyStr], Dict[str, AnyStr]], None, None]) -> bool: """ Long names and annotations should display on several lines when they don't fit in a single line. """ return True def demo_cross_reference() -> None: r""" The inline markup construct ```object``` is used to create links to the documentation for other Python objects. 'text' is the text that should be displayed for the link, and 'object' is the name of the Python object that should be linked to. If you wish to use the name of the Python object as the text for the link, you can simply write ```object``` -> `object`. - `demo_typing_arguments` """ class _PrivateClass: """ This is the docstring of a private class. """ def method_inside_private(self) -> bool: """ A public method inside a private class. :return: Something. """ return True def _private_inside_private(self) -> List[str]: """ Returns something. :rtype: `list` """ return [] class DemoClass(ABC, _PrivateClass): """ This is the docstring of this class. .. versionchanged:: 1.1 This class now inherits from `_PrivateClass` and demonstrate the ``.. versionchanged::`` directive support. .. versionchanged:: 1.2 Add `read_and_write_delete` property. """ def __init__(self, one: str, two: bytes) -> None: """ Documentation for class initialization. :param one: Docs for first argument. :param two: Docs for second argument. """ @property def read_only(self) -> int: """ This is a read-only property. """ return 1 @property def read_and_write(self) -> int: """ This is a read-write property. """ return 1 @read_and_write.setter def read_and_write(self, value: int) -> None: """ This is a docstring for setter. """ @property def read_and_write_delete(self) -> int: """ This is a read-write-delete property. """ return 1 @read_and_write_delete.setter def read_and_write_delete(self, value: int) -> None: """ This is a docstring for setter. """ @read_and_write_delete.deleter def read_and_write_delete(self) -> None: """ This is a docstring for deleter. """ class IContact(zope.interface.Interface): """ Example of an interface with schemas. Provides access to basic contact information. """ first = zope.schema.TextLine(description="First name") email = zope.schema.TextLine(description="Electronic mail address") address = zope.schema.Text(description="Postal address") def send_email(text: str) -> None: pass pydoctor-21.12.1/docs/sample_template/000077500000000000000000000000001416703725300176355ustar00rootroot00000000000000pydoctor-21.12.1/docs/sample_template/extra.css000066400000000000000000000002501416703725300214670ustar00rootroot00000000000000#banner { background: rgb(150, 150, 150); background-image: linear-gradient(bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, .3)); width: 100%; padding: 10px; } pydoctor-21.12.1/docs/sample_template/header.html000066400000000000000000000013311416703725300217510ustar00rootroot00000000000000 pydoctor-21.12.1/docs/source/000077500000000000000000000000001416703725300157615ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/api/000077500000000000000000000000001416703725300165325ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/api/index.rst000066400000000000000000000003461416703725300203760ustar00rootroot00000000000000API Reference ============= This file will be overwritten by the pydoctor build triggered at the end of the Sphinx build. It's a hack to be able to reference the API index page from inside Sphinx and have it as part of the TOC. pydoctor-21.12.1/docs/source/codedoc.rst000066400000000000000000000321271416703725300201200ustar00rootroot00000000000000How to Document Your Code ========================= Docstrings ---------- In Python, a string at the top of a module, class or function is called a *docstring*. For example:: """This docstring describes the purpose of this module.""" class C: """This docstring describes the purpose of this class.""" def m(self): """This docstring describes the purpose of this method.""" Pydoctor also supports *attribute docstrings*:: CONST = 123 """This docstring describes a module level constant.""" class C: cvar = None """This docstring describes a class variable.""" def __init__(self): self.ivar = [] """This docstring describes an instance variable.""" Attribute docstrings are not part of the Python language itself (`PEP 224 `_ was rejected), so these docstrings are not available at runtime. For long docstrings, start with a short summary, followed by an empty line:: def f(): """This line is used as the summary. More detail about the workings of this function can be added here. They will be displayed in the documentation of the function itself but omitted from the summary table. """ Since docstrings are Python strings, escape sequences such as ``\n`` will be parsed as if the corresponding character---for example a newline---occurred at the position of the escape sequence in the source code. To have the text ``\n`` in a docstring at runtime and in the generated documentation, you either have escape it twice in the source: ``\\n`` or use the ``r`` prefix for a raw string literal. The following example shows the raw string approach:: def iter_lines(stream): r"""Iterate through the lines in the given text stream, with newline characters (\n) removed. """ for line in stream: yield line.rstrip('\n') Further reading: - `Python Tutorial: Documentation Strings `_ - `PEP 257 -- Docstring Conventions `_ - `Python Language Reference: String and Bytes literals `_ Docstring assignments --------------------- Simple assignments to the ``__doc__`` attribute of a class or function are recognized by pydoctor:: class CustomException(Exception): __doc__ = MESSAGE = "Oops!" Non-trivial assignments to ``__doc__`` are not supported. A warning will be logged by pydoctor as a reminder that the assignment will not be part of the generated API documentation:: if LOUD_DOCS: f.__doc__ = f.__doc__.upper() Assignments to ``__doc__`` inside functions are ignored by pydoctor. This can be used to avoid warnings when you want to modify runtime docstrings without affecting the generated API documentation:: def mark_unavailable(func): func.__doc__ = func.__doc__ + '\n\nUnavailable on this system.' if not is_supported('thing'): mark_unavailable(do_the_thing) Augmented assignments like ``+=`` are currently ignored as well, but that is an implementation limitation rather than a design decision, so this might change in the future. Constants --------- The value of a constant is rendered with syntax highlighting. See `module `_ demonstrating the constant values rendering. Following `PEP8 `_, any variable defined with all upper case name will be considered as a constant. Additionally, starting with Python 3.8, one can use the `typing.Final `_ qualifier to declare a constant. For instance, these variables will be recognized as constants:: from typing import Final X = 3.14 y: Final = ['a', 'b'] In Python 3.6 and 3.7, you can use the qualifier present in the `typing_extensions` instead of `typing.Final`:: from typing_extensions import Final z: Final = 'relative/path' Fields ------ Pydoctor supports most of the common fields usable in Sphinx, and some others. Epytext fields are written with arobase, like ``@field:`` or ``@field arg:``. ReStructuredText fields are written with colons, like ``:field:`` or ``:field arg:``. Here are the supported fields (written with ReStructuredText format, but same fields are supported with Epytext): - ``:cvar foo:``, document a class variable. Applicable in the context of the docstring of a class. - ``:ivar foo:``, document a instance variable. Applicable in the context of the docstring of a class. - ``:var foo:``, document a variable. Applicable in the context of the docstring of a module or class. If used in the context of a class, behaves just like ``@ivar:``. - ``:note:``, add a note section. - ``:param bar:`` (synonym: ``@arg bar:``), document a function's (or method's) parameter. Applicable in the context of the docstring of a function of method. - ``:keyword:``, document a function's (or method's) keyword parameter (``**kwargs``). - ``:type bar: C{list}``, document the type of an argument/keyword or variable, depending on the context. - ``:return:`` (synonym: ``@returns:``), document the return type of a function (or method). - ``:rtype:`` (synonym: ``@returntype:``), document the type of the return value of a function (or method). - ``:yield:`` (synonym: ``@yields:``), document the values yielded by a generator function (or method). - ``:ytype:`` (synonym: ``@yieldtype:``), document the type of the values yielded by a generator function (or method). - ``:raise ValueError:`` (synonym: ``@raises ValueError:``), document the potential exception a function (or method) can raise. - ``:warn RuntimeWarning:`` (synonym: ``@warns ValueError:``), document the potential warning a function (or method) can trigger. - ``:see:`` (synonym: ``@seealso:``), add a see also section. - ``:since:``, document the date and/or version since a component is present in the API. - ``:author:``, document the author of a component, generally a module. .. note:: Currently, any other fields will be considered "unknown" and will be flagged as such. See `"fields" issues `_ for discussions and improvements. .. note:: Unlike Sphinx, ``vartype`` and ``kwtype`` are not recognized as valid fields, we simply use ``type`` everywhere. .. _codedoc-fields: Type fields ~~~~~~~~~~~ Type fields, namely ``type``, ``rtype`` and ``ytype``, can be interpreted, such that, instead of being just a regular text field, types can be linked automatically. For reStructuredText and Epytext documentation format, enable this behaviour with the option:: --process-fields The type auto-linking is always enabled for Numpy and Google style documentation formats. Like in Sphinx, regular types and container types such as lists and dictionaries can be linked automatically:: :type priority: int :type priorities: list[int] :type mapping: dict(str, int) :type point: tuple[float, float] Natural language types can be linked automatically if separated by the words “or”, "and", "to", "of" or the comma:: :rtype: float or str :returntype: list of str or list[int] :ytype: tuple of str, int and float :yieldtype: mapping of str to int Additionally, it's still possible to include regular text description inside a type specification:: :rtype: a result that needs a longer text description or str :rtype: tuple of a result that needs a longer text description and str Some special keywords will be recognized: "optional" and "default":: :type value: list[float], optional :type value: int, default: -1 :type value: dict(str, int), default: same as default_dict .. note:: Literals caracters - numbers and strings within quotes - will be automatically rendered like docutils literals. .. note:: It's not currently possible to combine parameter type and description inside the same ``param`` field, see issue `#267 `_. Type annotations ---------------- Type annotations in your source code will be included in the API documentation that pydoctor generates. For example:: colors: dict[str, int] = { 'red': 0xFF0000, 'green': 0x00FF00, 'blue': 0x0000FF } def inverse(name: str) -> int: return colors[name] ^ 0xFFFFFF If your project still supports Python versions prior to 3.6, you can also use type comments:: from typing import Optional favorite_color = None # type: Optional[str] However, the ability to extract type comments only exists in the parser of Python 3.8 and later, so make sure you run pydoctor using a recent Python version, or the type comments will be ignored. There is basic type inference support for variables/constants that are assigned literal values. Unlike for example mypy, pydoctor cannot infer the type for computed values:: FIBONACCI = [1, 1, 2, 3, 5, 8, 13] # pydoctor will automatically determine the type: list[int] SQUARES = [n ** 2 for n in range(10)] # pydoctor needs an annotation to document this type Further reading: - `Python Standard Library: typing -- Support for type hints `_ - `PEP 483 -- The Theory of Type Hints `_ Properties ---------- A method with a decoration ending in ``property`` or ``Property`` will be included in the generated API documentation as an attribute rather than a method:: class Knight: @property def name(self): return self._name @abc.abstractproperty def age(self): raise NotImplementedError @customProperty def quest(self): return f'Find the {self._object}' All you have to do for pydoctor to recognize your custom properties is stick to this naming convention. Using ``attrs`` --------------- If you use the ``attrs`` library to define attributes on your classes, you can use inline docstrings combined with type annotations to provide pydoctor with all the information it needs to document those attributes:: import attr @attr.s(auto_attribs=True) class SomeClass: a_number: int = 42 """One number.""" list_of_numbers: list[int] """Multiple numbers.""" If you are using explicit ``attr.ib`` definitions instead of ``auto_attribs``, pydoctor will try to infer the type of the attribute from the default value, but will need help in the form of type annotations or comments for collections and custom types:: from typing import List import attr @attr.s class SomeClass: a_number = attr.ib(default=42) """One number.""" list_of_numbers = attr.ib(factory=list) # type: List[int] """Multiple numbers.""" Private API ----------- Modules, classes and functions of which the name starts with an underscore are considered *private*. These will not be shown by default, but there is a button in the generated documentation to reveal them. An exception to this rule is *dunders*: names that start and end with double underscores, like ``__str__`` and ``__eq__``, which are always considered public:: class _Private: """This class won't be shown unless explicitly revealed.""" class Public: """This class is public, but some of its methods are private.""" def public(self): """This is a public method.""" def _private(self): """For internal use only.""" def __eq__(self, other): """Is this object equal to 'other'? This method is public. """ Re-exporting ------------ If your project is a library or framework of significant size, you might want to split the implementation over multiple private modules while keeping the public API importable from a single module. This is supported using pydoctor's re-export feature. A documented element which is defined in one (typically private) module can be imported into another module and re-exported by naming it in the ``__all__`` special variable. Doing so will move its documentation to the module from where it was re-exported, which is where users of your project will be importing it from. In the following example, the documentation of ``MyClass`` is written in the ``my_project.core._impl`` module, which is imported into the top-level ``__init__.py`` and then re-exported by including ``"MyClass"`` in the value of ``__all__``. As a result, the documentation for ``MyClass`` can be read in the documentation of the top-level ``my_project`` package:: ├── README.rst ├── my_project │ ├── __init__.py <-- Re-exports my_project.core._impl.MyClass │ ├── core as my_project.MyClass │ │ ├── __init__.py │ │ ├── _impl.py <-- Defines and documents MyClass The content of ``my_project/__init__.py`` includes:: from .core._impl import MyClass __all__ = ["MyClass"] pydoctor-21.12.1/docs/source/conf.py000066400000000000000000000143171416703725300172660ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) import os import subprocess import pathlib # -- Project information ----------------------------------------------------- project = 'pydoctor' copyright = '2020, Michael Hudson-Doyle and various contributors (see Git history)' author = 'Michael Hudson-Doyle and various contributors (see Git history)' from pydoctor import __version__ as version # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx_rtd_theme", "sphinx.ext.intersphinx", "pydoctor.sphinx_ext._help_output", "pydoctor.sphinx_ext.build_apidocs", "sphinxcontrib.spelling", ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # Definitions that will be made available to every document. rst_epilog = """ .. include:: """ # Configure spell checker. spelling_word_list_filename = 'spelling_wordlist.txt' # Configure intersphinx magic intersphinx_mapping = { 'twisted': ('https://twistedmatrix.com/documents/current/api/', None), } # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Try to find URL fragment for the GitHub source page based on current # branch or tag. _git_reference = subprocess.getoutput('git rev-parse --abbrev-ref HEAD') if _git_reference == 'HEAD': # It looks like the branch has no name. # Fallback to commit ID. _git_reference = subprocess.getoutput('git rev-parse HEAD') if os.environ.get('READTHEDOCS', '') == 'True': rtd_version = os.environ.get('READTHEDOCS_VERSION', '') if '.' in rtd_version: # It looks like we have a tag build. _git_reference = rtd_version _pydoctor_root = pathlib.Path(__file__).parent.parent.parent _common_args = [ f'--html-viewsource-base=https://github.com/twisted/pydoctor/tree/{_git_reference}', f'--project-base-dir={_pydoctor_root}', '--intersphinx=https://docs.python.org/3/objects.inv', '--intersphinx=https://twistedmatrix.com/documents/current/api/objects.inv', '--intersphinx=https://urllib3.readthedocs.io/en/latest/objects.inv', '--intersphinx=https://requests.readthedocs.io/en/latest/objects.inv', '--intersphinx=https://www.attrs.org/en/stable/objects.inv', '--intersphinx=https://www.sphinx-doc.org/en/stable/objects.inv', '--intersphinx=https://tristanlatr.github.io/apidocs/docutils/objects.inv', ] pydoctor_args = { 'main': [ '--html-output={outdir}/api/', # Make sure to have a trailing delimiter for better usage coverage. '--project-name=pydoctor', f'--project-version={version}', '--docformat=epytext', '--project-url=../index.html', f'{_pydoctor_root}/pydoctor', ] + _common_args, 'custom_template_demo': [ '--html-output={outdir}/custom_template_demo/', '--project-name=pydoctor with a twisted theme', f'--project-version={version}', '--docformat=epytext', '--project-url=../customize.html', '--theme=base', f'--template-dir={_pydoctor_root}/docs/sample_template', f'{_pydoctor_root}/pydoctor', ] + _common_args, 'epydoc_demo': [ '--html-output={outdir}/docformat/epytext', '--project-name=pydoctor-epytext-demo', '--project-version=1.3.0', '--docformat=epytext', '--intersphinx=https://zopeschema.readthedocs.io/en/latest/objects.inv', '--intersphinx=https://zopeinterface.readthedocs.io/en/latest/objects.inv', '--project-url=../epytext.html', f'{_pydoctor_root}/docs/epytext_demo', ] + _common_args, 'restructuredtext_demo': [ '--html-output={outdir}/docformat/restructuredtext', '--project-name=pydoctor-restructuredtext-demo', '--project-version=1.0.0', '--docformat=restructuredtext', '--project-url=../restructuredtext.html', '--process-types', f'{_pydoctor_root}/docs/restructuredtext_demo', ] + _common_args, 'numpy_demo': [ '--html-output={outdir}/docformat/numpy', '--project-name=pydoctor-numpy-style-demo', '--project-version=1.0.0', '--docformat=numpy', '--project-url=../google-numpy.html', f'{_pydoctor_root}/docs/numpy_demo', f'{_pydoctor_root}/pydoctor/napoleon' ] + _common_args, 'google_demo': [ '--html-output={outdir}/docformat/google', '--project-name=pydoctor-google-style-demo', '--project-version=1.0.0', '--docformat=google', '--project-url=../google-numpy.html', f'{_pydoctor_root}/docs/google_demo', ] + _common_args, } pydoctor_url_path = { 'main': '/en/{rtd_version}/api', 'epydoc_demo': '/en/{rtd_version}/docformat/epytext/', 'restructuredtext_demo': '/en/{rtd_version}/docformat/restructuredtext/', 'numpy_demo': '/en/{rtd_version}/docformat/numpy/', 'google_demo': '/en/{rtd_version}/docformat/google/', } pydoctor-21.12.1/docs/source/contrib.rst000066400000000000000000000117061416703725300201600ustar00rootroot00000000000000Contribute ========== What can you do --------------- If you like the project and think you could help with making it better, there are many ways you can do it: - Create a new issue for new feature proposal or a bug - Triage old issues that needs a refresh - Implement existing issues (there are quite some of them, choose whatever you like) - Help with improving the documentation (We still have work to do!) - Spread a word about the project to your colleagues, friends, blogs or any other channels - Any other things you could imagine Any contribution would be of great help and I will highly appreciate it! If you have any questions, please create a new issue. Pre-commit checks ----------------- Make sure all the tests pass and the code pass the coding standard checks:: tox -p all That should be the minimum check to run on your local system. A pull request will trigger more tests and most probably there is a tox environment dedicated to that extra test. Review process and requirements ------------------------------- - Code changes and code added should have tests: untested code is buggy code. Except special cases, overall test coverage should be increased. - If your pull request is a work in progress, please mark it as draft such that reviewers do not loose time on a PR that is not ready yet. - All code changes must be reviewed by at least one person who is not an author of the code being added. This helps prevent bugs from slipping through the net and gives another source for improvements. If the author of the PR is one of the core developers of pydoctor* and no one has reviewed their PR after 9 calendar days, they can review the code changes themselves and proceed with next steps. - When one is done with the review, always say what the next step should be: for example, if the author is a core developer, can they merge the PR after making a few minor fixes? If your review feedback is more substantial, should they ask for another review? \* A core developer is anyone with a write access to the repository that have an intimate knowledge of pydoctor internals, or, alternatively the specific aspect in which they are contributing to (i.e. Sphinx docs, setup, pytest, etc.). Read more about reviewing: - `How to be a good reviewer `_. - `Leave well enough alone `_. Releasing and publishing a new package -------------------------------------- Publishing to PyPI is done via a GitHub Actions workflow that is triggered when a tag is pushed. Version is configured in the ``setup.cfg``. The following process ensures correct version management: - Create a branch: name it by the name of the new major ``pydoctor`` version, i.e. ``21.9.x``, re-use that same branch for bug-fixes. - On the branch, update the version and release notes. - Update the HTML templates version (meta tag ``pydoctor-template-version``) when there is a change from a version to another. For instance, check the diff of the HTML templates since version ``21.9.1`` with the following git command:: git diff 21.9.1 pydoctor/themes/*/*.html .. note:: The HTML template version can also be updated in the PR in which the actual HTML template change is done. - Create a PR for that branch, wait for tests to pass and get an approval. - Create a tag based on the ``HEAD`` of the release branch, name it by the full version number of the ``pydoctor`` version, i.e. ``21.9.1``, this will trigger the release. For instance:: git tag 21.9.1 git push --tags - Update the version on the branch and append ``.dev0`` to the current version number. In this way, stable versions only exist for a brief period of time (if someone tries to do a ``pip install`` from the git source, they will get a ``.dev0`` version instead of a misleading stable version number. - Update the README file and add an empty placeholder for unreleased changes. - Merge the branch Author Design Notes ------------------- I guess I've always been interested in more-or-less static analysis of Python code and have over time developed some fairly strong opinions on the Right Way\ |trade| to do it. The first of these is that pydoctor works on an entire *system* of packages and modules, not just a ``.py`` file at a time. The second, and this only struck me with full force as I have written pydoctor, is that it's much the best approach to proceed incrementally, and outside-in. First, you scan the directory structure to and compute the package/module structure, then parse each module, then do some analysis on what you've found, then generate html. Finally, pydoctor should never crash, no matter what code you feed it (this seems a basic idea for a documentation generator, but it's not that universally applied, it seems). Missing information is OK, crashing out is not. This probably isn't as true as it should be at the moment. pydoctor-21.12.1/docs/source/custom_template_demo/000077500000000000000000000000001416703725300221725ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/custom_template_demo/index.rst000066400000000000000000000004321416703725300240320ustar00rootroot00000000000000:orphan: API Reference with a Twisted theme ================================== This file will be overwritten by the pydoctor build triggered at the end of the Sphinx build. It's a hack to be able to reference the API index page from inside Sphinx and have it as part of the TOC. pydoctor-21.12.1/docs/source/customize.rst000066400000000000000000000041631416703725300205410ustar00rootroot00000000000000 Customize Output ================ Tweak HTML templates -------------------- They are 3 placeholders designed to be overwritten to include custom HTML and CSS into the pages. - ``header.html``: at the very beginning of the body - ``subheader.html``: after the main header, before the page title - ``extra.css``: extra CSS sheet for layout customization To override a placeholder, write your custom HTML or CSS files to a directory and use the following option:: --template-dir=./pydoctor_templates If you want more customization, you can override the default templates in `pydoctor/themes/base `_ with the same method. HTML templates have their own versioning system and warnings will be triggered when an outdated custom template is used. .. admonition:: Demo theme example There is a demo template inspired by Twisted web page for which the source code is `here `_. You can try the result by checking `this page `_. .. note:: This example is using new ``pydoctor`` option, ``--theme=base``. This means that bootstrap CSS will not be copied to build directory. Use a custom system class ------------------------- You can subclass the :py:class:`pydoctor.zopeinterface.ZopeInterfaceSystem` and pass your custom class dotted name with the following argument:: --system-class=mylib._pydoctor.CustomSystem System class allows you to dynamically show/hide classes or methods. This is also used by the Twisted project to handle deprecation. See the :py:class:`twisted:twisted.python._pydoctor.TwistedSystem` custom class documentation. Navigate to the source code for a better overview. Use a custom writer class ------------------------- You can subclass the :py:class:`pydoctor.templatewriter.TemplateWriter` and pass your custom class dotted name with the following argument:: --html-class=mylib._pydoctor.CustomTemplateWriter .. warning:: Pydoctor does not have a stable API yet. Code customization is prone to break in future versions. pydoctor-21.12.1/docs/source/docformat/000077500000000000000000000000001416703725300177375ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/docformat/epytext.rst000066400000000000000000000013321416703725300221720ustar00rootroot00000000000000Epytext ======= .. toctree:: :maxdepth: 1 epytext/epytext_demo Read the `the epytext manual `_ for full documentation. Pydoctor has extended ``epydoc``'s parser and uses it as a library to parse epytext formatted docstrings. All markup should work except the indexed terms ``X{}`` tag, which has been removed. Fields ------ See :ref:`fields section `. .. note:: Not everything from the `epydoc fields manual `_ is applicable. Some fields might still display as unknown. .. note:: In any case, *plaintext* docstring format will be used if docstrings can't be parsed with *epytext* parser. pydoctor-21.12.1/docs/source/docformat/epytext/000077500000000000000000000000001416703725300214415ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/docformat/epytext/epytext_demo.rst000066400000000000000000000000641416703725300247010ustar00rootroot00000000000000Epytext demo package ==================== :orphan: pydoctor-21.12.1/docs/source/docformat/google-numpy.rst000066400000000000000000000040121416703725300231100ustar00rootroot00000000000000Google and Numpy ================ .. toctree:: :maxdepth: 1 google/google_demo numpy/numpy_demo Pydoctor now supports numpydoc and google style docstrings! Docstrings will be first converted to reStructuredText and then parsed with ``docutils``. Any supported `reST markup `_ can be use to supplement google-style or numpy-style markup. The main difference between the two styles is that Google uses indentation to separate sections, whereas NumPy uses underlines. This means that 2 blank lines are needed to end a NumPy section that is followed by a regular paragraph (i.e. not another section header) .. note:: We have forked and enhanced the `napoleon Sphinx extension `_. For more information, refer to :py:mod:`pydoctor.napoleon` documentation. For complete markup details, refer to the `Google style `_ or `NumpyDoc style `_ reference documentation. Sections -------- List of supported sections: - ``Args``, ``Arguments``, ``Parameters`` - ``Keyword Args``, ``Keyword Arguments`` - ``Return(s)``, ``Yield(s)`` (if you use type annotations a ``Returns`` section will always be present) - ``Raise(s)``, ``Warn(s)`` - ``See Also``, ``See`` - ``Example(s)`` - ``Note(s)``, ``Warning(s)`` and other admonitions - ``Attributes`` (Items will be translated into ``ivar`` fields.) Sections supported on a "best effort" basis: - ``Methods``: Items will be included into a generic "Methods" section. - ``References``: Rendered as a generic section. - ``Other Parameters``, ``Receive(s)``: Parameters described in those sections will be merged with regular parameters. .. ReST syntax violations might be reported with a slightly incorrect line number because of this pre-processing. (uncommented this when pydoctor/issues/237 is solved) pydoctor-21.12.1/docs/source/docformat/google/000077500000000000000000000000001416703725300212135ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/docformat/google/google_demo.rst000066400000000000000000000000641416703725300242250ustar00rootroot00000000000000Google-style demo package ========================= pydoctor-21.12.1/docs/source/docformat/index.rst000066400000000000000000000022231416703725300215770ustar00rootroot00000000000000Documentation Formats ===================== The following sections roughly documents the supported docstrings formatting. As an additional reference, small python packages demonstrates how docstrings are rendered. .. toctree:: :maxdepth: 1 epytext restructuredtext google-numpy Choose your docstring format with the option:: --docformat= The following format keywords are recognized: - ``epytext`` - ``restructuredtext`` - ``google`` - ``numpy`` - ``plaintext`` To override the default markup language for a module, define a module-level string variable ``__docformat__``, containing the name of the module's markup language:: __docformat__ = "reStructuredText" __docformat__ = "Epytext" .. note:: Language code can be added. It is currently ignored, though it might be used it the future to generate ``lang`` attribute in HTML or as configuration for a spell checker:: __docformat__ = "reStructuredText en" Parser name and language code are **case insensitve**. If a package defines ``__docformat__`` in its ``__init__.py`` file, all modules (including subpackages) in that package will inherit its value. pydoctor-21.12.1/docs/source/docformat/list-restructuredtext-support.rst000066400000000000000000000161311416703725300266160ustar00rootroot00000000000000:orphan: List of ReST directives ======================= .. list-table:: List of ReST directives and status whether they are supported or unsupported by PyDoctor :header-rows: 1 * - Directive - Defined by - Supported * - ``.. include::`` - `docutils `__ - Yes * - ``.. contents::`` - `docutils `__ - Yes * - ``.. image::`` - `docutils `__ - Yes * - ``.. |time| date:: %H:%M`` - `docutils `__ - Yes * - ``.. figure::`` - `docutils `__ - Yes * - ``.. |T| replace:: term`` - `docutils `__ - Yes * - ``.. unicode::`` - `docutils `__ - Yes * - ``.. raw::`` - `docutils `__ - Yes * - ``.. class::`` - `docutils `__ - No * - ``.. role::`` - `docutils `__ - Yes * - ``.. default-role::`` - `docutils `__ - Should not be changed. * - ``.. line-block::`` - `docutils `__ - No * - ``.. code::`` - `docutils `__ - Yes * - ``.. python::`` - pydoctor - Yes * - ``.. math::`` - `docutils `__ - Yes * - ``.. highlights::`` - `docutils `__ - No * - ``.. pull-quote::`` - `docutils `__ - No * - ``.. container::`` - `docutils `__ - Yes * - ``.. table::`` - `docutils `__ - Yes * - ``.. csv-table::`` - `docutils `__ - Yes * - ``.. list-table::`` - `docutils `__ - Yes * - ``.. warning::`` and other admonitions - `docutils `__ - Yes. This includes: attention, caution, danger, error, hint, important, note, tip, warning and the generic admonitions. * - ``.. versionadded::`` - `Sphinx `__ - Yes * - ``.. versionchanged::`` - `Sphinx `__ - Yes * - ``.. deprecated::`` - `Sphinx `__ - Yes * - ``.. centered::`` - `Sphinx `__ - No * - ``.. digraph::`` - `epydoc `__ - No * - ``.. classtree::`` - `epydoc `__ - No * - ``.. packagetree::`` - `epydoc `__ - No * - ``.. importgraph::`` - `epydoc `__ - No * - ``.. callgraph::`` - `epydoc `__ - No * - ``.. hlist::`` - `Sphinx `__ - No * - ``.. highlight::`` - `Sphinx `__ - No * - ``.. code-block::`` - `Sphinx `__ - No * - ``.. literalinclude::`` - `Sphinx `__ - No * - ``.. glossary::`` - `Sphinx `__ - No * - ``.. index::`` - `Sphinx `__ - No * - ``.. sectionauthor::`` - `Sphinx `__ - No * - ``.. codeauthor::`` - `Sphinx `__ - No * - ``.. topic::`` - `docutils `__ - No * - ``.. sidebar::`` - `docutils `__ - No * - ``.. rubric::`` - `docutils `__ - No * - ``.. epigraph::`` - `docutils `__ - No * - ``.. compound::`` - `docutils `__ - No * - ``.. sectnum::`` - `docutils `__ - No * - ``.. header::`` - `docutils `__ - No * - ``.. footer::`` - `docutils `__ - No * - ``.. meta::`` - `docutils `__ - No * - ``.. title::`` - `docutils `__ - No *This list is not exhaustive* pydoctor-21.12.1/docs/source/docformat/numpy/000077500000000000000000000000001416703725300211075ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/docformat/numpy/numpy_demo.rst000066400000000000000000000000621416703725300240130ustar00rootroot00000000000000Numpy-style demo package ======================== pydoctor-21.12.1/docs/source/docformat/restructuredtext.rst000066400000000000000000000115741416703725300241410ustar00rootroot00000000000000reStructuredText ================ .. toctree:: :maxdepth: 1 restructuredtext/restructuredtext_demo For the language syntax documentation, read the `ReST docutils syntax reference `_. Fields ------ See :ref:`fields section `. In addition to the standard set of fields, the reStructuredText parser also supports **consolidated fields**, which combine the documentation for several objects into a single field. These consolidated fields may be written using either a `bulleted list `_ or a `definition list `_. - If a consolidated field is written as a bulleted list, then each list item must begin with the field's argument, marked as `interpreted text `_, and followed by a colon or dash. - If a consolidated field is written as a definition list, then each definition item's term should contain the field's argument, (it is not mandatory for it being marked as interpreted text). The following example shows the use of a definition list to define the ``Parameters`` consolidated field with type definition. Note that *docutils* requires a space before and after the ``:`` used to mark classifiers. .. code:: python def fox_speed(size, weight, age): """ :Parameters: size The size of the fox (in meters) weight : float The weight of the fox (in stones) age : int The age of the fox (in years) """ Using a bulleted list. .. code:: python def fox_speed(size:float, weight:float, age:int): """ :Parameters: - `size`: The size of the fox (in meters) - `weight`: The weight of the fox (in stones) - `age`: The age of the fox (in years) """ The following consolidated fields are currently supported by PyDoctor: .. table:: Consolidated Fields ============================== ============================== Consolidated Field Tag Corresponding Base Field Tag ============================== ============================== ``:Parameters:`` ``:param:`` ``:Keywords:`` ``:keyword:`` ``:Exceptions:`` ``:except:`` ``:Variables:`` ``:var:`` ``:IVariables:`` ``:ivar:`` ``:CVariables:`` ``:cvar:`` ``:Types:`` ``:type:`` ============================== ============================== Fields are case *insensitive*. Cross-references ---------------- PyDoctor replaces the Docutils' default `interpreted text role `_ with the creation of `documentation cross-reference links `_. If you want to create a cross-reference link to the ``module.Example`` class, simply put backticks around it, typing:: `module.Example` .. note:: Sphinx interpreted text roles for code references like ``:obj:`` or ``:meth:`` are not required and will be ignored. Directives ---------- Here is a list of the supported ReST directives by package of origin: - `docutils`: ``.. include::``, ``.. contents::``, ``.. image::``, ``.. figure::``, ``.. unicode::``, ``.. raw::``, ``.. math::``, ``.. role::``, ``.. table::``, ``.. warning::``, ``.. note::`` and other admonitions, and a few others. - `epydoc`: None - `Sphinx`: ``.. deprecated::``, ``.. versionchanged::``, ``.. versionadded::`` - `pydoctor`: ``.. python::`` `Full list of supported and unsupported directives `_ Colorized snippets directive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using reStructuredText markup it is possible to specify Python snippets in a `doctest block `_. If the Python prompt gets in your way when you try to copy and paste and you are not interested in self-testing docstrings, the python directive will let you obtain a simple block of colorized text:: .. python:: def fib(n): """Print a Fibonacci series.""" a, b = 0, 1 while b < n: print b, a, b = b, a+b .. note:: Currently, ReST violations will be reported at a line corresponding to the beginning of the docstring. See `pydoctor/issues/237 `_. .. note:: HTML element's classes generated by our custom ``HTMLTranslator`` have a ``"rst-"`` prefix .. note:: In any case, *plaintext* docstring format will be used if docstrings can't be parsed with *restructuredtext* parser. pydoctor-21.12.1/docs/source/docformat/restructuredtext/000077500000000000000000000000001416703725300233775ustar00rootroot00000000000000pydoctor-21.12.1/docs/source/docformat/restructuredtext/restructuredtext_demo.rst000066400000000000000000000001061416703725300305720ustar00rootroot00000000000000reStructuredText demo package ============================= :orphan: pydoctor-21.12.1/docs/source/faq.rst000066400000000000000000000053251416703725300172670ustar00rootroot00000000000000Frequently Asked Questions ========================== Why? ---- ``pydoctor`` was written to be used by the `Twisted project `_ which was using `epydoc `_ but was becoming increasingly unhappy with it for various reasons. In addition, development on Epydoc seemed to have halted. The needs of the Twisted project are still the main driving force for ``pydoctor``'s development, but it is getting to the point where there's some chance that it is useful for your project too. Who wrote ``pydoctor``? ------------------------ Michael "mwhudson" Hudson, PyPy, Launchpad and sometimes Twisted hacker, with help from Christopher "radix" Armstrong and Jonathan "jml" Lange and advice and ideas from many people who hang out in #twisted on freenode. More recently, Maarten ter Huurne ("mth"), took the lead. Always backed with `numerous contributors `_. Why would I use it? ------------------- ``pydoctor`` is probably best suited to documenting a library that have some degree of internal subclassing. It also has support for `zope.interface `_, and can recognize interfaces and classes which implement such interfaces. How is it different from ``sphinx-autodoc`` ------------------------------------------- ``sphinx-autodoc`` can be complex and the output is sometimes overwhelming, ``pydoctor`` will generate one page per class, module and package, it tries to keeps it simple and present information in a efficient way with tables. Sphinx narrative documentation can seamlessly link to API documentation formatted by pydoctor. Please refer to the `Sphinx Integration `_ section for details. What does the output look like? ------------------------------- It looks `like this `_, which is the Twisted API documentation. The output is reasonably simple. Who is using ``pydoctor``? -------------------------- Here are some projects using ``pydoctor``: - `Twisted `_ - `Incremental `_ - `OTFBot `_ - `python-igraph `_ - `Wokkel `_ - `msiempy `_ - `git-buildpackage `_ - `pycma `_ - `cocopp `_ - `python-Wappalyzer `_ - and others How do I use it? ---------------- Please review the `Quick Start `_ section. pydoctor-21.12.1/docs/source/help.rst000066400000000000000000000001531416703725300174420ustar00rootroot00000000000000Command Line Options ==================== Below are the available command line options: .. help_output:: pydoctor-21.12.1/docs/source/index.rst000066400000000000000000000006341416703725300176250ustar00rootroot00000000000000Introduction ============ Welcome to ``pydoctor``'s documentation! .. toctree:: :maxdepth: 4 :caption: Contents: quickstart codedoc docformat/index sphinx-integration customize help faq transition contrib readme .. toctree:: :maxdepth: 1 :caption: Quick links api/index GitHub PyPI pydoctor-21.12.1/docs/source/publish-github-action.rst000066400000000000000000000040751416703725300227220ustar00rootroot00000000000000:orphan: Simple GitHub Action to publish API docs ---------------------------------------- Here is an example of a simple GitHub Action to automatically generate your documentation with Pydoctor and publish it to your default GitHub Pages website. Just substitute `(projectname)` and `(packagedirectory)` with the appropriate information. :: name: apidocs on: - push jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install requirements for documentation generation run: | python -m pip install --upgrade pip setuptools wheel python -m pip install docutils pydoctor - name: Generate API documentation with pydoctor run: | # Run pydoctor build pydoctor \ --project-name=(projectname) \ --project-url=https://github.com/$GITHUB_REPOSITORY \ --html-viewsource-base=https://github.com/$GITHUB_REPOSITORY/tree/$GITHUB_SHA \ --make-html \ --html-output=./apidocs \ --project-base-dir="$(pwd)" \ --docformat=restructuredtext \ --intersphinx=https://docs.python.org/3/objects.inv \ ./(packagedirectory) - name: Push API documentation to Github Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./apidocs commit_message: "Generate API documentation" .. note:: As mentioned in the ``actions-gh-pages`` `documentation`__, the first workflow run won't actually publish the documentation to GitHub Pages. GitHub Pages needs to be enabled afterwards in the repository settings, select ``gh-pages`` branch, then re-run your workflow. The website will be located at `https://(user).github.io/(repo)/`. __ https://github.com/peaceiris/actions-gh-pages pydoctor-21.12.1/docs/source/quickstart.rst000066400000000000000000000035141416703725300207100ustar00rootroot00000000000000Quick Start =========== Installation ------------ Pydoctor can be installed from PyPI:: $ pip install -U pydoctor Example ------- The following example uses most common options to generate pydoctor's own API docs under the ``docs/api`` folder. It will add a link to the project website in the header of each page, show a link to its source code beside every documented object and resolve links to Python standard library objects. The result looks like `this `_. :: pydoctor \ --project-name=pydoctor \ --project-version=1.2.0 \ --project-url=https://github.com/twisted/pydoctor/ \ --html-viewsource-base=https://github.com/twisted/pydoctor/tree/20.7.2 \ --make-html \ --html-output=docs/api \ --project-base-dir="." \ --docformat=epytext \ --intersphinx=https://docs.python.org/3/objects.inv \ ./pydoctor .. note:: This example assume that you have cloned and installed ``pydoctor`` and you are running the ``pydoctor`` build from Unix and the current directory is the root folder of the Python project. .. tip:: First run pydoctor with ``--docformat=plaintext`` to focus on eventual python code parsing errors. Then, enable docstring parsing by selecting another `docformat `_. .. warning:: The ``--html-viewsource-base`` argument should point to a tag or a commit SHA rather than a branch since line numbers are not going to match otherwise when commits are added to the branch after the documentation has been published. Publish your documentation -------------------------- Output files are static HTML pages which require no extra server-side support. Here is a `GitHub Action example `_ to automatically publish your API documentation to your default GitHub Pages website. pydoctor-21.12.1/docs/source/readme.rst000066400000000000000000000001121416703725300177420ustar00rootroot00000000000000Readme ====== This is the README.rst file .. include:: ../../README.rst pydoctor-21.12.1/docs/source/spelling_wordlist.txt000066400000000000000000000003521416703725300222660ustar00rootroot00000000000000backticks coroutine docstring docstrings Docutils epydoc epytext freenode Intersphinx jml Lange monkeypatch msiempy mth mwhudson mypy pre pydoctor readme reStructuredText subclassing tox Wokkel runtime Numpy numpy reST py customizablepydoctor-21.12.1/docs/source/sphinx-integration.rst000066400000000000000000000106521416703725300223510ustar00rootroot00000000000000 Sphinx Integration ================== Sphinx object inventories can be used to create links in both ways between documentation generated by pydoctor and by Sphinx. Linking from pydoctor to external API docs ------------------------------------------ It can link to external API documentation using a Sphinx objects inventory with the following cumulative configuration option:: --intersphinx=https://docs.python.org/3/objects.inv .. note:: The URL must point to the the ``objects.inv``. Then, your interpreted text, with backticks (`````) using `restructuredtext` and with ``L{}`` tag using `epytext`, will be linked to the Python element. Example:: `datetime.datetime` L{datetime.datetime} Simple as that! Linking from Sphinx to your pydoctor API docs --------------------------------------------- pydoctor's HTML generator will also generate a Sphinx objects inventory that can be used with the following mapping: * packages, modules -> ``:py:mod:`` * classes -> ``:py:class:`` * functions -> ``:py:func:`` * methods -> ``:py:meth:`` * attributes -> ``:py:attr:`` You can use this mapping in Sphinx via the `Intersphinx extension`__. __ https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html For an up to date lists of API links, run pydoctor before building the Sphinx documentation. You can use the ``--make-intersphinx`` option to only generate the object inventory file. You will then reference this file inside the Sphinx `intersphinx_mapping`. Note that relative paths are relative to the Sphinx source directory. You might need to exit the source and reference the build directory:: intersphinx_mapping = { 'twisted': ('https://twistedmatrix.com/documents/current/api/', '../../build/apidocs/objects.inv'), } Link to elements :py:func:`with custom text ` with:: :py:func:`with custom text ` Link to elements with default label :py:class:`twisted:twisted.web.client.HTTPDownloader` with:: :py:class:`twisted:twisted.web.client.HTTPDownloader` Building pydoctor together with Sphinx HTML build ------------------------------------------------- When running pydoctor with HTML generation it will generate a set of static HTML files that can be used any HTTP server. Under some circumstances (ex Read The Docs) you might want to trigger the pydoctor API docs build together with the Sphinx build. This can be done by using the :py:mod:`pydoctor.sphinx_ext.build_apidocs` extension. Inside your Sphinx ``conf.py`` file enable and configure the extension in this way:: extensions.append("pydoctor.sphinx_ext.build_apidocs") pydoctor_args = [ '--project-name=YOUR-PROJECT-NAME', '--project-version=YOUR-PUBLIC-VERSION', '--project-url=YOUR-PROJECT-HOME-URL', '--docformat=epytext', '--intersphinx=https://docs.python.org/3/objects.inv', '--html-viewsource-base=https://github.com/ORG/REPO/tree/default', '--html-output={outdir}/api', '--project-base-dir=path/to/source/code', 'path/to/source/code/package1' ] pydoctor_url_path = '/en/{rtd_version}/api/' You can pass almost any argument to ``pydoctor_args`` in the same way you call ``pydoctor`` from the command line. You don't need to pass the ``--make-html``, ``--make-intersphinx`` or ``--quiet`` arguments. The extension will add them automatically. The ``pydoctor_url_path`` is an URL path, relative to your public API documentation site. ``{rtd_version}`` will be replaced with the Read The Docs version (``stable`` , ``latest``, tag name). You only need to define this argument if you need to have Intersphinx links from your Sphinx narrative documentation to your pydoctor API documentation. As a hack to integrate the pydoctor API docs ``index.html`` with the Sphinx TOC and document reference, you can create an ``index.rst`` at the location where the pydoctor ``index.html`` is hosted. The Sphinx ``index.html`` will be generated during the Sphinx build process and later overwritten by the pydoctor build process. It is possible to call pydoctor multiple times (with different arguments) as part of the same build process. For this you need to define ``pydoctor_args`` as a dict. The key is the human readable build name and the value for each dict member is the list of arguments. See pydoctor's own `conf.py `_ for usage example. pydoctor-21.12.1/docs/source/transition.rst000066400000000000000000000015151416703725300207070ustar00rootroot00000000000000Transition to ``pydoctor`` ========================== From ``epydoc`` --------------- If you are looking for a successor to ``epydoc`` after moving to Python 3, ``pydoctor`` is the right tool for your project! - ``pydoctor`` dropped support for the ``X{}`` tag. All other epytext markup syntax should be fully supported. From ``pdoc3`` -------------- - ``pydoctor`` do not support Markdown docstrings. The easiest is to use *restructuredtext* docstring format as they are sharing numerous markup syntax. - ``pydoctor`` can only generate HTML, if you are using Markdown output, consider using ``pdocs``. - All references to ``__pdoc__`` module variable should be deleted as they are not supported. If you dynamically generated documentation, you should create a separate script and include it's output with an ``.. include::`` directive. pydoctor-21.12.1/docs/tests/000077500000000000000000000000001416703725300156235ustar00rootroot00000000000000pydoctor-21.12.1/docs/tests/__init__.py000066400000000000000000000000001416703725300177220ustar00rootroot00000000000000pydoctor-21.12.1/docs/tests/test.py000066400000000000000000000134441416703725300171620ustar00rootroot00000000000000# # Run tests after the documentation is executed. # # These tests are designed to be executed inside tox, after sphinx-build. # import os import pathlib from sphinx.ext.intersphinx import inspect_main from pydoctor import __version__ BASE_DIR = pathlib.Path(os.environ.get('TOX_INI_DIR', os.getcwd())) / 'build' / 'docs' def test_help_output_extension(): """ The help output extension will include the CLI help on the Sphinx page. """ with open(BASE_DIR / 'help.html', 'r') as stream: page = stream.read() assert '--project-url=PROJECTURL' in page, page def test_rtd_pydoctor_call(): """ With the pydoctor Sphinx extension, the pydoctor API HTML files are generated. """ # The pydoctor index is generated and overwrites the Sphinx files. with open(BASE_DIR / 'api' / 'index.html', 'r') as stream: page = stream.read() assert 'moduleIndex.html' in page, page def test_rtd_pydoctor_multiple_call(): """ With the pydoctor Sphinx extension can call pydoctor for more than one API doc source. """ with open(BASE_DIR / 'docformat' / 'epytext' / 'index.html', 'r') as stream: page = stream.read() assert 'pydoctor-epytext-demo' in page, page def test_rtd_extension_inventory(): """ The Sphinx inventory is available during normal sphinx-build. """ with open(BASE_DIR / 'sphinx-integration.html', 'r') as stream: page = stream.read() assert 'href="/en/latest/api/pydoctor.sphinx_ext.build_apidocs.html"' in page, page def test_sphinx_object_inventory_version(capsys): """ The Sphinx inventory is generated with the project version in the header. """ # The pydoctor own inventory. apidocs_inv = BASE_DIR / 'api' / 'objects.inv' with open(apidocs_inv, 'rb') as stream: page = stream.read() assert page.startswith( b'# Sphinx inventory version 2\n' b'# Project: pydoctor\n' b'# Version: ' + __version__.encode() + b'\n' ), page # Check that inventory can be parsed by Sphinx own extension. inspect_main([str(apidocs_inv)]) out, err = capsys.readouterr() assert '' == err assert 'pydoctor.driver.main' in out, out def test_sphinx_object_inventory_version_epytext_demo(): """ The Sphinx inventory for demo/showcase code has a fixed version and name, passed via docs/source/conf.py. """ with open(BASE_DIR / 'docformat' / 'epytext' / 'objects.inv', 'rb') as stream: page = stream.read() assert page.startswith( b'# Sphinx inventory version 2\n' b'# Project: pydoctor-epytext-demo\n' b'# Version: 1.3.0\n' ), page def test_index_contains_infos(): """ Test if index.html contains the following informations: - meta generator tag - nav and links to modules, classes, names - link to the root package - pydoctor github link in the footer """ infos = (f'pydoctor, the root package.', 'pydoctor',) with open(BASE_DIR / 'api' / 'index.html', 'r', encoding='utf-8') as stream: page = stream.read() for i in infos: assert i in page, page def test_page_contains_infos(): """ Test if pydoctor.driver.html contains the following informations: - meta generator tag - nav and links to modules, classes, names - js script source - pydoctor github link in the footer """ infos = (f'', 'pydoctor',) with open(BASE_DIR / 'api' / 'pydoctor.driver.html', 'r', encoding='utf-8') as stream: page = stream.read() for i in infos: assert i in page, page def test_custom_template_contains_infos(): """ Test if the custom template index.html contains the following informations: - meta generator tag - nav and links to modules, classes, names - pydoctor github link in the footer - the custom header - link to teh extra.css """ infos = (f'pydoctor', 'Twisted', '',) with open(BASE_DIR / 'custom_template_demo' / 'index.html', 'r', encoding='utf-8') as stream: page = stream.read() for i in infos: assert i in page, page def test_meta_pydoctor_template_version_tag_gets_removed(): """ Test if the index.html effectively do not contains the meta pydoctor template version tag """ with open(BASE_DIR / 'api' / 'index.html', 'r', encoding='utf-8') as stream: page = stream.read() assert ' ast.Module: """Parse the contents of a Python source file.""" with open(path, 'rb') as f: src = f.read() + b'\n' return _parse(src, filename=str(path)) if sys.version_info >= (3,8): _parse = partial(ast.parse, type_comments=True) else: _parse = ast.parse def node2fullname(expr: Optional[ast.expr], ctx: model.Documentable) -> Optional[str]: dottedname = node2dottedname(expr) if dottedname is None: return None return ctx.expandName('.'.join(dottedname)) def _maybeAttribute(cls: model.Class, name: str) -> bool: """Check whether a name is a potential attribute of the given class. This is used to prevent an assignment that wraps a method from creating an attribute that would overwrite or shadow that method. @return: L{True} if the name does not exist or is an existing (possibly inherited) attribute, L{False} otherwise """ obj = cls.find(name) return obj is None or isinstance(obj, model.Attribute) def _handleAliasing( ctx: model.CanContainImportsDocumentable, target: str, expr: Optional[ast.expr] ) -> bool: """If the given expression is a name assigned to a target that is not yet in use, create an alias. @return: L{True} iff an alias was created. """ if target in ctx.contents: return False full_name = node2fullname(expr, ctx) if full_name is None: return False ctx._localNameToFullName_map[target] = full_name return True _attrs_decorator_signature = signature(attrs) """Signature of the L{attr.s} class decorator.""" def _uses_auto_attribs(call: ast.Call, module: model.Module) -> bool: """Does the given L{attr.s()} decoration contain C{auto_attribs=True}? @param call: AST of the call to L{attr.s()}. This function will assume that L{attr.s()} is called without verifying that. @param module: Module that contains the call, used for error reporting. @return: L{True} if L{True} is passed for C{auto_attribs}, L{False} in all other cases: if C{auto_attribs} is not passed, if an explicit L{False} is passed or if an error was reported. """ try: args = bind_args(_attrs_decorator_signature, call) except TypeError as ex: message = str(ex).replace("'", '"') module.report( f"Invalid arguments for attr.s(): {message}", lineno_offset=call.lineno ) return False auto_attribs_expr = args.arguments.get('auto_attribs') if auto_attribs_expr is None: return False try: value = ast.literal_eval(auto_attribs_expr) except ValueError: module.report( 'Unable to figure out value for "auto_attribs" argument ' 'to attr.s(), maybe too complex', lineno_offset=call.lineno ) return False if not isinstance(value, bool): module.report( f'Value for "auto_attribs" argument to attr.s() ' f'has type "{type(value).__name__}", expected "bool"', lineno_offset=call.lineno ) return False return value def is_attrib(expr: Optional[ast.expr], ctx: model.Documentable) -> bool: """Does this expression return an C{attr.ib}?""" return isinstance(expr, ast.Call) and node2fullname(expr.func, ctx) in ( 'attr.ib', 'attr.attrib', 'attr.attr' ) _attrib_signature = signature(attrib) """Signature of the L{attr.ib} function for defining class attributes.""" def attrib_args(expr: ast.expr, ctx: model.Documentable) -> Optional[BoundArguments]: """Get the arguments passed to an C{attr.ib} definition. @return: The arguments, or L{None} if C{expr} does not look like an C{attr.ib} definition or the arguments passed to it are invalid. """ if isinstance(expr, ast.Call) and node2fullname(expr.func, ctx) in ( 'attr.ib', 'attr.attrib', 'attr.attr' ): try: return bind_args(_attrib_signature, expr) except TypeError as ex: message = str(ex).replace("'", '"') ctx.module.report( f"Invalid arguments for attr.ib(): {message}", lineno_offset=expr.lineno ) return None def is_using_typing_final(obj: model.Attribute) -> bool: """ Detect if C{obj}'s L{Attribute.annotation} is using L{typing.Final}. """ final_qualifiers = ("typing.Final", "typing_extensions.Final") fullName = node2fullname(obj.annotation, obj) if fullName in final_qualifiers: return True if isinstance(obj.annotation, ast.Subscript): # Final[...] or typing.Final[...] expressions if isinstance(obj.annotation.value, (ast.Name, ast.Attribute)): value = obj.annotation.value fullName = node2fullname(value, obj) if fullName in final_qualifiers: return True return False def is_constant(obj: model.Attribute) -> bool: """ Detect if the given assignment is a constant. To detect whether a assignment is a constant, this checks two things: - all-caps variable name - typing.Final annotation @note: Must be called after setting obj.annotation to detect variables using Final. """ return obj.name.isupper() or is_using_typing_final(obj) def is_attribute_overridden(obj: model.Attribute, new_value: Optional[ast.expr]) -> bool: """ Detect if the optional C{new_value} expression override the one already stored in the L{Attribute.value} attribute. """ return obj.value is not None and new_value is not None def _extract_annotation_subscript(annotation: ast.Subscript) -> ast.AST: """ Extract the "str, bytes" part from annotations like "Union[str, bytes]". """ ann_slice = annotation.slice if sys.version_info < (3,9) and isinstance(ann_slice, ast.Index): return ann_slice.value else: return ann_slice def extract_final_subscript(annotation: ast.Subscript) -> ast.expr: """ Extract the "str" part from annotations like "Final[str]". @raises ValueError: If the "Final" annotation is not valid. """ ann_slice = _extract_annotation_subscript(annotation) if isinstance(ann_slice, (ast.ExtSlice, ast.Slice, ast.Tuple)): raise ValueError("Annotation is invalid, it should not contain slices.") else: assert isinstance(ann_slice, ast.expr) return ann_slice class ModuleVistor(ast.NodeVisitor): currAttr: Optional[model.Documentable] newAttr: Optional[model.Documentable] def __init__(self, builder: 'ASTBuilder', module: model.Module): self.builder = builder self.system = builder.system self.module = module def default(self, node: ast.AST) -> None: body: Optional[Sequence[ast.stmt]] = getattr(node, 'body', None) if body is not None: self.currAttr = None for child in body: self.newAttr = None self.visit(child) self.currAttr = self.newAttr self.newAttr = None def visit_Module(self, node: ast.Module) -> None: assert self.module.docstring is None self.builder.push(self.module, 0) if len(node.body) > 0 and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str): self.module.setDocstring(node.body[0].value) epydoc2stan.extract_fields(self.module) self.default(node) self.builder.pop(self.module) def visit_ClassDef(self, node: ast.ClassDef) -> Optional[model.Class]: # Ignore classes within functions. parent = self.builder.current if isinstance(parent, model.Function): return None rawbases = [] bases = [] baseobjects = [] for n in node.bases: if isinstance(n, ast.Name): str_base = n.id else: str_base = astor.to_source(n).strip() rawbases.append(str_base) full_name = parent.expandName(str_base) bases.append(full_name) baseobj = self.system.objForFullName(full_name) if not isinstance(baseobj, model.Class): baseobj = None baseobjects.append(baseobj) lineno = node.lineno if node.decorator_list: lineno = node.decorator_list[0].lineno cls: model.Class = self.builder.pushClass(node.name, lineno) cls.decorators = [] cls.rawbases = rawbases cls.bases = bases cls.baseobjects = baseobjects if len(node.body) > 0 and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str): cls.setDocstring(node.body[0].value) epydoc2stan.extract_fields(cls) if node.decorator_list: for decnode in node.decorator_list: args: Optional[Sequence[ast.expr]] if isinstance(decnode, ast.Call): base = node2fullname(decnode.func, parent) args = decnode.args if base in ('attr.s', 'attr.attrs', 'attr.attributes'): cls.auto_attribs |= _uses_auto_attribs(decnode, parent.module) else: base = node2fullname(decnode, parent) args = None if base is None: # pragma: no cover # There are expressions for which node2data() returns None, # but I cannot find any that don't lead to a SyntaxError # when used in a decorator. cls.report("cannot make sense of class decorator") else: cls.decorators.append((base, args)) cls.raw_decorators = node.decorator_list if node.decorator_list else [] for b in cls.baseobjects: if b is not None: b.subclasses.append(cls) self.default(node) self.builder.popClass() return cls def visit_ImportFrom(self, node: ast.ImportFrom) -> None: ctx = self.builder.current if not isinstance(ctx, model.CanContainImportsDocumentable): self.builder.warning("processing import statement in odd context", str(ctx)) return modname = node.module level = node.level if level: # Relative import. parent: Optional[model.Documentable] = ctx.parentMod if isinstance(ctx.module, model.Package): level -= 1 for _ in range(level): if parent is None: break parent = parent.parent if parent is None: assert ctx.parentMod is not None ctx.parentMod.report( "relative import level (%d) too high" % node.level, lineno_offset=node.lineno ) return if modname is None: modname = parent.fullName() else: modname = f'{parent.fullName()}.{modname}' else: # The module name can only be omitted on relative imports. assert modname is not None if node.names[0].name == '*': self._importAll(modname) else: self._importNames(modname, node.names) def _importAll(self, modname: str) -> None: """Handle a C{from import *} statement.""" mod = self.system.getProcessedModule(modname) if mod is None: # We don't have any information about the module, so we don't know # what names to import. self.builder.warning("import * from unknown", modname) return self.builder.warning("import *", modname) # Get names to import: use __all__ if available, otherwise take all # names that are not private. names = mod.all if names is None: names = [ name for name in chain(mod.contents.keys(), mod._localNameToFullName_map.keys()) if not name.startswith('_') ] # Add imported names to our module namespace. assert isinstance(self.builder.current, model.CanContainImportsDocumentable) _localNameToFullName = self.builder.current._localNameToFullName_map expandName = mod.expandName for name in names: _localNameToFullName[name] = expandName(name) def _importNames(self, modname: str, names: Iterable[ast.alias]) -> None: """Handle a C{from import } statement.""" # Process the module we're importing from. mod = self.system.getProcessedModule(modname) # Fetch names to export. current = self.builder.current if isinstance(current, model.Module): exports = current.all if exports is None: exports = [] else: assert isinstance(current, model.CanContainImportsDocumentable) # Don't export names imported inside classes or functions. exports = [] _localNameToFullName = current._localNameToFullName_map for al in names: orgname, asname = al.name, al.asname if asname is None: asname = orgname # Move re-exported objects into current module. if asname in exports and mod is not None: try: ob = mod.contents[orgname] except KeyError: self.builder.warning("cannot find re-exported name", f'{modname}.{orgname}') else: if mod.all is None or orgname not in mod.all: self.system.msg( "astbuilder", "moving %r into %r" % (ob.fullName(), current.fullName()) ) # Must be a Module since the exports is set to an empty list if it's not. assert isinstance(current, model.Module) ob.reparent(current, asname) continue # If we're importing from a package, make sure imported modules # are processed (getProcessedModule() ignores non-modules). if isinstance(mod, model.Package): self.system.getProcessedModule(f'{modname}.{orgname}') _localNameToFullName[asname] = f'{modname}.{orgname}' def visit_Import(self, node: ast.Import) -> None: """Process an import statement. The grammar for the statement is roughly: mod_as := DOTTEDNAME ['as' NAME] import_stmt := 'import' mod_as (',' mod_as)* and this is translated into a node which is an instance of Import wih an attribute 'names', which is in turn a list of 2-tuples (dotted_name, as_name) where as_name is None if there was no 'as foo' part of the statement. """ if not isinstance(self.builder.current, model.CanContainImportsDocumentable): self.builder.warning("processing import statement in odd context", str(self.builder.current)) return _localNameToFullName = self.builder.current._localNameToFullName_map for al in node.names: fullname, asname = al.name, al.asname if asname is not None: _localNameToFullName[asname] = fullname def _handleOldSchoolMethodDecoration(self, target: str, expr: Optional[ast.expr]) -> bool: if not isinstance(expr, ast.Call): return False func = expr.func if not isinstance(func, ast.Name): return False func_name = func.id args = expr.args if len(args) != 1: return False arg, = args if not isinstance(arg, ast.Name): return False if target == arg.id and func_name in ['staticmethod', 'classmethod']: target_obj = self.builder.current.contents.get(target) if isinstance(target_obj, model.Function): # _handleOldSchoolMethodDecoration must only be called in a class scope. assert target_obj.kind is model.DocumentableKind.METHOD if func_name == 'staticmethod': target_obj.kind = model.DocumentableKind.STATIC_METHOD elif func_name == 'classmethod': target_obj.kind = model.DocumentableKind.CLASS_METHOD return True return False def _warnsConstantAssigmentOverride(self, obj: model.Attribute, lineno_offset: int) -> None: obj.report(f'Assignment to constant "{obj.name}" overrides previous assignment ' f'at line {obj.linenumber}, the original value will not be part of the docs.', section='ast', lineno_offset=lineno_offset) def _warnsConstantReAssigmentInInstance(self, obj: model.Attribute, lineno_offset: int = 0) -> None: obj.report(f'Assignment to constant "{obj.name}" inside an instance is ignored, this value will not be part of the docs.', section='ast', lineno_offset=lineno_offset) def _handleConstant(self, obj: model.Attribute, value: Optional[ast.expr], lineno: int) -> None: """Must be called after obj.setLineNumber() to have the right line number in the warning.""" if is_attribute_overridden(obj, value): if obj.kind in (model.DocumentableKind.CONSTANT, model.DocumentableKind.VARIABLE, model.DocumentableKind.CLASS_VARIABLE): # Module/Class level warning, regular override. self._warnsConstantAssigmentOverride(obj=obj, lineno_offset=lineno-obj.linenumber) else: # Instance level warning caught at the time of the constant detection. self._warnsConstantReAssigmentInInstance(obj) obj.value = value obj.kind = model.DocumentableKind.CONSTANT # A hack to to display variables annotated with Final with the real type instead. if is_using_typing_final(obj): if isinstance(obj.annotation, ast.Subscript): try: annotation = extract_final_subscript(obj.annotation) except ValueError as e: obj.report(str(e), section='ast', lineno_offset=lineno-obj.linenumber) obj.annotation = _infer_type(value) if value else None else: # Will not display as "Final[str]" but rather only "str" obj.annotation = annotation else: # Just plain "Final" annotation. # Simply ignore it because it's duplication of information. obj.annotation = _infer_type(value) if value else None def _handleModuleVar(self, target: str, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: if target in MODULE_VARIABLES_META_PARSERS: # This is metadata, not a variable that needs to be documented, # and therefore doesn't need an Attribute instance. return parent = self.builder.current obj = parent.resolveName(target) if obj is None: obj = self.builder.addAttribute(name=target, kind=None, parent=parent) if isinstance(obj, model.Attribute): if annotation is None and expr is not None: annotation = _infer_type(expr) obj.annotation = annotation obj.setLineNumber(lineno) if is_constant(obj): self._handleConstant(obj=obj, value=expr, lineno=lineno) else: obj.kind = model.DocumentableKind.VARIABLE # We store the expr value for all Attribute in order to be able to # check if they have been initialized or not. obj.value = expr self.newAttr = obj def _handleAssignmentInModule(self, target: str, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: module = self.builder.current assert isinstance(module, model.Module) if not _handleAliasing(module, target, expr): self._handleModuleVar(target, annotation, expr, lineno) def _handleClassVar(self, name: str, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: cls = self.builder.current assert isinstance(cls, model.Class) if not _maybeAttribute(cls, name): return # Class variables can only be Attribute, so it's OK to cast obj = cast(Optional[model.Attribute], cls.contents.get(name)) if obj is None: obj = self.builder.addAttribute(name=name, kind=None, parent=cls) if obj.kind is None: instance = is_attrib(expr, cls) or ( cls.auto_attribs and annotation is not None and not ( isinstance(annotation, ast.Subscript) and node2fullname(annotation.value, cls) == 'typing.ClassVar' ) ) obj.kind = model.DocumentableKind.INSTANCE_VARIABLE if instance else model.DocumentableKind.CLASS_VARIABLE if expr is not None: if annotation is None: annotation = self._annotation_from_attrib(expr, cls) if annotation is None: annotation = _infer_type(expr) obj.annotation = annotation obj.setLineNumber(lineno) if is_constant(obj): self._handleConstant(obj=obj, value=expr, lineno=lineno) else: obj.value = expr self.newAttr = obj def _handleInstanceVar(self, name: str, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: func = self.builder.current if not isinstance(func, model.Function): return cls = func.parent if not isinstance(cls, model.Class): return if not _maybeAttribute(cls, name): return # Class variables can only be Attribute, so it's OK to cast obj = cast(Optional[model.Attribute], cls.contents.get(name)) if obj is None: obj = self.builder.addAttribute(name=name, kind=None, parent=cls) if annotation is None and expr is not None: annotation = _infer_type(expr) obj.annotation = annotation obj.setLineNumber(lineno) # Maybe an instance variable overrides a constant, # so we check before setting the kind to INSTANCE_VARIABLE. if obj.kind is model.DocumentableKind.CONSTANT: self._warnsConstantReAssigmentInInstance(obj, lineno_offset=lineno-obj.linenumber) else: obj.kind = model.DocumentableKind.INSTANCE_VARIABLE obj.value = expr self.newAttr = obj def _handleAssignmentInClass(self, target: str, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: cls = self.builder.current assert isinstance(cls, model.Class) if not _handleAliasing(cls, target, expr): self._handleClassVar(target, annotation, expr, lineno) def _handleDocstringUpdate(self, targetNode: ast.expr, expr: Optional[ast.expr], lineno: int ) -> None: def warn(msg: str) -> None: module = self.builder.currentMod assert module is not None module.report(msg, section='ast', lineno_offset=lineno) # Ignore docstring updates in functions. scope = self.builder.current if isinstance(scope, model.Function): return # Figure out target object. full_name = node2fullname(targetNode, scope) if full_name is None: warn("Unable to figure out target for __doc__ assignment") # Don't return yet: we might have to warn about the value too. obj = None else: obj = self.system.objForFullName(full_name) if obj is None: warn("Unable to figure out target for __doc__ assignment: " "computed full name not found: " + full_name) # Determine docstring value. try: if expr is None: # The expr is None for detupling assignments, which can # be described as "too complex". raise ValueError() docstring: object = ast.literal_eval(expr) except ValueError: warn("Unable to figure out value for __doc__ assignment, " "maybe too complex") return if not isinstance(docstring, str): warn("Ignoring value assigned to __doc__: not a string") return if obj is not None: obj.docstring = docstring # TODO: It might be better to not perform docstring parsing until # we have the final docstrings for all objects. obj.parsed_docstring = None def _handleAssignment(self, targetNode: ast.expr, annotation: Optional[ast.expr], expr: Optional[ast.expr], lineno: int ) -> None: if isinstance(targetNode, ast.Name): target = targetNode.id scope = self.builder.current if isinstance(scope, model.Module): self._handleAssignmentInModule(target, annotation, expr, lineno) elif isinstance(scope, model.Class): if not self._handleOldSchoolMethodDecoration(target, expr): self._handleAssignmentInClass(target, annotation, expr, lineno) elif isinstance(targetNode, ast.Attribute): value = targetNode.value if targetNode.attr == '__doc__': self._handleDocstringUpdate(value, expr, lineno) elif isinstance(value, ast.Name) and value.id == 'self': self._handleInstanceVar(targetNode.attr, annotation, expr, lineno) def visit_Assign(self, node: ast.Assign) -> None: lineno = node.lineno expr = node.value type_comment: Optional[str] = getattr(node, 'type_comment', None) if type_comment is None: annotation = None else: annotation = self._unstring_annotation(ast.Str(type_comment, lineno=lineno)) for target in node.targets: if isinstance(target, ast.Tuple): for elem in target.elts: # Note: We skip type and aliasing analysis for this case, # but we do record line numbers. self._handleAssignment(elem, None, None, lineno) else: self._handleAssignment(target, annotation, expr, lineno) def visit_AnnAssign(self, node: ast.AnnAssign) -> None: annotation = self._unstring_annotation(node.annotation) self._handleAssignment(node.target, annotation, node.value, node.lineno) def visit_Expr(self, node: ast.Expr) -> None: value = node.value if isinstance(value, ast.Str): attr = self.currAttr if attr is not None: attr.setDocstring(value) self.generic_visit(node) def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: self._handleFunctionDef(node, is_async=True) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: self._handleFunctionDef(node, is_async=False) def _handleFunctionDef(self, node: Union[ast.AsyncFunctionDef, ast.FunctionDef], is_async: bool ) -> None: # Ignore inner functions. parent = self.builder.current if isinstance(parent, model.Function): return lineno = node.lineno if node.decorator_list: lineno = node.decorator_list[0].lineno docstring: Optional[ast.Str] = None if len(node.body) > 0 and isinstance(node.body[0], ast.Expr) \ and isinstance(node.body[0].value, ast.Str): docstring = node.body[0].value func_name = node.name is_property = False is_classmethod = False is_staticmethod = False if isinstance(parent, model.Class) and node.decorator_list: for d in node.decorator_list: if isinstance(d, ast.Call): deco_name = node2dottedname(d.func) else: deco_name = node2dottedname(d) if deco_name is None: continue if deco_name[-1].endswith('property') or deco_name[-1].endswith('Property'): is_property = True elif deco_name == ['classmethod']: is_classmethod = True elif deco_name == ['staticmethod']: is_staticmethod = True elif len(deco_name) >= 2 and deco_name[-1] in ('setter', 'deleter'): # Rename the setter/deleter, so it doesn't replace # the property object. func_name = '.'.join(deco_name[-2:]) if is_property: attr = self._handlePropertyDef(node, docstring, lineno) if is_classmethod: attr.report(f'{attr.fullName()} is both property and classmethod') if is_staticmethod: attr.report(f'{attr.fullName()} is both property and staticmethod') return func = self.builder.pushFunction(func_name, lineno) func.is_async = is_async if docstring is not None: func.setDocstring(docstring) func.decorators = node.decorator_list if is_staticmethod: if is_classmethod: func.report(f'{func.fullName()} is both classmethod and staticmethod') else: func.kind = model.DocumentableKind.STATIC_METHOD elif is_classmethod: func.kind = model.DocumentableKind.CLASS_METHOD # Position-only arguments were introduced in Python 3.8. posonlyargs: Sequence[ast.arg] = getattr(node.args, 'posonlyargs', ()) num_pos_args = len(posonlyargs) + len(node.args.args) defaults = node.args.defaults default_offset = num_pos_args - len(defaults) def get_default(index: int) -> Optional[ast.expr]: assert 0 <= index < num_pos_args, index index -= default_offset return None if index < 0 else defaults[index] parameters: List[Parameter] = [] def add_arg(name: str, kind: Any, default: Optional[ast.expr]) -> None: default_val = Parameter.empty if default is None else _ValueFormatter(default, ctx=func) parameters.append(Parameter(name, kind, default=default_val)) for index, arg in enumerate(posonlyargs): add_arg(arg.arg, Parameter.POSITIONAL_ONLY, get_default(index)) for index, arg in enumerate(node.args.args, start=len(posonlyargs)): add_arg(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, get_default(index)) vararg = node.args.vararg if vararg is not None: add_arg(vararg.arg, Parameter.VAR_POSITIONAL, None) assert len(node.args.kwonlyargs) == len(node.args.kw_defaults) for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): add_arg(arg.arg, Parameter.KEYWORD_ONLY, default) kwarg = node.args.kwarg if kwarg is not None: add_arg(kwarg.arg, Parameter.VAR_KEYWORD, None) try: signature = Signature(parameters) except ValueError as ex: func.report(f'{func.fullName()} has invalid parameters: {ex}') signature = Signature() func.signature = signature func.annotations = self._annotations_from_function(node) self.default(node) self.builder.popFunction() def _handlePropertyDef(self, node: Union[ast.AsyncFunctionDef, ast.FunctionDef], docstring: Optional[ast.Str], lineno: int ) -> model.Attribute: attr = self.builder.addAttribute(name=node.name, kind=model.DocumentableKind.PROPERTY, parent=self.builder.current) attr.setLineNumber(lineno) if docstring is not None: attr.setDocstring(docstring) assert attr.docstring is not None pdoc = epydoc2stan.parse_docstring(attr, attr.docstring, attr) other_fields = [] for field in pdoc.fields: tag = field.tag() if tag == 'return': if not pdoc.has_body: pdoc = field.body() # Avoid format_summary() going back to the original # empty-body docstring. attr.docstring = '' elif tag == 'rtype': attr.parsed_type = field.body() else: other_fields.append(field) pdoc.fields = other_fields attr.parsed_docstring = pdoc if node.returns is not None: attr.annotation = self._unstring_annotation(node.returns) attr.decorators = node.decorator_list return attr def _annotation_from_attrib(self, expr: ast.expr, ctx: model.Documentable ) -> Optional[ast.expr]: """Get the type of an C{attr.ib} definition. @param expr: The expression's AST. @param ctx: The context in which this expression is evaluated. @return: A type annotation, or None if the expression is not an C{attr.ib} definition or contains no type information. """ args = attrib_args(expr, ctx) if args is not None: typ = args.arguments.get('type') if typ is not None: return self._unstring_annotation(typ) default = args.arguments.get('default') if default is not None: return _infer_type(default) return None def _annotations_from_function( self, func: Union[ast.AsyncFunctionDef, ast.FunctionDef] ) -> Mapping[str, Optional[ast.expr]]: """Get annotations from a function definition. @param func: The function definition's AST. @return: Mapping from argument name to annotation. The name C{return} is used for the return type. Unannotated arguments are omitted. """ def _get_all_args() -> Iterator[ast.arg]: base_args = func.args # New on Python 3.8 -- handle absence gracefully try: yield from base_args.posonlyargs except AttributeError: pass yield from base_args.args varargs = base_args.vararg if varargs: varargs.arg = epydoc2stan.VariableArgument(varargs.arg) yield varargs yield from base_args.kwonlyargs kwargs = base_args.kwarg if kwargs: kwargs.arg = epydoc2stan.KeywordArgument(kwargs.arg) yield kwargs def _get_all_ast_annotations() -> Iterator[Tuple[str, Optional[ast.expr]]]: for arg in _get_all_args(): yield arg.arg, arg.annotation returns = func.returns if returns: yield 'return', returns return { # Include parameter names even if they're not annotated, so that # we can use the key set to know which parameters exist and warn # when non-existing parameters are documented. name: None if value is None else self._unstring_annotation(value) for name, value in _get_all_ast_annotations() } def _unstring_annotation(self, node: ast.expr) -> ast.expr: """Replace all strings in the given expression by parsed versions. @return: The unstringed node. If parsing fails, an error is logged and the original node is returned. """ try: expr = _AnnotationStringParser().visit(node) except SyntaxError as ex: module = self.builder.currentMod assert module is not None module.report(f'syntax error in annotation: {ex}', lineno_offset=node.lineno) return node else: assert isinstance(expr, ast.expr), expr return expr class _ValueFormatter: """ Class to encapsulate a python value and translate it to HTML when calling L{repr()} on the L{_ValueFormatter}. Used for presenting default values of parameters. """ def __init__(self, value: Any, ctx: model.Documentable): self._colorized = colorize_inline_pyval(value) """ The colorized value as L{ParsedDocstring}. """ self._linker = epydoc2stan._EpydocLinker(ctx) """ Linker. """ def __repr__(self) -> str: """ Present the python value as HTML. Without the englobing tags. """ # Using node2stan.node2html instead of flatten(to_stan()). # This avoids calling flatten() twice. return ''.join(node2stan.node2html(self._colorized.to_node(), self._linker)) class _AnnotationStringParser(ast.NodeTransformer): """Implementation of L{ModuleVistor._unstring_annotation()}. When given an expression, the node returned by L{ast.NodeVisitor.visit()} will also be an expression. If any string literal contained in the original expression is either invalid Python or not a singular expression, L{SyntaxError} is raised. """ def _parse_string(self, value: str) -> ast.expr: statements = ast.parse(value).body if len(statements) != 1: raise SyntaxError("expected expression, found multiple statements") stmt, = statements if isinstance(stmt, ast.Expr): # Expression wrapped in an Expr statement. expr = self.visit(stmt.value) assert isinstance(expr, ast.expr), expr return expr else: raise SyntaxError("expected expression, found statement") def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript: value = self.visit(node.value) if isinstance(value, ast.Name) and value.id == 'Literal': # Literal[...] expression; don't unstring the arguments. slice = node.slice elif isinstance(value, ast.Attribute) and value.attr == 'Literal': # typing.Literal[...] expression; don't unstring the arguments. slice = node.slice else: # Other subscript; unstring the slice. slice = self.visit(node.slice) return ast.copy_location(ast.Subscript(value, slice, node.ctx), node) # For Python >= 3.8: def visit_Constant(self, node: ast.Constant) -> ast.expr: value = node.value if isinstance(value, str): return ast.copy_location(self._parse_string(value), node) else: const = self.generic_visit(node) assert isinstance(const, ast.Constant), const return const # For Python < 3.8: def visit_Str(self, node: ast.Str) -> ast.expr: return ast.copy_location(self._parse_string(node.s), node) def _infer_type(expr: ast.expr) -> Optional[ast.expr]: """Infer an expression's type. @param expr: The expression's AST. @return: A type annotation, or None if the expression has no obvious type. """ try: value: object = ast.literal_eval(expr) except ValueError: return None else: ann = _annotation_for_value(value) if ann is None: return None else: return ast.fix_missing_locations(ast.copy_location(ann, expr)) def _annotation_for_value(value: object) -> Optional[ast.expr]: if value is None: return None name = type(value).__name__ if isinstance(value, (dict, list, set, tuple)): ann_elem = _annotation_for_elements(value) if isinstance(value, dict): ann_value = _annotation_for_elements(value.values()) if ann_value is None: ann_elem = None elif ann_elem is not None: ann_elem = ast.Tuple(elts=[ann_elem, ann_value]) if ann_elem is not None: if name == 'tuple': ann_elem = ast.Tuple(elts=[ann_elem, ast.Ellipsis()]) return ast.Subscript(value=ast.Name(id=name), slice=ast.Index(value=ann_elem)) return ast.Name(id=name) def _annotation_for_elements(sequence: Iterable[object]) -> Optional[ast.expr]: names = set() for elem in sequence: ann = _annotation_for_value(elem) if isinstance(ann, ast.Name): names.add(ann.id) else: # Nested sequences are too complex. return None if len(names) == 1: name = names.pop() return ast.Name(id=name) else: # Empty sequence or no uniform type. return None DocumentableT = TypeVar('DocumentableT', bound=model.Documentable) class ASTBuilder: ModuleVistor = ModuleVistor def __init__(self, system: model.System): self.system = system self.current = cast(model.Documentable, None) self.currentMod: Optional[model.Module] = None self._stack: List[model.Documentable] = [] self.ast_cache: Dict[Path, Optional[ast.Module]] = {} def _push(self, cls: Type[DocumentableT], name: str, lineno: int) -> DocumentableT: obj = cls(self.system, name, self.current) self.system.addObject(obj) self.push(obj, lineno) return obj def _pop(self, cls: Type[model.Documentable]) -> None: assert isinstance(self.current, cls) self.pop(self.current) def push(self, obj: model.Documentable, lineno: int) -> None: self._stack.append(self.current) self.current = obj if isinstance(obj, model.Module): assert self.currentMod is None obj.parentMod = self.currentMod = obj elif self.currentMod is not None: if obj.parentMod is not None: assert obj.parentMod is self.currentMod else: obj.parentMod = self.currentMod else: assert obj.parentMod is None if lineno: obj.setLineNumber(lineno) def pop(self, obj: model.Documentable) -> None: assert self.current is obj, f"{self.current!r} is not {obj!r}" self.current = self._stack.pop() if isinstance(obj, model.Module): self.currentMod = None def pushClass(self, name: str, lineno: int) -> model.Class: return self._push(self.system.Class, name, lineno) def popClass(self) -> None: self._pop(self.system.Class) def pushFunction(self, name: str, lineno: int) -> model.Function: return self._push(self.system.Function, name, lineno) def popFunction(self) -> None: self._pop(self.system.Function) def addAttribute(self, name: str, kind: Optional[model.DocumentableKind], parent: model.Documentable ) -> model.Attribute: system = self.system parentMod = self.currentMod attr = system.Attribute(system, name, parent) attr.kind = kind attr.parentMod = parentMod system.addObject(attr) return attr def warning(self, message: str, detail: str) -> None: self.system._warning(self.current, message, detail) def processModuleAST(self, mod_ast: ast.Module, mod: model.Module) -> None: for name, node in findModuleLevelAssign(mod_ast): try: module_var_parser = MODULE_VARIABLES_META_PARSERS[name] except KeyError: continue else: module_var_parser(node, mod) self.ModuleVistor(self, mod).visit(mod_ast) def parseFile(self, path: Path) -> Optional[ast.Module]: try: return self.ast_cache[path] except KeyError: mod: Optional[ast.Module] = None try: mod = parseFile(path) except (SyntaxError, ValueError): self.warning("cannot parse", str(path)) self.ast_cache[path] = mod return mod model.System.defaultBuilder = ASTBuilder def findModuleLevelAssign(mod_ast: ast.Module) -> Iterator[Tuple[str, ast.Assign]]: """ Find module level Assign. Yields tuples containing the assigment name and the Assign node. """ for node in mod_ast.body: if isinstance(node, ast.Assign) and \ len(node.targets) == 1 and \ isinstance(node.targets[0], ast.Name): yield (node.targets[0].id, node) def parseAll(node: ast.Assign, mod: model.Module) -> None: """Find and attempt to parse into a list of names the C{__all__} variable of a module's AST and set L{Module.all} accordingly.""" if not isinstance(node.value, (ast.List, ast.Tuple)): mod.report( 'Cannot parse value assigned to "__all__"', section='all', lineno_offset=node.lineno) return names = [] for idx, item in enumerate(node.value.elts): try: name: object = ast.literal_eval(item) except ValueError: mod.report( f'Cannot parse element {idx} of "__all__"', section='all', lineno_offset=node.lineno) else: if isinstance(name, str): names.append(name) else: mod.report( f'Element {idx} of "__all__" has ' f'type "{type(name).__name__}", expected "str"', section='all', lineno_offset=node.lineno) if mod.all is not None: mod.report( 'Assignment to "__all__" overrides previous assignment', section='all', lineno_offset=node.lineno) mod.all = names def parseDocformat(node: ast.Assign, mod: model.Module) -> None: """ Find C{__docformat__} variable of this module's AST and set L{Module.docformat} accordingly. This is all valid:: __docformat__ = "reStructuredText en" __docformat__ = "epytext" __docformat__ = "restructuredtext" """ try: value = ast.literal_eval(node.value) except ValueError: mod.report( 'Cannot parse value assigned to "__docformat__": not a string', section='docformat', lineno_offset=node.lineno) return if not isinstance(value, str): mod.report( 'Cannot parse value assigned to "__docformat__": not a string', section='docformat', lineno_offset=node.lineno) return if not value.strip(): mod.report( 'Cannot parse value assigned to "__docformat__": empty value', section='docformat', lineno_offset=node.lineno) return # Language is ignored and parser name is lowercased. value = value.split(" ", 1)[0].lower() if mod._docformat is not None: mod.report( 'Assignment to "__docformat__" overrides previous assignment', section='docformat', lineno_offset=node.lineno) mod.docformat = value MODULE_VARIABLES_META_PARSERS: Mapping[str, Callable[[ast.Assign, model.Module], None]] = { '__all__': parseAll, '__docformat__': parseDocformat } pydoctor-21.12.1/pydoctor/astutils.py000066400000000000000000000021401416703725300176130ustar00rootroot00000000000000""" Various bits of reusable code related to L{ast.AST} node processing. """ from typing import Optional, List from inspect import BoundArguments, Signature import ast def node2dottedname(node: Optional[ast.expr]) -> Optional[List[str]]: """ Resove expression composed by L{ast.Attribute} and L{ast.Name} nodes to a list of names. """ parts = [] while isinstance(node, ast.Attribute): parts.append(node.attr) node = node.value if isinstance(node, ast.Name): parts.append(node.id) else: return None parts.reverse() return parts def bind_args(sig: Signature, call: ast.Call) -> BoundArguments: """ Binds the arguments of a function call to that function's signature. @raise TypeError: If the arguments do not match the signature. """ kwargs = { kw.arg: kw.value for kw in call.keywords # When keywords are passed using '**kwargs', the 'arg' field will # be None. We don't currently support keywords passed that way. if kw.arg is not None } return sig.bind(*call.args, **kwargs) pydoctor-21.12.1/pydoctor/driver.py000066400000000000000000000475321416703725300172540ustar00rootroot00000000000000"""The command-line parsing and entry point.""" from optparse import SUPPRESS_HELP, Option, OptionParser, OptionValueError, Values from pathlib import Path from typing import TYPE_CHECKING, List, Sequence, Tuple, Type, TypeVar, cast import datetime import os import sys from pydoctor.themes import get_themes from pydoctor import model, zopeinterface, __version__ from pydoctor.templatewriter import IWriter, TemplateError, TemplateLookup from pydoctor.sphinx import (MAX_AGE_HELP, USER_INTERSPHINX_CACHE, SphinxInventoryWriter, prepareCache) from pydoctor.epydoc.markup import get_supported_docformats if TYPE_CHECKING: from typing_extensions import NoReturn else: NoReturn = None # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. if sys.version_info < (3, 9): import importlib_resources else: import importlib.resources as importlib_resources BUILDTIME_FORMAT = '%Y-%m-%d %H:%M:%S' def error(msg: str, *args: object) -> NoReturn: if args: msg = msg%args print(msg, file=sys.stderr) sys.exit(1) T = TypeVar('T') def findClassFromDottedName( dottedname: str, optionname: str, base_class: Type[T] ) -> Type[T]: """ Looks up a class by full name. Watch out, prints a message and SystemExits on error! """ if '.' not in dottedname: error("%stakes a dotted name", optionname) parts = dottedname.rsplit('.', 1) try: mod = __import__(parts[0], globals(), locals(), parts[1]) except ImportError: error("could not import module %s", parts[0]) try: cls = getattr(mod, parts[1]) except AttributeError: error("did not find %s in module %s", parts[1], parts[0]) if not issubclass(cls, base_class): error("%s is not a subclass of %s", cls, base_class) return cast(Type[T], cls) MAKE_HTML_DEFAULT = object() def resolve_path(path: str) -> Path: """Parse a given path string to a L{Path} object. The path is converted to an absolute path, as required by L{System.setSourceHref()}. The path does not need to exist. """ # We explicitly make the path relative to the current working dir # because on Windows resolve() does not produce an absolute path # when operating on a non-existing path. return Path(Path.cwd(), path).resolve() def parse_path(option: Option, opt: str, value: str) -> Path: """Parse a path value given to an option to a L{Path} object using L{resolve_path()}. """ try: return resolve_path(value) except Exception as ex: raise OptionValueError(f"{opt}: invalid path: {ex}") class CustomOption(Option): TYPES = Option.TYPES + ("path",) TYPE_CHECKER = dict(Option.TYPE_CHECKER, path=parse_path) def getparser() -> OptionParser: parser = OptionParser( option_class=CustomOption, version=__version__, usage="usage: %prog [options] SOURCEPATH...") parser.add_option( '-c', '--config', dest='configfile', help=("Use config from this file (any command line" "options override settings from the file).")) parser.add_option( '--system-class', dest='systemclass', help=("A dotted name of the class to use to make a system.")) parser.add_option( '--project-name', dest='projectname', help=("The project name, shown at the top of each HTML page.")) parser.add_option( '--project-version', dest='projectversion', default='', metavar='VERSION', help=( "The version of the project for which the API docs are generated. " "Defaults to empty string." )) parser.add_option( '--project-url', dest='projecturl', help=("The project url, appears in the html if given.")) parser.add_option( '--project-base-dir', dest='projectbasedirectory', type='path', help=("Path to the base directory of the project. Source links " "will be computed based on this value."), metavar="PATH",) parser.add_option( '--testing', dest='testing', action='store_true', help=("Don't complain if the run doesn't have any effects.")) parser.add_option( '--pdb', dest='pdb', action='store_true', help=("Like py.test's --pdb.")) parser.add_option( '--make-html', action='store_true', dest='makehtml', default=MAKE_HTML_DEFAULT, help=("Produce html output." " Enabled by default if options '--testing' or '--make-intersphinx' are not specified. ")) parser.add_option( '--make-intersphinx', action='store_true', dest='makeintersphinx', default=False, help=("Produce (only) the objects.inv intersphinx file.")) parser.add_option( '--add-package', action='append', dest='packages', metavar='PACKAGEDIR', default=[], help=SUPPRESS_HELP) parser.add_option( '--add-module', action='append', dest='modules', metavar='MODULE', default=[], help=SUPPRESS_HELP) parser.add_option( '--prepend-package', action='store', dest='prependedpackage', help=("Pretend that all packages are within this one. " "Can be used to document part of a package.")) _docformat_choices = get_supported_docformats() parser.add_option( '--docformat', dest='docformat', action='store', default='epytext', type="choice", choices=list(_docformat_choices), help=("Format used for parsing docstrings. " f"Supported values: {', '.join(_docformat_choices)}"), metavar='FORMAT') parser.add_option( '--template-dir', action='append', dest='templatedir', default=[], help=("Directory containing custom HTML templates. Can repeat."), metavar='PATH', ) parser.add_option('--theme', dest='theme', default='classic', choices=list(get_themes()) , help=("The theme to use when building your API documentation. "), ) parser.add_option( '--html-subject', dest='htmlsubjects', action='append', help=("The fullName of objects to generate API docs for" " (generates everything by default)."), metavar='PACKAGE/MOD/CLASS') parser.add_option( '--html-summary-pages', dest='htmlsummarypages', action='store_true', default=False, help=("Only generate the summary pages.")) parser.add_option( '--html-output', dest='htmloutput', default='apidocs', help=("Directory to save HTML files to (default 'apidocs')"), metavar='PATH',) parser.add_option( '--html-writer', dest='htmlwriter', help=("Dotted name of writer class to use (default " "'pydoctor.templatewriter.TemplateWriter')."), metavar='CLASS',) parser.add_option( '--html-viewsource-base', dest='htmlsourcebase', help=("This should be the path to the trac browser for the top " "of the svn checkout we are documenting part of."), metavar='URL',) parser.add_option( '--process-types', dest='processtypes', action='store_true', help="Process the 'type' and 'rtype' fields, add links and inline markup automatically. " "This settings should not be enabled when using google or numpy docformat because the types are always processed by default.",) parser.add_option( '--buildtime', dest='buildtime', help=("Use the specified build time over the current time. " "Format: %s" % BUILDTIME_FORMAT), metavar='TIME') parser.add_option( '-W', '--warnings-as-errors', action='store_true', dest='warnings_as_errors', default=False, help=("Return exit code 3 on warnings.")) parser.add_option( '-v', '--verbose', action='count', dest='verbosity', default=0, help=("Be noisier. Can be repeated for more noise.")) parser.add_option( '-q', '--quiet', action='count', dest='quietness', default=0, help=("Be quieter.")) def verbose_about_callback(option: Option, opt_str: str, value: str, parser: OptionParser) -> None: assert parser.values is not None d = parser.values.verbosity_details d[value] = d.get(value, 0) + 1 parser.add_option( '--verbose-about', metavar="stage", action="callback", type=str, default={}, dest='verbosity_details', callback=verbose_about_callback, help=("Be noiser during a particular stage of generation.")) parser.add_option( '--introspect-c-modules', default=False, action='store_true', help=("Import and introspect any C modules found.")) parser.add_option( '--intersphinx', action='append', dest='intersphinx', metavar='URL_TO_OBJECTS.INV', default=[], help=( "Use Sphinx objects inventory to generate links to external " "documentation. Can be repeated.")) parser.add_option( '--enable-intersphinx-cache', dest='enable_intersphinx_cache_deprecated', action='store_true', default=False, help=SUPPRESS_HELP ) parser.add_option( '--disable-intersphinx-cache', dest='enable_intersphinx_cache', action='store_false', default=True, help="Disable Intersphinx cache." ) parser.add_option( '--intersphinx-cache-path', dest='intersphinx_cache_path', default=USER_INTERSPHINX_CACHE, help="Where to cache intersphinx objects.inv files.", metavar='PATH', ) parser.add_option( '--clear-intersphinx-cache', dest='clear_intersphinx_cache', action='store_true', default=False, help=("Clear the Intersphinx cache " "specified by --intersphinx-cache-path."), ) parser.add_option( '--intersphinx-cache-max-age', dest='intersphinx_cache_max_age', default='1d', help=MAX_AGE_HELP, metavar='DURATION', ) parser.add_option( '--pyval-repr-maxlines', dest='pyvalreprmaxlines', default=7, type=int, help='Maxinum number of lines for a constant value representation. Use 0 for unlimited.') parser.add_option( '--pyval-repr-linelen', dest='pyvalreprlinelen', default=80, type=int, help='Maxinum number of caracters for a constant value representation line. Use 0 for unlimited.') return parser def readConfigFile(options: Values) -> None: # this is all a bit horrible. rethink, then rewrite! for i, line in enumerate(open(options.configfile)): line = line.strip() if not line or line.startswith('#'): continue if ':' not in line: error("don't understand line %d of %s", i+1, options.configfile) k, v = line.split(':', 1) k = k.strip() v = os.path.expanduser(v.strip()) if not hasattr(options, k): error("invalid option %r on line %d of %s", k, i+1, options.configfile) pre_v = getattr(options, k) if not pre_v: if isinstance(pre_v, list): setattr(options, k, v.split(',')) else: setattr(options, k, v) else: if not isinstance(pre_v, list): setattr(options, k, v) def parse_args(args: Sequence[str]) -> Tuple[Values, List[str]]: parser = getparser() options, args = parser.parse_args(args) options.verbosity -= options.quietness _warn_deprecated_options(options) return options, args def _warn_deprecated_options(options: Values) -> None: """ Check the CLI options and warn on deprecated options. """ if options.enable_intersphinx_cache_deprecated: print("The --enable-intersphinx-cache option is deprecated; " "the cache is now enabled by default.", file=sys.stderr, flush=True) if options.modules: print("The --add-module option is deprecated; " "pass modules as positional arguments instead.", file=sys.stderr, flush=True) if options.packages: print("The --add-package option is deprecated; " "pass packages as positional arguments instead.", file=sys.stderr, flush=True) def main(args: Sequence[str] = sys.argv[1:]) -> int: """ This is the console_scripts entry point for pydoctor CLI. @param args: Command line arguments to run the CLI. """ options, args = parse_args(args) exitcode = 0 if options.configfile: readConfigFile(options) cache = prepareCache(clearCache=options.clear_intersphinx_cache, enableCache=options.enable_intersphinx_cache, cachePath=options.intersphinx_cache_path, maxAge=options.intersphinx_cache_max_age) try: # step 1: make/find the system if options.systemclass: systemclass = findClassFromDottedName( options.systemclass, '--system-class', model.System) else: systemclass = zopeinterface.ZopeInterfaceSystem system = systemclass(options) system.fetchIntersphinxInventories(cache) if options.htmlsourcebase: if options.projectbasedirectory is None: error("you must specify --project-base-dir " "when using --html-viewsource-base") system.sourcebase = options.htmlsourcebase # step 1.5: check that we're actually going to accomplish something here args = list(args) + options.modules + options.packages if options.makehtml == MAKE_HTML_DEFAULT: if not options.testing and not options.makeintersphinx: options.makehtml = True else: options.makehtml = False # Support source date epoch: # https://reproducible-builds.org/specs/source-date-epoch/ try: system.buildtime = datetime.datetime.utcfromtimestamp( int(os.environ['SOURCE_DATE_EPOCH'])) except ValueError as e: error(str(e)) except KeyError: pass if options.buildtime: try: system.buildtime = datetime.datetime.strptime( options.buildtime, BUILDTIME_FORMAT) except ValueError as e: error(str(e)) # step 2: add any packages and modules if args: prependedpackage = None if options.prependedpackage: for m in options.prependedpackage.split('.'): prependedpackage = system.Package( system, m, prependedpackage) system.addObject(prependedpackage) initmodule = system.Module(system, '__init__', prependedpackage) system.addObject(initmodule) added_paths = set() for arg in args: path = resolve_path(arg) if path in added_paths: continue if options.projectbasedirectory is not None: # Note: Path.is_relative_to() was only added in Python 3.9, # so we have to use this workaround for now. try: path.relative_to(options.projectbasedirectory) except ValueError as ex: error(f"Source path lies outside base directory: {ex}") if path.is_dir(): system.msg('addPackage', f"adding directory {path}") if not (path / '__init__.py').is_file(): error(f"Source directory lacks __init__.py: {path}") system.addPackage(path, prependedpackage) elif path.is_file(): system.msg('addModuleFromPath', f"adding module {path}") system.addModuleFromPath(path, prependedpackage) elif path.exists(): error(f"Source path is neither file nor directory: {path}") else: error(f"Source path does not exist: {path}") added_paths.add(path) else: error("No source paths given.") # step 3: move the system to the desired state if system.options.projectname is None: name = '/'.join(system.root_names) system.msg('warning', f"Guessing '{name}' for project name.", thresh=0) system.projectname = name else: system.projectname = system.options.projectname system.process() # step 4: make html, if desired if options.makehtml: options.makeintersphinx = True from pydoctor import templatewriter if options.htmlwriter: writerclass = findClassFromDottedName( # ignore mypy error: Only concrete class can be given where "Type[IWriter]" is expected options.htmlwriter, '--html-writer', IWriter) # type: ignore[misc] else: writerclass = templatewriter.TemplateWriter system.msg('html', 'writing html to %s using %s.%s'%( options.htmloutput, writerclass.__module__, writerclass.__name__)) writer: IWriter # Always init the writer with the 'base' set of templates at least. template_lookup = TemplateLookup( importlib_resources.files('pydoctor.themes') / 'base') # Handle theme selection, 'classic' by default. if system.options.theme != 'base': template_lookup.add_templatedir( importlib_resources.files('pydoctor.themes') / system.options.theme) # Handle custom HTML templates if system.options.templatedir: try: for t in system.options.templatedir: template_lookup.add_templatedir(Path(t)) except TemplateError as e: error(str(e)) build_directory = Path(options.htmloutput) writer = writerclass(build_directory, template_lookup=template_lookup) writer.prepOutputDirectory() subjects: Sequence[model.Documentable] = () if options.htmlsubjects: subjects = [system.allobjects[fn] for fn in options.htmlsubjects] else: writer.writeSummaryPages(system) if not options.htmlsummarypages: subjects = system.rootobjects writer.writeIndividualFiles(subjects) if system.docstring_syntax_errors: def p(msg: str) -> None: system.msg('docstring-summary', msg, thresh=-1, topthresh=1) p("these %s objects' docstrings contain syntax errors:" %(len(system.docstring_syntax_errors),)) exitcode = 2 for fn in sorted(system.docstring_syntax_errors): p(' '+fn) if system.violations and options.warnings_as_errors: # Update exit code if the run has produced warnings. exitcode = 3 if options.makeintersphinx: if not options.makehtml: subjects = system.rootobjects # Generate Sphinx inventory. sphinx_inventory = SphinxInventoryWriter( logger=system.msg, project_name=system.projectname, project_version=system.options.projectversion, ) if not os.path.exists(options.htmloutput): os.makedirs(options.htmloutput) sphinx_inventory.generate( subjects=subjects, basepath=options.htmloutput, ) except: if options.pdb: import pdb pdb.post_mortem(sys.exc_info()[2]) raise return exitcode pydoctor-21.12.1/pydoctor/epydoc/000077500000000000000000000000001416703725300166575ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/epydoc/__init__.py000066400000000000000000000040451416703725300207730ustar00rootroot00000000000000# epydoc # # Copyright (C) 2005 Edward Loper # Author: Edward Loper # URL: # """ epydoc is an automatic Python reference documentation generator. pydoctor uses parts of the epydoc source as a library. Package Organization ==================== Docstring markup parsing is handled by the `markup` package. See the submodule list for more information about the submodules and subpackages. :author: `Edward Loper `__ :see: `The epydoc webpage `__ :see: `The epytext markup language manual `__ :: :license: IBM Open Source License :copyright: |copy| 2006 Edward Loper :newfield contributor: Contributor, Contributors (Alphabetical Order) :contributor: `Glyph Lefkowitz `__ :contributor: `Edward Loper `__ :contributor: `Bruce Mitchener `__ :contributor: `Jeff O'Halloran `__ :contributor: `Simon Pamies `__ :contributor: `Christian Reis `__ :contributor: `Daniele Varrazzo `__ :contributor: `Jonathan Guyer `__ .. |copy| unicode:: 0xA9 .. copyright sign """ __docformat__ = 'restructuredtext en' __version__ = '3.0.1' """The version of epydoc""" __author__ = 'Edward Loper ' """The primary author of eypdoc""" __url__ = 'http://epydoc.sourceforge.net' """The URL for epydoc's homepage""" __license__ = 'IBM Open Source License' """The license governing the use and distribution of epydoc""" # Changes needed for docs: # - document the method for deciding what's public/private # - epytext: fields are defined slightly differently (@group) # - new fields # - document __extra_epydoc_fields__ and @newfield # - Add a faq? # - @type a,b,c: ... # - new command line option: --command-line-order pydoctor-21.12.1/pydoctor/epydoc/doctest.py000066400000000000000000000163371416703725300207100ustar00rootroot00000000000000# # doctest.py: Syntax Highlighting for doctest blocks # Edward Loper # # Created [06/28/03 02:52 AM] # """ Syntax highlighting for blocks of Python code. """ __docformat__ = 'epytext en' from typing import Iterator, Match, Union import builtins import re from twisted.web.template import Tag, tags __all__ = ['colorize_codeblock', 'colorize_doctest'] #: A list of the names of all Python keywords. _KEYWORDS = [ 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield' ] # The following are technically keywords since Python 3, # but we don't want to colorize them as such: 'None', 'True', 'False'. #: A list of all Python builtins. _BUILTINS = [_BI for _BI in dir(builtins) if not _BI.startswith('__')] #: A regexp group that matches keywords. _KEYWORD_GRP = '|'.join(rf'\b{_KW}\b' for _KW in _KEYWORDS) #: A regexp group that matches Python builtins. _BUILTIN_GRP = r'(?>>" prompts. _PROMPT1_GRP = r'^[ \t]*>>>(?:[ \t]|$)' #: A regexp group that matches Python "..." prompts. _PROMPT2_GRP = r'^[ \t]*\.\.\.(?:[ \t]|$)' #: A regexp group that matches function and class definitions. _DEFINE_GRP = r'\b(?:def|class)[ \t]+\w+' #: A regexp that decomposes function definitions. DEFINE_FUNC_RE = re.compile(r'(?P\w+)(?P\s+)(?P\w+)') #: A regexp that matches Python prompts PROMPT_RE = re.compile(f'({_PROMPT1_GRP}|{_PROMPT2_GRP})', re.MULTILINE | re.DOTALL) #: A regexp that matches Python "..." prompts. PROMPT2_RE = re.compile(f'({_PROMPT2_GRP})', re.MULTILINE | re.DOTALL) #: A regexp that matches doctest exception blocks. EXCEPT_RE = re.compile(r'^[ \t]*Traceback \(most recent call last\):.*', re.DOTALL | re.MULTILINE) #: A regexp that matches doctest directives. DOCTEST_DIRECTIVE_RE = re.compile(r'#[ \t]*doctest:.*') #: A regexp that matches all of the regions of a doctest block #: that should be colored. DOCTEST_RE = re.compile( '(' rf'(?P{_STRING_GRP})|(?P{_COMMENT_GRP})|' rf'(?P{_DEFINE_GRP})|' rf'(?P{_KEYWORD_GRP})|(?P{_BUILTIN_GRP})|' rf'(?P{_PROMPT1_GRP})|(?P{_PROMPT2_GRP})|(?P\Z)' ')', re.MULTILINE | re.DOTALL) #: This regular expression is used to find doctest examples in a #: string. This is copied from the standard Python doctest.py #: module (after the refactoring in Python 2.4+). DOCTEST_EXAMPLE_RE = re.compile(r''' # Source consists of a PS1 line followed by zero or more PS2 lines. (?P (?:^(?P [ ]*) >>> .*) # PS1 line (?:\n [ ]* \.\.\. .*)* # PS2 lines \n?) # Want consists of any non-blank lines that do not start with PS1. (?P (?:(?![ ]*$) # Not a blank line (?![ ]*>>>) # Not a line starting with PS1 .*$\n? # But any other line )*) ''', re.MULTILINE | re.VERBOSE) def colorize_codeblock(s: str) -> Tag: """ Colorize a string containing only Python code. This method differs from L{colorize_doctest} in that it will not search for doctest prompts when deciding how to colorize the string. This code consists of a C{
} block with class=py-doctest.
    Syntax highlighting is performed using the following CSS classes:

      - C{py-keyword} -- a Python keyword (for, if, etc.)
      - C{py-builtin} -- a Python builtin name (abs, dir, etc.)
      - C{py-string} -- a string literal
      - C{py-comment} -- a comment
      - C{py-except} -- an exception traceback (up to the next >>>)
      - C{py-output} -- the output from a doctest block.
      - C{py-defname} -- the name of a function or class defined by
        a C{def} or C{class} statement.
    """

    return tags.pre('\n', *colorize_codeblock_body(s), class_='py-doctest')

def colorize_doctest(s: str) -> Tag:
    """
    Perform syntax highlighting on the given doctest string, and
    return the resulting HTML code.

    This code consists of a C{
} block with class=py-doctest.
    Syntax highlighting is performed using the following CSS classes:

      - C{py-prompt} -- the Python PS1 prompt (>>>)
      - C{py-more} -- the Python PS2 prompt (...)
      - the CSS classes output by L{colorize_codeblock}
    """

    return tags.pre('\n', *colorize_doctest_body(s), class_='py-doctest')

def colorize_doctest_body(s: str) -> Iterator[Union[str, Tag]]:
    idx = 0
    for match in DOCTEST_EXAMPLE_RE.finditer(s):
        # Parse the doctest example:
        pysrc, want = match.group('source', 'want')
        # Pre-example text:
        yield s[idx:match.start()]
        # Example source code:
        yield from colorize_codeblock_body(pysrc)
        # Example output:
        if want:
            style = 'py-except' if EXCEPT_RE.match(want) else 'py-output'
            for line in want.rstrip().split('\n'):
                yield tags.span(line, class_=style)
                yield '\n'
        idx = match.end()
    # Add any remaining post-example text.
    yield s[idx:]

def colorize_codeblock_body(s: str) -> Iterator[Union[Tag, str]]:
    idx = 0
    for match in DOCTEST_RE.finditer(s):
        start = match.start()
        if idx < start:
            yield s[idx:start]
        yield from subfunc(match)
        idx = match.end()
    # DOCTEST_RE matches end-of-string.
    assert idx == len(s)

def subfunc(match: Match[str]) -> Iterator[Union[Tag, str]]:
    text = match.group(1)
    if match.group('PROMPT1'):
        yield tags.span(text, class_='py-prompt')
    elif match.group('PROMPT2'):
        yield tags.span(text, class_='py-more')
    elif match.group('KEYWORD'):
        yield tags.span(text, class_='py-keyword')
    elif match.group('BUILTIN'):
        yield tags.span(text, class_='py-builtin')
    elif match.group('COMMENT'):
        yield tags.span(text, class_='py-comment')
    elif match.group('STRING'):
        idx = 0
        while True:
            nxt = text.find('\n', idx)
            line = text[idx:] if nxt == -1 else text[idx:nxt]
            m = PROMPT2_RE.match(line)
            if m:
                prompt_end = m.end()
                yield tags.span(line[:prompt_end], class_='py-more')
                line = line[prompt_end:]
            if line:
                yield tags.span(line, class_='py-string')
            if nxt == -1:
                break
            yield '\n'
            idx = nxt + 1
    elif match.group('DEFINE'):
        m = DEFINE_FUNC_RE.match(text)
        assert m is not None
        yield tags.span(m.group('def'), class_='py-keyword')
        yield m.group('space')
        yield tags.span(m.group('name'), class_='py-defname')
    elif match.group('EOS') is None:
        raise AssertionError('Unexpected match')
pydoctor-21.12.1/pydoctor/epydoc/docutils.py000066400000000000000000000035411416703725300210620ustar00rootroot00000000000000"""
Collection of helper functions and classes related to the creation L{docutils} nodes.
"""
from typing import Iterable, Iterator, Optional

from docutils import nodes

__docformat__ = 'epytext en'

def _set_nodes_parent(nodes: Iterable[nodes.Node], parent: nodes.Element) -> Iterator[nodes.Node]:
    """
    Set the L{nodes.Node.parent} attribute of the C{nodes} to the defined C{parent}. 
    
    @returns: An iterator containing the modified nodes.
    """
    for node in nodes:
        node.parent = parent
        yield node

def set_node_attributes(node: nodes.Node, 
                        document: Optional[nodes.document] = None, 
                        lineno: Optional[int] = None, 
                        children: Optional[Iterable[nodes.Node]] = None) -> nodes.Node:
    """
    Set the attributes of a Node and return the modified node.
    This is required to manually construct a docutils document that is consistent.

    @param node: A node to edit.
    @param document: The L{nodes.Node.document} attribute.
    @param lineno: The L{nodes.Node.line} attribute.
    @param children: The L{nodes.Element.children} attribute. Special care is taken 
        to appropriately set the L{nodes.Node.parent} attribute on the child nodes. 
    """
    if lineno is not None:
        node.line = lineno
    
    if document:
        node.document = document

    if children:
        assert isinstance(node, nodes.Element), (f'Cannot set the children on Text node: "{node.astext()}". '
                                                 f'Children: {children}')
        node.extend(_set_nodes_parent(children, node))

    return node

class wbr(nodes.inline):
    """
    Word break opportunity.
    """
    def __init__(self) -> None:
        super().__init__('', '')

class obj_reference(nodes.title_reference):
    """
    A reference to a documentable object.
    """
pydoctor-21.12.1/pydoctor/epydoc/markup/000077500000000000000000000000001416703725300201565ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/epydoc/markup/__init__.py000066400000000000000000000256041416703725300222760ustar00rootroot00000000000000#
# epydoc package file
#
# A python documentation Module
# Edward Loper
#

"""
Markup language support for docstrings.  Each submodule defines a
parser for a single markup language.  These parsers convert an
object's docstring to a L{ParsedDocstring}, a standard intermediate
representation that can be used to generate output.

A C{ParsedDocstring} is used for output generation
(L{to_stan()}).
It also stores the fields that were extracted from the docstring
during parsing (L{fields}).

The C{parse_docstring()} functions in the format modules take a docstring,
parse it and return a format-specific subclass of C{ParsedDocstring}.
A docstring's fields are separated from the body during parsing.

The C{ParsedDocstring} output generation method
(L{to_stan()}) uses a
L{DocstringLinker} to link the docstring output with the rest of
the documentation that epydoc generates.  C{DocstringLinker}s are
responsible for formatting cross-references
(L{link_xref() }).

Markup errors are represented using L{ParseError}s.  These exception
classes record information about the cause, location, and severity of
each error.
"""
__docformat__ = 'epytext en'

from importlib import import_module
from typing import Callable, List, Optional, Sequence, Iterator, TYPE_CHECKING
import abc
import sys
from inspect import getmodulename

# In newer Python versions, use importlib.resources from the standard library.
# On older versions, a compatibility package must be installed from PyPI.
if sys.version_info < (3, 9):
    import importlib_resources
else:
    import importlib.resources as importlib_resources

from docutils import nodes
from twisted.web.template import Tag

if TYPE_CHECKING:
    from twisted.web.template import Flattenable
    from pydoctor.model import Documentable

from pydoctor import node2stan

##################################################
## Contents
##################################################
#
# 1. ParsedDocstring abstract base class
# 2. Field class
# 3. Docstring Linker
# 4. ParseError exceptions
#

def get_supported_docformats() -> Iterator[str]:
    """
    Get the list of currently supported docformat.
    """
    for fileName in (path.name for path in importlib_resources.files('pydoctor.epydoc.markup').iterdir()):
        moduleName = getmodulename(fileName)
        if moduleName is None or moduleName.startswith("_"):
            continue
        else:
            yield moduleName

def get_parser_by_name(docformat: str, obj: Optional['Documentable'] = None) -> Callable[[str, List['ParseError'], bool], 'ParsedDocstring']:
    """
    Get the C{parse_docstring(str, List[ParseError], bool) -> ParsedDocstring} function based on a parser name. 

    @raises ImportError: If the parser could not be imported, probably meaning that your are missing a dependency
        or it could be that the docformat name do not match any know L{pydoctor.epydoc.markup} submodules.
    """
    mod = import_module(f'pydoctor.epydoc.markup.{docformat}')
    # We can safely ignore this mypy warning, since we can be sure the 'get_parser' function exist and is "correct".
    return mod.get_parser(obj) # type:ignore[no-any-return]

##################################################
## ParsedDocstring
##################################################
class ParsedDocstring(abc.ABC):
    """
    A standard intermediate representation for parsed docstrings that
    can be used to generate output.  Parsed docstrings are produced by
    markup parsers such as L{pydoctor.epydoc.markup.epytext.parse_docstring()}
    or L{pydoctor.epydoc.markup.restructuredtext.parse_docstring()}.

    Subclasses must implement L{has_body()} and L{to_node()}.
    """

    def __init__(self, fields: Sequence['Field']):
        self.fields = fields
        """
        A list of L{Field}s, each of which encodes a single field.
        The field's bodies are encoded as C{ParsedDocstring}s.
        """

        self._stan: Optional[Tag] = None

    @abc.abstractproperty
    def has_body(self) -> bool:
        """Does this docstring have a non-empty body?

        The body is the part of the docstring that remains after the fields
        have been split off.
        """
        raise NotImplementedError()

    def to_stan(self, docstring_linker: 'DocstringLinker') -> Tag:
        """
        Translate this docstring to a Stan tree.

        @note: The default implementation relies on functionalities 
            provided by L{node2stan.node2stan} and L{ParsedDocstring.to_node()}.

        @param docstring_linker: An HTML translator for crossreference
            links into and out of the docstring.
        @return: The docstring presented as a stan tree.
        """
        if self._stan is not None:
            return self._stan
        self._stan = Tag('', children=node2stan.node2stan(self.to_node(), docstring_linker).children)
        return self._stan
    
    @abc.abstractmethod
    def to_node(self) -> nodes.document:
        """
        Translate this docstring to a L{docutils.nodes.document}.

        @return: The docstring presented as a L{docutils.nodes.document}.
        """
        raise NotImplementedError()

##################################################
## Fields
##################################################
class Field:
    """
    The contents of a docstring's field.  Docstring fields are used
    to describe specific aspects of an object, such as a parameter of
    a function or the author of a module.  Each field consists of a
    tag, an optional argument, and a body:
      - The tag specifies the type of information that the field
        encodes.
      - The argument specifies the object that the field describes.
        The argument may be C{None} or a C{string}.
      - The body contains the field's information.

    Tags are automatically downcased and stripped; and arguments are
    automatically stripped.
    """

    def __init__(self, tag: str, arg: Optional[str], body: ParsedDocstring, lineno: int):
        self._tag = tag.lower().strip()
        self._arg = None if arg is None else arg.strip()
        self._body = body
        self.lineno = lineno

    def tag(self) -> str:
        """
        @return: This field's tag.
        """
        return self._tag

    def arg(self) -> Optional[str]:
        """
        @return: This field's argument, or C{None} if this field has no argument.
        """
        return self._arg

    def body(self) -> ParsedDocstring:
        """
        @return: This field's body.
        """
        return self._body

    def __repr__(self) -> str:
        if self._arg is None:
            return f''
        else:
            return f''

##################################################
## Docstring Linker (resolves crossreferences)
##################################################
class DocstringLinker:
    """
    A resolver for crossreference links out of a C{ParsedDocstring}.
    C{DocstringLinker} is used by C{ParsedDocstring} to look up the
    target URL for crossreference links.
    """

    def link_to(self, target: str, label: "Flattenable") -> Tag:
        """
        Format a link to a Python identifier.
        This will resolve the identifier like Python itself would.

        @param target: The name of the Python identifier that
            should be linked to.
        @param label: The label to show for the link.
        @return: The link, or just the label if the target was not found.
        """
        raise NotImplementedError()

    def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag:
        """
        Format a cross-reference link to a Python identifier.
        This will resolve the identifier to any reasonable target,
        even if it has to look in places where Python itself would not.

        @param target: The name of the Python identifier that
            should be linked to.
        @param label: The label to show for the link.
        @param lineno: The line number within the docstring at which the
            crossreference is located.
        @return: The link, or just the label if the target was not found.
            In either case, the returned top-level tag will be C{}.
        """
        raise NotImplementedError()

    def resolve_identifier(self, identifier: str) -> Optional[str]:
        """
        Resolve a Python identifier.
        This will resolve the identifier like Python itself would.

        @param identifier: The name of the Python identifier that
            should be linked to.
        @return: The URL of the target, or L{None} if not found.
        """
        raise NotImplementedError()


##################################################
## ParseError exceptions
##################################################

class ParseError(Exception):
    """
    The base class for errors generated while parsing docstrings.
    """

    def __init__(self,
            descr: str,
            linenum: Optional[int] = None,
            is_fatal: bool = True
            ):
        """
        @param descr: A description of the error.
        @param linenum: The line on which the error occured within
            the docstring.  The linenum of the first line is 0.
        @param is_fatal: True if this is a fatal error.
        """
        self._descr = descr
        self._linenum = linenum
        self._fatal = is_fatal

    def is_fatal(self) -> bool:
        """
        @return: true if this is a fatal error.  If an error is fatal,
            then epydoc should ignore the output of the parser, and
            parse the docstring as plaintext.
        """
        return self._fatal

    def linenum(self) -> Optional[int]:
        """
        @return: The line number on which the error occured (including
        any offset).  If the line number is unknown, then return
        C{None}.
        """
        if self._linenum is None: return None
        else: return self._linenum + 1

    def descr(self) -> str:
        """
        @return: A description of the error.
        """
        return self._descr

    def __str__(self) -> str:
        """
        Return a string representation of this C{ParseError}.  This
        multi-line string contains a description of the error, and
        specifies where it occured.

        @return: the informal representation of this C{ParseError}.
        """
        if self._linenum is not None:
            return f'Line {self._linenum + 1:d}: {self.descr()}'
        else:
            return self.descr()

    def __repr__(self) -> str:
        """
        Return the formal representation of this C{ParseError}.
        C{ParseError}s have formal representations of the form::
           

        @return: the formal representation of this C{ParseError}.
        """
        if self._linenum is None:
            return ''
        else:
            return f''
pydoctor-21.12.1/pydoctor/epydoc/markup/_napoleon.py000066400000000000000000000062021416703725300225020ustar00rootroot00000000000000"""
This module contains a class to wrap shared behaviour between 
L{pydoctor.epydoc.markup.numpy} and L{pydoctor.epydoc.markup.google}. 
"""
from typing import List, Optional, Type

from pydoctor.epydoc.markup import ParsedDocstring, ParseError
from pydoctor.epydoc.markup import restructuredtext
from pydoctor.napoleon.docstring import GoogleDocstring, NumpyDocstring
from pydoctor.model import Attribute, Documentable


class NapoelonDocstringParser:
    """
    Parse google-style or numpy-style docstrings.

    First wrap the L{pydoctor.napoleon} converter classes, then call
    L{pydoctor.epydoc.markup.restructuredtext.parse_docstring} with the
    converted reStructuredText docstring.

    If the L{Documentable} instance is an L{Attribute}, the docstring
    will be parsed differently.
    """

    def __init__(self, obj: Optional[Documentable] = None):
        """
        @param obj: Documentable object we're parsing the docstring for.
        """
        self.obj = obj

    def parse_google_docstring(
        self, docstring: str, errors: List[ParseError], processtypes: bool = True
    ) -> ParsedDocstring:
        """
        Parse the given docstring, which is formatted as Google style docstring.
        Return a L{ParsedDocstring} representation of its contents.

        @param docstring: The docstring to parse
        @param errors: A list where any errors generated during parsing
            will be stored.
        """
        return self._parse_docstring(
            docstring, errors, GoogleDocstring, )

    def parse_numpy_docstring(
        self, docstring: str, errors: List[ParseError], processtypes: bool = True
    ) -> ParsedDocstring:
        """
        Parse the given docstring, which is formatted as NumPy style docstring.
        Return a L{ParsedDocstring} representation of its contents.

        @param docstring: The docstring to parse
        @param errors: A list where any errors generated during parsing
            will be stored.
        @param processtypes: processtypes is always ``True`` for google and numpy docstrings.
        """
        return self._parse_docstring(
            docstring, errors, NumpyDocstring, )

    def _parse_docstring(
        self,
        docstring: str,
        errors: List[ParseError],
        docstring_cls: Type[GoogleDocstring],
    ) -> ParsedDocstring:

        docstring_obj = docstring_cls(
            docstring, is_attribute=isinstance(self.obj, Attribute)
        )

        parsed_doc = self._parse_docstring_obj(docstring_obj, errors)

        return parsed_doc

    @staticmethod
    def _parse_docstring_obj(
        docstring_obj: GoogleDocstring, errors: List[ParseError]
    ) -> ParsedDocstring:
        """
        Helper method to parse L{GoogleDocstring} or L{NumpyDocstring} objects.
        """
        # log any warnings
        for warn, lineno in docstring_obj.warnings:
            # TODO: double check the line number and add tests that goes here!
            errors.append(ParseError(warn, lineno, is_fatal=False))
        # Get the converted reST string and parse it with docutils
        return restructuredtext.parse_docstring(str(docstring_obj), errors, processtypes=True)
pydoctor-21.12.1/pydoctor/epydoc/markup/_pyval_repr.py000066400000000000000000001235401416703725300230570ustar00rootroot00000000000000# epydoc -- Marked-up Representations for Python Values
#
# Copyright (C) 2005 Edward Loper
# Author: Edward Loper 
# URL: 
#

"""
Syntax highlighter for Python values.  Currently provides special
colorization support for:

  - lists, tuples, sets, frozensets, dicts
  - numbers
  - strings
  - compiled regexps
  - a variety of AST expressions

The highlighter also takes care of line-wrapping, and automatically
stops generating repr output as soon as it has exceeded the specified
number of lines (which should make it faster than pprint for large
values).  It does I{not} bother to do automatic cycle detection,
because maxlines is typically around 5, so it's really not worth it.

The syntax-highlighted output is encoded using a
L{ParsedDocstring}, which can then be used to generate output in
a variety of formats.

B{Implementation note}: we use exact tests for builtin classes (list, etc)
rather than using isinstance, because subclasses might override
C{__repr__}.

B{Usage}: 
>>> 
"""

__docformat__ = 'epytext en'

import re
import ast
import functools
import sys
import sre_constants
from inspect import signature
from typing import Any, AnyStr, Union, Callable, Dict, Iterable, Sequence, Optional, List, Tuple, cast

import attr
import astor.op_util
from docutils import nodes, utils
from twisted.web.template import Tag

from pydoctor.epydoc import sre_parse36
from pydoctor.epydoc.markup import DocstringLinker
from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring
from pydoctor.epydoc.docutils import set_node_attributes, wbr, obj_reference
from pydoctor.astutils import node2dottedname, bind_args
from pydoctor.node2stan import gettext

def decode_with_backslashreplace(s: bytes) -> str:
    r"""
    Convert the given 8-bit string into unicode, treating any
    character c such that ord(c)<128 as an ascii character, and
    converting any c such that ord(c)>128 into a backslashed escape
    sequence.
        >>> decode_with_backslashreplace(b'abc\xff\xe8')
        'abc\\xff\\xe8'
    """
    # s.encode('string-escape') is not appropriate here, since it
    # also adds backslashes to some ascii chars (eg \ and ').

    return (s
            .decode('latin1')
            .encode('ascii', 'backslashreplace')
            .decode('ascii'))

@attr.s(auto_attribs=True)
class _MarkedColorizerState:
    length: int
    charpos: int
    lineno: int
    linebreakok: bool

class _ColorizerState:
    """
    An object uesd to keep track of the current state of the pyval
    colorizer.  The L{mark()}/L{restore()} methods can be used to set
    a backup point, and restore back to that backup point.  This is
    used by several colorization methods that first try colorizing
    their object on a single line (setting linebreakok=False); and
    then fall back on a multi-line output if that fails.  
    """
    def __init__(self) -> None:
        self.result: List[nodes.Node] = []
        self.charpos = 0
        self.lineno = 1
        self.linebreakok = True
        self.warnings: List[str] = []

    def mark(self) -> _MarkedColorizerState:
        return _MarkedColorizerState(
                    length=len(self.result), 
                    charpos=self.charpos,
                    lineno=self.lineno, 
                    linebreakok=self.linebreakok)

    def restore(self, mark: _MarkedColorizerState) -> List[nodes.Node]:
        """
        Return what's been trimmed from the result.
        """
        (self.charpos, self.lineno, 
        self.linebreakok) = (mark.charpos, mark.lineno, 
                                        mark.linebreakok)
        trimmed = self.result[mark.length:]
        del self.result[mark.length:]
        return trimmed

class _Parentage(ast.NodeTransformer):
    """
    Add C{parent} attribute to ast nodes instances.
    """
    # stolen from https://stackoverflow.com/a/68845448
    parent: Optional[ast.AST] = None

    def visit(self, node: ast.AST) -> ast.AST:
        setattr(node, 'parent', self.parent)
        self.parent = node
        node = super().visit(node)
        if isinstance(node, ast.AST):
            self.parent = getattr(node, 'parent')
        return node

# TODO: add support for comparators when needed. 
class _OperatorDelimiter:
    """
    A context manager that can add enclosing delimiters to nested operators when needed. 
    
    Adapted from C{astor} library, thanks.
    """

    def __init__(self, colorizer: 'PyvalColorizer', state: _ColorizerState, 
                 node: Union[ast.UnaryOp, ast.BinOp, ast.BoolOp]) -> None:

        self.discard = True
        """No parenthesis by default."""

        self.colorizer = colorizer
        self.state = state
        self.marked = state.mark()

        # We use a hack to populate a "parent" attribute on AST nodes.
        # See _Parentage class, applied in PyvalColorizer._colorize_ast()
        parent_node: Optional[ast.AST] = getattr(node, 'parent', None)

        if isinstance(parent_node, (ast.UnaryOp, ast.BinOp, ast.BoolOp)):
            precedence = astor.op_util.get_op_precedence(node.op)
            parent_precedence = astor.op_util.get_op_precedence(parent_node.op)
            # Add parenthesis when precedences are equal to avoid confusions 
            # and correctly handle the Pow special case without too much annoyance.
            if precedence <= parent_precedence:
                self.discard = False

    def __enter__(self) -> '_OperatorDelimiter':
        return self

    def __exit__(self, *exc_info: Any) -> None:
        if not self.discard:
            trimmed = self.state.restore(self.marked)
            self.colorizer._output('(', self.colorizer.GROUP_TAG, self.state)
            self.state.result.extend(trimmed)
            self.colorizer._output(')', self.colorizer.GROUP_TAG, self.state)

class _Maxlines(Exception):
    """A control-flow exception that is raised when PyvalColorizer
    exeeds the maximum number of allowed lines."""

class _Linebreak(Exception):
    """A control-flow exception that is raised when PyvalColorizer
    generates a string containing a newline, but the state object's
    linebreakok variable is False."""

class ColorizedPyvalRepr(ParsedRstDocstring):
    """
    @ivar is_complete: True if this colorized repr completely describes
       the object.
    """
    def __init__(self, document: nodes.document, is_complete: bool, warnings: List[str]) -> None:
        super().__init__(document, ())
        self.is_complete = is_complete
        self.warnings = warnings
        """
        List of warnings
        """
    
    def to_stan(self, docstring_linker: DocstringLinker) -> Tag:
        try:
            return Tag('code')(super().to_stan(docstring_linker))
        except Exception as e:
            # Just in case...
            self.warnings.append(f"Cannot convert repr to renderable object, error: {str(e)}. Using plaintext.")
            return Tag('code')(gettext(self.to_node()))

def colorize_pyval(pyval: Any, linelen:Optional[int], maxlines:int, linebreakok:bool=True) -> ColorizedPyvalRepr:
    """
    @return: A L{ColorizedPyvalRepr} describing the given pyval.
    """
    return PyvalColorizer(linelen=linelen, maxlines=maxlines, linebreakok=linebreakok).colorize(pyval)

def colorize_inline_pyval(pyval: Any) -> ColorizedPyvalRepr:
    """
    Used to colorize type annotations and parameters default values.
    @returns: C{L{colorize_pyval}(pyval, linelen=None, linebreakok=False)}
    """
    return colorize_pyval(pyval, linelen=None, maxlines=1, linebreakok=False)

def _get_str_func(pyval:  AnyStr) -> Callable[[str], AnyStr]:
    func = cast(Callable[[str], AnyStr], str if isinstance(pyval, str) else \
        functools.partial(bytes, encoding='utf-8', errors='replace'))
    return func
def _str_escape(s: str) -> str:
    """
    Encode a string such that it's correctly represented inside simple quotes.
    """
    # displays unicode caracters as is.
    def enc(c: str) -> str:
        if c == "'":
            c = r"\'"
        elif c == '\t': 
            c = r'\t'
        elif c == '\r': 
            c = r'\r'
        elif c == '\n': 
            c = r'\n'
        elif c == '\f': 
            c = r'\f'
        elif c == '\v': 
            c = r'\v'
        elif c == "\\": 
            c = r'\\'
        return c
    return ''.join(map(enc, s))
def _bytes_escape(b: bytes) -> str:
    return repr(b)[2:-1]

class PyvalColorizer:
    """
    Syntax highlighter for Python values.
    """

    def __init__(self, linelen:Optional[int], maxlines:int, linebreakok:bool=True):
        self.linelen: Optional[int] = linelen if linelen!=0 else None
        self.maxlines: Union[int, float] = maxlines if maxlines!=0 else float('inf')
        self.linebreakok = linebreakok

    #////////////////////////////////////////////////////////////
    # Colorization Tags & other constants
    #////////////////////////////////////////////////////////////

    GROUP_TAG = None # was 'variable-group'     # e.g., "[" and "]"
    COMMA_TAG = None # was 'variable-op'        # The "," that separates elements
    COLON_TAG = None # was 'variable-op'        # The ":" in dictionaries
    CONST_TAG = None                 # None, True, False
    NUMBER_TAG = None                # ints, floats, etc
    QUOTE_TAG = 'variable-quote'     # Quotes around strings.
    STRING_TAG = 'variable-string'   # Body of string literals
    LINK_TAG = 'variable-link'       # Links to other documentables, extracted from AST names and attributes.
    ELLIPSIS_TAG = 'variable-ellipsis'
    LINEWRAP_TAG = 'variable-linewrap'
    UNKNOWN_TAG = 'variable-unknown'

    RE_CHAR_TAG = None
    RE_GROUP_TAG = 're-group'
    RE_REF_TAG = 're-ref'
    RE_OP_TAG = 're-op'
    RE_FLAGS_TAG = 're-flags'

    ELLIPSIS = nodes.inline('...', '...', classes=[ELLIPSIS_TAG])
    LINEWRAP = nodes.inline('', chr(8629), classes=[LINEWRAP_TAG])
    UNKNOWN_REPR = nodes.inline('??', '??', classes=[UNKNOWN_TAG])
    WORD_BREAK_OPPORTUNITY = wbr()
    NEWLINE = nodes.Text('\n', '\n')

    GENERIC_OBJECT_RE = re.compile(r'^<(?P.*) at (?P0x[0-9a-f]+)>$', re.IGNORECASE)

    RE_COMPILE_SIGNATURE = signature(re.compile)

    def colorize(self, pyval: Any) -> ColorizedPyvalRepr:
        """
        Entry Point.
        """
        # Create an object to keep track of the colorization.
        state = _ColorizerState()
        state.linebreakok = self.linebreakok
        # Colorize the value.  If we reach maxlines, then add on an
        # ellipsis marker and call it a day.
        try:
            self._colorize(pyval, state)
        except (_Maxlines, _Linebreak):
            if self.linebreakok:
                state.result.append(self.NEWLINE)
                state.result.append(self.ELLIPSIS)
            else:
                if state.result[-1] is self.LINEWRAP:
                    state.result.pop()
                self._trim_result(state.result, 3)
                state.result.append(self.ELLIPSIS)
            is_complete = False
        else:
            is_complete = True
        
        # Put it all together.
        document = utils.new_document('pyval_repr')
        # This ensure the .parent and .document attributes of the child nodes are set correcly.
        set_node_attributes(document, children=[set_node_attributes(node, document=document) for node in state.result])
        return ColorizedPyvalRepr(document, is_complete, state.warnings)
    
    def _colorize(self, pyval: Any, state: _ColorizerState) -> None:

        pyvaltype = type(pyval)
        
        # Individual "is" checks are required here to be sure we don't consider 0 as True and 1 as False!
        if pyval is False or pyval is True or pyval is None or pyval is NotImplemented:
            # Link built-in constants to the standard library.
            # Ellipsis is not included here, both because its code syntax is
            # different from its constant's name and because its documentation
            # is not relevant to annotations.
            self._output(str(pyval), self.CONST_TAG, state, link=True)
        elif pyvaltype is int or pyvaltype is float or pyvaltype is complex:
            self._output(str(pyval), self.NUMBER_TAG, state)
        elif pyvaltype is str:
            self._colorize_str(pyval, state, '', escape_fcn=_str_escape)
        elif pyvaltype is bytes:
            self._colorize_str(pyval, state, b'b', escape_fcn=_bytes_escape)
        elif pyvaltype is tuple:
            # tuples need an ending comma when they contains only one value.
            self._multiline(self._colorize_iter, pyval, state, prefix='(', 
                            suffix=(',' if len(pyval) <= 1 else '')+')')
        elif pyvaltype is set:
            self._multiline(self._colorize_iter, pyval,
                            state, prefix='set([', suffix='])')
        elif pyvaltype is frozenset:
            self._multiline(self._colorize_iter, pyval,
                            state, prefix='frozenset([', suffix='])')
        elif pyvaltype is dict:
            self._multiline(self._colorize_dict,
                            list(pyval.items()),
                            state, prefix='{', suffix='}')
        elif pyvaltype is list:
            self._multiline(self._colorize_iter, pyval, state, prefix='[', suffix=']')
        elif issubclass(pyvaltype, ast.AST):
            self._colorize_ast(pyval, state)
        else:
            # Unknow live object
            try:
                pyval_repr = repr(pyval)
                if not isinstance(pyval_repr, str):
                    pyval_repr = str(pyval_repr) #type: ignore[unreachable]
            except Exception:
                state.warnings.append(f"Cannot colorize object of type '{pyval.__class__.__name__}', repr() raised an exception.")
                state.result.append(self.UNKNOWN_REPR)
            else:
                match = self.GENERIC_OBJECT_RE.search(pyval_repr)
                if match:
                    self._output(f"<{match.groupdict().get('descr')}>", None, state)
                else:
                    self._output(pyval_repr, None, state)

    def _trim_result(self, result: List[nodes.Node], num_chars: int) -> None:
        while num_chars > 0:
            if not result: 
                return
            if isinstance(result[-1], nodes.Element):
                if len(result[-1].children) >= 1:
                    data = result[-1][-1].astext()
                    trim = min(num_chars, len(data))
                    result[-1][-1] = nodes.Text(data[:-trim])
                    if not result[-1][-1].astext(): 
                        if len(result[-1].children) == 1:
                            result.pop()
                        else:
                            result[-1].pop()
                else:
                    trim = 0
                    result.pop()
                num_chars -= trim
            else:
                # Must be Text if it's not an Element
                trim = min(num_chars, len(result[-1]))
                result[-1] = nodes.Text(result[-1].astext()[:-trim])
                if not result[-1].astext(): 
                    result.pop()
                num_chars -= trim

    #////////////////////////////////////////////////////////////
    # Object Colorization Functions
    #////////////////////////////////////////////////////////////

    def _insert_comma(self, indent: int, state: _ColorizerState) -> None:
        if state.linebreakok:
            self._output(',', self.COMMA_TAG, state)
            self._output('\n'+' '*indent, None, state)
        else:
            self._output(', ', self.COMMA_TAG, state)

    def _multiline(self, func: Callable[..., None], pyval: Iterable[Any], state: _ColorizerState, **kwargs: Any) -> None:
        """
        Helper for container-type colorizers.  First, try calling
        C{func(pyval, state, **kwargs)} with linebreakok set to false;
        and if that fails, then try again with it set to true.
        """
        linebreakok = state.linebreakok
        mark = state.mark()

        try:
            state.linebreakok = False
            func(pyval, state, **kwargs)
            state.linebreakok = linebreakok

        except _Linebreak:
            if not linebreakok:
                raise
            state.restore(mark)
            func(pyval, state, **kwargs)

    def _colorize_iter(self, pyval: Iterable[Any], state: _ColorizerState, 
                       prefix: Optional[AnyStr] = None, 
                       suffix: Optional[AnyStr] = None) -> None:
        if prefix is not None:
            self._output(prefix, self.GROUP_TAG, state)
        indent = state.charpos
        for i, elt in enumerate(pyval):
            if i>=1:
                self._insert_comma(indent, state)
            # word break opportunity for inline values
            state.result.append(self.WORD_BREAK_OPPORTUNITY)
            self._colorize(elt, state)
        if suffix is not None:
            self._output(suffix, self.GROUP_TAG, state)

    def _colorize_dict(self, items: Iterable[Tuple[Any, Any]], state: _ColorizerState, prefix: str, suffix: str) -> None:
        self._output(prefix, self.GROUP_TAG, state)
        indent = state.charpos
        for i, (key, val) in enumerate(items):
            if i>=1:
                self._insert_comma(indent, state)
            state.result.append(self.WORD_BREAK_OPPORTUNITY)
            self._colorize(key, state)
            self._output(': ', self.COLON_TAG, state)
            self._colorize(val, state)
        self._output(suffix, self.GROUP_TAG, state)
    
    def _colorize_str(self, pyval: AnyStr, state: _ColorizerState, prefix: AnyStr, 
                      escape_fcn: Callable[[AnyStr], str]) -> None:
        
        str_func = _get_str_func(pyval)

        #  Decide which quote to use.
        if str_func('\n') in pyval and state.linebreakok:
            quote = str_func("'''")
        else: 
            quote = str_func("'")
        
        # Open quote.
        self._output(prefix, None, state)
        self._output(quote, self.QUOTE_TAG, state)

        # Divide the string into lines.
        if state.linebreakok:
            lines = pyval.split(str_func('\n'))
        else:
            lines = [pyval]
        # Body
        for i, line in enumerate(lines):
            if i>0:
                self._output(str_func('\n'), None, state)

            # It's not redundant when line is bytes
            line = cast(AnyStr, escape_fcn(line)) # type:ignore[redundant-cast]
            
            self._output(line, self.STRING_TAG, state)
        # Close quote.
        self._output(quote, self.QUOTE_TAG, state)

    #////////////////////////////////////////////////////////////
    # Support for AST
    #////////////////////////////////////////////////////////////

    # Nodes not explicitely handled that would be nice to handle.
    #   f-strings, 
    #   comparators, 
    #   generator expressions, 
    #   Slice and ExtSlice

    @staticmethod
    def _is_ast_constant(node: ast.AST) -> bool:
        return isinstance(node, (ast.Num, ast.Str, ast.Bytes, 
                                 ast.Constant, ast.NameConstant, ast.Ellipsis))
    @staticmethod
    def _get_ast_constant_val(node: ast.AST) -> Any:
        # Deprecated since version 3.8: Replaced by Constant
        if isinstance(node, ast.Num): 
            return(node.n)
        if isinstance(node, (ast.Str, ast.Bytes)):
           return(node.s)
        if isinstance(node, (ast.Constant, ast.NameConstant)):
            return(node.value)
        if isinstance(node, ast.Ellipsis):
            return(...)
        
    def _colorize_ast_constant(self, pyval: ast.AST, state: _ColorizerState) -> None:
        val = self._get_ast_constant_val(pyval)
        # Handle elipsis
        if val != ...:
            self._colorize(val, state)
        else:
            self._output('...', self.ELLIPSIS_TAG, state)

    def _colorize_ast(self, pyval: ast.AST, state: _ColorizerState) -> None:
        # Set nodes parent in order to check theirs precedences and add delimiters when needed.
        if not getattr(pyval, 'parent', None):
            _Parentage().visit(pyval)

        if self._is_ast_constant(pyval): 
            self._colorize_ast_constant(pyval, state)
        elif isinstance(pyval, ast.UnaryOp):
            self._colorize_ast_unary_op(pyval, state)
        elif isinstance(pyval, ast.BinOp):
            self._colorize_ast_binary_op(pyval, state)
        elif isinstance(pyval, ast.BoolOp):
            self._colorize_ast_bool_op(pyval, state)
        elif isinstance(pyval, ast.List):
            self._multiline(self._colorize_iter, pyval.elts, state, prefix='[', suffix=']')
        elif isinstance(pyval, ast.Tuple):
            self._multiline(self._colorize_iter, pyval.elts, state, prefix='(', suffix=')')
        elif isinstance(pyval, ast.Set):
            self._multiline(self._colorize_iter, pyval.elts, state, prefix='set([', suffix='])')
        elif isinstance(pyval, ast.Dict):
            items = list(zip(pyval.keys, pyval.values))
            self._multiline(self._colorize_dict, items, state, prefix='{', suffix='}')
        elif isinstance(pyval, ast.Name):
            self._colorize_ast_name(pyval, state)
        elif isinstance(pyval, ast.Attribute):
            self._colorize_ast_attribute(pyval, state)
        elif isinstance(pyval, ast.Subscript):
            self._colorize_ast_subscript(pyval, state)
        elif isinstance(pyval, ast.Call):
            self._colorize_ast_call(pyval, state)
        elif isinstance(pyval, ast.Starred):
            self._output('*', None, state)
            self._colorize_ast(pyval.value, state)
        elif isinstance(pyval, ast.keyword):
            if pyval.arg is not None:
                self._output(pyval.arg, None, state)
                self._output('=', None, state)
            else:
                self._output('**', None, state)
            self._colorize_ast(pyval.value, state)
        else:
            self._colorize_ast_generic(pyval, state)
    
    def _colorize_ast_unary_op(self, pyval: ast.UnaryOp, state: _ColorizerState) -> None:
        with _OperatorDelimiter(self, state, pyval):
            if isinstance(pyval.op, ast.USub):
                self._output('-', None, state)
            elif isinstance(pyval.op, ast.UAdd):
                self._output('+', None, state)
            elif isinstance(pyval.op, ast.Not):
                self._output('not ', None, state)
            elif isinstance(pyval.op, ast.Invert):
                self._output('~', None, state)
            else:
                state.warnings.append(f"Unknow unrary operator: {pyval}")
                self._colorize_ast_generic(pyval, state)

            self._colorize(pyval.operand, state)
    
    def _colorize_ast_binary_op(self, pyval: ast.BinOp, state: _ColorizerState) -> None:
        with _OperatorDelimiter(self, state, pyval):
            # Colorize first operand
            self._colorize(pyval.left, state)

            # Colorize operator
            if isinstance(pyval.op, ast.Sub):
                self._output('-', None, state)
            elif isinstance(pyval.op, ast.Add):
                self._output('+', None, state)
            elif isinstance(pyval.op, ast.Mult):
                self._output('*', None, state)
            elif isinstance(pyval.op, ast.Div):
                self._output('/', None, state)
            elif isinstance(pyval.op, ast.FloorDiv):
                self._output('//', None, state)
            elif isinstance(pyval.op, ast.Mod):
                self._output('%', None, state)
            elif isinstance(pyval.op, ast.Pow):
                self._output('**', None, state)
            elif isinstance(pyval.op, ast.LShift):
                self._output('<<', None, state)
            elif isinstance(pyval.op, ast.RShift):
                self._output('>>', None, state)
            elif isinstance(pyval.op, ast.BitOr):
                self._output('|', None, state)
            elif isinstance(pyval.op, ast.BitXor):
                self._output('^', None, state)
            elif isinstance(pyval.op, ast.BitAnd):
                self._output('&', None, state)
            elif isinstance(pyval.op, ast.MatMult):
                self._output('@', None, state)
            else:
                state.warnings.append(f"Unknow binary operator: {pyval}")
                self._colorize_ast_generic(pyval, state)

            # Colorize second operand
            self._colorize(pyval.right, state)
    
    def _colorize_ast_bool_op(self, pyval: ast.BoolOp, state: _ColorizerState) -> None:
        with _OperatorDelimiter(self, state, pyval):
            _maxindex = len(pyval.values)-1

            for index, value in enumerate(pyval.values):
                self._colorize(value, state)

                if index != _maxindex:
                    if isinstance(pyval.op, ast.And):
                        self._output(' and ', None, state)
                    elif isinstance(pyval.op, ast.Or):
                        self._output(' or ', None, state)

    def _colorize_ast_name(self, pyval: ast.Name, state: _ColorizerState) -> None:
        self._output(pyval.id, self.LINK_TAG, state, link=True)

    def _colorize_ast_attribute(self, pyval: ast.Attribute, state: _ColorizerState) -> None:
        parts = []
        curr: ast.expr = pyval
        while isinstance(curr, ast.Attribute):
            parts.append(curr.attr)
            curr = curr.value
        if not isinstance(curr, ast.Name):
            self._colorize_ast_generic(pyval, state)
            return
        parts.append(curr.id)
        parts.reverse()
        self._output('.'.join(parts), self.LINK_TAG, state, link=True)

    def _colorize_ast_subscript(self, node: ast.Subscript, state: _ColorizerState) -> None:

        self._colorize(node.value, state)

        sub: ast.AST = node.slice
        if sys.version_info < (3,9) and isinstance(sub, ast.Index):
            # In Python < 3.9, non-slices are always wrapped in an Index node.
            sub = sub.value
        self._output('[', self.GROUP_TAG, state)
        if isinstance(sub, ast.Tuple):
            self._multiline(self._colorize_iter, sub.elts, state)
        else:
            state.result.append(self.WORD_BREAK_OPPORTUNITY)
            self._colorize(sub, state)
       
        self._output(']', self.GROUP_TAG, state)
    
    def _colorize_ast_call(self, node: ast.Call, state: _ColorizerState) -> None:
        
        if node2dottedname(node.func) == ['re', 'compile']:
            # Colorize regexps from re.compile AST arguments.
            self._colorize_ast_re(node, state)
        else:
            # Colorize other forms of callables.
            self._colorize_ast_call_generic(node, state)

    def _colorize_ast_call_generic(self, node: ast.Call, state: _ColorizerState) -> None:
        self._colorize(node.func, state)
        self._output('(', self.GROUP_TAG, state)
        indent = state.charpos
        self._multiline(self._colorize_iter, node.args, state)
        if len(node.keywords)>0:
            if len(node.args)>0:
                self._insert_comma(indent, state)
            self._multiline(self._colorize_iter, node.keywords, state)
        self._output(')', self.GROUP_TAG, state)

    def _colorize_ast_re(self, node:ast.Call, state: _ColorizerState) -> None:
        
        try:
            # Can raise TypeError
            args = bind_args(self.RE_COMPILE_SIGNATURE, node)
        except TypeError:
            self._colorize_ast_call_generic(node, state)
            return
        
        ast_pattern = args.arguments['pattern']

        # Cannot colorize regex
        if not self._is_ast_constant(ast_pattern):
            self._colorize_ast_call_generic(node, state)
            return

        pat = self._get_ast_constant_val(ast_pattern)
        
        # Just in case regex pattern is not valid type
        if not isinstance(pat, (bytes, str)):
            state.warnings.append("Cannot colorize regular expression: pattern must be bytes or str.")
            self._colorize_ast_call_generic(node, state)
            return

        mark = state.mark()
        
        self._output("re.compile", None, state, link=True)
        self._output('(', self.GROUP_TAG, state)
        indent = state.charpos
        
        try:
            # Can raise ValueError or re.error
            # Value of type variable "AnyStr" cannot be "Union[bytes, str]": Yes it can.
            self._colorize_re_pattern_str(pat, state) #type:ignore[type-var]
        except (ValueError, re.error) as e:
            # Colorize the ast.Call as any other node if the pattern parsing fails.
            state.restore(mark)
            state.warnings.append(f"Cannot colorize regular expression, error: {str(e)}")
            self._colorize_ast_call_generic(node, state)
            return

        ast_flags = args.arguments.get('flags')
        if ast_flags is not None:
            self._insert_comma(indent, state)
            self._colorize_ast(ast_flags, state)

        self._output(')', self.GROUP_TAG, state)

    def _colorize_ast_generic(self, pyval: ast.AST, state: _ColorizerState) -> None:
        try:
            source = astor.to_source(pyval).strip()
        except Exception: #  No defined handler for node of type 
            state.result.append(self.UNKNOWN_REPR)
        else:
            # TODO: Maybe try to colorize anyway, without links, with epydoc.doctest ?
            self._output(source, None, state)
        
    #////////////////////////////////////////////////////////////
    # Support for Regexes
    #////////////////////////////////////////////////////////////

    def _colorize_re_pattern_str(self, pat: AnyStr, state: _ColorizerState) -> None:
        # Currently, the colorizer do not render multiline regex patterns correctly because we don't
        # recover the flag values from re.compile() arguments (so we don't know when re.VERBOSE is used for instance). 
        # With default flags, newlines are mixed up with literals \n and probably more fun stuff like that.
        # Turns out the sre_parse.parse() function treats caracters "\n" and "\\n" the same way.
        
        # If the pattern string is composed by mutiple lines, simply use the string colorizer instead.
        # It's more informative to have the proper newlines than the fancy regex colors. 

        # Note: Maybe this decision is driven by a misunderstanding of regular expression.

        str_func = _get_str_func(pat)
        if str_func('\n') in pat:
            if isinstance(pat, bytes):
                self._colorize_str(pat, state, b'b', escape_fcn=_bytes_escape)
            else:
                self._colorize_str(pat, state, '', escape_fcn=_str_escape)
        else:
            if isinstance(pat, bytes):
                self._colorize_re_pattern(pat, state, b'rb')
            else:
                self._colorize_re_pattern(pat, state, 'r')
    
    def _colorize_re_pattern(self, pat: AnyStr, state: _ColorizerState, prefix: AnyStr) -> None:

        # Parse the regexp pattern.
        # The regex pattern strings are always parsed with the default flags.
        # Flag values are displayed as regular ast.Call arguments. 

        tree: sre_parse36.SubPattern = sre_parse36.parse(pat, 0)
        # from python 3.8 SubPattern.pattern is named SubPattern.state, but we don't care right now because we use sre_parse36
        pattern = tree.pattern
        groups = dict([(num,name) for (name,num) in
                       pattern.groupdict.items()])
        flags: int = pattern.flags
        
        # Open quote. Never triple quote regex patterns string, anyway parterns that includes an '\n' caracter are displayed as regular strings.
        quote = "'"
        self._output(prefix, None, state)
        self._output(quote, self.QUOTE_TAG, state)
        
        if flags != sre_constants.SRE_FLAG_UNICODE:
            # If developers included flags in the regex string, display them.
            # By default, do not display the '(?u)'
            self._colorize_re_flags(flags, state)
        
        # Colorize it!
        self._colorize_re_tree(tree.data, state, True, groups)

        # Close quote.
        self._output(quote, self.QUOTE_TAG, state)

    def _colorize_re_flags(self, flags: int, state: _ColorizerState) -> None:
        if flags:
            flags_list = [c for (c,n) in sorted(sre_parse36.FLAGS.items())
                        if (n&flags)]
            flags_str = '(?%s)' % ''.join(flags_list)
            self._output(flags_str, self.RE_FLAGS_TAG, state)

    def _colorize_re_tree(self, tree: Sequence[Tuple[sre_constants._NamedIntConstant, Any]],
                          state: _ColorizerState, noparen: bool, groups: Dict[int, str]) -> None:

        if len(tree) > 1 and not noparen:
            self._output('(', self.RE_GROUP_TAG, state)

        for elt in tree:
            op = elt[0]
            args = elt[1]

            if op == sre_constants.LITERAL:
                c = chr(cast(int, args))
                # Add any appropriate escaping.
                if c in '.^$\\*+?{}[]|()\'': 
                    c = '\\' + c
                elif c == '\t': 
                    c = r'\t'
                elif c == '\r': 
                    c = r'\r'
                elif c == '\n': 
                    c = r'\n'
                elif c == '\f': 
                    c = r'\f'
                elif c == '\v': 
                    c = r'\v'
                # Keep unicode chars as is, so do nothing if ord(c) > 65535
                elif ord(c) > 255 and ord(c) <= 65535: 
                   c = rb'\u%04x' % ord(c) # type:ignore[assignment]
                elif (ord(c)<32 or ord(c)>=127) and ord(c) <= 65535: 
                    c = rb'\x%02x' % ord(c) # type:ignore[assignment]
                self._output(c, self.RE_CHAR_TAG, state)

            elif op == sre_constants.ANY:
                self._output('.', self.RE_CHAR_TAG, state)

            elif op == sre_constants.BRANCH:
                if args[0] is not None:
                    raise ValueError('Branch expected None arg but got %s'
                                     % args[0])
                for i, item in enumerate(args[1]):
                    if i > 0:
                        self._output('|', self.RE_OP_TAG, state)
                    self._colorize_re_tree(item, state, True, groups)

            elif op == sre_constants.IN:
                if (len(args) == 1 and args[0][0] == sre_constants.CATEGORY):
                    self._colorize_re_tree(args, state, False, groups)
                else:
                    self._output('[', self.RE_GROUP_TAG, state)
                    self._colorize_re_tree(args, state, True, groups)
                    self._output(']', self.RE_GROUP_TAG, state)

            elif op == sre_constants.CATEGORY:
                if args == sre_constants.CATEGORY_DIGIT: val = r'\d'
                elif args == sre_constants.CATEGORY_NOT_DIGIT: val = r'\D'
                elif args == sre_constants.CATEGORY_SPACE: val = r'\s'
                elif args == sre_constants.CATEGORY_NOT_SPACE: val = r'\S'
                elif args == sre_constants.CATEGORY_WORD: val = r'\w'
                elif args == sre_constants.CATEGORY_NOT_WORD: val = r'\W'
                else: raise ValueError('Unknown category %s' % args)
                self._output(val, self.RE_CHAR_TAG, state)

            elif op == sre_constants.AT:
                if args == sre_constants.AT_BEGINNING_STRING: val = r'\A'
                elif args == sre_constants.AT_BEGINNING: val = '^'
                elif args == sre_constants.AT_END: val = '$'
                elif args == sre_constants.AT_BOUNDARY: val = r'\b'
                elif args == sre_constants.AT_NON_BOUNDARY: val = r'\B'
                elif args == sre_constants.AT_END_STRING: val = r'\Z'
                else: raise ValueError('Unknown position %s' % args)
                self._output(val, self.RE_CHAR_TAG, state)

            elif op in (sre_constants.MAX_REPEAT, sre_constants.MIN_REPEAT):
                minrpt = args[0]
                maxrpt = args[1]
                if maxrpt == sre_constants.MAXREPEAT:
                    if minrpt == 0:   val = '*'
                    elif minrpt == 1: val = '+'
                    else: val = '{%d,}' % (minrpt)
                elif minrpt == 0:
                    if maxrpt == 1: val = '?'
                    else: val = '{,%d}' % (maxrpt)
                elif minrpt == maxrpt:
                    val = '{%d}' % (maxrpt)
                else:
                    val = '{%d,%d}' % (minrpt, maxrpt)
                if op == sre_constants.MIN_REPEAT:
                    val += '?'

                self._colorize_re_tree(args[2], state, False, groups)
                self._output(val, self.RE_OP_TAG, state)

            elif op == sre_constants.SUBPATTERN:
                if args[0] is None:
                    self._output(r'(?:', self.RE_GROUP_TAG, state)
                elif args[0] in groups:
                    self._output(r'(?P<', self.RE_GROUP_TAG, state)
                    self._output(groups[args[0]], self.RE_REF_TAG, state)
                    self._output('>', self.RE_GROUP_TAG, state)
                elif isinstance(args[0], int):
                    # This is cheating:
                    self._output('(', self.RE_GROUP_TAG, state)
                else:
                    self._output('(?P<', self.RE_GROUP_TAG, state)
                    self._output(args[0], self.RE_REF_TAG, state)
                    self._output('>', self.RE_GROUP_TAG, state)
                self._colorize_re_tree(args[3], state, True, groups)
                self._output(')', self.RE_GROUP_TAG, state)

            elif op == sre_constants.GROUPREF:
                self._output('\\%d' % args, self.RE_REF_TAG, state)

            elif op == sre_constants.RANGE:
                self._colorize_re_tree( ((sre_constants.LITERAL, args[0]),),
                                        state, False, groups )
                self._output('-', self.RE_OP_TAG, state)
                self._colorize_re_tree( ((sre_constants.LITERAL, args[1]),),
                                        state, False, groups )

            elif op == sre_constants.NEGATE:
                self._output('^', self.RE_OP_TAG, state)

            elif op == sre_constants.ASSERT:
                if args[0] > 0:
                    self._output('(?=', self.RE_GROUP_TAG, state)
                else:
                    self._output('(?<=', self.RE_GROUP_TAG, state)
                self._colorize_re_tree(args[1], state, True, groups)
                self._output(')', self.RE_GROUP_TAG, state)

            elif op == sre_constants.ASSERT_NOT:
                if args[0] > 0:
                    self._output('(?!', self.RE_GROUP_TAG, state)
                else:
                    self._output('(? 1 and not noparen:
            self._output(')', self.RE_GROUP_TAG, state)

    #////////////////////////////////////////////////////////////
    # Output function
    #////////////////////////////////////////////////////////////

    def _output(self, s: AnyStr, css_class: Optional[str], 
                state: _ColorizerState, link: bool = False) -> None:
        """
        Add the string C{s} to the result list, tagging its contents
        with the specified C{css_class}. Any lines that go beyond L{PyvalColorizer.linelen} will
        be line-wrapped.  If the total number of lines exceeds
        L{PyvalColorizer.maxlines}, then raise a L{_Maxlines} exception.
        """
        # Make sure the string is unicode.
        if isinstance(s, bytes):
            s = cast(AnyStr, decode_with_backslashreplace(s))
        assert isinstance(s, str)
        # Split the string into segments.  The first segment is the
        # content to add to the current line, and the remaining
        # segments are new lines.
        segments = s.split('\n')

        for i, segment in enumerate(segments):
            # If this isn't the first segment, then add a newline to
            # split it from the previous segment.
            if i > 0:
                if (state.lineno+1) > self.maxlines:
                    raise _Maxlines()
                if not state.linebreakok:
                    raise _Linebreak()
                state.result.append(self.NEWLINE)
                state.lineno += 1
                state.charpos = 0
            
            segment_len = len(segment) 

            # If the segment fits on the current line, then just call
            # markup to tag it, and store the result.
            # Don't break links into separate segments, neither quotes.
            if (self.linelen is None or 
                state.charpos + segment_len <= self.linelen 
                or link is True 
                or css_class in ('variable-quote',)):

                state.charpos += segment_len

                if link is True:
                    element = obj_reference('', segment, refuid=segment)
                elif css_class is not None:
                    element = nodes.inline('', segment, classes=[css_class])
                else:
                    element = nodes.Text(segment)

                state.result.append(element)

            # If the segment doesn't fit on the current line, then
            # line-wrap it, and insert the remainder of the line into
            # the segments list that we're iterating over.  (We'll go
            # the beginning of the next line at the start of the
            # next iteration through the loop.)
            else:
                assert isinstance(self.linelen, int)
                split = self.linelen-state.charpos
                segments.insert(i+1, segment[split:])
                segment = segment[:split]

                if css_class is not None:
                    element = nodes.inline('', segment, classes=[css_class])
                else:
                    element = nodes.Text(segment)
                state.result += [element, self.LINEWRAP]
	pydoctor-21.12.1/pydoctor/epydoc/markup/_types.py000066400000000000000000000173641416703725300220460ustar00rootroot00000000000000"""
Render types from L{docutils.nodes.document} objects. 

This module provides yet another L{ParsedDocstring} subclass.
"""

from typing import Callable, Dict, List, Tuple, Union

from pydoctor.epydoc.markup import DocstringLinker, ParseError, ParsedDocstring, get_parser_by_name
from pydoctor.node2stan import node2stan
from pydoctor.napoleon.docstring import TokenType, TypeDocstring

from docutils import nodes
from twisted.web.template import Tag, tags

class ParsedTypeDocstring(TypeDocstring, ParsedDocstring):
    """
    Add L{ParsedDocstring} interface on top of L{TypeDocstring} and 
    allow to parse types from L{nodes.Node} objects, providing the C{--process-types} option.
    """

    FIELDS = ('type', 'rtype', 'ytype', 'returntype', 'yieldtype')

    _tokens: List[Tuple[Union[str, nodes.Node], TokenType]]

    def __init__(self, annotation: Union[nodes.document, str],
                 warns_on_unknown_tokens: bool = False, lineno: int = 0) -> None:
        ParsedDocstring.__init__(self, ())
        if isinstance(annotation, nodes.document):
            TypeDocstring.__init__(self, '', warns_on_unknown_tokens)

            _tokens = self._tokenize_node_type_spec(annotation)
            self._tokens = self._build_tokens(_tokens)
        else:
            TypeDocstring.__init__(self, annotation, warns_on_unknown_tokens)
        
        
        # We need to store the line number because we need to pass it to DocstringLinker.link_xref
        self._lineno = lineno

    @property
    def has_body(self) -> bool:
        return len(self._tokens)>0

    def to_node(self) -> nodes.document:
        """
        Not implemented.
        """
        raise NotImplementedError()

    def to_stan(self, docstring_linker: DocstringLinker) -> Tag:
        """
        Present the type as a stan tree. 
        """
        return self._convert_type_spec_to_stan(docstring_linker)

    def _tokenize_node_type_spec(self, spec: nodes.document) -> List[Union[str, nodes.Node]]:
        
        class Tokenizer(nodes.GenericNodeVisitor):
            
            def __init__(self, document: nodes.document) -> None:
                super().__init__(document)
                self.tokens: List[Union[str, nodes.Node]] = []
                self.rest = nodes.document
                self.warnings: List[str] = []

            def default_visit(self, node: nodes.Node) -> None:
                # Tokenize only the first level text in paragraph only,
                # Simply warn and ignore the rest.

                parent = node.parent
                super_parent = parent.parent if parent else None
                
                # first level
                if isinstance(parent, nodes.document) and not isinstance(node, nodes.paragraph):
                    self.warnings.append(f"Unexpected element in type specification field: element '{node.__class__.__name__}'. "
                                          "This value should only contain regular paragraphs with text or inline markup.")
                    raise nodes.SkipNode()
                
                # second level
                if isinstance(super_parent, nodes.document):
                    # only text in paragraph nodes are taken into account
                    if isinstance(node, nodes.Text):
                        # Tokenize the Text node with the same method TypeDocstring uses.
                        self.tokens.extend(TypeDocstring._tokenize_type_spec(node.astext()))
                    else:
                        self.tokens.append(node)
                        raise nodes.SkipNode()
    
        tokenizer = Tokenizer(spec)
        spec.walk(tokenizer)
        self.warnings.extend(tokenizer.warnings)
        return tokenizer.tokens

    def _convert_obj_tokens_to_stan(self, tokens: List[Tuple[Union[str, nodes.Node], TokenType]], 
                                    docstring_linker: DocstringLinker) -> List[Tuple[Union[str, Tag, nodes.Node], TokenType]]:
        """
        Convert L{TokenType.OBJ} and PEP 484 like L{TokenType.DELIMITER} type to stan, merge them together. Leave the rest untouched. 

        Exemple:

        >>> tokens = [("list", TokenType.OBJ), ("(", TokenType.DELIMITER), ("int", TokenType.OBJ), (")", TokenType.DELIMITER)]
        >>> ann._convert_obj_tokens_to_stan(tokens, NotFoundLinker())
        ... [(Tag('code', children=['list', '(', 'int', ')']), TokenType.OBJ)]
        
        @param tokens: List of tuples: C{(token, type)}
        """

        combined_tokens: List[Tuple[Union[str, Tag], TokenType]] = []

        open_parenthesis = 0
        open_square_braces = 0

        for _token, _type in tokens:

            if (_type is TokenType.DELIMITER and _token in ('[', '(', ')', ']')) \
               or _type is TokenType.OBJ: 
                if _token == "[": open_square_braces += 1
                elif _token == "(": open_parenthesis += 1

                if _type is TokenType.OBJ:
                    _token = docstring_linker.link_xref(
                                _token, _token, self._lineno)

                if open_square_braces + open_parenthesis > 0:
                    try: last_processed_token = combined_tokens[-1]
                    except IndexError:
                        combined_tokens.append((_token, _type))
                    else:
                        if last_processed_token[1] is TokenType.OBJ \
                           and isinstance(last_processed_token[0], Tag):
                            # Merge with last Tag
                            if _type is TokenType.OBJ:
                                assert isinstance(_token, Tag)
                                last_processed_token[0](*_token.children)
                            else:
                                last_processed_token[0](_token)
                        else:
                            combined_tokens.append((_token, _type))
                else:
                    combined_tokens.append((_token, _type))
                
                if _token == "]": open_square_braces -= 1
                elif _token == ")": open_parenthesis -= 1

            else:
                # the token will be processed in _convert_type_spec_to_stan() method.
                combined_tokens.append((_token, _type))

        return combined_tokens

    def _convert_type_spec_to_stan(self, docstring_linker: DocstringLinker) -> Tag:
        """
        Convert type to L{Tag} object.
        """

        tokens = self._convert_obj_tokens_to_stan(self._tokens, docstring_linker)

        warnings: List[ParseError] = []

        converters: Dict[TokenType, Callable[[Union[str, Tag]], Union[str, Tag]]] = {
            TokenType.LITERAL:      lambda _token: tags.span(_token, class_="literal"),
            TokenType.CONTROL:      lambda _token: tags.em(_token),
            TokenType.REFERENCE:    lambda _token: get_parser_by_name('restructuredtext')(_token, warnings, False).to_stan(docstring_linker) if isinstance(_token, str) else _token, 
            TokenType.UNKNOWN:      lambda _token: get_parser_by_name('restructuredtext')(_token, warnings, False).to_stan(docstring_linker) if isinstance(_token, str) else _token, 
            TokenType.OBJ:          lambda _token: _token, # These convertions (OBJ and DELIMITER) are done in _convert_obj_tokens_to_stan().
            TokenType.DELIMITER:    lambda _token: _token, 
            TokenType.ANY:          lambda _token: _token, 
        }

        for w in warnings:
            self.warnings.append(w.descr())

        converted = Tag('')

        for token, type_ in tokens:
            assert token is not None
            if isinstance(token, nodes.Node):
                token = node2stan(token, docstring_linker)
            assert isinstance(token, (str, Tag))
            converted_token = converters[type_](token)
            converted(converted_token)

        return converted
pydoctor-21.12.1/pydoctor/epydoc/markup/epytext.py000066400000000000000000001537371416703725300222520ustar00rootroot00000000000000#
# epytext.py: epydoc formatted docstring parsing
# Edward Loper
#
# Created [04/10/01 12:00 AM]
#

"""
Parser for epytext strings.  Epytext is a lightweight markup whose
primary intended application is Python documentation strings.  This
parser converts Epytext strings to a simple DOM-like representation
(encoded as a tree of L{Element} objects and strings).  Epytext
strings can contain the following I{structural blocks}:

    - C{epytext}: The top-level element of the DOM tree.
    - C{para}: A paragraph of text.  Paragraphs contain no newlines,
      and all spaces are soft.
    - C{section}: A section or subsection.
    - C{field}: A tagged field.  These fields provide information
      about specific aspects of a Python object, such as the
      description of a function's parameter, or the author of a
      module.
    - C{literalblock}: A block of literal text.  This text should be
      displayed as it would be displayed in plaintext.  The
      parser removes the appropriate amount of leading whitespace
      from each line in the literal block.
    - C{doctestblock}: A block containing sample python code,
      formatted according to the specifications of the C{doctest}
      module.
    - C{ulist}: An unordered list.
    - C{olist}: An ordered list.
    - C{li}: A list item.  This tag is used both for unordered list
      items and for ordered list items.

Additionally, the following I{inline regions} may be used within
C{para} blocks:

    - C{code}:   Source code and identifiers.
    - C{math}:   Mathematical expressions.
    - C{index}:  A term which should be included in an index, if one
                 is generated.
    - C{italic}: Italicized text.
    - C{bold}:   Bold-faced text.
    - C{uri}:    A Universal Resource Indicator (URI) or Universal
                 Resource Locator (URL)
    - C{link}:   A Python identifier which should be hyperlinked to
                 the named object's documentation, when possible.

The returned DOM tree will conform to the the following Document Type
Description::

   

   

   

   

   
   
   
   

   
   

   
   
   
   
   

   
   
   
   

   
   
   
   
   
   

   
   

@var SYMBOLS: A list of the of escape symbols that are supported by epydoc.  Currently the following symbols are supported ::

    # Arrows
    '<-', '->', '^', 'v',

    # Greek letters
    'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta',
    'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu',
    'nu', 'xi', 'omicron', 'pi', 'rho', 'sigma',
    'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega',
    'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta',
    'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu',
    'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',
    'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega',

    # HTML character entities
    'larr', 'rarr', 'uarr', 'darr', 'harr', 'crarr',
    'lArr', 'rArr', 'uArr', 'dArr', 'hArr',
    'copy', 'times', 'forall', 'exist', 'part',
    'empty', 'isin', 'notin', 'ni', 'prod', 'sum',
    'prop', 'infin', 'ang', 'and', 'or', 'cap', 'cup',
    'int', 'there4', 'sim', 'cong', 'asymp', 'ne',
    'equiv', 'le', 'ge', 'sub', 'sup', 'nsub',
    'sube', 'supe', 'oplus', 'otimes', 'perp',

    # Alternate (long) names
    'infinity', 'integral', 'product',
    '>=', '<=',

"""
# Note: the symbol list is appended to the docstring automatically,
# below.

__docformat__ = 'epytext en'

# Code organization..
#   1. parse()
#   2. tokenize()
#   3. colorize()
#   4. helpers
#   5. testing

from typing import Any, Callable, Iterable, List, Optional, Sequence, Union, cast, overload
import re

from docutils import utils, nodes
from twisted.web.template import Tag

from pydoctor.epydoc.markup import Field, ParseError, ParsedDocstring
from pydoctor.epydoc.markup._types import ParsedTypeDocstring
from pydoctor.epydoc.docutils import set_node_attributes
from pydoctor.model import Documentable

##################################################
## DOM-Like Encoding
##################################################

class Element:
    """
    A very simple DOM-like representation for parsed epytext
    documents.  Each epytext document is encoded as a tree whose nodes
    are L{Element} objects, and whose leaves are C{string}s.  Each
    node is marked by a L{tag} and zero or more attributes, L{attribs}.  Each
    attribute is a mapping from a string key to a string value.
    """
    def __init__(self, tag: str, *children: Union[str, 'Element'], **attribs: Any):
        self.tag = tag
        """A string tag indicating the type of this element."""

        self.children = list(children)
        """A list of the children of this element."""

        self.attribs = attribs
        """A dictionary mapping attribute names to attribute values for this element."""

    def __str__(self) -> str:
        """
        Return a string representation of this element, using XML
        notation.
        @note: Doesn't escape '<' or '&' or '>', so the result is only XML-like
            and cannot actually be parsed as XML.
        """
        attribs = ''.join(f' {k}={v!r}' for k, v in self.attribs.items())
        content = ''.join(str(child) for child in self.children)
        return f'<{self.tag}{attribs}>{content}'

    def __repr__(self) -> str:
        attribs = ''.join(f', {k}={v!r}' for k, v in self.attribs.items())
        args = ''.join(f', {c!r}' for c in self.children)
        return f'Element({self.tag}{args}{attribs})'

##################################################
## Constants
##################################################

# The possible heading underline characters, listed in order of
# heading depth.
_HEADING_CHARS = '=-~'

# Escape codes.  These should be needed very rarely.
_ESCAPES = {'lb':'{', 'rb': '}'}

# Symbols.  These can be generated via S{...} escapes.
SYMBOLS = [
    # Arrows
    '<-', '->', '^', 'v',

    # Greek letters
    'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta',
    'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu',
    'nu', 'xi', 'omicron', 'pi', 'rho', 'sigma',
    'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega',
    'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta',
    'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu',
    'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',
    'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega',

    # HTML character entities
    'larr', 'rarr', 'uarr', 'darr', 'harr', 'crarr',
    'lArr', 'rArr', 'uArr', 'dArr', 'hArr',
    'copy', 'times', 'forall', 'exist', 'part',
    'empty', 'isin', 'notin', 'ni', 'prod', 'sum',
    'prop', 'infin', 'ang', 'and', 'or', 'cap', 'cup',
    'int', 'there4', 'sim', 'cong', 'asymp', 'ne',
    'equiv', 'le', 'ge', 'sub', 'sup', 'nsub',
    'sube', 'supe', 'oplus', 'otimes', 'perp',

    # Alternate (long) names
    'infinity', 'integral', 'product',
    '>=', '<=',
    ]
# Convert to a set, for quick lookup
_SYMBOLS = set(SYMBOLS)

# Add symbols to the docstring.
symblist = '      '
symblist += ';\n      '.join(' - C{E{S}{%s}}=S{%s}' % (symbol, symbol)
                             for symbol in SYMBOLS)
__doc__ = __doc__.replace('<<>>', symblist)
del symblist

# Tags for colorizing text.
_COLORIZING_TAGS = {
    'C': 'code',
    'M': 'math',
    'I': 'italic',
    'B': 'bold',
    'U': 'uri',
    'L': 'link',       # A Python identifier that should be linked to
    'E': 'escape',     # escapes characters or creates symbols
    'S': 'symbol',
    }

# Which tags can use "link syntax" (e.g., U{Python})?
_LINK_COLORIZING_TAGS = ['link', 'uri']

##################################################
## Structuring (Top Level)
##################################################

@overload
def parse(text: str) -> Element: ...

@overload
def parse(text: str, errors: List[ParseError]) -> Optional[Element]: ...

def parse(text: str, errors: Optional[List[ParseError]] = None) -> Optional[Element]:
    """
    Return a DOM tree encoding the contents of an epytext string.  Any
    errors generated during parsing will be stored in C{errors}.

    @param text: The epytext string to parse.
    @param errors: A list where any errors generated during parsing
        will be stored.  If no list is specified, then fatal errors
        will generate exceptions, and non-fatal errors will be
        ignored.
    @return: a DOM tree encoding the contents of an epytext string,
        or C{None} if non-fatal errors were encountered and no C{errors}
        accumulator was provided.
    @raise ParseError: If C{errors} is C{None} and an error is
        encountered while parsing.
    """
    # Initialize errors list.
    if errors is None:
        errors = []
        raise_on_error = True
    else:
        raise_on_error = False
    
    # Preprocess the string.
    text = re.sub('\015\012', '\012', text)
    text = text.expandtabs()

    # Tokenize the input string.
    tokens = _tokenize(text, errors)

    # Have we encountered a field yet?
    encountered_field = False

    # Create an document to hold the epytext.
    doc = Element('epytext')

    # Maintain two parallel stacks: one contains DOM elements, and
    # gives the ancestors of the current block.  The other contains
    # indentation values, and gives the indentation of the
    # corresponding DOM elements.  An indentation of "None" reflects
    # an unknown indentation.  However, the indentation must be
    # greater than, or greater than or equal to, the indentation of
    # the prior element (depending on what type of DOM element it
    # corresponds to).  No 2 consecutive indent_stack values will be
    # ever be "None."  Use initial dummy elements in the stack, so we
    # don't have to worry about bounds checking.
    stack = [cast(Element, None), doc]
    indent_stack = [-1, None]

    for token in tokens:
        # Uncomment this for debugging:
        #print('%s: %s\n%s: %s\n' %
        #       (''.join('%-11s' % (t and t.tag) for t in stack),
        #        token.tag, ''.join('%-11s' % i for i in indent_stack),
        #        token.indent))

        # Pop any completed blocks off the stack.
        _pop_completed_blocks(token, stack, indent_stack)

        # If Token has type PARA, colorize and add the new paragraph
        if token.tag == Token.PARA:
            _add_para(token, stack, indent_stack, errors)

        # If Token has type HEADING, add the new section
        elif token.tag == Token.HEADING:
            _add_section(token, stack, indent_stack, errors)

        # If Token has type LBLOCK, add the new literal block
        elif token.tag == Token.LBLOCK:
            stack[-1].children.append(token.to_dom())

        # If Token has type DTBLOCK, add the new doctest block
        elif token.tag == Token.DTBLOCK:
            stack[-1].children.append(token.to_dom())

        # If Token has type BULLET, add the new list/list item/field
        elif token.tag == Token.BULLET:
            _add_list(token, stack, indent_stack, errors)
        else:
            raise AssertionError(f"Unknown token type: {token.tag}")

        # Check if the DOM element we just added was a field..
        if stack[-1].tag == 'field':
            encountered_field = True
        elif encountered_field:
            if len(stack) <= 3:
                estr = ("Fields must be the final elements in an "+
                        "epytext string.")
                errors.append(StructuringError(estr, token.startline))

    # If there was an error, then signal it!
    if any(e.is_fatal() for e in errors):
        if raise_on_error:
            raise errors[0]
        else:
            return None

    # Return the top-level epytext DOM element.
    return doc

def _pop_completed_blocks(
        token: 'Token',
        stack: List[Element],
        indent_stack: List[Optional[int]]
        ) -> None:
    """
    Pop any completed blocks off the stack.  This includes any
    blocks that we have dedented past, as well as any list item
    blocks that we've dedented to.  The top element on the stack
    should only be a list if we're about to start a new list
    item (i.e., if the next token is a bullet).
    """
    indent = token.indent
    if indent is not None:
        while (len(stack) > 2):
            pop = False

            # Dedent past a block
            if indent_stack[-1] is not None and indent < indent_stack[-1]:
                pop = True
            elif indent_stack[-1] is None and indent < cast(int, indent_stack[-2]):
                pop = True

            # Dedent to a list item, if it is follwed by another list
            # item with the same indentation.
            elif (token.tag == 'bullet' and indent==indent_stack[-2] and
                  stack[-1].tag in ('li', 'field')): pop = True

            # End of a list (no more list items available)
            elif (stack[-1].tag in ('ulist', 'olist') and
                  (token.tag != 'bullet' or token.contents[-1] == ':')):
                pop = True

            # Pop the block, if it's complete.  Otherwise, we're done.
            if not pop: return
            stack.pop()
            indent_stack.pop()

def _add_para(
        para_token: 'Token',
        stack: List[Element],
        indent_stack: List[Optional[int]],
        errors: List[ParseError]
        ) -> None:
    """Colorize the given paragraph, and add it to the DOM tree."""
    # Check indentation, and update the parent's indentation
    # when appropriate.
    if indent_stack[-1] is None:
        indent_stack[-1] = para_token.indent
    if para_token.indent == indent_stack[-1]:
        # Colorize the paragraph and add it.
        para = _colorize(para_token, errors)
        stack[-1].children.append(para)
    else:
        estr = "Improper paragraph indentation."
        errors.append(StructuringError(estr, para_token.startline))

def _add_section(
        heading_token: 'Token',
        stack: List[Element],
        indent_stack: List[Optional[int]],
        errors: List[ParseError]
        ) -> None:
    """Add a new section to the DOM tree, with the given heading."""
    if indent_stack[-1] is None:
        indent_stack[-1] = heading_token.indent
    elif indent_stack[-1] != heading_token.indent:
        estr = "Improper heading indentation."
        errors.append(StructuringError(estr, heading_token.startline))

    # Check for errors.
    for tok in stack[2:]:
        if tok.tag != 'section':
            estr = "Headings must occur at the top level."
            errors.append(StructuringError(estr, heading_token.startline))
            break
    index = cast(int, heading_token.level) + 2
    if index > len(stack):
        estr = "Wrong underline character for heading."
        errors.append(StructuringError(estr, heading_token.startline))

    # Pop the appropriate number of headings so we're at the
    # correct level.
    stack[index:] = []
    indent_stack[index:] = []

    # Colorize the heading
    head = _colorize(heading_token, errors, 'heading')

    # Add the section's and heading's DOM elements.
    sec = Element('section')
    stack[-1].children.append(sec)
    stack.append(sec)
    sec.children.append(head)
    indent_stack.append(None)

def _add_list(
        bullet_token: 'Token',
        stack: List[Element],
        indent_stack: List[Optional[int]],
        errors: List[ParseError]
        ) -> None:
    """
    Add a new list item or field to the DOM tree, with the given
    bullet or field tag.  When necessary, create the associated
    list.
    """
    # Determine what type of bullet it is.
    if bullet_token.contents[-1] == '-':
        list_type = 'ulist'
    elif bullet_token.contents[-1] == '.':
        list_type = 'olist'
    elif bullet_token.contents[-1] == ':':
        list_type = 'fieldlist'
    else:
        raise AssertionError(f'Bad Bullet: {bullet_token.contents!r}')

    # Is this a new list?
    newlist = False
    if stack[-1].tag != list_type:
        newlist = True
    elif list_type == 'olist' and stack[-1].tag == 'olist':
        old_listitem = cast(Element, stack[-1].children[-1])
        old_bullet = old_listitem.attribs['bullet'].split('.')[:-1]
        new_bullet = bullet_token.contents.split('.')[:-1]
        if (new_bullet[:-1] != old_bullet[:-1] or
            int(new_bullet[-1]) != int(old_bullet[-1])+1):
            newlist = True

    # Create the new list.
    if newlist:
        if stack[-1].tag == 'fieldlist':
            # The new list item is not a field list item (since this
            # is a new list); but it's indented the same as the field
            # list.  This either means that they forgot to indent the
            # list, or they are trying to put something after the
            # field list.  The first one seems more likely, so we'll
            # just warn about that (to avoid confusion).
            estr = "Lists must be indented."
            errors.append(StructuringError(estr, bullet_token.startline))
        if stack[-1].tag in ('ulist', 'olist', 'fieldlist'):
            stack.pop()
            indent_stack.pop()

        if (list_type != 'fieldlist' and indent_stack[-1] is not None and
            bullet_token.indent == indent_stack[-1]):
            # Ignore this error if there's text on the same line as
            # the comment-opening quote -- epydoc can't reliably
            # determine the indentation for that line.
            if bullet_token.startline != 1 or bullet_token.indent != 0:
                estr = "Lists must be indented."
                errors.append(StructuringError(estr, bullet_token.startline))

        if list_type == 'fieldlist':
            # Fieldlist should be at the top-level.
            for tok in stack[2:]:
                if tok.tag != 'section':
                    estr = "Fields must be at the top level."
                    errors.append(
                        StructuringError(estr, bullet_token.startline))
                    break
            stack[2:] = []
            indent_stack[2:] = []

        # Add the new list.
        lst = Element(list_type)
        stack[-1].children.append(lst)
        stack.append(lst)
        indent_stack.append(bullet_token.indent)
        if list_type == 'olist':
            start = bullet_token.contents.split('.')[:-1]
            if start != '1':
                lst.attribs['start'] = start[-1]

    # Fields are treated somewhat specially: A 'fieldlist'
    # node is created to make the parsing simpler, but fields
    # are adjoined directly into the 'epytext' node, not into
    # the 'fieldlist' node.
    if list_type == 'fieldlist':
        li = Element('field', lineno=str(bullet_token.startline))
        token_words = bullet_token.contents[1:-1].split(None, 1)
        tag_elt = Element('tag')
        tag_elt.children.append(token_words[0])
        li.children.append(tag_elt)

        if len(token_words) > 1:
            arg_elt = Element('arg')
            arg_elt.children.append(token_words[1])
            li.children.append(arg_elt)
    else:
        li = Element('li')
        if list_type == 'olist':
            li.attribs['bullet'] = bullet_token.contents

    # Add the bullet.
    stack[-1].children.append(li)
    stack.append(li)
    indent_stack.append(None)

##################################################
## Tokenization
##################################################

class Token:
    """
    C{Token}s are an intermediate data structure used while
    constructing the structuring DOM tree for a formatted docstring.
    There are five types of C{Token}:

        - Paragraphs
        - Literal blocks
        - Doctest blocks
        - Headings
        - Bullets

    The text contained in each C{Token} is stored in the
    C{contents} variable.  The string in this variable has been
    normalized.  For paragraphs, this means that it has been converted
    into a single line of text, with newline/indentation replaced by
    single spaces.  For literal blocks and doctest blocks, this means
    that the appropriate amount of leading whitespace has been removed
    from each line.

    Each C{Token} has an indentation level associated with it,
    stored in the C{indent} variable.  This indentation level is used
    by the structuring procedure to assemble hierarchical blocks.

    @type tag: C{string}
    @ivar tag: This C{Token}'s type.  Possible values are C{Token.PARA}
        (paragraph), C{Token.LBLOCK} (literal block), C{Token.DTBLOCK}
        (doctest block), C{Token.HEADINGC}, and C{Token.BULLETC}.

    @type startline: C{int}
    @ivar startline: The line on which this C{Token} begins.  This
        line number is only used for issuing errors.

    @type contents: C{string}
    @ivar contents: The normalized text contained in this C{Token}.

    @type indent: C{int} or C{None}
    @ivar indent: The indentation level of this C{Token} (in
        number of leading spaces).  A value of C{None} indicates an
        unknown indentation; this is used for list items and fields
        that begin with one-line paragraphs.

    @type level: C{int} or C{None}
    @ivar level: The heading-level of this C{Token} if it is a
        heading; C{None}, otherwise.  Valid heading levels are 0, 1,
        and 2.

    @type PARA: C{string}
    @cvar PARA: The C{tag} value for paragraph C{Token}s.
    @type LBLOCK: C{string}
    @cvar LBLOCK: The C{tag} value for literal C{Token}s.
    @type DTBLOCK: C{string}
    @cvar DTBLOCK: The C{tag} value for doctest C{Token}s.
    @type HEADING: C{string}
    @cvar HEADING: The C{tag} value for heading C{Token}s.
    @type BULLET: C{string}
    @cvar BULLET: The C{tag} value for bullet C{Token}s.  This C{tag}
        value is also used for field tag C{Token}s, since fields
        function syntactically the same as list items.
    """
    # The possible token types.
    PARA = 'para'
    LBLOCK = 'literalblock'
    DTBLOCK = 'doctestblock'
    HEADING = 'heading'
    BULLET = 'bullet'

    def __init__(self,
            tag: str,
            startline: int,
            contents: str,
            indent: Optional[int],
            level: Optional[int] = None
            ):
        """
        Create a new C{Token}.

        @param tag: The type of the new C{Token}.
        @param startline: The line on which the new C{Token} begins.
        @param contents: The normalized contents of the new C{Token}.
        @param indent: The indentation of the new C{Token} (in number
            of leading spaces).  A value of C{None} indicates an
            unknown indentation.
        @param level: The heading-level of this C{Token} if it is a
            heading; C{None}, otherwise.
        """
        self.tag = tag
        self.startline = startline
        self.contents = contents
        self.indent = indent
        self.level = level

    def __repr__(self) -> str:
        """
        @rtype: C{string}
        @return: the formal representation of this C{Token}.
            C{Token}s have formal representaitons of the form::
                
        """
        return f''

    def to_dom(self) -> Element:
        """
        @return: a DOM representation of this C{Token}.
        """
        e = Element(self.tag)
        e.children.append(self.contents)
        return e

# Construct regular expressions for recognizing bullets.  These are
# global so they don't have to be reconstructed each time we tokenize
# a docstring.
_ULIST_BULLET = r'[-]( +|$)'
_OLIST_BULLET = r'(\d+[.])+( +|$)'
_FIELD_BULLET = r'@\w+( [^{}:\n]+)?:'
_BULLET_RE = re.compile(_ULIST_BULLET + '|' +
                        _OLIST_BULLET + '|' +
                        _FIELD_BULLET)
_LIST_BULLET_RE = re.compile(_ULIST_BULLET + '|' + _OLIST_BULLET)
_FIELD_BULLET_RE = re.compile(_FIELD_BULLET)
del _ULIST_BULLET, _OLIST_BULLET, _FIELD_BULLET

def _tokenize_doctest(
        lines: List[str],
        start: int,
        block_indent: int,
        tokens: List[Token],
        errors: List[ParseError]
        ) -> int:
    """
    Construct a L{Token} containing the doctest block starting at
    C{lines[start]}, and append it to C{tokens}.  C{block_indent}
    should be the indentation of the doctest block.  Any errors
    generated while tokenizing the doctest block will be appended to
    C{errors}.

    @param lines: The list of lines to be tokenized
    @param start: The index into C{lines} of the first line of the
        doctest block to be tokenized.
    @param block_indent: The indentation of C{lines[start]}.  This is
        the indentation of the doctest block.
    @param errors: A list where any errors generated during parsing
        will be stored.  If no list is specified, then errors will
        generate exceptions.
    @return: The line number of the first line following the doctest
        block.
    """
    # If they dedent past block_indent, keep track of the minimum
    # indentation.  This is used when removing leading indentation
    # from the lines of the doctest block.
    min_indent = block_indent

    linenum = start + 1
    while linenum < len(lines):
        # Find the indentation of this line.
        line = lines[linenum]
        indent = len(line) - len(line.lstrip())

        # A blank line ends doctest block.
        if indent == len(line): break

        # A Dedent past block_indent is an error.
        if indent < block_indent:
            min_indent = min(min_indent, indent)
            estr = 'Improper doctest block indentation.'
            errors.append(TokenizationError(estr, linenum))

        # Go on to the next line.
        linenum += 1

    # Add the token, and return the linenum after the token ends.
    contents = '\n'.join(ln[min_indent:] for ln in lines[start:linenum])
    tokens.append(Token(Token.DTBLOCK, start, contents, block_indent))
    return linenum

def _tokenize_literal(
        lines: List[str],
        start: int,
        block_indent: int,
        tokens: List[Token],
        errors: List[ParseError]
        ) -> int:
    """
    Construct a L{Token} containing the literal block starting at
    C{lines[start]}, and append it to C{tokens}.  C{block_indent}
    should be the indentation of the literal block.  Any errors
    generated while tokenizing the literal block will be appended to
    C{errors}.

    @param lines: The list of lines to be tokenized
    @param start: The index into C{lines} of the first line of the
        literal block to be tokenized.
    @param block_indent: The indentation of C{lines[start]}.  This is
        the indentation of the literal block.
    @param errors: A list of the errors generated by parsing.  Any
        new errors generated while will tokenizing this paragraph
        will be appended to this list.
    @return: The line number of the first line following the literal
        block.
    """
    linenum = start + 1
    while linenum < len(lines):
        # Find the indentation of this line.
        line = lines[linenum]
        indent = len(line) - len(line.lstrip())

        # A Dedent to block_indent ends the literal block.
        # (Ignore blank likes, though)
        if len(line) != indent and indent <= block_indent:
            break

        # Go on to the next line.
        linenum += 1

    # Add the token, and return the linenum after the token ends.
    contents = '\n'.join(ln[block_indent:] for ln in lines[start:linenum])
    contents = re.sub(r'(\A[ \n]*\n)|(\n[ \n]*\Z)', '', contents)
    tokens.append(Token(Token.LBLOCK, start, contents, block_indent))
    return linenum

def _tokenize_listart(
        lines: List[str],
        start: int,
        bullet_indent: int,
        tokens: List[Token],
        errors: List[ParseError]
        ) -> int:
    """
    Construct L{Token}s for the bullet and the first paragraph of the
    list item (or field) starting at C{lines[start]}, and append them
    to C{tokens}.  C{bullet_indent} should be the indentation of the
    list item.  Any errors generated while tokenizing will be
    appended to C{errors}.

    @param lines: The list of lines to be tokenized
    @param start: The index into C{lines} of the first line of the
        list item to be tokenized.
    @param bullet_indent: The indentation of C{lines[start]}.  This is
        the indentation of the list item.
    @param errors: A list of the errors generated by parsing.  Any
        new errors generated while will tokenizing this paragraph
        will be appended to this list.
    @return: The line number of the first line following the list
        item's first paragraph.
    """
    linenum = start + 1
    para_indent = None
    doublecolon = lines[start].rstrip()[-2:] == '::'

    # Get the contents of the bullet.
    match = _BULLET_RE.match(lines[start], bullet_indent)
    assert match is not None
    para_start = match.end()
    bcontents = lines[start][bullet_indent : para_start].strip()

    while linenum < len(lines):
        # Find the indentation of this line.
        line = lines[linenum]
        indent = len(line) - len(line.lstrip())

        # "::" markers end paragraphs.
        if doublecolon: break
        if line.rstrip()[-2:] == '::': doublecolon = True

        # A blank line ends the token
        if indent == len(line): break

        # Dedenting past bullet_indent ends the list item.
        if indent < bullet_indent: break

        # A line beginning with a bullet ends the token.
        if _BULLET_RE.match(line, indent): break

        # If this is the second line, set the paragraph indentation, or
        # end the token, as appropriate.
        if para_indent is None: para_indent = indent

        # A change in indentation ends the token
        if indent != para_indent: break

        # Go on to the next line.
        linenum += 1

    # Add the bullet token.
    tokens.append(Token(Token.BULLET, start, bcontents, bullet_indent))

    # Add the paragraph token.
    pcontents = ' '.join(
        [lines[start][para_start:].strip()] +
        [ln.strip() for ln in lines[start+1:linenum]]
        ).strip()
    if pcontents:
        tokens.append(Token(Token.PARA, start, pcontents, para_indent))

    # Return the linenum after the paragraph token ends.
    return linenum

def _tokenize_para(
        lines: List[str],
        start: int,
        para_indent: int,
        tokens: List[Token],
        errors: List[ParseError]
        ) -> int:
    """
    Construct a L{Token} containing the paragraph starting at
    C{lines[start]}, and append it to C{tokens}.  C{para_indent}
    should be the indentation of the paragraph .  Any errors
    generated while tokenizing the paragraph will be appended to
    C{errors}.

    @param lines: The list of lines to be tokenized
    @param start: The index into C{lines} of the first line of the
        paragraph to be tokenized.
    @param para_indent: The indentation of C{lines[start]}.  This is
        the indentation of the paragraph.
    @param errors: A list of the errors generated by parsing.  Any
        new errors generated while will tokenizing this paragraph
        will be appended to this list.
    @return: The line number of the first line following the
        paragraph.
    """
    linenum = start + 1
    doublecolon = False
    while linenum < len(lines):
        # Find the indentation of this line.
        line = lines[linenum]
        indent = len(line) - len(line.lstrip())

        # "::" markers end paragraphs.
        if doublecolon: break
        if line.rstrip()[-2:] == '::': doublecolon = True

        # Blank lines end paragraphs
        if indent == len(line): break

        # Indentation changes end paragraphs
        if indent != para_indent: break

        # List bullets end paragraphs
        if _BULLET_RE.match(line, indent): break

        # Check for mal-formatted field items.
        if line[indent] == '@':
            estr = "Possible mal-formatted field item."
            errors.append(TokenizationError(estr, linenum, is_fatal=False))

        # Go on to the next line.
        linenum += 1

    contents = [ln.strip() for ln in lines[start:linenum]]

    # Does this token look like a heading?
    if ((len(contents) < 2) or
        (contents[1][0] not in _HEADING_CHARS) or
        (abs(len(contents[0])-len(contents[1])) > 5)):
        looks_like_heading = False
    else:
        looks_like_heading = True
        for char in contents[1]:
            if char != contents[1][0]:
                looks_like_heading = False
                break

    if looks_like_heading:
        if len(contents[0]) != len(contents[1]):
            estr = ("Possible heading typo: the number of "+
                    "underline characters must match the "+
                    "number of heading characters.")
            errors.append(TokenizationError(estr, start, is_fatal=False))
        else:
            level = _HEADING_CHARS.index(contents[1][0])
            tokens.append(Token(Token.HEADING, start,
                                contents[0], para_indent, level))
            return start+2

    # Add the paragraph token, and return the linenum after it ends.
    tokens.append(Token(Token.PARA, start, ' '.join(contents), para_indent))
    return linenum

def _tokenize(text: str, errors: List[ParseError]) -> List[Token]:
    """
    Split a given formatted docstring into an ordered list of
    L{Token}s, according to the epytext markup rules.

    @param text: The epytext string
    @param errors: A list where any errors generated during parsing
        will be stored.  If no list is specified, then errors will
        generate exceptions.
    @return: a list of the L{Token}s that make up the given string.
    """
    tokens: List[Token] = []
    lines = text.split('\n')

    # Scan through the lines, determining what @type of token we're
    # dealing with, and tokenizing it, as appropriate.
    linenum = 0
    while linenum < len(lines):
        # Get the current line and its indentation.
        line = lines[linenum]
        indent = len(line)-len(line.lstrip())

        if indent == len(line):
            # Ignore blank lines.
            linenum += 1
            continue
        elif line[indent:indent+4] == '>>> ':
            # blocks starting with ">>> " are doctest block tokens.
            linenum = _tokenize_doctest(lines, linenum, indent,
                                        tokens, errors)
        elif _BULLET_RE.match(line, indent):
            # blocks starting with a bullet are LI start tokens.
            linenum = _tokenize_listart(lines, linenum, indent,
                                        tokens, errors)
            if tokens[-1].indent is not None:
                indent = tokens[-1].indent
        else:
            # Check for mal-formatted field items.
            if line[indent] == '@':
                estr = "Possible mal-formatted field item."
                errors.append(TokenizationError(estr, linenum, is_fatal=False))

            # anything else is either a paragraph or a heading.
            linenum = _tokenize_para(lines, linenum, indent, tokens, errors)

        # Paragraph tokens ending in '::' initiate literal blocks.
        if (tokens[-1].tag == Token.PARA and
            tokens[-1].contents[-2:] == '::'):
            tokens[-1].contents = tokens[-1].contents[:-1]
            linenum = _tokenize_literal(lines, linenum, indent, tokens, errors)

    return tokens


##################################################
## Inline markup ("colorizing")
##################################################

# Assorted regular expressions used for colorizing.
_BRACE_RE = re.compile(r'{|}')
_TARGET_RE = re.compile(r'^(.*?)\s*<(?:URI:|URL:)?([^<>]+)>$')

def _colorize(token: Token, errors: List[ParseError], tagName: str = 'para') -> Element:
    """
    Given a string containing the contents of a paragraph, produce a
    DOM C{Element} encoding that paragraph.  Colorized regions are
    represented using DOM C{Element}s, and text is represented using
    DOM C{Text}s.

    @param errors: A list of errors.  Any newly generated errors will
        be appended to this list.
    @type errors: C{list} of C{string}

    @param tagName: The element tag for the DOM C{Element} that should
        be generated.
    @type tagName: C{string}

    @return: a DOM C{Element} encoding the given paragraph.
    @returntype: C{Element}
    """
    text = token.contents

    # Maintain a stack of DOM elements, containing the ancestors of
    # the text currently being analyzed.  New elements are pushed when
    # "{" is encountered, and old elements are popped when "}" is
    # encountered.
    stack = [Element(tagName)]

    # This is just used to make error-reporting friendlier.  It's a
    # stack parallel to "stack" containing the index of each element's
    # open brace.
    openbrace_stack = [0]

    # Process the string, scanning for '{' and '}'s.  start is the
    # index of the first unprocessed character.  Each time through the
    # loop, we process the text from the first unprocessed character
    # to the next open or close brace.
    start = 0
    while 1:
        match = _BRACE_RE.search(text, start)
        if match is None: break
        end = match.start()

        # Open braces start new colorizing elements.  When preceeded
        # by a capital letter, they specify a colored region, as
        # defined by the _COLORIZING_TAGS dictionary.  Otherwise,
        # use a special "literal braces" element (with tag "litbrace"),
        # and convert them to literal braces once we find the matching
        # close-brace.
        if match.group() == '{':
            if (end>0) and 'A' <= text[end-1] <= 'Z':
                if (end-1) > start:
                    stack[-1].children.append(text[start:end-1])
                if text[end-1] not in _COLORIZING_TAGS:
                    estr = "Unknown inline markup tag."
                    errors.append(ColorizingError(estr, token, end-1))
                    stack.append(Element('unknown'))
                else:
                    tag = _COLORIZING_TAGS[text[end-1]]
                    stack.append(Element(tag))
            else:
                if end > start:
                    stack[-1].children.append(text[start:end])
                stack.append(Element('litbrace'))
            openbrace_stack.append(end)
            stack[-2].children.append(stack[-1])

        # Close braces end colorizing elements.
        elif match.group() == '}':
            # Check for (and ignore) unbalanced braces.
            if len(stack) <= 1:
                estr = "Unbalanced '}'."
                errors.append(ColorizingError(estr, token, end))
                start = end + 1
                continue

            # Add any remaining text.
            if end > start:
                stack[-1].children.append(text[start:end])

            # Special handling for symbols:
            if stack[-1].tag == 'symbol':
                if (len(stack[-1].children) != 1 or
                    not isinstance(stack[-1].children[0], str)):
                    estr = "Invalid symbol code."
                    errors.append(ColorizingError(estr, token, end))
                else:
                    symb = stack[-1].children[0]
                    if symb in _SYMBOLS:
                        # It's a symbol
                        stack[-2].children[-1] = Element('symbol', symb)
                    else:
                        estr = "Invalid symbol code."
                        errors.append(ColorizingError(estr, token, end))

            # Special handling for escape elements:
            if stack[-1].tag == 'escape':
                if (len(stack[-1].children) != 1 or
                    not isinstance(stack[-1].children[0], str)):
                    estr = "Invalid escape code."
                    errors.append(ColorizingError(estr, token, end))
                else:
                    escp = stack[-1].children[0]
                    if escp in _ESCAPES:
                        # It's an escape from _ESCPAES
                        stack[-2].children[-1] = _ESCAPES[escp]
                    elif len(escp) == 1:
                        # It's a single-character escape (eg E{.})
                        stack[-2].children[-1] = escp
                    else:
                        estr = "Invalid escape code."
                        errors.append(ColorizingError(estr, token, end))

            # Special handling for literal braces elements:
            if stack[-1].tag == 'litbrace':
                stack[-2].children[-1:] = ['{'] + cast(List[str], stack[-1].children) + ['}']

            # Special handling for link-type elements:
            if stack[-1].tag in _LINK_COLORIZING_TAGS:
                _colorize_link(stack[-1], token, end, errors)

            # Pop the completed element.
            openbrace_stack.pop()
            stack.pop()

        start = end+1

    # Add any final text.
    if start < len(text):
        stack[-1].children.append(text[start:])

    if len(stack) != 1:
        estr = "Unbalanced '{'."
        errors.append(ColorizingError(estr, token, openbrace_stack[-1]))

    return stack[0]

def _colorize_link(link: Element, token: Token, end: int, errors: List[ParseError]) -> None:
    variables = link.children[:]

    # If the last child isn't text, we know it's bad.
    if len(variables)==0 or not isinstance(variables[-1], str):
        estr = f"Bad {link.tag} target."
        errors.append(ColorizingError(estr, token, end))
        return

    # Did they provide an explicit target?
    match2 = _TARGET_RE.match(variables[-1])
    if match2:
        (text, target) = match2.groups()
        variables[-1] = text
    # Can we extract an implicit target?
    elif len(variables) == 1:
        target = cast(str, variables[0])
    else:
        estr = f"Bad {link.tag} target."
        errors.append(ColorizingError(estr, token, end))
        return

    # Construct the name element.
    name_elt = Element('name', *variables)

    # Clean up the target.  For URIs, assume http or mailto if they
    # don't specify (no relative urls)
    target = re.sub(r'\s', '', target)
    if link.tag=='uri':
        if not re.match(r'\w+:', target):
            if re.match(r'\w+@(\w+)(\.\w+)*', target):
                target = 'mailto:' + target
            else:
                target = 'http://'+target
    elif link.tag=='link':
        # Remove arg lists for functions (e.g., L{_colorize_link()})
        target = re.sub(r'\(.*\)$', '', target)
        if not re.match(r'^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*$', target):
            estr = "Bad link target."
            errors.append(ColorizingError(estr, token, end))
            return

    # Construct the target element.
    target_elt = Element('target', target, lineno=str(token.startline))

    # Add them to the link element.
    link.children = [name_elt, target_elt]

##################################################
## Parse Errors
##################################################

class TokenizationError(ParseError):
    """
    An error generated while tokenizing a formatted documentation
    string.
    """

class StructuringError(ParseError):
    """
    An error generated while structuring a formatted documentation
    string.
    """

class ColorizingError(ParseError):
    """
    An error generated while colorizing a paragraph.
    """
    def __init__(self, descr: str, token: Token, charnum: int, is_fatal: bool = True):
        """
        Construct a new colorizing exception.

        @param descr: A short description of the error.
        @param token: The token where the error occured
        @param charnum: The character index of the position in
            C{token} where the error occured.
        """
        ParseError.__init__(self, descr, token.startline, is_fatal)
        self.token = token
        self.charnum = charnum

    CONTEXT_RANGE = 20
    def descr(self) -> str:
        RANGE = self.CONTEXT_RANGE
        if self.charnum <= RANGE:
            left = self.token.contents[0:self.charnum]
        else:
            left = '...'+self.token.contents[self.charnum-RANGE:self.charnum]
        if (len(self.token.contents)-self.charnum) <= RANGE:
            right = self.token.contents[self.charnum:]
        else:
            right = (self.token.contents[self.charnum:self.charnum+RANGE]
                     + '...')
        return f"{self._descr}\n\n{left}{right}\n{' '*len(left)}^"

#################################################################
##                    SUPPORT FOR EPYDOC
#################################################################

def parse_docstring(docstring: str, errors: List[ParseError], processtypes: bool = False) -> ParsedDocstring:
    """
    Parse the given docstring, which is formatted using epytext; and
    return a L{ParsedDocstring} representation of its contents.

    @param docstring: The docstring to parse
    @param errors: A list where any errors generated during parsing
        will be stored.
    @param processtypes: Use L{ParsedTypeDocstring} to parsed 'type' fields.
    """
    tree = parse(docstring, errors)
    if tree is None:
        return ParsedEpytextDocstring(None, ())

    tree_children = cast(List[Element], tree.children)

    fields = []
    if tree_children and tree_children[-1].tag == 'fieldlist':
        # Take field list out of the document tree.
        field_list = tree_children.pop()
        field_children = cast(List[Element], field_list.children)

        for field in field_children:
            # Get the tag
            tag = cast(str, cast(Element, field.children.pop(0)).children[0]).lower()

            # Get the argument.
            if field.children and cast(Element, field.children[0]).tag == 'arg':
                arg: Optional[str] = \
                    cast(str, cast(Element, field.children.pop(0)).children[0])
            else:
                arg = None

            # Process the field.
            field.tag = 'epytext'

            field_parsed_doc: ParsedDocstring = ParsedEpytextDocstring(field, ())

            lineno = int(field.attribs['lineno'])
            
            # This allows epytext markup to use TypeDocstring as well with a CLI option: --process-types
            if processtypes and tag in ParsedTypeDocstring.FIELDS:
                field_parsed_doc = ParsedTypeDocstring(field_parsed_doc.to_node(), lineno=lineno)
                for warning_msg in field_parsed_doc.warnings:
                    errors.append(ParseError(warning_msg, lineno, is_fatal=False))
            
            fields.append(Field(tag, arg, field_parsed_doc, lineno))

    # Save the remaining docstring as the description.
    if tree_children and tree_children[0].children:
        return ParsedEpytextDocstring(tree, fields)
    else:
        return ParsedEpytextDocstring(None, fields)

def get_parser(obj: Optional[Documentable]) -> Callable[[str, List[ParseError], bool], ParsedDocstring]:
    """
    Get the L{parse_docstring} function. 
    """
    return parse_docstring

class ParsedEpytextDocstring(ParsedDocstring):
    SYMBOL_TO_CODEPOINT = {
        # Symbols
        '<-': 8592, '->': 8594, '^': 8593, 'v': 8595,

        # Greek letters
        'alpha': 945, 'beta': 946, 'gamma': 947,
        'delta': 948, 'epsilon': 949, 'zeta': 950,
        'eta': 951, 'theta': 952, 'iota': 953,
        'kappa': 954, 'lambda': 955, 'mu': 956,
        'nu': 957, 'xi': 958, 'omicron': 959,
        'pi': 960, 'rho': 961, 'sigma': 963,
        'tau': 964, 'upsilon': 965, 'phi': 966,
        'chi': 967, 'psi': 968, 'omega': 969,
        'Alpha': 913, 'Beta': 914, 'Gamma': 915,
        'Delta': 916, 'Epsilon': 917, 'Zeta': 918,
        'Eta': 919, 'Theta': 920, 'Iota': 921,
        'Kappa': 922, 'Lambda': 923, 'Mu': 924,
        'Nu': 925, 'Xi': 926, 'Omicron': 927,
        'Pi': 928, 'Rho': 929, 'Sigma': 931,
        'Tau': 932, 'Upsilon': 933, 'Phi': 934,
        'Chi': 935, 'Psi': 936, 'Omega': 937,

        # HTML character entities
        'larr': 8592, 'rarr': 8594, 'uarr': 8593,
        'darr': 8595, 'harr': 8596, 'crarr': 8629,
        'lArr': 8656, 'rArr': 8658, 'uArr': 8657,
        'dArr': 8659, 'hArr': 8660,
        'copy': 169, 'times': 215, 'forall': 8704,
        'exist': 8707, 'part': 8706,
        'empty': 8709, 'isin': 8712, 'notin': 8713,
        'ni': 8715, 'prod': 8719, 'sum': 8721,
        'prop': 8733, 'infin': 8734, 'ang': 8736,
        'and': 8743, 'or': 8744, 'cap': 8745, 'cup': 8746,
        'int': 8747, 'there4': 8756, 'sim': 8764,
        'cong': 8773, 'asymp': 8776, 'ne': 8800,
        'equiv': 8801, 'le': 8804, 'ge': 8805,
        'sub': 8834, 'sup': 8835, 'nsub': 8836,
        'sube': 8838, 'supe': 8839, 'oplus': 8853,
        'otimes': 8855, 'perp': 8869,

        # Alternate (long) names
        'infinity': 8734, 'integral': 8747, 'product': 8719,
        '<=': 8804, '>=': 8805,
        }

    def __init__(self, body: Optional[Element], fields: Sequence['Field']):
        ParsedDocstring.__init__(self, fields)
        self._tree = body
        # Caching:
        self._stan: Optional[Tag] = None
        self._document: Optional[nodes.document] = None

    def __str__(self) -> str:
        return str(self._tree)

    @property
    def has_body(self) -> bool:
        return self._tree is not None
    
    def to_node(self) -> nodes.document:

        if self._document is not None:
            return self._document
        
        self._document = utils.new_document('epytext')
        
        if self._tree is not None:
            node, = self._to_node(self._tree)
            # The contents is encapsulated inside a section node. 
            # Reparent the contents of the second level to the root level. 
            self._document = set_node_attributes(self._document, children=node.children)
        
        return self._document
    
    def _to_node(self, tree: Element) -> Iterable[nodes.Node]:
        
        # Process the children first.
        variables: List[nodes.Node] = []
        for child in tree.children:
            if isinstance(child, str):
                variables.append(set_node_attributes(nodes.Text(child), document=self._document))
            else:
                variables.extend(self._to_node(child))

        # Perform the approriate action for the DOM tree type.
        if tree.tag == 'para':
            # tree.attribs.get('inline') does not exist anymore.
            # the choice to render the 

tags is handled in HTMLTranslator.should_be_compact_paragraph(), not here anymore yield set_node_attributes(nodes.paragraph('', ''), document=self._document, children=variables) elif tree.tag == 'code': yield set_node_attributes(nodes.literal('', ''), document=self._document, children=variables) elif tree.tag == 'uri': label, target = variables yield set_node_attributes(nodes.reference( '', internal=False, refuri=target), document=self._document, children=label.children) elif tree.tag == 'link': label, target = variables assert isinstance(target, nodes.Text) assert isinstance(label, nodes.inline) # Figure the line number to warn on precise lines. # This is needed only for links currently. lineno = int(cast(Element, tree.children[1]).attribs['lineno']) yield set_node_attributes(nodes.title_reference( '', '', refuri=target.astext()), document=self._document, lineno=lineno, children=label.children) elif tree.tag == 'name': # name can contain nested inline markup, so we use nodes.inline instead of nodes.Text yield set_node_attributes(nodes.inline('', ''), document=self._document, children=variables) elif tree.tag == 'target': value, = variables yield set_node_attributes(nodes.Text(value), document=self._document) elif tree.tag == 'italic': yield set_node_attributes(nodes.emphasis('', ''), document=self._document, children=variables) elif tree.tag == 'math': node = set_node_attributes(nodes.math('', ''), document=self._document, children=variables) node['classes'].append('math') yield node elif tree.tag == 'bold': yield set_node_attributes(nodes.strong('', ''), document=self._document, children=variables) elif tree.tag == 'ulist': yield set_node_attributes(nodes.bullet_list(''), document=self._document, children=variables) elif tree.tag == 'olist': yield set_node_attributes(nodes.enumerated_list(''), document=self._document, children=variables) elif tree.tag == 'li': yield set_node_attributes(nodes.list_item(''), document=self._document, children=variables) elif tree.tag == 'heading': yield set_node_attributes(nodes.title('', ''), document=self._document, children=variables) elif tree.tag == 'literalblock': yield set_node_attributes(nodes.literal_block('', ''), document=self._document, children=variables) elif tree.tag == 'doctestblock': yield set_node_attributes(nodes.doctest_block(tree.children[0], tree.children[0]), document=self._document) elif tree.tag in ('fieldlist', 'tag', 'arg'): raise AssertionError("There should not be any field lists left") elif tree.tag in ('section', 'epytext'): yield set_node_attributes(nodes.section(''), document=self._document, children=variables) elif tree.tag == 'symbol': symbol = cast(str, tree.children[0]) char = chr(self.SYMBOL_TO_CODEPOINT[symbol]) yield set_node_attributes(nodes.inline(symbol, char), document=self._document) else: raise AssertionError(f"Unknown epytext DOM element {tree.tag!r}") pydoctor-21.12.1/pydoctor/epydoc/markup/google.py000066400000000000000000000011641416703725300220060ustar00rootroot00000000000000""" Parser for google-style docstrings. @See: L{pydoctor.epydoc.markup.numpy} @See: L{pydoctor.epydoc.markup._napoleon} """ from typing import Callable, List, Optional from pydoctor.model import Documentable from pydoctor.epydoc.markup import ParseError, ParsedDocstring from pydoctor.epydoc.markup._napoleon import NapoelonDocstringParser def get_parser(obj: Optional[Documentable]) -> Callable[[str, List[ParseError], bool], ParsedDocstring]: """ Returns the parser function. Behaviour will depend on the documentable type and system options. """ return NapoelonDocstringParser(obj).parse_google_docstring pydoctor-21.12.1/pydoctor/epydoc/markup/numpy.py000066400000000000000000000011631416703725300217010ustar00rootroot00000000000000""" Parser for numpy-style docstrings. @See: L{pydoctor.epydoc.markup.google} @See: L{pydoctor.epydoc.markup._napoleon} """ from typing import Callable, List, Optional from pydoctor.model import Documentable from pydoctor.epydoc.markup import ParseError, ParsedDocstring from pydoctor.epydoc.markup._napoleon import NapoelonDocstringParser def get_parser(obj: Optional[Documentable]) -> Callable[[str, List[ParseError], bool], ParsedDocstring]: """ Returns the parser function. Behaviour will depend on the documentable type and system options. """ return NapoelonDocstringParser(obj).parse_numpy_docstring pydoctor-21.12.1/pydoctor/epydoc/markup/plaintext.py000066400000000000000000000047111416703725300225430ustar00rootroot00000000000000# # plaintext.py: plaintext docstring parsing # Edward Loper # # Created [04/10/01 12:00 AM] # """ Parser for plaintext docstrings. Plaintext docstrings are rendered as verbatim output, preserving all whitespace. """ __docformat__ = 'epytext en' from typing import List, Callable, Optional from docutils import nodes from twisted.web.template import Tag, tags from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring, ParseError from pydoctor.model import Documentable def parse_docstring(docstring: str, errors: List[ParseError], processtypes: bool = False) -> ParsedDocstring: """ Parse the given docstring, which is formatted as plain text; and return a L{ParsedDocstring} representation of its contents. @param docstring: The docstring to parse @param errors: A list where any errors generated during parsing will be stored. """ return ParsedPlaintextDocstring(docstring) def get_parser(obj: Optional[Documentable]) -> Callable[[str, List[ParseError], bool], ParsedDocstring]: """ Just return the L{parse_docstring} function. """ return parse_docstring class ParsedPlaintextDocstring(ParsedDocstring): def __init__(self, text: str): ParsedDocstring.__init__(self, ()) self._text = text # Caching: # self._document: Optional[nodes.document] = None @property def has_body(self) -> bool: return bool(self._text) # plaintext parser overrides the default to_stan() method for performance and design reasons. # We don't want to use docutils to process the plaintext format because we won't # actually use the document tree ,it does not contains any additionnalt information compared to the raw docstring. # Also, the consolidated fields handling in restructuredtext.py relies on this "pre" class. def to_stan(self, docstring_linker: DocstringLinker) -> Tag: return tags.p(self._text, class_='pre') def to_node(self) -> nodes.document: raise NotImplementedError() # TODO: Delete this code when we're sure this is the right thing to do. # if self._document is not None: # return self._document # else: # self._document = utils.new_document('plaintext') # self._document = set_node_attributes(self._document, # children=set_nodes_parent((nodes.literal_block(rawsource=self._text, text=self._text)), self._document)) # return self._document pydoctor-21.12.1/pydoctor/epydoc/markup/restructuredtext.py000066400000000000000000000463501416703725300242000ustar00rootroot00000000000000# # restructuredtext.py: ReStructuredText docstring parsing # Edward Loper # # Created [06/28/03 02:52 AM] # """ Epydoc parser for ReStructuredText strings. ReStructuredText is the standard markup language used by the Docutils project. L{parse_docstring()} provides the primary interface to this module; it returns a L{ParsedRstDocstring}, which supports all of the methods defined by L{ParsedDocstring}. L{ParsedRstDocstring} is basically just a L{ParsedDocstring} wrapper for the C{docutils.nodes.document} class. B{Creating C{ParsedRstDocstring}s}: C{ParsedRstDocstring}s are created by the L{parse_docstring} function, using the C{docutils.core.publish_string()} method, with the following helpers: - An L{_EpydocReader} is used to capture all error messages as it parses the docstring. - A L{_DocumentPseudoWriter} is used to extract the document itself, without actually writing any output. The document is saved for further processing. The settings for the writer are copied from C{docutils.writers.html4css1.Writer}, since those settings will be used when we actually write the docstring to html. @var CONSOLIDATED_FIELDS: A dictionary encoding the set of 'consolidated fields' that can be used. Each consolidated field is marked by a single tag, and contains a single bulleted list, where each list item starts with an identifier, marked as interpreted text (C{`...`}). This module automatically splits these consolidated fields into individual fields. The keys of C{CONSOLIDATED_FIELDS} are the names of possible consolidated fields; and the values are the names of the field tags that should be used for individual entries in the list. """ __docformat__ = 'epytext en' from typing import Callable, Iterable, List, Optional, Sequence, Set, cast import re from docutils import nodes from docutils.core import publish_string from docutils.writers import Writer from docutils.parsers.rst.directives.admonitions import BaseAdmonition # type: ignore[import] from docutils.readers.standalone import Reader as StandaloneReader from docutils.utils import Reporter, new_document from docutils.parsers.rst import Directive, directives #type: ignore[attr-defined] from docutils.transforms import Transform, frontmatter from pydoctor.epydoc.markup import Field, ParseError, ParsedDocstring from pydoctor.epydoc.markup.plaintext import ParsedPlaintextDocstring from pydoctor.epydoc.markup._types import ParsedTypeDocstring from pydoctor.model import Documentable #: A dictionary whose keys are the "consolidated fields" that are #: recognized by epydoc; and whose values are the corresponding epydoc #: field names that should be used for the individual fields. CONSOLIDATED_FIELDS = { 'parameters': 'param', 'arguments': 'arg', 'exceptions': 'except', 'variables': 'var', 'ivariables': 'ivar', 'cvariables': 'cvar', 'groups': 'group', 'types': 'type', 'keywords': 'keyword', } #: A list of consolidated fields whose bodies may be specified using a #: definition list, rather than a bulleted list. For these fields, the #: 'classifier' for each term in the definition list is translated into #: a @type field. CONSOLIDATED_DEFLIST_FIELDS = ['param', 'arg', 'var', 'ivar', 'cvar', 'keyword'] def parse_docstring(docstring: str, errors: List[ParseError], processtypes: bool = False) -> ParsedDocstring: """ Parse the given docstring, which is formatted using ReStructuredText; and return a L{ParsedDocstring} representation of its contents. @param docstring: The docstring to parse @param errors: A list where any errors generated during parsing will be stored. @param processtypes: Use L{ParsedTypeDocstring} to parsed 'type' fields. """ writer = _DocumentPseudoWriter() reader = _EpydocReader(errors) # Outputs errors to the list. # Credits: mhils - Maximilian Hils from the pdoc repository https://github.com/mitmproxy/pdoc # Strip Sphinx interpreted text roles for code references: :obj:`foo` -> `foo` docstring = re.sub( r"(:py)?:(mod|func|data|const|class|meth|attr|exc|obj):", "", docstring ) publish_string(docstring, writer=writer, reader=reader, settings_overrides={'report_level':10000, 'halt_level':10000, 'warning_stream':None}) document = writer.document visitor = _SplitFieldsTranslator(document, errors, processtypes=processtypes) document.walk(visitor) return ParsedRstDocstring(document, visitor.fields) def get_parser(obj:Documentable) -> Callable[[str, List[ParseError], bool], ParsedDocstring]: """ Get the L{parse_docstring} function. """ return parse_docstring class OptimizedReporter(Reporter): """A reporter that ignores all debug messages. This is used to shave a couple seconds off of epydoc's run time, since docutils isn't very fast about processing its own debug messages. """ def debug(self, *args: object, **kwargs: object) -> None: pass class ParsedRstDocstring(ParsedDocstring): """ An encoded version of a ReStructuredText docstring. The contents of the docstring are encoded in the L{_document} instance variable. """ def __init__(self, document: nodes.document, fields: Sequence[Field]): self._document = document """A ReStructuredText document, encoding the docstring.""" document.reporter = OptimizedReporter( document.reporter.source, 'SEVERE', 'SEVERE', '') ParsedDocstring.__init__(self, fields) @property def has_body(self) -> bool: return any( isinstance(child, nodes.Text) or child.children for child in self._document.children ) def to_node(self) -> nodes.document: return self._document def __repr__(self) -> str: return '' class _EpydocReader(StandaloneReader): """ A reader that captures all errors that are generated by parsing, and appends them to a list as L{ParseError}. """ def __init__(self, errors: List[ParseError]): self._errors = errors StandaloneReader.__init__(self) def get_transforms(self) -> List[Transform]: # Remove the DocInfo transform, to ensure that :author: fields # are correctly handled. return [t for t in StandaloneReader.get_transforms(self) if t != frontmatter.DocInfo] def new_document(self) -> nodes.document: document = new_document(self.source.source_path, self.settings) # Capture all warning messages. document.reporter.attach_observer(self.report) # Return the new document. return document def report(self, error: nodes.system_message) -> None: level: int = error['level'] is_fatal = level >= Reporter.ERROR_LEVEL linenum: Optional[int] = error.get('line') msg = ''.join(c.astext() for c in error) self._errors.append(ParseError(msg, linenum, is_fatal)) class _DocumentPseudoWriter(Writer): """ A pseudo-writer for the docutils framework, that can be used to access the document itself. The output of C{_DocumentPseudoWriter} is just an empty string; but after it has been used, the most recently processed document is available as the instance variable C{document}. """ document: nodes.document """The most recently processed document.""" def translate(self) -> None: self.output = '' class _SplitFieldsTranslator(nodes.NodeVisitor): """ A docutils translator that removes all fields from a document, and collects them into the instance variable C{fields} @ivar fields: The fields of the most recently walked document. @type fields: C{list} of L{Field} """ ALLOW_UNMARKED_ARG_IN_CONSOLIDATED_FIELD = True """If true, then consolidated fields are not required to mark arguments with C{`backticks`}. (This is currently only implemented for consolidated fields expressed as definition lists; consolidated fields expressed as unordered lists still require backticks for now.""" def __init__(self, document: nodes.document, errors: List[ParseError], processtypes: bool = False): nodes.NodeVisitor.__init__(self, document) self._errors = errors self.fields: List[Field] = [] self._newfields: Set[str] = set() self._processtypes = processtypes def visit_document(self, node: nodes.Node) -> None: self.fields = [] def visit_field(self, node: nodes.Node) -> None: # Remove the field from the tree. node.parent.remove(node) # Extract the field name & optional argument # FIXME: https://github.com/twisted/pydoctor/issues/267 # Support combined parameter type and description, if the type is a single word like:: # :param str user_agent: user agent tag = node[0].astext().split(None, 1) tagname = tag[0] if len(tag)>1: arg = tag[1] else: arg = None # Handle special fields: fbody = node[1] if arg is None: for (list_tag, entry_tag) in CONSOLIDATED_FIELDS.items(): if tagname.lower() == list_tag: try: self.handle_consolidated_field(fbody, entry_tag) return except ValueError as e: estr = 'Unable to split consolidated field ' estr += f'"{tagname}" - {e}' self._errors.append(ParseError(estr, node.line, is_fatal=False)) # Use a @newfield to let it be displayed as-is. if tagname.lower() not in self._newfields: newfield = Field('newfield', tagname.lower(), ParsedPlaintextDocstring(tagname), node.line - 1) self.fields.append(newfield) self._newfields.add(tagname.lower()) self._add_field(tagname, arg, fbody, node.line) def _add_field(self, tagname: str, arg: Optional[str], fbody: Iterable[nodes.Node], lineno: int ) -> None: field_doc = self.document.copy() for child in fbody: field_doc.append(child) # This allows restructuredtext markup to use TypeDocstring as well with a CLI option: --process-types field_parsed_doc: ParsedDocstring if self._processtypes and tagname in ParsedTypeDocstring.FIELDS: field_parsed_doc = ParsedTypeDocstring(field_doc) for warning_msg in field_parsed_doc.warnings: self._errors.append(ParseError(warning_msg, lineno, is_fatal=False)) else: field_parsed_doc = ParsedRstDocstring(field_doc, ()) self.fields.append(Field(tagname, arg, field_parsed_doc, lineno - 1)) def visit_field_list(self, node: nodes.Node) -> None: # Remove the field list from the tree. The visitor will still walk # over the node's children. node.parent.remove(node) def handle_consolidated_field(self, body: Sequence[nodes.Node], tagname: str) -> None: """ Attempt to handle a consolidated section. """ if len(body) != 1: raise ValueError('does not contain a single list.') elif body[0].tagname == 'bullet_list': self.handle_consolidated_bullet_list(body[0], tagname) elif (body[0].tagname == 'definition_list' and tagname in CONSOLIDATED_DEFLIST_FIELDS): self.handle_consolidated_definition_list(body[0], tagname) elif tagname in CONSOLIDATED_DEFLIST_FIELDS: raise ValueError('does not contain a bulleted list or ' 'definition list.') else: raise ValueError('does not contain a bulleted list.') def handle_consolidated_bullet_list(self, items: Iterable[nodes.Node], tagname: str) -> None: # Check the contents of the list. In particular, each list # item should have the form: # - `arg`: description... n = 0 _BAD_ITEM = ("list item %d is not well formed. Each item must " "consist of a single marked identifier (e.g., `x`), " "optionally followed by a colon or dash and a " "description.") for item in items: n += 1 if item.tagname != 'list_item' or len(item) == 0: raise ValueError('bad bulleted list (bad child %d).' % n) if item[0].tagname != 'paragraph': if item[0].tagname == 'definition_list': raise ValueError(('list item %d contains a definition '+ 'list (it\'s probably indented '+ 'wrong).') % n) else: raise ValueError(_BAD_ITEM % n) if len(item[0]) == 0: raise ValueError(_BAD_ITEM % n) if item[0][0].tagname != 'title_reference': raise ValueError(_BAD_ITEM % n) # Everything looks good; convert to multiple fields. for item in items: # Extract the arg arg = item[0][0].astext() # Extract the field body, and remove the arg fbody = item[:] fbody[0] = fbody[0].copy() fbody[0][:] = item[0][1:] # Remove the separating ":", if present if (len(fbody[0]) > 0 and isinstance(fbody[0][0], nodes.Text)): text = fbody[0][0].astext() if text[:1] in ':-': fbody[0][0] = nodes.Text( text[1:].lstrip(), fbody[0][0].astext() ) elif text[:2] in (' -', ' :'): fbody[0][0] = nodes.Text( text[2:].lstrip(), fbody[0][0].astext() ) # Wrap the field body, and add a new field self._add_field(tagname, arg, fbody, fbody[0].line) def handle_consolidated_definition_list(self, items: Iterable[nodes.Node], tagname: str) -> None: # Check the list contents. n = 0 _BAD_ITEM = ("item %d is not well formed. Each item's term must " "consist of a single marked identifier (e.g., `x`), " "optionally followed by a space, colon, space, and " "a type description.") for item in items: n += 1 if (item.tagname != 'definition_list_item' or len(item) < 2 or item[-1].tagname != 'definition'): raise ValueError('bad definition list (bad child %d).' % n) if len(item) > 3: raise ValueError(_BAD_ITEM % n) if not ((item[0][0].tagname == 'title_reference') or (self.ALLOW_UNMARKED_ARG_IN_CONSOLIDATED_FIELD and isinstance(item[0][0], nodes.Text))): raise ValueError(_BAD_ITEM % n) for child in item[0][1:]: if child.astext() != '': raise ValueError(_BAD_ITEM % n) # Extract it. for item in items: # The basic field. arg = item[0][0].astext() lineno = item[0].line fbody = item[-1] self._add_field(tagname, arg, fbody, lineno) # If there's a classifier, treat it as a type. if len(item) == 3: type_descr = item[1] self._add_field('type', arg, type_descr, lineno) def unknown_visit(self, node: nodes.Node) -> None: 'Ignore all unknown nodes' versionlabels = { 'versionadded': 'New in version %s', 'versionchanged': 'Changed in version %s', 'deprecated': 'Deprecated since version %s', } versionlabel_classes = { 'versionadded': 'added', 'versionchanged': 'changed', 'deprecated': 'deprecated', } class VersionChange(Directive): """ Directive to describe a change/addition/deprecation in a specific version. """ class versionmodified(nodes.Admonition, nodes.TextElement): """Node for version change entries. Currently used for "versionadded", "versionchanged" and "deprecated" directives. """ has_content = True required_arguments = 1 optional_arguments = 1 final_argument_whitespace = True def run(self) -> List[nodes.Node]: node = self.versionmodified() node.document = self.state.document node['type'] = self.name node['version'] = self.arguments[0] text = versionlabels[self.name] % self.arguments[0] if len(self.arguments) == 2: inodes, messages = self.state.inline_text(self.arguments[1], self.lineno + 1) para = nodes.paragraph(self.arguments[1], '', *inodes) node.append(para) else: messages = [] if self.content: self.state.nested_parse(self.content, self.content_offset, node) classes = ['versionmodified', versionlabel_classes[self.name]] if len(node): if isinstance(node[0], nodes.paragraph) and node[0].rawsource: content = nodes.inline(node[0].rawsource) content.source = node[0].source content.line = node[0].line content += node[0].children node[0].replace_self(nodes.paragraph('', '', content)) para = cast(nodes.paragraph, node[0]) para.insert(0, nodes.inline('', '%s: ' % text, classes=classes)) else: para = nodes.paragraph('', '', nodes.inline('', '%s.' % text, classes=classes), ) node.append(para) ret = [node] # type: List[nodes.Node] ret += messages return ret # Do like Sphinx does for the seealso directive. class SeeAlso(BaseAdmonition): """ An admonition mentioning things to look at as reference. """ class seealso(nodes.Admonition, nodes.Element): """Custom "see also" admonition node.""" node_class = seealso class PythonCodeDirective(Directive): """ A custom restructuredtext directive which can be used to display syntax-highlighted Python code blocks. This directive takes no arguments, and the body should contain only Python code. This directive can be used instead of doctest blocks when it is inconvenient to list prompts on each line, or when you would prefer that the output not contain prompts (e.g., to make copy/paste easier). """ has_content = True def run(self) -> List[nodes.Node]: text = '\n'.join(self.content) node = nodes.doctest_block(text, text, codeblock=True) return [ node ] directives.register_directive('python', PythonCodeDirective) directives.register_directive('versionadded', VersionChange) directives.register_directive('versionchanged', VersionChange) directives.register_directive('deprecated', VersionChange) directives.register_directive('seealso', SeeAlso) pydoctor-21.12.1/pydoctor/epydoc/sre_parse36.py000066400000000000000000001124061416703725300213710ustar00rootroot00000000000000# Code copied from Python 3.6 - Python Software Foundation - GNU General Public License v3.0 # # The motivation to add the ``sre_parse36`` module is to provide a # colorizer for regular expressions that produce the *same* expression # as initially provided (the way epydoc did it). # It's packaged with pydoctor for the simplicity of not having to install another requirement form PyPi. # # The handling of non-capturing groups changed from Python 3.7, we can't # back reproduce the original regular expression from a ``SubPattern`` # instance anymore. This regression is tracked at https://bugs.python.org/issue45674. # It seems that it won't be fixed. # # The the issue is that in Python 3.7 and beyond, it not possible to # differentiate capturing groups and non-capturing from a ``SubPattern`` # intance. # # Demontration: # ```python # >>> import sre_parse # >>> sre_parse.parse("(?:foo (?:bar) | (?:baz))").dump() # BRANCH # LITERAL 102 # LITERAL 111 # LITERAL 111 # LITERAL 32 # LITERAL 98 # LITERAL 97 # LITERAL 114 # LITERAL 32 # OR # LITERAL 32 # LITERAL 98 # LITERAL 97 # LITERAL 122 # ``` # # Whereas in Python 3.6: # # ```python # >>> import sre_parse # >>> sre_parse.parse("(?:foo (?:bar) | (?:baz))").dump() # SUBPATTERN None 0 0 # BRANCH # LITERAL 102 # LITERAL 111 # LITERAL 111 # LITERAL 32 # SUBPATTERN None 0 0 # LITERAL 98 # LITERAL 97 # LITERAL 114 # LITERAL 32 # OR # LITERAL 32 # SUBPATTERN None 0 0 # LITERAL 98 # LITERAL 97 # LITERAL 122 # ``` # # ------------------------------- # # # Secret Labs' Regular Expression Engine # # convert re-style regular expression to sre pattern # # Copyright (c) 1998-2001 by Secret Labs AB. All rights reserved. # # See the sre.py file for information on usage and redistribution. # """Internal support module for sre""" # XXX: show string offset and offending character for all errors from sre_constants import * SPECIAL_CHARS = ".\\[{()*+?^$|" REPEAT_CHARS = "*+?{" DIGITS = frozenset("0123456789") OCTDIGITS = frozenset("01234567") HEXDIGITS = frozenset("0123456789abcdefABCDEF") ASCIILETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") WHITESPACE = frozenset(" \t\n\r\v\f") _REPEATCODES = frozenset({MIN_REPEAT, MAX_REPEAT}) _UNITCODES = frozenset({ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY}) ESCAPES = { r"\a": (LITERAL, ord("\a")), r"\b": (LITERAL, ord("\b")), r"\f": (LITERAL, ord("\f")), r"\n": (LITERAL, ord("\n")), r"\r": (LITERAL, ord("\r")), r"\t": (LITERAL, ord("\t")), r"\v": (LITERAL, ord("\v")), r"\\": (LITERAL, ord("\\")) } CATEGORIES = { r"\A": (AT, AT_BEGINNING_STRING), # start of string r"\b": (AT, AT_BOUNDARY), r"\B": (AT, AT_NON_BOUNDARY), r"\d": (IN, [(CATEGORY, CATEGORY_DIGIT)]), r"\D": (IN, [(CATEGORY, CATEGORY_NOT_DIGIT)]), r"\s": (IN, [(CATEGORY, CATEGORY_SPACE)]), r"\S": (IN, [(CATEGORY, CATEGORY_NOT_SPACE)]), r"\w": (IN, [(CATEGORY, CATEGORY_WORD)]), r"\W": (IN, [(CATEGORY, CATEGORY_NOT_WORD)]), r"\Z": (AT, AT_END_STRING), # end of string } FLAGS = { # standard flags "i": SRE_FLAG_IGNORECASE, "L": SRE_FLAG_LOCALE, "m": SRE_FLAG_MULTILINE, "s": SRE_FLAG_DOTALL, "x": SRE_FLAG_VERBOSE, # extensions "a": SRE_FLAG_ASCII, "t": SRE_FLAG_TEMPLATE, "u": SRE_FLAG_UNICODE, } GLOBAL_FLAGS = (SRE_FLAG_ASCII | SRE_FLAG_LOCALE | SRE_FLAG_UNICODE | SRE_FLAG_DEBUG | SRE_FLAG_TEMPLATE) class Verbose(Exception): pass class Pattern: # master pattern object. keeps track of global attributes def __init__(self): self.flags = 0 self.groupdict = {} self.groupwidths = [None] # group 0 self.lookbehindgroups = None @property def groups(self): return len(self.groupwidths) def opengroup(self, name=None): gid = self.groups self.groupwidths.append(None) if self.groups > MAXGROUPS: raise error("too many groups") if name is not None: ogid = self.groupdict.get(name, None) if ogid is not None: raise error("redefinition of group name %r as group %d; " "was group %d" % (name, gid, ogid)) self.groupdict[name] = gid return gid def closegroup(self, gid, p): self.groupwidths[gid] = p.getwidth() def checkgroup(self, gid): return gid < self.groups and self.groupwidths[gid] is not None def checklookbehindgroup(self, gid, source): if self.lookbehindgroups is not None: if not self.checkgroup(gid): raise source.error('cannot refer to an open group') if gid >= self.lookbehindgroups: raise source.error('cannot refer to group defined in the same ' 'lookbehind subpattern') class SubPattern: # a subpattern, in intermediate form def __init__(self, pattern, data=None): self.pattern = pattern if data is None: data = [] self.data = data self.width = None def dump(self, level=0): nl = True seqtypes = (tuple, list) for op, av in self.data: print(level*" " + str(op), end='') if op is IN: # member sublanguage print() for op, a in av: print((level+1)*" " + str(op), a) elif op is BRANCH: print() for i, a in enumerate(av[1]): if i: print(level*" " + "OR") a.dump(level+1) elif op is GROUPREF_EXISTS: condgroup, item_yes, item_no = av print('', condgroup) item_yes.dump(level+1) if item_no: print(level*" " + "ELSE") item_no.dump(level+1) elif isinstance(av, seqtypes): nl = False for a in av: if isinstance(a, SubPattern): if not nl: print() a.dump(level+1) nl = True else: if not nl: print(' ', end='') print(a, end='') nl = False if not nl: print() else: print('', av) def __repr__(self): return repr(self.data) def __len__(self): return len(self.data) def __delitem__(self, index): del self.data[index] def __getitem__(self, index): if isinstance(index, slice): return SubPattern(self.pattern, self.data[index]) return self.data[index] def __setitem__(self, index, code): self.data[index] = code def insert(self, index, code): self.data.insert(index, code) def append(self, code): self.data.append(code) def getwidth(self): # determine the width (min, max) for this subpattern if self.width is not None: return self.width lo = hi = 0 for op, av in self.data: if op is BRANCH: i = MAXREPEAT - 1 j = 0 for av in av[1]: l, h = av.getwidth() i = min(i, l) j = max(j, h) lo = lo + i hi = hi + j elif op is CALL: i, j = av.getwidth() lo = lo + i hi = hi + j elif op is SUBPATTERN: i, j = av[-1].getwidth() lo = lo + i hi = hi + j elif op in _REPEATCODES: i, j = av[2].getwidth() lo = lo + i * av[0] hi = hi + j * av[1] elif op in _UNITCODES: lo = lo + 1 hi = hi + 1 elif op is GROUPREF: i, j = self.pattern.groupwidths[av] lo = lo + i hi = hi + j elif op is GROUPREF_EXISTS: i, j = av[1].getwidth() if av[2] is not None: l, h = av[2].getwidth() i = min(i, l) j = max(j, h) else: i = 0 lo = lo + i hi = hi + j elif op is SUCCESS: break self.width = min(lo, MAXREPEAT - 1), min(hi, MAXREPEAT) return self.width class Tokenizer: def __init__(self, string): self.istext = isinstance(string, str) self.string = string if not self.istext: string = str(string, 'latin1') self.decoded_string = string self.index = 0 self.next = None self.__next() def __next(self): index = self.index try: char = self.decoded_string[index] except IndexError: self.next = None return if char == "\\": index += 1 try: char += self.decoded_string[index] except IndexError: raise error("bad escape (end of pattern)", self.string, len(self.string) - 1) from None self.index = index + 1 self.next = char def match(self, char): if char == self.next: self.__next() return True return False def get(self): this = self.next self.__next() return this def getwhile(self, n, charset): result = '' for _ in range(n): c = self.next if c not in charset: break result += c self.__next() return result def getuntil(self, terminator): result = '' while True: c = self.next self.__next() if c is None: if not result: raise self.error("missing group name") raise self.error("missing %s, unterminated name" % terminator, len(result)) if c == terminator: if not result: raise self.error("missing group name", 1) break result += c return result @property def pos(self): return self.index - len(self.next or '') def tell(self): return self.index - len(self.next or '') def seek(self, index): self.index = index self.__next() def error(self, msg, offset=0): return error(msg, self.string, self.tell() - offset) def _class_escape(source, escape): # handle escape code inside character class code = ESCAPES.get(escape) if code: return code code = CATEGORIES.get(escape) if code and code[0] is IN: return code try: c = escape[1:2] if c == "x": # hexadecimal escape (exactly two digits) escape += source.getwhile(2, HEXDIGITS) if len(escape) != 4: raise source.error("incomplete escape %s" % escape, len(escape)) return LITERAL, int(escape[2:], 16) elif c == "u" and source.istext: # unicode escape (exactly four digits) escape += source.getwhile(4, HEXDIGITS) if len(escape) != 6: raise source.error("incomplete escape %s" % escape, len(escape)) return LITERAL, int(escape[2:], 16) elif c == "U" and source.istext: # unicode escape (exactly eight digits) escape += source.getwhile(8, HEXDIGITS) if len(escape) != 10: raise source.error("incomplete escape %s" % escape, len(escape)) c = int(escape[2:], 16) chr(c) # raise ValueError for invalid code return LITERAL, c elif c in OCTDIGITS: # octal escape (up to three digits) escape += source.getwhile(2, OCTDIGITS) c = int(escape[1:], 8) if c > 0o377: raise source.error('octal escape value %s outside of ' 'range 0-0o377' % escape, len(escape)) return LITERAL, c elif c in DIGITS: raise ValueError if len(escape) == 2: if c in ASCIILETTERS: raise source.error('bad escape %s' % escape, len(escape)) return LITERAL, ord(escape[1]) except ValueError: pass raise source.error("bad escape %s" % escape, len(escape)) def _escape(source, escape, state): # handle escape code in expression code = CATEGORIES.get(escape) if code: return code code = ESCAPES.get(escape) if code: return code try: c = escape[1:2] if c == "x": # hexadecimal escape escape += source.getwhile(2, HEXDIGITS) if len(escape) != 4: raise source.error("incomplete escape %s" % escape, len(escape)) return LITERAL, int(escape[2:], 16) elif c == "u" and source.istext: # unicode escape (exactly four digits) escape += source.getwhile(4, HEXDIGITS) if len(escape) != 6: raise source.error("incomplete escape %s" % escape, len(escape)) return LITERAL, int(escape[2:], 16) elif c == "U" and source.istext: # unicode escape (exactly eight digits) escape += source.getwhile(8, HEXDIGITS) if len(escape) != 10: raise source.error("incomplete escape %s" % escape, len(escape)) c = int(escape[2:], 16) chr(c) # raise ValueError for invalid code return LITERAL, c elif c == "0": # octal escape escape += source.getwhile(2, OCTDIGITS) return LITERAL, int(escape[1:], 8) elif c in DIGITS: # octal escape *or* decimal group reference (sigh) if source.next in DIGITS: escape += source.get() if (escape[1] in OCTDIGITS and escape[2] in OCTDIGITS and source.next in OCTDIGITS): # got three octal digits; this is an octal escape escape += source.get() c = int(escape[1:], 8) if c > 0o377: raise source.error('octal escape value %s outside of ' 'range 0-0o377' % escape, len(escape)) return LITERAL, c # not an octal escape, so this is a group reference group = int(escape[1:]) if group < state.groups: if not state.checkgroup(group): raise source.error("cannot refer to an open group", len(escape)) state.checklookbehindgroup(group, source) return GROUPREF, group raise source.error("invalid group reference %d" % group, len(escape) - 1) if len(escape) == 2: if c in ASCIILETTERS: raise source.error("bad escape %s" % escape, len(escape)) return LITERAL, ord(escape[1]) except ValueError: pass raise source.error("bad escape %s" % escape, len(escape)) def _parse_sub(source, state, verbose, nested): # parse an alternation: a|b|c items = [] itemsappend = items.append sourcematch = source.match start = source.tell() while True: itemsappend(_parse(source, state, verbose, nested + 1, not nested and not items)) if not sourcematch("|"): break if len(items) == 1: return items[0] subpattern = SubPattern(state) subpatternappend = subpattern.append # check if all items share a common prefix while True: prefix = None for item in items: if not item: break if prefix is None: prefix = item[0] elif item[0] != prefix: break else: # all subitems start with a common "prefix". # move it out of the branch for item in items: del item[0] subpatternappend(prefix) continue # check next one break # check if the branch can be replaced by a character set for item in items: if len(item) != 1 or item[0][0] is not LITERAL: break else: # we can store this as a character set instead of a # branch (the compiler may optimize this even more) subpatternappend((IN, [item[0] for item in items])) return subpattern subpattern.append((BRANCH, (None, items))) return subpattern def _parse_sub_cond(source, state, condgroup, verbose, nested): item_yes = _parse(source, state, verbose, nested + 1) if source.match("|"): item_no = _parse(source, state, verbose, nested + 1) if source.next == "|": raise source.error("conditional backref with more than two branches") else: item_no = None subpattern = SubPattern(state) subpattern.append((GROUPREF_EXISTS, (condgroup, item_yes, item_no))) return subpattern def _parse(source, state, verbose, nested, first=False): # parse a simple pattern subpattern = SubPattern(state) # precompute constants into local variables subpatternappend = subpattern.append sourceget = source.get sourcematch = source.match _len = len _ord = ord while True: this = source.next if this is None: break # end of pattern if this in "|)": break # end of subpattern sourceget() if verbose: # skip whitespace and comments if this in WHITESPACE: continue if this == "#": while True: this = sourceget() if this is None or this == "\n": break continue if this[0] == "\\": code = _escape(source, this, state) subpatternappend(code) elif this not in SPECIAL_CHARS: subpatternappend((LITERAL, _ord(this))) elif this == "[": here = source.tell() - 1 # character set set = [] setappend = set.append ## if sourcematch(":"): ## pass # handle character classes if sourcematch("^"): setappend((NEGATE, None)) # check remaining characters start = set[:] while True: this = sourceget() if this is None: raise source.error("unterminated character set", source.tell() - here) if this == "]" and set != start: break elif this[0] == "\\": code1 = _class_escape(source, this) else: code1 = LITERAL, _ord(this) if sourcematch("-"): # potential range that = sourceget() if that is None: raise source.error("unterminated character set", source.tell() - here) if that == "]": if code1[0] is IN: code1 = code1[1][0] setappend(code1) setappend((LITERAL, _ord("-"))) break if that[0] == "\\": code2 = _class_escape(source, that) else: code2 = LITERAL, _ord(that) if code1[0] != LITERAL or code2[0] != LITERAL: msg = "bad character range %s-%s" % (this, that) raise source.error(msg, len(this) + 1 + len(that)) lo = code1[1] hi = code2[1] if hi < lo: msg = "bad character range %s-%s" % (this, that) raise source.error(msg, len(this) + 1 + len(that)) setappend((RANGE, (lo, hi))) else: if code1[0] is IN: code1 = code1[1][0] setappend(code1) # XXX: should move set optimization to compiler! if _len(set)==1 and set[0][0] is LITERAL: subpatternappend(set[0]) # optimization elif _len(set)==2 and set[0][0] is NEGATE and set[1][0] is LITERAL: subpatternappend((NOT_LITERAL, set[1][1])) # optimization else: # XXX: should add charmap optimization here subpatternappend((IN, set)) elif this in REPEAT_CHARS: # repeat previous item here = source.tell() if this == "?": min, max = 0, 1 elif this == "*": min, max = 0, MAXREPEAT elif this == "+": min, max = 1, MAXREPEAT elif this == "{": if source.next == "}": subpatternappend((LITERAL, _ord(this))) continue min, max = 0, MAXREPEAT lo = hi = "" while source.next in DIGITS: lo += sourceget() if sourcematch(","): while source.next in DIGITS: hi += sourceget() else: hi = lo if not sourcematch("}"): subpatternappend((LITERAL, _ord(this))) source.seek(here) continue if lo: min = int(lo) if min >= MAXREPEAT: raise OverflowError("the repetition number is too large") if hi: max = int(hi) if max >= MAXREPEAT: raise OverflowError("the repetition number is too large") if max < min: raise source.error("min repeat greater than max repeat", source.tell() - here) else: raise AssertionError("unsupported quantifier %r" % (char,)) # figure out which item to repeat if subpattern: item = subpattern[-1:] else: item = None if not item or (_len(item) == 1 and item[0][0] is AT): raise source.error("nothing to repeat", source.tell() - here + len(this)) if item[0][0] in _REPEATCODES: raise source.error("multiple repeat", source.tell() - here + len(this)) if sourcematch("?"): subpattern[-1] = (MIN_REPEAT, (min, max, item)) else: subpattern[-1] = (MAX_REPEAT, (min, max, item)) elif this == ".": subpatternappend((ANY, None)) elif this == "(": start = source.tell() - 1 group = True name = None condgroup = None add_flags = 0 del_flags = 0 if sourcematch("?"): # options char = sourceget() if char is None: raise source.error("unexpected end of pattern") if char == "P": # python extensions if sourcematch("<"): # named group: skip forward to end of name name = source.getuntil(">") if not name.isidentifier(): msg = "bad character in group name %r" % name raise source.error(msg, len(name) + 1) elif sourcematch("="): # named backreference name = source.getuntil(")") if not name.isidentifier(): msg = "bad character in group name %r" % name raise source.error(msg, len(name) + 1) gid = state.groupdict.get(name) if gid is None: msg = "unknown group name %r" % name raise source.error(msg, len(name) + 1) if not state.checkgroup(gid): raise source.error("cannot refer to an open group", len(name) + 1) state.checklookbehindgroup(gid, source) subpatternappend((GROUPREF, gid)) continue else: char = sourceget() if char is None: raise source.error("unexpected end of pattern") raise source.error("unknown extension ?P" + char, len(char) + 2) elif char == ":": # non-capturing group group = None elif char == "#": # comment while True: if source.next is None: raise source.error("missing ), unterminated comment", source.tell() - start) if sourceget() == ")": break continue elif char in "=!<": # lookahead assertions dir = 1 if char == "<": char = sourceget() if char is None: raise source.error("unexpected end of pattern") if char not in "=!": raise source.error("unknown extension ?<" + char, len(char) + 2) dir = -1 # lookbehind lookbehindgroups = state.lookbehindgroups if lookbehindgroups is None: state.lookbehindgroups = state.groups p = _parse_sub(source, state, verbose, nested + 1) if dir < 0: if lookbehindgroups is None: state.lookbehindgroups = None if not sourcematch(")"): raise source.error("missing ), unterminated subpattern", source.tell() - start) if char == "=": subpatternappend((ASSERT, (dir, p))) else: subpatternappend((ASSERT_NOT, (dir, p))) continue elif char == "(": # conditional backreference group condname = source.getuntil(")") group = None if condname.isidentifier(): condgroup = state.groupdict.get(condname) if condgroup is None: msg = "unknown group name %r" % condname raise source.error(msg, len(condname) + 1) else: try: condgroup = int(condname) if condgroup < 0: raise ValueError except ValueError: msg = "bad character in group name %r" % condname raise source.error(msg, len(condname) + 1) from None if not condgroup: raise source.error("bad group number", len(condname) + 1) if condgroup >= MAXGROUPS: msg = "invalid group reference %d" % condgroup raise source.error(msg, len(condname) + 1) state.checklookbehindgroup(condgroup, source) elif char in FLAGS or char == "-": # flags flags = _parse_flags(source, state, char) if flags is None: # global flags if not first or subpattern: import warnings warnings.warn( 'Flags not at the start of the expression %r%s' % ( source.string[:20], # truncate long regexes ' (truncated)' if len(source.string) > 20 else '', ), DeprecationWarning, stacklevel=nested + 6 ) if (state.flags & SRE_FLAG_VERBOSE) and not verbose: raise Verbose continue add_flags, del_flags = flags group = None else: raise source.error("unknown extension ?" + char, len(char) + 1) # parse group contents if group is not None: try: group = state.opengroup(name) except error as err: raise source.error(err.msg, len(name) + 1) from None if condgroup: p = _parse_sub_cond(source, state, condgroup, verbose, nested + 1) else: sub_verbose = ((verbose or (add_flags & SRE_FLAG_VERBOSE)) and not (del_flags & SRE_FLAG_VERBOSE)) p = _parse_sub(source, state, sub_verbose, nested + 1) if not source.match(")"): raise source.error("missing ), unterminated subpattern", source.tell() - start) if group is not None: state.closegroup(group, p) subpatternappend((SUBPATTERN, (group, add_flags, del_flags, p))) elif this == "^": subpatternappend((AT, AT_BEGINNING)) elif this == "$": subpattern.append((AT, AT_END)) else: raise AssertionError("unsupported special character %r" % (char,)) return subpattern def _parse_flags(source, state, char): sourceget = source.get add_flags = 0 del_flags = 0 if char != "-": while True: add_flags |= FLAGS[char] char = sourceget() if char is None: raise source.error("missing -, : or )") if char in ")-:": break if char not in FLAGS: msg = "unknown flag" if char.isalpha() else "missing -, : or )" raise source.error(msg, len(char)) if char == ")": state.flags |= add_flags return None if add_flags & GLOBAL_FLAGS: raise source.error("bad inline flags: cannot turn on global flag", 1) if char == "-": char = sourceget() if char is None: raise source.error("missing flag") if char not in FLAGS: msg = "unknown flag" if char.isalpha() else "missing flag" raise source.error(msg, len(char)) while True: del_flags |= FLAGS[char] char = sourceget() if char is None: raise source.error("missing :") if char == ":": break if char not in FLAGS: msg = "unknown flag" if char.isalpha() else "missing :" raise source.error(msg, len(char)) assert char == ":" if del_flags & GLOBAL_FLAGS: raise source.error("bad inline flags: cannot turn off global flag", 1) if add_flags & del_flags: raise source.error("bad inline flags: flag turned on and off", 1) return add_flags, del_flags def fix_flags(src, flags): # Check and fix flags according to the type of pattern (str or bytes) if isinstance(src, str): if flags & SRE_FLAG_LOCALE: raise ValueError("cannot use LOCALE flag with a str pattern") if not flags & SRE_FLAG_ASCII: flags |= SRE_FLAG_UNICODE elif flags & SRE_FLAG_UNICODE: raise ValueError("ASCII and UNICODE flags are incompatible") else: if flags & SRE_FLAG_UNICODE: raise ValueError("cannot use UNICODE flag with a bytes pattern") if flags & SRE_FLAG_LOCALE and flags & SRE_FLAG_ASCII: raise ValueError("ASCII and LOCALE flags are incompatible") return flags def parse(str, flags=0, pattern=None): # parse 're' pattern into list of (opcode, argument) tuples source = Tokenizer(str) if pattern is None: pattern = Pattern() pattern.flags = flags pattern.str = str try: p = _parse_sub(source, pattern, flags & SRE_FLAG_VERBOSE, 0) except Verbose: # the VERBOSE flag was switched on inside the pattern. to be # on the safe side, we'll parse the whole thing again... pattern = Pattern() pattern.flags = flags | SRE_FLAG_VERBOSE pattern.str = str source.seek(0) p = _parse_sub(source, pattern, True, 0) p.pattern.flags = fix_flags(str, p.pattern.flags) if source.next is not None: assert source.next == ")" raise source.error("unbalanced parenthesis") if flags & SRE_FLAG_DEBUG: p.dump() return p def parse_template(source, pattern): # parse 're' replacement string into list of literals and # group references s = Tokenizer(source) sget = s.get groups = [] literals = [] literal = [] lappend = literal.append def addgroup(index, pos): if index > pattern.groups: raise s.error("invalid group reference %d" % index, pos) if literal: literals.append(''.join(literal)) del literal[:] groups.append((len(literals), index)) literals.append(None) groupindex = pattern.groupindex while True: this = sget() if this is None: break # end of replacement string if this[0] == "\\": # group c = this[1] if c == "g": name = "" if not s.match("<"): raise s.error("missing <") name = s.getuntil(">") if name.isidentifier(): try: index = groupindex[name] except KeyError: raise IndexError("unknown group name %r" % name) else: try: index = int(name) if index < 0: raise ValueError except ValueError: raise s.error("bad character in group name %r" % name, len(name) + 1) from None if index >= MAXGROUPS: raise s.error("invalid group reference %d" % index, len(name) + 1) addgroup(index, len(name) + 1) elif c == "0": if s.next in OCTDIGITS: this += sget() if s.next in OCTDIGITS: this += sget() lappend(chr(int(this[1:], 8) & 0xff)) elif c in DIGITS: isoctal = False if s.next in DIGITS: this += sget() if (c in OCTDIGITS and this[2] in OCTDIGITS and s.next in OCTDIGITS): this += sget() isoctal = True c = int(this[1:], 8) if c > 0o377: raise s.error('octal escape value %s outside of ' 'range 0-0o377' % this, len(this)) lappend(chr(c)) if not isoctal: addgroup(int(this[1:]), len(this) - 1) else: try: this = chr(ESCAPES[this][1]) except KeyError: if c in ASCIILETTERS: import warnings warnings.warn('bad escape %s' % this, DeprecationWarning, stacklevel=4) lappend(this) else: lappend(this) if literal: literals.append(''.join(literal)) if not isinstance(source, str): # The tokenizer implicitly decodes bytes objects as latin-1, we must # therefore re-encode the final representation. literals = [None if s is None else s.encode('latin-1') for s in literals] return groups, literals def expand_template(template, match): g = match.group empty = match.string[:0] groups, literals = template literals = literals[:] try: for index, group in groups: literals[index] = g(group) or empty except IndexError: raise error("invalid group reference %d" % index) return empty.join(literals) pydoctor-21.12.1/pydoctor/epydoc2stan.py000066400000000000000000001043031416703725300202020ustar00rootroot00000000000000""" Convert L{pydoctor.epydoc} parsed markup into renderable content. """ from collections import defaultdict from typing import ( TYPE_CHECKING, Callable, ClassVar, DefaultDict, Dict, Generator, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Union ) import ast import itertools import attr from pydoctor import model from pydoctor.epydoc.markup import Field as EpydocField, ParseError, get_parser_by_name from twisted.web.template import Tag, tags from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring import pydoctor.epydoc.markup.plaintext from pydoctor.epydoc.markup._pyval_repr import colorize_pyval, colorize_inline_pyval if TYPE_CHECKING: from twisted.web.template import Flattenable def get_parser(obj: model.Documentable) -> Callable[[str, List[ParseError], bool], ParsedDocstring]: """ Get the C{parse_docstring(str, List[ParseError], bool) -> ParsedDocstring} function. """ # Use module's __docformat__ if specified, else use system's. docformat = obj.module.docformat or obj.system.options.docformat try: return get_parser_by_name(docformat, obj) except ImportError as e: msg = 'Error trying to import %r parser:\n\n %s: %s\n\nUsing plain text formatting only.'%( docformat, e.__class__.__name__, e) obj.system.msg('epydoc2stan', msg, thresh=-1, once=True) return pydoctor.epydoc.markup.plaintext.parse_docstring def get_docstring( obj: model.Documentable ) -> Tuple[Optional[str], Optional[model.Documentable]]: for source in obj.docsources(): doc = source.docstring if doc: return doc, source if doc is not None: # Treat empty docstring as undocumented. return None, source return None, None def taglink(o: model.Documentable, page_url: str, label: Optional["Flattenable"] = None) -> Tag: if not o.isVisible: o.system.msg("html", "don't link to %s"%o.fullName()) if label is None: label = o.fullName() url = o.url if url.startswith(page_url + '#'): # When linking to an item on the same page, omit the path. # Besides shortening the HTML, this also avoids the page being reloaded # if the query string is non-empty. url = url[len(page_url):] ret: Tag = tags.a(label, href=url) return ret class _EpydocLinker(DocstringLinker): def __init__(self, obj: model.Documentable): self.obj = obj def look_for_name(self, name: str, candidates: Iterable[model.Documentable], lineno: int ) -> Optional[model.Documentable]: part0 = name.split('.')[0] potential_targets = [] for src in candidates: if part0 not in src.contents: continue target = src.resolveName(name) if target is not None and target not in potential_targets: potential_targets.append(target) if len(potential_targets) == 1: return potential_targets[0] elif len(potential_targets) > 1: self.obj.report( "ambiguous ref to %s, could be %s" % ( name, ', '.join(ob.fullName() for ob in potential_targets)), 'resolve_identifier_xref', lineno) return None def look_for_intersphinx(self, name: str) -> Optional[str]: """ Return link for `name` based on intersphinx inventory. Return None if link is not found. """ return self.obj.system.intersphinx.getLink(name) def link_to(self, identifier: str, label: "Flattenable") -> Tag: fullID = self.obj.expandName(identifier) target = self.obj.system.objForFullName(fullID) if target is not None: return taglink(target, self.obj.page_object.url, label) url = self.look_for_intersphinx(fullID) if url is not None: return tags.a(label, href=url) return tags.transparent(label) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: xref: "Flattenable" try: resolved = self._resolve_identifier_xref(target, lineno) except LookupError: xref = label else: if isinstance(resolved, model.Documentable): xref = taglink(resolved, self.obj.page_object.url, label) else: xref = tags.a(label, href=resolved) ret: Tag = tags.code(xref) return ret def resolve_identifier(self, identifier: str) -> Optional[str]: fullID = self.obj.expandName(identifier) target = self.obj.system.objForFullName(fullID) if target is not None: return target.url return self.look_for_intersphinx(fullID) def _resolve_identifier_xref(self, identifier: str, lineno: int ) -> Union[str, model.Documentable]: """ Resolve a crossreference link to a Python identifier. This will resolve the identifier to any reasonable target, even if it has to look in places where Python itself would not. @param identifier: The name of the Python identifier that should be linked to. @param lineno: The line number within the docstring at which the crossreference is located. @return: The referenced object within our system, or the URL of an external target (found via Intersphinx). @raise LookupError: If C{identifier} could not be resolved. """ # There is a lot of DWIM here. Look for a global match first, # to reduce the chance of a false positive. # Check if 'identifier' is the fullName of an object. target = self.obj.system.objForFullName(identifier) if target is not None: return target # Check if the fullID exists in an intersphinx inventory. fullID = self.obj.expandName(identifier) target_url = self.look_for_intersphinx(fullID) if not target_url: # FIXME: https://github.com/twisted/pydoctor/issues/125 # expandName is unreliable so in the case fullID fails, we # try our luck with 'identifier'. target_url = self.look_for_intersphinx(identifier) if target_url: return target_url # Since there was no global match, go look for the name in the # context where it was used. # Check if 'identifier' refers to an object by Python name resolution # in our context. Walk up the object tree and see if 'identifier' refers # to an object by Python name resolution in each context. src: Optional[model.Documentable] = self.obj while src is not None: target = src.resolveName(identifier) if target is not None: return target src = src.parent # Walk up the object tree again and see if 'identifier' refers to an # object in an "uncle" object. (So if p.m1 has a class C, the # docstring for p.m2 can say L{C} to refer to the class in m1). # If at any level 'identifier' refers to more than one object, complain. src = self.obj while src is not None: target = self.look_for_name(identifier, src.contents.values(), lineno) if target is not None: return target src = src.parent # Examine every module and package in the system and see if 'identifier' # names an object in each one. Again, if more than one object is # found, complain. target = self.look_for_name( identifier, self.obj.system.objectsOfType(model.Module), lineno) if target is not None: return target message = f'Cannot find link target for "{fullID}"' if identifier != fullID: message = f'{message}, resolved from "{identifier}"' root_idx = fullID.find('.') if root_idx != -1 and fullID[:root_idx] not in self.obj.system.root_names: message += ' (you can link to external docs with --intersphinx)' self.obj.report(message, 'resolve_identifier_xref', lineno) raise LookupError(identifier) @attr.s(auto_attribs=True) class FieldDesc: """ Combines informations from multiple L{Field} objects into one. Example:: :param foo: description of parameter foo :type foo: SomeClass """ _UNDOCUMENTED: ClassVar[Tag] = tags.span(class_='undocumented')("Undocumented") name: Optional[str] = None """Field name, i.e. C{:param :}""" type: Optional[Tag] = None """Formatted type""" body: Optional[Tag] = None def format(self) -> Generator[Tag, None, None]: """ @return: Iterator that yields one or two C{tags.td}. """ formatted = self.body or self._UNDOCUMENTED fieldNameTd: List[Tag] = [] if self.name: name = self.name # Add the stars to the params names just before generating the field stan, not before. if isinstance(name, VariableArgument): name = f"*{name}" elif isinstance(name, KeywordArgument): name = f"**{name}" stan_name = tags.span(class_="fieldArg")(name) if self.type: stan_name(":") fieldNameTd.append(stan_name) if self.type: fieldNameTd.append(self.type) if fieldNameTd: # : | yield tags.td(class_="fieldArgContainer")(*fieldNameTd) yield tags.td(class_="fieldArgDesc")(formatted) else: # yield tags.td(formatted, colspan="2") class RaisesDesc(FieldDesc): """Description of an exception that can be raised by function/method.""" def format(self) -> Generator[Tag, None, None]: assert self.type is not None # TODO: Why can't it be None? yield tags.td(tags.code(self.type), class_="fieldArgContainer") yield tags.td(self.body or self._UNDOCUMENTED) def format_desc_list(label: str, descs: Sequence[FieldDesc]) -> Iterator[Tag]: """ Format list of L{FieldDesc}. Used for param, returns, raises, etc. Generates a 2-columns layout as follow:: +------------------------------------+ |

tags, start at

# h1 is reserved for the page nodes.title. self.section_level += 1 # Handle interpreted text (crossreferences) def visit_title_reference(self, node: nodes.Node) -> None: # TODO: 'node.line' is None for reStructuredText based docstring for some reason. # https://github.com/twisted/pydoctor/issues/237 lineno = node.line or 0 self._handle_reference(node, link_func=lambda target, label: self._linker.link_xref(target, label, lineno)) # Handle internal references def visit_obj_reference(self, node: nodes.Node) -> None: self._handle_reference(node, link_func=self._linker.link_to) def _handle_reference(self, node: nodes.Node, link_func: Callable[[str, "Flattenable"], "Flattenable"]) -> None: label: "Flattenable" if 'refuri' in node.attributes: # Epytext parsed or manually constructed nodes. label, target = node2stan(node.children, self._linker), node.attributes['refuri'] else: # RST parsed. m = _TARGET_RE.match(node.astext()) if m: label, target = m.groups() else: label = target = node.astext() # Support linking to functions and methods with () at the end if target.endswith('()'): target = target[:len(target)-2] self.body.append(flatten(link_func(target, label))) raise nodes.SkipNode() def should_be_compact_paragraph(self, node: nodes.Node) -> bool: if self.document.children == [node]: return True else: return super().should_be_compact_paragraph(node) # type: ignore[no-any-return] def visit_document(self, node: nodes.Node) -> None: pass def depart_document(self, node: nodes.Node) -> None: pass def starttag(self, node: nodes.Node, tagname: str, suffix: str = '\n', **attributes: Any) -> str: """ This modified version of starttag makes a few changes to HTML tags, to prevent them from conflicting with epydoc. In particular: - existing class attributes are prefixed with C{'rst-'} - existing names are prefixed with C{'rst-'} - hrefs starting with C{'#'} are prefixed with C{'rst-'} - hrefs not starting with C{'#'} are given target='_top' - all headings (C{}) are given the css class C{'heading'} """ # Get the list of all attribute dictionaries we need to munge. attr_dicts = [attributes] if isinstance(node, nodes.Node): attr_dicts.append(node.attributes) if isinstance(node, dict): attr_dicts.append(node) # Munge each attribute dictionary. Unfortunately, we need to # iterate through attributes one at a time because some # versions of docutils don't case-normalize attributes. for attr_dict in attr_dicts: for key, val in list(attr_dict.items()): # Prefix all CSS classes with "rst-"; and prefix all # names with "rst-" to avoid conflicts. if key.lower() in ('class', 'id', 'name'): if not val.startswith('rst-'): attr_dict[key] = f'rst-{val}' elif key.lower() in ('classes', 'ids', 'names'): attr_dict[key] = [f'rst-{cls}' if not cls.startswith('rst-') else cls for cls in val] elif key.lower() == 'href': if attr_dict[key][:1]=='#': href = attr_dict[key][1:] # We check that the class doesn't alrealy start with "rst-" if not href.startswith('rst-'): attr_dict[key] = f'#rst-{href}' else: # If it's an external link, open it in a new # page. attr_dict['target'] = '_top' # For headings, use class="heading" if re.match(r'^h\d+$', tagname): attributes['class'] = ' '.join([attributes.get('class',''), 'heading']).strip() return super().starttag(node, tagname, suffix, **attributes) # type: ignore[no-any-return] def visit_doctest_block(self, node: nodes.Node) -> None: pysrc = node[0].astext() if node.get('codeblock'): self.body.append(flatten(colorize_codeblock(pysrc))) else: self.body.append(flatten(colorize_doctest(pysrc))) raise nodes.SkipNode() # Other ressources on how to extend docutils: # https://docutils.sourceforge.io/docs/user/tools.html # https://docutils.sourceforge.io/docs/dev/hacking.html # https://docutils.sourceforge.io/docs/howto/rst-directives.html # docutils apidocs: # http://code.nabla.net/doc/docutils/api/docutils.html#package-structure # this part of the HTMLTranslator is based on sphinx's HTMLTranslator: # https://github.com/sphinx-doc/sphinx/blob/3.x/sphinx/writers/html.py#L271 def _visit_admonition(self, node: nodes.Node, name: str) -> None: self.body.append(self.starttag( node, 'div', CLASS=('admonition ' + _valid_identifier(name)))) node.insert(0, nodes.title(name, name.title())) self.set_first_last(node) def visit_note(self, node: nodes.Node) -> None: self._visit_admonition(node, 'note') def depart_note(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_warning(self, node: nodes.Node) -> None: self._visit_admonition(node, 'warning') def depart_warning(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_attention(self, node: nodes.Node) -> None: self._visit_admonition(node, 'attention') def depart_attention(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_caution(self, node: nodes.Node) -> None: self._visit_admonition(node, 'caution') def depart_caution(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_danger(self, node: nodes.Node) -> None: self._visit_admonition(node, 'danger') def depart_danger(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_error(self, node: nodes.Node) -> None: self._visit_admonition(node, 'error') def depart_error(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_hint(self, node: nodes.Node) -> None: self._visit_admonition(node, 'hint') def depart_hint(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_important(self, node: nodes.Node) -> None: self._visit_admonition(node, 'important') def depart_important(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_tip(self, node: nodes.Node) -> None: self._visit_admonition(node, 'tip') def depart_tip(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_wbr(self, node: nodes.Node) -> None: self.body.append('') def depart_wbr(self, node: nodes.Node) -> None: pass def visit_seealso(self, node: nodes.Node) -> None: self._visit_admonition(node, 'see also') def depart_seealso(self, node: nodes.Node) -> None: self.depart_admonition(node) def visit_versionmodified(self, node: nodes.Node) -> None: self.body.append(self.starttag(node, 'div', CLASS=node['type'])) def depart_versionmodified(self, node: nodes.Node) -> None: self.body.append('\n') pydoctor-21.12.1/pydoctor/sphinx.py000066400000000000000000000310211416703725300172540ustar00rootroot00000000000000""" Support for Sphinx compatibility. """ import logging import os import shutil import textwrap import zlib from typing import ( TYPE_CHECKING, Callable, ContextManager, Dict, IO, Iterable, Mapping, Optional, Tuple ) import appdirs import attr import requests from cachecontrol import CacheControl from cachecontrol.caches import FileCache from cachecontrol.heuristics import ExpiresAfter if TYPE_CHECKING: from pydoctor.model import Documentable from typing_extensions import Protocol class CacheT(Protocol): def get(self, url: str) -> Optional[bytes]: ... else: Documentable = object CacheT = object logger = logging.getLogger(__name__) class SphinxInventory: """ Sphinx inventory handler. """ def __init__( self, logger: Callable[..., None], project_name: Optional[str] = None ): """ @param project_name: Dummy argument to stay compatible with L{twisted.python._pydoctor}. """ self._links: Dict[str, Tuple[str, str]] = {} self._logger = logger def error(self, where: str, message: str) -> None: self._logger(where, message, thresh=-1) def update(self, cache: CacheT, url: str) -> None: """ Update inventory from URL. """ parts = url.rsplit('/', 1) if len(parts) != 2: self.error( 'sphinx', 'Failed to get remote base url for %s' % (url,)) return base_url = parts[0] data = cache.get(url) if not data: self.error( 'sphinx', 'Failed to get object inventory from %s' % (url, )) return payload = self._getPayload(base_url, data) self._links.update(self._parseInventory(base_url, payload)) def _getPayload(self, base_url: str, data: bytes) -> str: """ Parse inventory and return clear text payload without comments. """ payload = b'' while True: parts = data.split(b'\n', 1) if len(parts) != 2: payload = data break if not parts[0].startswith(b'#'): payload = data break data = parts[1] try: decompressed = zlib.decompress(payload) except zlib.error: self.error( 'sphinx', 'Failed to uncompress inventory from %s' % (base_url,)) return '' try: return decompressed.decode('utf-8') except UnicodeError: self.error( 'sphinx', 'Failed to decode inventory from %s' % (base_url,)) return '' def _parseInventory( self, base_url: str, payload: str ) -> Dict[str, Tuple[str, str]]: """ Parse clear text payload and return a dict with module to link mapping. """ result = {} for line in payload.splitlines(): try: name, typ, prio, location, display = _parseInventoryLine(line) except ValueError: self.error( 'sphinx', 'Failed to parse line "%s" for %s' % (line, base_url), ) continue if not typ.startswith('py:'): # Non-Python references are ignored. continue result[name] = (base_url, location) return result def getLink(self, name: str) -> Optional[str]: """ Return link for `name` or None if no link is found. """ base_url, relative_link = self._links.get(name, (None, None)) if not relative_link: return None # For links ending with $, replace it with full name. if relative_link.endswith('$'): relative_link = relative_link[:-1] + name return f'{base_url}/{relative_link}' def _parseInventoryLine(line: str) -> Tuple[str, str, int, str, str]: """ Parse a single line from a Sphinx inventory. @raise ValueError: If the line does not conform to the syntax. """ parts = line.split(' ') # The format is a bit of a mess: spaces are used as separators, but # there are also columns that can contain spaces. # Use the numeric priority column as a reference point, since that is # what sphinx.util.inventory.InventoryFile.load_v2() does as well. prio_idx = 2 try: while True: try: prio = int(parts[prio_idx]) break except ValueError: prio_idx += 1 except IndexError: raise ValueError("Could not find priority column") name = ' '.join(parts[: prio_idx - 1]) typ = parts[prio_idx - 1] location = parts[prio_idx + 1] display = ' '.join(parts[prio_idx + 2 :]) if not display: raise ValueError("Display name column cannot be empty") return name, typ, prio, location, display class SphinxInventoryWriter: """ Sphinx inventory handler. """ def __init__(self, logger: Callable[..., None], project_name: str, project_version: str): self._project_name = project_name self._project_version = project_version self._logger = logger def info(self, where: str, message: str) -> None: self._logger(where, message) def error(self, where: str, message: str) -> None: self._logger(where, message, thresh=-1) def generate(self, subjects: Iterable[Documentable], basepath: str) -> None: """ Generate Sphinx objects inventory version 2 at `basepath`/objects.inv. """ path = os.path.join(basepath, 'objects.inv') self.info('sphinx', 'Generating objects inventory at %s' % (path,)) with self._openFileForWriting(path) as target: target.write(self._generateHeader()) content = self._generateContent(subjects) target.write(zlib.compress(content)) def _openFileForWriting(self, path: str) -> ContextManager[IO[bytes]]: """ Helper for testing. """ return open(path, 'wb') def _generateHeader(self) -> bytes: """ Return header for project with name. """ return f"""# Sphinx inventory version 2 # Project: {self._project_name} # Version: {self._project_version} # The rest of this file is compressed with zlib. """.encode('utf-8') def _generateContent(self, subjects: Iterable[Documentable]) -> bytes: """ Write inventory for all `subjects`. """ content = [] for obj in subjects: if not obj.isVisible: continue content.append(self._generateLine(obj).encode('utf-8')) content.append(self._generateContent(obj.contents.values())) return b''.join(content) def _generateLine(self, obj: Documentable) -> str: """ Return inventory line for object. name domain_name:type priority URL display_name Domain name is always: py Priority is always: -1 Display name is always: - """ # Avoid circular import. from pydoctor import model full_name = obj.fullName() url = obj.url display = '-' if isinstance(obj, model.Module): domainname = 'module' elif isinstance(obj, model.Class): domainname = 'class' elif isinstance(obj, model.Function): if obj.kind is model.DocumentableKind.FUNCTION: domainname = 'function' else: domainname = 'method' elif isinstance(obj, model.Attribute): domainname = 'attribute' else: domainname = 'obj' self.error( 'sphinx', "Unknown type %r for %s." % (type(obj), full_name,)) return f'{full_name} py:{domainname} -1 {url} {display}\n' USER_INTERSPHINX_CACHE = appdirs.user_cache_dir("pydoctor") @attr.s(auto_attribs=True) class _Unit: """ A unit of time for maximum age parsing. @see: L{parseMaxAge} """ name: str """The name of the unit.""" minimum: int """The minimum value, inclusive.""" maximum: int """The maximum value, exclusive.""" # timedelta stores seconds and minutes internally as ints. Limit them # to a 32 bit value. Per the documentation, days are limited to # 999999999, and weeks are converted to days by multiplying 7. _maxAgeUnits = { "s": _Unit("seconds", minimum=1, maximum=2 ** 32 - 1), "m": _Unit("minutes", minimum=1, maximum=2 ** 32 - 1), "h": _Unit("hours", minimum=1, maximum=2 ** 32 - 1), "d": _Unit("days", minimum=1, maximum=999999999 + 1), "w": _Unit("weeks", minimum=1, maximum=(999999999 + 1) // 7), } _maxAgeUnitNames = ", ".join( f"{indicator} ({unit.name})" for indicator, unit in _maxAgeUnits.items() ) MAX_AGE_HELP = textwrap.dedent( f""" The maximum age of any entry in the cache. Of the format where is one of {_maxAgeUnitNames}. """ ) MAX_AGE_DEFAULT = '1w' class InvalidMaxAge(Exception): """ Raised when a string cannot be parsed as a maximum age. """ def parseMaxAge(maxAge: str) -> Dict[str, int]: """ Parse a string into a maximum age dictionary. @param maxAge: A string consisting of an integer number followed by a single character unit. @return: A dictionary whose keys match L{datetime.timedelta}'s arguments. @raises InvalidMaxAge: when a string cannot be parsed. """ try: amount = int(maxAge[:-1]) except (ValueError, TypeError): raise InvalidMaxAge("Maximum age must be parseable as integer.") try: unit = _maxAgeUnits[maxAge[-1]] except (IndexError, KeyError): raise InvalidMaxAge( f"Maximum age's units must be one of {_maxAgeUnitNames}") if not (unit.minimum <= amount < unit.maximum): raise InvalidMaxAge( f"Maximum age in {unit.name} must be " f"greater than or equal to {unit.minimum} " f"and less than {unit.maximum}") return {unit.name: amount} @attr.s(auto_attribs=True) class IntersphinxCache(CacheT): """ An Intersphinx cache. """ _session: requests.Session """A session that may or may not cache requests.""" _logger: logging.Logger = logger @classmethod def fromParameters( cls, sessionFactory: Callable[[], requests.Session], cachePath: str, maxAgeDictionary: Mapping[str, int] ) -> 'IntersphinxCache': """ Construct an instance with the given parameters. @param sessionFactory: A zero-argument L{callable} that returns a L{requests.Session}. @param cachePath: Path of the cache directory. @param maxAgeDictionary: A mapping describing the maximum age of any cache entry. @see: L{parseMaxAge} """ session = CacheControl(sessionFactory(), cache=FileCache(cachePath), heuristic=ExpiresAfter(**maxAgeDictionary)) return cls(session) def get(self, url: str) -> Optional[bytes]: """ Retrieve a URL using the cache. @param url: The URL to retrieve. @return: The body of the URL, or L{None} on failure. """ try: return self._session.get(url).content except Exception: self._logger.exception( "Could not retrieve intersphinx object.inv from %s", url ) return None def prepareCache( clearCache: bool, enableCache: bool, cachePath: str, maxAge: str, sessionFactory: Callable[[], requests.Session] = requests.Session, ) -> IntersphinxCache: """ Prepare an Intersphinx cache. @param clearCache: Remove the cache? @param enableCache: Enable the cache? @param cachePath: Path of the cache directory. @param maxAge: The maximum age in seconds of cached Intersphinx C{objects.inv} files. @param sessionFactory: (optional) A zero-argument L{callable} that returns a L{requests.Session}. @return: A L{IntersphinxCache} instance. """ if clearCache: shutil.rmtree(cachePath) if enableCache: maxAgeDictionary = parseMaxAge(maxAge) return IntersphinxCache.fromParameters( sessionFactory, cachePath, maxAgeDictionary, ) return IntersphinxCache(sessionFactory()) pydoctor-21.12.1/pydoctor/sphinx_ext/000077500000000000000000000000001416703725300175655ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/sphinx_ext/__init__.py000066400000000000000000000000621416703725300216740ustar00rootroot00000000000000""" Public and private extensions for Sphinx. """ pydoctor-21.12.1/pydoctor/sphinx_ext/_help_output.py000066400000000000000000000026071416703725300226530ustar00rootroot00000000000000""" Private extension that produces the pydoctor help output to be included in the documentation. """ from docutils import nodes from docutils.parsers.rst import Directive from contextlib import redirect_stdout from io import StringIO from typing import Any, Dict, List, TYPE_CHECKING from pydoctor import __version__ from pydoctor.driver import parse_args if TYPE_CHECKING: from sphinx.application import Sphinx class HelpOutputDirective(Directive): """ Directive that will generate the pydoctor help as block literal. It takes no options or input value. """ has_content = True def run(self) -> List[nodes.Node]: """ Called by docutils each time the directive is found. """ stream = StringIO() try: with redirect_stdout(stream): parse_args(['--help']) except SystemExit: # The stdlib --help handling triggers system exit. pass text = ['pydoctor --help'] + stream.getvalue().splitlines()[1:] return [nodes.literal_block(text='\n'.join(text), language='text')] def setup(app: 'Sphinx') -> Dict[str, Any]: """ Called by Sphinx when the extensions is loaded. """ app.add_directive('help_output', HelpOutputDirective) return { 'version': __version__, 'parallel_read_safe': True, 'parallel_write_safe': True, } pydoctor-21.12.1/pydoctor/sphinx_ext/build_apidocs.py000066400000000000000000000127221416703725300227440ustar00rootroot00000000000000""" Generate the API docs using pydoctor to be integrated into Sphinx build system. This was designed to generate pydoctor HTML files as part of the Read The Docs build process. Inside the Sphinx conf.py file you need to define the following configuration options: - C{pydoctor_url_path} - defined the URL path to the API documentation You can use C{{rtd_version}} to have the URL automatically updated based on Read The Docs build. - (private usage) a mapping with values URL path definition. Make sure each definition will produce a unique URL. - C{pydoctor_args} - Sequence with all the pydoctor command line arguments used to trigger the build. - (private usage) a mapping with values as sequence of pydoctor command line arguments. The following format placeholders are resolved for C{pydoctor_args} at runtime: - C{{outdir}} - the Sphinx output dir You must call pydoctor with C{--quiet} argument as otherwise any extra output is converted into Sphinx warnings. """ import os import pathlib import shutil from contextlib import redirect_stdout from io import StringIO from typing import Any, Sequence, Mapping from sphinx.application import Sphinx from sphinx.errors import ConfigError from sphinx.util import logging from pydoctor import __version__ from pydoctor.driver import main, parse_args logger = logging.getLogger(__name__) def on_build_finished(app: Sphinx, exception: Exception) -> None: """ Called when Sphinx build is done. """ if not app.builder or app.builder.name != 'html': return runs = app.config.pydoctor_args placeholders = { 'outdir': app.outdir, } if not isinstance(runs, Mapping): # We have a single pydoctor call runs = {'main': runs} for key, value in runs.items(): arguments = _get_arguments(value, placeholders) options, _ = parse_args(arguments) output_path = pathlib.Path(options.htmloutput) sphinx_files = output_path.with_suffix('.sphinx_files') temp_path = output_path.with_suffix('.pydoctor_temp') shutil.rmtree(sphinx_files, ignore_errors=True) output_path.rename(sphinx_files) temp_path.rename(output_path) def on_builder_inited(app: Sphinx) -> None: """ Called to build the API documentation HTML files and inject our own intersphinx inventory object. """ if not app.builder or app.builder.name != 'html': return rtd_version = 'latest' if os.environ.get('READTHEDOCS', '') == 'True': rtd_version = os.environ.get('READTHEDOCS_VERSION', 'latest') config = app.config if not config.pydoctor_args: raise ConfigError("Missing 'pydoctor_args'.") placeholders = { 'outdir': app.outdir, } runs = config.pydoctor_args if not isinstance(runs, Mapping): # We have a single pydoctor call runs = {'main': runs} pydoctor_url_path = config.pydoctor_url_path if not isinstance(pydoctor_url_path, Mapping): pydoctor_url_path = {'main': pydoctor_url_path} for key, value in runs.items(): arguments = _get_arguments(value, placeholders) options, _ = parse_args(arguments) output_path = pathlib.Path(options.htmloutput) temp_path = output_path.with_suffix('.pydoctor_temp') # Update intersphinx_mapping. url_path = pydoctor_url_path.get(key) if url_path: intersphinx_mapping = config.intersphinx_mapping url = url_path.format(**{'rtd_version': rtd_version}) inv = (str(temp_path / 'objects.inv'),) intersphinx_mapping[f'{key}-api-docs'] = (None, (url, inv)) # Build the API docs in temporary path. shutil.rmtree(temp_path, ignore_errors=True) _run_pydoctor(key, arguments) output_path.rename(temp_path) def _run_pydoctor(name: str, arguments: Sequence[str]) -> None: """ Call pydoctor with arguments. @param name: A human-readable description of this pydoctor build. @param arguments: Command line arguments used to call pydoctor. """ logger.info(f"Building '{name}' pydoctor API docs as:") logger.info('\n'.join(arguments)) with StringIO() as stream: with redirect_stdout(stream): main(args=arguments) for line in stream.getvalue().splitlines(): logger.warning(line) def _get_arguments(arguments: Sequence[str], placeholders: Mapping[str, str]) -> Sequence[str]: """ Return the resolved arguments for pydoctor build. @param arguments: Sequence of proto arguments used to call pydoctor. @return: Sequence with actual acguments use to call pydoctor. """ args = ['--make-html', '--quiet'] for argument in arguments: args.append(argument.format(**placeholders)) return args def setup(app: Sphinx) -> Mapping[str, Any]: """ Called by Sphinx when the extension is initialized. @return: The extension version and runtime options. """ app.add_config_value("pydoctor_args", None, "env") app.add_config_value("pydoctor_url_path", None, "env") # Make sure we have a lower priority than intersphinx extension. app.connect('builder-inited', on_builder_inited, priority=490) app.connect('build-finished', on_build_finished) return { 'version': __version__, 'parallel_read_safe': True, 'parallel_write_safe': True, } pydoctor-21.12.1/pydoctor/stanutils.py000066400000000000000000000033701416703725300177770ustar00rootroot00000000000000""" Utilities related to Stan tree building and HTML flattening. """ import re from typing import Union, List, TYPE_CHECKING from twisted.web.template import Tag, XMLString, flattenString from twisted.python.failure import Failure if TYPE_CHECKING: from twisted.web.template import Flattenable _RE_CONTROL = re.compile(( '[' + ''.join( ch for ch in map(chr, range(0, 32)) if ch not in '\r\n\t\f' ) + ']' ).encode()) def html2stan(html: Union[bytes, str]) -> Tag: """ Convert an HTML string to a Stan tree. @param html: An HTML fragment; multiple roots are allowed. @return: The fragment as a tree with a transparent root node. """ if isinstance(html, str): html = html.encode('utf8') html = _RE_CONTROL.sub(lambda m:b'\\x%02x' % ord(m.group()), html) stan = XMLString(b'
%s
' % html).load()[0] assert isinstance(stan, Tag) assert stan.tagName == 'div' stan.tagName = '' return stan def flatten(stan: "Flattenable") -> str: """ Convert a document fragment from a Stan tree to HTML. @param stan: Document fragment to flatten. @return: An HTML string representation of the C{stan} tree. """ ret: List[bytes] = [] err: List[Failure] = [] flattenString(None, stan).addCallback(ret.append).addErrback(err.append) if err: raise err[0].value else: return ret[0].decode() def flatten_text(stan: Union[Tag, str]) -> str: """Return the text inside a stan tree. @note: Only compatible with L{Tag} objects. """ text = '' if isinstance(stan, (str)): text += stan else: for child in stan.children: if isinstance(child, (Tag, str)): text += flatten_text(child) return text pydoctor-21.12.1/pydoctor/templatewriter/000077500000000000000000000000001416703725300204445ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/templatewriter/__init__.py000066400000000000000000000411271416703725300225620ustar00rootroot00000000000000"""Render pydoctor data as HTML.""" from typing import Iterable, Iterator, Optional, Union, cast, TYPE_CHECKING if TYPE_CHECKING: from typing_extensions import Protocol, runtime_checkable else: Protocol = object def runtime_checkable(f): return f import abc from pathlib import Path, PurePath import warnings import sys from xml.dom import minidom # Newer APIs from importlib_resources should arrive to stdlib importlib.resources in Python 3.9. if TYPE_CHECKING: if sys.version_info >= (3, 9): from importlib.abc import Traversable else: Traversable = Path else: Traversable = object from twisted.web.iweb import ITemplateLoader from twisted.web.template import TagLoader, XMLString, Element, tags from pydoctor.templatewriter.util import CaseInsensitiveDict from pydoctor.model import System, Documentable DOCTYPE = b'''\ ''' def parse_xml(text: str) -> minidom.Document: """ Create a L{minidom} representaton of the XML string. """ try: # TODO: submit a PR to typeshed to add a return type for parseString() return cast(minidom.Document, minidom.parseString(text)) except Exception as e: raise ValueError(f"Failed to parse template as XML: {e}") from e class TemplateError(Exception): """Raised when there is an problem with a template. TemplateErrors are fatal.""" class UnsupportedTemplateVersion(TemplateError): """Raised when custom template is designed for a newer version of pydoctor""" class OverrideTemplateNotAllowed(TemplateError): """Raised when a template path overrides a path of a different type (HTML/static/directory).""" class FailedToCreateTemplate(TemplateError): """Raised when a template could not be created because of an error""" @runtime_checkable class IWriter(Protocol): """ Interface class for pydoctor output writer. """ def __init__(self, build_directory: Path, template_lookup: 'TemplateLookup') -> None: ... def prepOutputDirectory(self) -> None: """ Called first. """ def writeSummaryPages(self, system: System) -> None: """ Called second. """ def writeIndividualFiles(self, obs: Iterable[Documentable]) -> None: """ Called last. """ class Template(abc.ABC): """ Represents a pydoctor template file. It holds references to template information. It's an additionnal level of abstraction to hook to the writer class. Use L{Template.fromfile} or L{Template.fromdir} to create Templates. @see: L{TemplateLookup}, L{StaticTemplate} and L{HtmlTemplate} @note: Directories are not L{Template}. The L{Template.name} attribute is the relative path to the template file, it may include subdirectories in it! Currently, subdirectories should only contains static templates. This is because the subdirectory creation is handled in L{StaticTemplate.write()}. """ def __init__(self, name: str): self.name = name """Template filename, may include subdirectories.""" @classmethod def fromdir(cls, basedir: Union[Traversable, Path], subdir: Optional[PurePath] = None) -> Iterator['Template']: """ Scan a directory for templates. @param basedir: A L{Path} or L{Traversable} object that should point to the root directory of the template directory structure. @param subdir: The subdirectory inside the template directory structure that we want to scan, relative to the C{basedir}. Scan the C{basedir} if C{None}. @raises FailedToCreateTemplate: If the path is not a directory or do not exist. """ path = basedir.joinpath(subdir.as_posix()) if subdir else basedir subdir = subdir or PurePath() if not path.is_dir(): raise FailedToCreateTemplate(f"Template folder do not exist or is not a directory: {path}") for entry in path.iterdir(): entry_path = subdir.joinpath(entry.name) if entry.is_dir(): yield from Template.fromdir(basedir, entry_path) else: template = Template.fromfile(basedir, entry_path) if template: yield template @classmethod def fromfile(cls, basedir: Union[Traversable, Path], templatepath: PurePath) -> Optional['Template']: """ Create a concrete template object. Type depends on the file extension. @param basedir: A L{Path} or L{Traversable} object that should point to the root directory of the template directory structure. @param templatepath: The path to the template file, relative to the C{basedir}. @returns: The template object or C{None} if a the path entry is not a file. @raises FailedToCreateTemplate: If there is an error while creating the template. """ path = basedir.joinpath(templatepath.as_posix()) if not path.is_file(): return None template: Template try: # Only try to decode the file text if the file is an HTML template if templatepath.suffix.lower() == '.html': try: text = path.read_text(encoding='utf-8') except UnicodeDecodeError as e: raise FailedToCreateTemplate("Cannot decode HTML Template" f" as UTF-8: '{path}'. {e}") from e else: # The template name is the relative path to the template. # Template files in subdirectories will have a name like: 'static/bar.svg'. template = HtmlTemplate(name=templatepath.as_posix(), text=text) else: # Treat the file as binary data. data = path.read_bytes() template = StaticTemplate(name=templatepath.as_posix(), data=data) # Catch io errors only once for the whole block, it's ok to do that since # we're reading only one file per call to fromfile() except IOError as e: raise FailedToCreateTemplate(f"Cannot read Template: '{path}'." " I/O error: {e}") from e return template class StaticTemplate(Template): """ Static template: no rendering, will be copied as is to build directory. For CSS and JS templates. """ def __init__(self, name: str, data: bytes) -> None: super().__init__(name) self.data: bytes = data """ Contents of the template file as L{bytes}. """ def write(self, build_directory: Path) -> None: """ Directly write the contents of this static template as is to the build dir. """ outfile = build_directory.joinpath(self.name) outfile.parent.mkdir(exist_ok=True, parents=True) with outfile.open('wb') as fobjb: fobjb.write(self.data) class HtmlTemplate(Template): """ HTML template that works with the Twisted templating system and use L{xml.dom.minidom} to parse the C{pydoctor-template-version} meta tag. @ivar text: Contents of the template file as UFT-8 decoded L{str}. @ivar version: Template version, C{-1} if no version could be read in the XML file. HTML Templates should have a version identifier as follow:: The version indentifier should be a integer. @ivar loader: Object used to render the final HTML file with the Twisted templating system. This is a L{ITemplateLoader}. """ def __init__(self, name: str, text: str): super().__init__(name=name) self.text = text if len(self.text.strip()) == 0: self._dom: Optional[minidom.Document] = None self.version = -1 self.loader: ITemplateLoader = TagLoader(tags.transparent) else: self._dom = parse_xml(self.text) self.version = self._extract_version(self._dom, self.name) self.loader = XMLString(self._dom.toxml()) @staticmethod def _extract_version(dom: minidom.Document, template_name: str) -> int: # If no meta pydoctor-template-version tag found, # it's most probably a placeholder template. version = -1 for meta in dom.getElementsByTagName("meta"): if meta.getAttribute("name") != "pydoctor-template-version": continue # Remove the meta tag as soon as found meta.parentNode.removeChild(meta) if not meta.hasAttribute("content"): warnings.warn(f"Could not read '{template_name}' template version: " f"the 'content' attribute is missing") continue version_str = meta.getAttribute("content") try: version = int(version_str) except ValueError: warnings.warn(f"Could not read '{template_name}' template version: " "the 'content' attribute must be an integer") else: break return version class TemplateLookup: """ The L{TemplateLookup} handles the HTML template files locations. A little bit like C{mako.lookup.TemplateLookup} but more simple. The location of the files depends wether the users set a template directory with the option C{--template-dir} and/or with the option C{--theme}, any files in a template directory will be loaded. This object allow the customization of any templates. For HTML templates, this can lead to warnings when upgrading pydoctor, then, please update your template from our repo. @note: The HTML templates versions are independent of the pydoctor version and are idependent from each other. @note: Template operations are case insensitive. @see: L{Template}, L{StaticTemplate}, L{HtmlTemplate} """ def __init__(self, path: Union[Traversable, Path]) -> None: """ Loads all templates from the given C{path} into the lookup. @param path: A L{Path} or L{Traversable} object pointing to a directory to load the default set of templates from. """ self._templates: CaseInsensitiveDict[Template] = CaseInsensitiveDict() self.add_templatedir(path) def _add_overriding_html_template(self, template: HtmlTemplate, current_template: HtmlTemplate) -> None: default_version = current_template.version template_version = template.version if default_version != -1 and template_version != -1: if template_version < default_version: warnings.warn(f"Your custom template '{template.name}' is out of date, " "information might be missing. " "Latest templates are available to download from our github." ) elif template_version > default_version: raise UnsupportedTemplateVersion(f"It appears that your custom template '{template.name}' " "is designed for a newer version of pydoctor." "Rendering will most probably fail. Upgrade to latest " "version of pydoctor with 'pip install -U pydoctor'. ") self._templates[template.name] = template def _raise_if_overrides_directory(self, template_name: str) -> None: # Since we cannot have a file named the same as a directory, # we must reject files that overrides direcotries. template_lowername = template_name.lower() for t in self.templates: current_lowername = t.name.lower() if current_lowername.startswith(f"{template_lowername}/"): raise OverrideTemplateNotAllowed(f"Cannot override a directory with " f"a template. Rename '{template_name}' to something else.") def add_template(self, template: Template) -> None: """ Add a template to the lookup. The custom template override the default. If the file doesn't already exist in the lookup, we assume it is additional data used by the custom template. For HTML, compare the new Template version with the currently loaded template, issue warnings if template are outdated. @raises UnsupportedTemplateVersion: If the custom template is designed for a newer version of pydoctor. @raises OverrideTemplateNotAllowed: If this template path overrides a path of a different type (HTML/static/directory). """ self._raise_if_overrides_directory(template.name) try: current_template = self._templates[template.name] except KeyError: self._templates[template.name] = template else: # The real template name might not have the same casing as current_template.name. # This variable is only used in error messages. _real_template_name = template.name # The L{Template.name} attribute is overriden # to make it match the original (case sensitive) name. # This way, we are sure to stay consistent in the output file names (keeping the original), # while accepting any casing variation in the template directory. template.name = current_template.name if isinstance(current_template, StaticTemplate): if isinstance(template, StaticTemplate): self._templates[template.name] = template else: raise OverrideTemplateNotAllowed(f"Cannot override a static template with " f"a HTML template. Rename '{_real_template_name}' to something else.") # we can assume the template is HTML since there is only # two types of concrete templates elif isinstance(current_template, HtmlTemplate): if isinstance(template, HtmlTemplate): self._add_overriding_html_template(template, current_template) else: raise OverrideTemplateNotAllowed(f"Cannot override an HTML template with " f"a static template. Rename '{_real_template_name}' to something else.") def add_templatedir(self, path: Union[Path, Traversable]) -> None: """ Scan a directory and add all templates in the given directory to the lookup. """ for template in Template.fromdir(path): self.add_template(template) def get_template(self, filename: str) -> Template: """ Lookup a template based on its filename. Return the custom template if provided, else the default template. @param filename: File name, (ie 'index.html') @return: The Template object @raises KeyError: If no template file is found with the given name """ try: t = self._templates[filename] except KeyError as e: raise KeyError(f"Cannot find template '{filename}' in template lookup: {self}. " f"Valid filenames are: {list(self._templates)}") from e return t def get_loader(self, filename: str) -> ITemplateLoader: """ Lookup a HTML template loader based on its filename. @raises ValueError: If the template is not an HTML file. """ template = self.get_template(filename) if not isinstance(template, HtmlTemplate): raise ValueError(f"Failed to get loader of template '{filename}': Not an HTML file.") return template.loader @property def templates(self) -> Iterable[Template]: """ All templates that can be looked up. For each name, the custom template will be included if it exists, otherwise the default template. """ return self._templates.values() class TemplateElement(Element, abc.ABC): """ Renderable element based on a template file. """ filename: str = NotImplemented """ Associated template filename. """ @classmethod def lookup_loader(cls, template_lookup: TemplateLookup) -> ITemplateLoader: """ Lookup the element L{ITemplateLoader} with the C{TemplateLookup}. """ return template_lookup.get_loader(cls.filename) from pydoctor.templatewriter.writer import TemplateWriter __all__ = ["TemplateWriter"] # re-export as pydoctor.templatewriter.TemplateWriter pydoctor-21.12.1/pydoctor/templatewriter/pages/000077500000000000000000000000001416703725300215435ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/templatewriter/pages/__init__.py000066400000000000000000000464231416703725300236650ustar00rootroot00000000000000"""The classes that turn L{Documentable} instances into objects we can render.""" from typing import ( TYPE_CHECKING, Dict, Iterator, List, Optional, Mapping, Sequence, Tuple, Type, Union ) import ast import abc from twisted.web.iweb import IRenderable, ITemplateLoader, IRequest from twisted.web.template import Element, Tag, renderer, tags from pydoctor.stanutils import html2stan from pydoctor import epydoc2stan, model, zopeinterface, __version__ from pydoctor.astbuilder import node2fullname from pydoctor.templatewriter import util, TemplateLookup, TemplateElement from pydoctor.templatewriter.pages.table import ChildTable from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval if TYPE_CHECKING: from typing_extensions import Final from twisted.web.template import Flattenable from pydoctor.templatewriter.pages.attributechild import AttributeChild from pydoctor.templatewriter.pages.functionchild import FunctionChild def objects_order(o: model.Documentable) -> Tuple[int, int, str]: """ Function to use as the value of standard library's L{sorted} function C{key} argument such that the objects are sorted by: Privacy, Kind and Name. Example:: children = sorted((o for o in ob.contents.values() if o.isVisible), key=objects_order) """ return (-o.privacyClass.value, -o.kind.value if o.kind else 0, o.fullName().lower()) def format_decorators(obj: Union[model.Function, model.Attribute]) -> Iterator["Flattenable"]: for dec in obj.decorators or (): if isinstance(dec, ast.Call): fn = node2fullname(dec.func, obj) # We don't want to show the deprecated decorator; # it shows up as an infobox. if fn in ("twisted.python.deprecate.deprecated", "twisted.python.deprecate.deprecatedProperty"): break # Colorize decorators! doc = colorize_inline_pyval(dec) stan = doc.to_stan(epydoc2stan._EpydocLinker(obj)) # Report eventual warnings. It warns when a regex failed to parse or the html2stan() function fails. for message in doc.warnings: obj.report(message) yield '@', stan.children, tags.br() def format_signature(function: model.Function) -> "Flattenable": """ Return a stan representation of a nicely-formatted source-like function signature for the given L{Function}. Arguments default values are linked to the appropriate objects when possible. """ return html2stan(str(function.signature)) class DocGetter: """L{epydoc2stan} bridge.""" def get(self, ob: model.Documentable, summary: bool = False) -> Tag: if summary: return epydoc2stan.format_summary(ob) else: return epydoc2stan.format_docstring(ob) def get_type(self, ob: model.Documentable) -> Optional[Tag]: return epydoc2stan.type2stan(ob) class Nav(TemplateElement): """ Common navigation header. """ filename = 'nav.html' def __init__(self, system: model.System, loader: ITemplateLoader) -> None: super().__init__(loader) self.system = system class Head(TemplateElement): """ Common metadata. """ filename = 'head.html' def __init__(self, title: str, loader: ITemplateLoader) -> None: super().__init__(loader) self._title = title @renderer def title(self, request: IRequest, tag: Tag) -> str: return self._title class Page(TemplateElement): """ Abstract base class for output pages. Defines special HTML placeholders that are designed to be overriden by users: "header.html", "subheader.html" and "footer.html". """ def __init__(self, system: model.System, template_lookup: TemplateLookup, loader: Optional[ITemplateLoader] = None): self.system = system self.template_lookup = template_lookup if not loader: loader = self.lookup_loader(template_lookup) super().__init__(loader) def render(self, request: Optional[IRequest]) -> Tag: return tags.transparent(super().render(request)).fillSlots(**self.slot_map) @property def slot_map(self) -> Dict[str, "Flattenable"]: system = self.system if system.options.projecturl: project_tag = tags.a(href=system.options.projecturl, class_="projecthome") else: project_tag = tags.transparent project_tag(system.projectname) return dict( project=project_tag, pydoctor_version=__version__, buildtime=system.buildtime.strftime("%Y-%m-%d %H:%M:%S"), ) @abc.abstractmethod def title(self) -> str: raise NotImplementedError() @renderer def head(self, request: IRequest, tag: Tag) -> IRenderable: return Head(self.title(), Head.lookup_loader(self.template_lookup)) @renderer def nav(self, request: IRequest, tag: Tag) -> IRenderable: return Nav(self.system, Nav.lookup_loader(self.template_lookup)) @renderer def header(self, request: IRequest, tag: Tag) -> IRenderable: return Element(self.template_lookup.get_loader('header.html')) @renderer def subheader(self, request: IRequest, tag: Tag) -> IRenderable: return Element(self.template_lookup.get_loader('subheader.html')) @renderer def footer(self, request: IRequest, tag: Tag) -> IRenderable: return Element(self.template_lookup.get_loader('footer.html')) class CommonPage(Page): filename = 'common.html' ob: model.Documentable def __init__(self, ob: model.Documentable, template_lookup: TemplateLookup, docgetter: Optional[DocGetter]=None): super().__init__(ob.system, template_lookup) self.ob = ob if docgetter is None: docgetter = DocGetter() self.docgetter = docgetter @property def page_url(self) -> str: return self.ob.page_object.url def title(self) -> str: return self.ob.fullName() def heading(self) -> Tag: return tags.h1(class_=util.css_class(self.ob))( tags.code(self.namespace(self.ob)) ) def category(self) -> str: kind = self.ob.kind assert kind is not None return f"{epydoc2stan.format_kind(kind).lower()} documentation" def namespace(self, obj: model.Documentable) -> List[Union[Tag, str]]: page_url = self.page_url parts: List[Union[Tag, str]] = [] ob: Optional[model.Documentable] = obj while ob: if ob.documentation_location is model.DocLocation.OWN_PAGE: if parts: parts.extend(['.', tags.wbr]) parts.append(tags.code(epydoc2stan.taglink(ob, page_url, ob.name))) ob = ob.parent parts.reverse() return parts @renderer def deprecated(self, request: object, tag: Tag) -> "Flattenable": msg = self.ob._deprecated_info if msg is None: return () else: return tags.div(msg, role="alert", class_="deprecationNotice alert alert-warning") @renderer def source(self, request: object, tag: Tag) -> "Flattenable": sourceHref = util.srclink(self.ob) if not sourceHref: return () return tag(href=sourceHref) @renderer def inhierarchy(self, request: object, tag: Tag) -> "Flattenable": return () def extras(self) -> List["Flattenable"]: return [] def docstring(self) -> "Flattenable": return self.docgetter.get(self.ob) def children(self) -> Sequence[model.Documentable]: return sorted( (o for o in self.ob.contents.values() if o.isVisible), key=objects_order) def packageInitTable(self) -> "Flattenable": return () @renderer def baseTables(self, request: object, tag: Tag) -> "Flattenable": return () def mainTable(self) -> "Flattenable": children = self.children() if children: return ChildTable(self.docgetter, self.ob, children, ChildTable.lookup_loader(self.template_lookup)) else: return () def methods(self) -> Sequence[model.Documentable]: return sorted((o for o in self.ob.contents.values() if o.documentation_location is model.DocLocation.PARENT_PAGE and o.isVisible), key=objects_order) def childlist(self) -> List[Union["AttributeChild", "FunctionChild"]]: from pydoctor.templatewriter.pages.attributechild import AttributeChild from pydoctor.templatewriter.pages.functionchild import FunctionChild r: List[Union["AttributeChild", "FunctionChild"]] = [] func_loader = FunctionChild.lookup_loader(self.template_lookup) attr_loader = AttributeChild.lookup_loader(self.template_lookup) for c in self.methods(): if isinstance(c, model.Function): r.append(FunctionChild(self.docgetter, c, self.functionExtras(c), func_loader)) elif isinstance(c, model.Attribute): r.append(AttributeChild(self.docgetter, c, self.functionExtras(c), attr_loader)) else: assert False, type(c) return r def functionExtras(self, ob: model.Documentable) -> List["Flattenable"]: return [] def functionBody(self, ob: model.Documentable) -> "Flattenable": return self.docgetter.get(ob) @property def slot_map(self) -> Dict[str, "Flattenable"]: slot_map = super().slot_map slot_map.update( heading=self.heading(), category=self.category(), extras=self.extras(), docstring=self.docstring(), mainTable=self.mainTable(), packageInitTable=self.packageInitTable(), childlist=self.childlist(), ) return slot_map class ModulePage(CommonPage): def extras(self) -> List["Flattenable"]: r = super().extras() sourceHref = util.srclink(self.ob) if sourceHref: r.append(tags.a("(source)", href=sourceHref, class_="sourceLink")) return r class PackagePage(ModulePage): def children(self) -> Sequence[model.Documentable]: return sorted( (o for o in self.ob.contents.values() if isinstance(o, model.Module) and o.isVisible), key=objects_order) def packageInitTable(self) -> "Flattenable": children = sorted( (o for o in self.ob.contents.values() if not isinstance(o, model.Module) and o.isVisible), key=objects_order) if children: loader = ChildTable.lookup_loader(self.template_lookup) return [ tags.p("From ", tags.code("__init__.py"), ":", class_="fromInitPy"), ChildTable(self.docgetter, self.ob, children, loader) ] else: return () def methods(self) -> Sequence[model.Documentable]: return [o for o in self.ob.contents.values() if o.documentation_location is model.DocLocation.PARENT_PAGE and o.isVisible] def overriding_subclasses( c: model.Class, name: str, firstcall: bool = True ) -> Iterator[model.Class]: if not firstcall and name in c.contents: yield c else: for sc in c.subclasses: if sc.isVisible: yield from overriding_subclasses(sc, name, False) def nested_bases(b: model.Class) -> Sequence[Tuple[model.Class, ...]]: r: List[Tuple[model.Class, ...]] = [(b,)] for b2 in b.baseobjects: if b2 is None: continue for n in nested_bases(b2): r.append(n + (b,)) return r def unmasked_attrs(baselist: Sequence[model.Documentable]) -> Sequence[model.Documentable]: maybe_masking = { o.name for b in baselist[1:] for o in b.contents.values() } return [o for o in baselist[0].contents.values() if o.isVisible and o.name not in maybe_masking] def assembleList( system: model.System, label: str, lst: Sequence[str], idbase: str, page_url: str ) -> Optional["Flattenable"]: lst2 = [] for name in lst: o = system.allobjects.get(name) if o is None or o.isVisible: lst2.append(name) lst = lst2 if not lst: return None def one(item: str) -> "Flattenable": if item in system.allobjects: return tags.code(epydoc2stan.taglink(system.allobjects[item], page_url)) else: return item def commasep(items: Sequence[str]) -> List["Flattenable"]: r = [] for item in items: r.append(one(item)) r.append(', ') del r[-1] return r p: List["Flattenable"] = [label] p.extend(commasep(lst)) return p class ClassPage(CommonPage): ob: model.Class def __init__(self, ob: model.Documentable, template_lookup: TemplateLookup, docgetter: Optional[DocGetter] = None ): super().__init__(ob, template_lookup, docgetter) self.baselists = [] for baselist in nested_bases(self.ob): attrs = unmasked_attrs(baselist) if attrs: self.baselists.append((baselist, attrs)) self.overridenInCount = 0 def extras(self) -> List["Flattenable"]: r = super().extras() sourceHref = util.srclink(self.ob) source: "Flattenable" if sourceHref: source = (" ", tags.a("(source)", href=sourceHref, class_="sourceLink")) else: source = tags.transparent r.append(tags.p(tags.code( tags.span("class", class_='py-keyword'), " ", tags.span(self.ob.name, class_='py-defname'), self.classSignature(), ":", source ))) scs = sorted(self.ob.subclasses, key=objects_order) if not scs: return r p = assembleList(self.ob.system, "Known subclasses: ", [o.fullName() for o in scs], "moreSubclasses", self.page_url) if p is not None: r.append(tags.p(p)) return r def classSignature(self) -> "Flattenable": r: List["Flattenable"] = [] zipped = list(zip(self.ob.rawbases, self.ob.bases, self.ob.baseobjects)) if zipped: r.append('(') for idx, (name, full_name, base) in enumerate(zipped): if idx != 0: r.append(', ') if base is None: # External class. url = self.ob.system.intersphinx.getLink(full_name) else: # Internal class. url = base.url if url is None: tag = tags.span else: tag = tags.a(href=url) r.append(tag(name, title=full_name)) r.append(')') return r @renderer def inhierarchy(self, request: object, tag: Tag) -> Tag: return tag(href="classIndex.html#"+self.ob.fullName()) @renderer def baseTables(self, request: object, item: Tag) -> "Flattenable": baselists = self.baselists[:] if not baselists: return [] if baselists[0][0][0] == self.ob: del baselists[0] loader = ChildTable.lookup_loader(self.template_lookup) return [item.clone().fillSlots( baseName=self.baseName(b), baseTable=ChildTable(self.docgetter, self.ob, sorted(attrs, key=objects_order), loader)) for b, attrs in baselists] def baseName(self, bases: Sequence[model.Class]) -> "Flattenable": page_url = self.page_url r: List["Flattenable"] = [] source_base = bases[0] r.append(tags.code(epydoc2stan.taglink(source_base, page_url, source_base.name))) bases_to_mention = bases[1:-1] if bases_to_mention: tail: List["Flattenable"] = [] for b in reversed(bases_to_mention): tail.append(tags.code(epydoc2stan.taglink(b, page_url, b.name))) tail.append(', ') del tail[-1] r.extend([' (via ', tail, ')']) return r def functionExtras(self, ob: model.Documentable) -> List["Flattenable"]: page_url = self.page_url name = ob.name r: List["Flattenable"] = [] for b in self.ob.allbases(include_self=False): if name not in b.contents: continue overridden = b.contents[name] r.append(tags.div(class_="interfaceinfo")( 'overrides ', tags.code(epydoc2stan.taglink(overridden, page_url)))) break ocs = sorted(overriding_subclasses(self.ob, name), key=objects_order) if ocs: self.overridenInCount += 1 idbase = 'overridenIn' + str(self.overridenInCount) l = assembleList(self.ob.system, 'overridden in ', [o.fullName() for o in ocs], idbase, self.page_url) if l is not None: r.append(tags.div(class_="interfaceinfo")(l)) return r class ZopeInterfaceClassPage(ClassPage): ob: zopeinterface.ZopeInterfaceClass def extras(self) -> List["Flattenable"]: r = super().extras() if self.ob.isinterface: namelist = [o.fullName() for o in sorted(self.ob.implementedby_directly, key=objects_order)] label = 'Known implementations: ' else: namelist = sorted(self.ob.implements_directly, key=lambda x:x.lower()) label = 'Implements interfaces: ' if namelist: l = assembleList(self.ob.system, label, namelist, "moreInterface", self.page_url) if l is not None: r.append(tags.p(l)) return r def interfaceMeth(self, methname: str) -> Optional[model.Documentable]: system = self.ob.system for interface in self.ob.allImplementedInterfaces: if interface in system.allobjects: io = system.allobjects[interface] assert isinstance(io, zopeinterface.ZopeInterfaceClass) for io2 in io.allbases(include_self=True): method: Optional[model.Documentable] = io2.contents.get(methname) if method is not None: return method return None def functionExtras(self, ob: model.Documentable) -> List["Flattenable"]: imeth = self.interfaceMeth(ob.name) r: List["Flattenable"] = [] if imeth: iface = imeth.parent assert iface is not None r.append(tags.div(class_="interfaceinfo")('from ', tags.code( epydoc2stan.taglink(imeth, self.page_url, iface.fullName()) ))) r.extend(super().functionExtras(ob)) return r commonpages: 'Final[Mapping[str, Type[CommonPage]]]' = { 'Module': ModulePage, 'Package': PackagePage, 'Class': ClassPage, 'ZopeInterfaceClass': ZopeInterfaceClassPage, } """List all page classes: ties documentable class name with the page class used for rendering""" pydoctor-21.12.1/pydoctor/templatewriter/pages/attributechild.py000066400000000000000000000053431416703725300251310ustar00rootroot00000000000000from typing import TYPE_CHECKING, List from twisted.web.iweb import ITemplateLoader from twisted.web.template import Tag, renderer, tags from pydoctor.model import Attribute, DocumentableKind from pydoctor import epydoc2stan from pydoctor.templatewriter import TemplateElement, util from pydoctor.templatewriter.pages import DocGetter, format_decorators if TYPE_CHECKING: from twisted.web.template import Flattenable class AttributeChild(TemplateElement): filename = 'attribute-child.html' def __init__(self, docgetter: DocGetter, ob: Attribute, extras: "Flattenable", loader: ITemplateLoader ): super().__init__(loader) self.docgetter = docgetter self.ob = ob self._functionExtras = extras @renderer def class_(self, request: object, tag: Tag) -> "Flattenable": class_ = util.css_class(self.ob) if self.ob.parent is not self.ob: class_ = 'base' + class_ return class_ @renderer def functionAnchor(self, request: object, tag: Tag) -> "Flattenable": return self.ob.fullName() @renderer def shortFunctionAnchor(self, request: object, tag: Tag) -> "Flattenable": return self.ob.name @renderer def decorator(self, request: object, tag: Tag) -> "Flattenable": return list(format_decorators(self.ob)) @renderer def attribute(self, request: object, tag: Tag) -> "Flattenable": attr: List["Flattenable"] = [tags.span(self.ob.name, class_='py-defname')] _type = self.docgetter.get_type(self.ob) if _type: attr.extend([': ', _type]) return attr @renderer def sourceLink(self, request: object, tag: Tag) -> "Flattenable": if self.ob.sourceHref: return tag.fillSlots(sourceHref=self.ob.sourceHref) else: return () @renderer def functionExtras(self, request: object, tag: Tag) -> "Flattenable": return self._functionExtras @renderer def functionBody(self, request: object, tag: Tag) -> "Flattenable": return self.docgetter.get(self.ob) @renderer def functionDeprecated(self, request: object, tag: Tag) -> "Flattenable": msg = self.ob._deprecated_info if msg is None: return () else: return tags.div(msg, role="alert", class_="deprecationNotice alert alert-warning") @renderer def constantValue(self, request: object, tag: Tag) -> "Flattenable": if self.ob.kind is not DocumentableKind.CONSTANT or self.ob.value is None: return tag.clear() # Attribute is a constant (with a value), then display it's value return epydoc2stan.format_constant_value(self.ob) pydoctor-21.12.1/pydoctor/templatewriter/pages/functionchild.py000066400000000000000000000050321416703725300247460ustar00rootroot00000000000000from typing import TYPE_CHECKING from twisted.web.iweb import ITemplateLoader from twisted.web.template import Tag, renderer, tags from pydoctor.model import Function from pydoctor.templatewriter import TemplateElement, util from pydoctor.templatewriter.pages import DocGetter, format_decorators, format_signature if TYPE_CHECKING: from twisted.web.template import Flattenable class FunctionChild(TemplateElement): filename = 'function-child.html' def __init__(self, docgetter: DocGetter, ob: Function, extras: "Flattenable", loader: ITemplateLoader ): super().__init__(loader) self.docgetter = docgetter self.ob = ob self._functionExtras = extras @renderer def class_(self, request: object, tag: Tag) -> "Flattenable": class_ = util.css_class(self.ob) if self.ob.parent is not self.ob: class_ = 'base' + class_ return class_ @renderer def functionAnchor(self, request: object, tag: Tag) -> "Flattenable": return self.ob.fullName() @renderer def shortFunctionAnchor(self, request: object, tag: Tag) -> "Flattenable": return self.ob.name @renderer def decorator(self, request: object, tag: Tag) -> "Flattenable": return list(format_decorators(self.ob)) @renderer def functionDef(self, request: object, tag: Tag) -> "Flattenable": def_stmt = 'async def' if self.ob.is_async else 'def' name = self.ob.name if name.endswith('.setter') or name.endswith('.deleter'): name = name[:name.rindex('.')] return [ tags.span(def_stmt, class_='py-keyword'), ' ', tags.span(name, class_='py-defname'), format_signature(self.ob), ':' ] @renderer def sourceLink(self, request: object, tag: Tag) -> "Flattenable": if self.ob.sourceHref: return tag.fillSlots(sourceHref=self.ob.sourceHref) else: return () @renderer def functionExtras(self, request: object, tag: Tag) -> "Flattenable": return self._functionExtras @renderer def functionBody(self, request: object, tag: Tag) -> "Flattenable": return self.docgetter.get(self.ob) @renderer def functionDeprecated(self, request: object, tag: Tag) -> "Flattenable": msg = self.ob._deprecated_info if msg is None: return () else: return tags.div(msg, role="alert", class_="deprecationNotice alert alert-warning") pydoctor-21.12.1/pydoctor/templatewriter/pages/table.py000066400000000000000000000052351416703725300232110ustar00rootroot00000000000000from typing import TYPE_CHECKING, Collection from twisted.web.iweb import ITemplateLoader from twisted.web.template import Element, Tag, TagLoader, renderer, tags from pydoctor import epydoc2stan from pydoctor.model import Documentable, Function from pydoctor.templatewriter import TemplateElement, util if TYPE_CHECKING: from twisted.web.template import Flattenable from pydoctor.templatewriter.pages import DocGetter class TableRow(Element): def __init__(self, loader: ITemplateLoader, docgetter: "DocGetter", ob: Documentable, child: Documentable, ): super().__init__(loader) self.docgetter = docgetter self.ob = ob self.child = child @renderer def class_(self, request: object, tag: Tag) -> "Flattenable": class_ = util.css_class(self.child) if self.child.parent is not self.ob: class_ = 'base' + class_ return class_ @renderer def kind(self, request: object, tag: Tag) -> Tag: child = self.child kind = child.kind assert kind is not None # 'kind is None' makes the object invisible kind_name = epydoc2stan.format_kind(kind) if isinstance(child, Function) and child.is_async: # The official name is "coroutine function", but that is both # a bit long and not as widely recognized. kind_name = f'Async {kind_name}' return tag.clear()(kind_name) @renderer def name(self, request: object, tag: Tag) -> Tag: return tag.clear()(tags.code( epydoc2stan.taglink(self.child, self.ob.url, self.child.name) )) @renderer def summaryDoc(self, request: object, tag: Tag) -> Tag: return tag.clear()(self.docgetter.get(self.child, summary=True)) class ChildTable(TemplateElement): last_id = 0 filename = 'table.html' def __init__(self, docgetter: "DocGetter", ob: Documentable, children: Collection[Documentable], loader: ITemplateLoader, ): super().__init__(loader) self.docgetter = docgetter self.children = children ChildTable.last_id += 1 self._id = ChildTable.last_id self.ob = ob @renderer def id(self, request: object, tag: Tag) -> str: return f'id{self._id}' @renderer def rows(self, request: object, tag: Tag) -> "Flattenable": return [ TableRow( TagLoader(tag), self.docgetter, self.ob, child) for child in self.children if child.isVisible ] pydoctor-21.12.1/pydoctor/templatewriter/summary.py000066400000000000000000000273441416703725300225250ustar00rootroot00000000000000"""Classes that generate the summary pages.""" from collections import defaultdict from typing import ( TYPE_CHECKING, DefaultDict, Dict, Iterable, List, Mapping, MutableSet, Sequence, Tuple, Type, Union, cast ) from twisted.web.template import Element, Tag, TagLoader, renderer, tags from pydoctor import epydoc2stan, model from pydoctor.templatewriter import TemplateLookup from pydoctor.templatewriter.pages import Page if TYPE_CHECKING: from twisted.web.template import Flattenable from typing_extensions import Final def moduleSummary(module: model.Module, page_url: str) -> Tag: r: Tag = tags.li( tags.code(epydoc2stan.taglink(module, page_url)), ' - ', epydoc2stan.format_summary(module) ) if module.isPrivate: r(class_='private') if not isinstance(module, model.Package): return r contents = [m for m in module.contents.values() if isinstance(m, model.Module) and m.isVisible] if not contents: return r ul = tags.ul() def fullName(obj: model.Documentable) -> str: return obj.fullName() for m in sorted(contents, key=fullName): ul(moduleSummary(m, page_url)) r(ul) return r def _lckey(x: model.Documentable) -> Tuple[str, str]: return (x.fullName().lower(), x.fullName()) class ModuleIndexPage(Page): filename = 'moduleIndex.html' def __init__(self, system: model.System, template_lookup: TemplateLookup): # Override L{Page.loader} because here the page L{filename} # does not equal the template filename. super().__init__(system=system, template_lookup=template_lookup, loader=template_lookup.get_loader('summary.html') ) def title(self) -> str: return "Module Index" @renderer def stuff(self, request: object, tag: Tag) -> Tag: tag.clear() tag([moduleSummary(o, self.filename) for o in self.system.rootobjects]) return tag @renderer def heading(self, request: object, tag: Tag) -> Tag: tag().clear() tag("Module Index") return tag def findRootClasses( system: model.System ) -> Sequence[Tuple[str, Union[model.Class, Sequence[model.Class]]]]: roots: Dict[str, Union[model.Class, List[model.Class]]] = {} for cls in system.objectsOfType(model.Class): if ' ' in cls.name or not cls.isVisible: continue if cls.bases: for name, base in zip(cls.bases, cls.baseobjects): if base is None or not base.isVisible: # The base object is in an external library or filtered out (not visible) # Take special care to avoid AttributeError: 'ZopeInterfaceClass' object has no attribute 'append'. if isinstance(roots.get(name), model.Class): roots[name] = [cast(model.Class, roots[name])] cast(List[model.Class], roots.setdefault(name, [])).append(cls) elif base.system is not system: # Edge case with multiple systems, is it even possible to run into this code? roots[base.fullName()] = base else: # This is a common root class. roots[cls.fullName()] = cls return sorted(roots.items(), key=lambda x:x[0].lower()) def isPrivate(obj: model.Documentable) -> bool: """Is the object itself private or does it live in a private context?""" while not obj.isPrivate: parent = obj.parent if parent is None: return False obj = parent return True def isClassNodePrivate(cls: model.Class) -> bool: """Are a class and all its subclasses are private?""" if not isPrivate(cls): return False for sc in cls.subclasses: if not isClassNodePrivate(sc): return False return True def subclassesFrom( hostsystem: model.System, cls: model.Class, anchors: MutableSet[str], page_url: str ) -> Tag: r: Tag = tags.li() if isClassNodePrivate(cls): r(class_='private') name = cls.fullName() if name not in anchors: r(tags.a(name=name)) anchors.add(name) r(tags.code(epydoc2stan.taglink(cls, page_url)), ' - ', epydoc2stan.format_summary(cls)) scs = [sc for sc in cls.subclasses if sc.system is hostsystem and ' ' not in sc.fullName() and sc.isVisible] if len(scs) > 0: ul = tags.ul() for sc in sorted(scs, key=_lckey): ul(subclassesFrom(hostsystem, sc, anchors, page_url)) r(ul) return r class ClassIndexPage(Page): filename = 'classIndex.html' def __init__(self, system: model.System, template_lookup: TemplateLookup): # Override L{Page.loader} because here the page L{filename} # does not equal the template filename. super().__init__(system=system, template_lookup=template_lookup, loader=template_lookup.get_loader('summary.html') ) def title(self) -> str: return "Class Hierarchy" @renderer def stuff(self, request: object, tag: Tag) -> Tag: t = tag anchors: MutableSet[str] = set() for b, o in findRootClasses(self.system): if isinstance(o, model.Class): t(subclassesFrom(self.system, o, anchors, self.filename)) else: item = tags.li(tags.code(b)) if all(isClassNodePrivate(sc) for sc in o): # This is an external class used only by private API; # mark the whole node private. item(class_='private') if o: ul = tags.ul() for sc in sorted(o, key=_lckey): ul(subclassesFrom(self.system, sc, anchors, self.filename)) item(ul) t(item) return t @renderer def heading(self, request: object, tag: Tag) -> Tag: tag.clear() tag("Class Hierarchy") return tag class LetterElement(Element): def __init__(self, loader: TagLoader, initials: Mapping[str, Sequence[model.Documentable]], letter: str ): super().__init__(loader=loader) self.initials = initials self.my_letter = letter @renderer def letter(self, request: object, tag: Tag) -> Tag: tag(self.my_letter) return tag @renderer def letterlinks(self, request: object, tag: Tag) -> Tag: letterlinks: List["Flattenable"] = [] for initial in sorted(self.initials): if initial == self.my_letter: letterlinks.append(initial) else: letterlinks.append(tags.a(href='#'+initial)(initial)) letterlinks.append(' - ') if letterlinks: del letterlinks[-1] tag(letterlinks) return tag @renderer def names(self, request: object, tag: Tag) -> "Flattenable": def link(obj: model.Documentable) -> Tag: # The "data-type" attribute helps doc2dash figure out what # category (class, method, etc.) an object belongs to. attributes = {} if obj.kind: attributes["data-type"] = epydoc2stan.format_kind(obj.kind) return tags.code( epydoc2stan.taglink(obj, NameIndexPage.filename), **attributes ) name2obs: DefaultDict[str, List[model.Documentable]] = defaultdict(list) for obj in self.initials[self.my_letter]: name2obs[obj.name].append(obj) r = [] for name in sorted(name2obs, key=lambda x:(x.lower(), x)): item: Tag = tag.clone()(name) obs = name2obs[name] if all(isPrivate(ob) for ob in obs): item(class_='private') if len(obs) == 1: item(' - ', link(obs[0])) else: ul = tags.ul() for ob in sorted(obs, key=_lckey): subitem = tags.li(link(ob)) if isPrivate(ob): subitem(class_='private') ul(subitem) item(ul) r.append(item) return r class NameIndexPage(Page): filename = 'nameIndex.html' def __init__(self, system: model.System, template_lookup: TemplateLookup): super().__init__(system=system, template_lookup=template_lookup) self.initials: Dict[str, List[model.Documentable]] = {} for ob in self.system.allobjects.values(): if ob.isVisible: self.initials.setdefault(ob.name[0].upper(), []).append(ob) def title(self) -> str: return "Index of Names" @renderer def heading(self, request: object, tag: Tag) -> Tag: return tag.clear()("Index of Names") @renderer def index(self, request: object, tag: Tag) -> "Flattenable": r = [] for i in sorted(self.initials): r.append(LetterElement(TagLoader(tag), self.initials, i)) return r class IndexPage(Page): filename = 'index.html' def title(self) -> str: return f"API Documentation for {self.system.projectname}" @renderer def onlyIfOneRoot(self, request: object, tag: Tag) -> "Flattenable": if len(self.system.rootobjects) != 1: return [] else: root, = self.system.rootobjects return tag.clear()( "Start at ", tags.code(epydoc2stan.taglink(root, self.filename)), ", the root ", epydoc2stan.format_kind(root.kind).lower(), ".") @renderer def onlyIfMultipleRoots(self, request: object, tag: Tag) -> "Flattenable": if len(self.system.rootobjects) == 1: return [] else: return tag @renderer def roots(self, request: object, tag: Tag) -> "Flattenable": r = [] for o in self.system.rootobjects: r.append(tag.clone().fillSlots(root=tags.code( epydoc2stan.taglink(o, self.filename) ))) return r @renderer def rootkind(self, request: object, tag: Tag) -> Tag: return tag.clear()('/'.join(sorted( epydoc2stan.format_kind(o.kind, plural=True).lower() for o in self.system.rootobjects ))) def hasdocstring(ob: model.Documentable) -> bool: for source in ob.docsources(): if source.docstring is not None: return True return False class UndocumentedSummaryPage(Page): filename = 'undoccedSummary.html' def __init__(self, system: model.System, template_lookup: TemplateLookup): # Override L{Page.loader} because here the page L{filename} # does not equal the template filename. super().__init__(system=system, template_lookup=template_lookup, loader=template_lookup.get_loader('summary.html') ) def title(self) -> str: return "Summary of Undocumented Objects" @renderer def heading(self, request: object, tag: Tag) -> Tag: return tag.clear()("Summary of Undocumented Objects") @renderer def stuff(self, request: object, tag: Tag) -> Tag: undoccedpublic = [o for o in self.system.allobjects.values() if o.isVisible and not hasdocstring(o)] undoccedpublic.sort(key=lambda o:o.fullName()) for o in undoccedpublic: kind = o.kind assert kind is not None # 'kind is None' makes the object invisible tag(tags.li( epydoc2stan.format_kind(kind), " - ", tags.code(epydoc2stan.taglink(o, self.filename)) )) return tag summarypages: 'Final[Iterable[Type[Page]]]' = [ ModuleIndexPage, ClassIndexPage, IndexPage, NameIndexPage, UndocumentedSummaryPage, ] pydoctor-21.12.1/pydoctor/templatewriter/util.py000066400000000000000000000074151416703725300220020ustar00rootroot00000000000000"""Miscellaneous utilities for the HTML writer.""" import warnings import collections.abc from typing import Any, Dict, Generic, Iterable, Iterator, Mapping, Optional, MutableMapping, Tuple, TypeVar, Union from pydoctor import model from pydoctor.epydoc2stan import format_kind def css_class(o: model.Documentable) -> str: """ A short, lower case description for use as a CSS class in HTML. Includes the kind and privacy. """ kind = o.kind assert kind is not None # if kind is None, object is invisible class_ = format_kind(kind).lower().replace(' ', '') if o.privacyClass is model.PrivacyClass.PRIVATE: class_ += ' private' return class_ def srclink(o: model.Documentable) -> Optional[str]: return o.sourceHref def templatefile(filename: str) -> None: """Deprecated: can be removed once Twisted stops patching this.""" warnings.warn("pydoctor.templatewriter.util.templatefile() " "is deprecated and returns None. It will be remove in future versions. " "Please use the templating system.") return None _VT = TypeVar('_VT') # Credits: psf/requests see https://github.com/psf/requests/blob/main/AUTHORS.rst class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]): """A case-insensitive ``dict``-like object. Implements all methods and operations of ``collections.MutableMapping`` as well as dict's ``copy``. Also provides ``lower_items``. All keys are expected to be strings. The structure remembers the case of the last key to be set, and ``iter(instance)``, ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` will contain case-sensitive keys. However, querying and contains testing is case insensitive:: cid = CaseInsensitiveDict() cid['Accept'] = 'application/json' cid['aCCEPT'] == 'application/json' # True list(cid) == ['Accept'] # True For example, ``headers['content-encoding']`` will return the value of a ``'Content-Encoding'`` response header, regardless of how the header name was originally stored. If the constructor, ``.update``, or equality comparison operations are given keys that have equal ``.lower()``s, the behavior is undefined. """ def __init__(self, data: Optional[Union[Mapping[str, _VT], Iterable[Tuple[str, _VT]]]] = None, **kwargs: Any) -> None: self._store: Dict[str, Tuple[str, _VT]] = collections.OrderedDict() if data is None: data = {} self.update(data, **kwargs) def __setitem__(self, key: str, value: _VT) -> None: # Use the lowercased key for lookups, but store the actual # key alongside the value. self._store[key.lower()] = (key, value) def __getitem__(self, key: str) -> _VT: return self._store[key.lower()][1] def __delitem__(self, key: str) -> None: del self._store[key.lower()] def __iter__(self) -> Iterator[str]: return (casedkey for casedkey, mappedvalue in self._store.values()) def __len__(self) -> int: return len(self._store) def lower_items(self) -> Iterator[Tuple[str, _VT]]: """Like iteritems(), but with all lowercase keys.""" return ( (lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items() ) def __eq__(self, other: Any) -> bool: if isinstance(other, collections.abc.Mapping): other = CaseInsensitiveDict(other) # Compare insensitively return dict(self.lower_items()) == dict(other.lower_items()) else: return NotImplemented # Copy is required def copy(self) -> 'CaseInsensitiveDict[_VT]': return CaseInsensitiveDict(self._store.values()) def __repr__(self) -> str: return str(dict(self.items())) pydoctor-21.12.1/pydoctor/templatewriter/writer.py000066400000000000000000000103211416703725300223270ustar00rootroot00000000000000"""Badly named module that contains the driving code for the rendering.""" from pathlib import Path from typing import IO, Iterable, Type, TYPE_CHECKING from pydoctor import model from pydoctor.templatewriter import ( DOCTYPE, pages, summary, TemplateLookup, IWriter, StaticTemplate ) from twisted.python.failure import Failure from twisted.web.template import flattenString if TYPE_CHECKING: from twisted.web.template import Flattenable def flattenToFile(fobj: IO[bytes], elem: "Flattenable") -> None: """ This method writes a page to a HTML file. @raises Exception: If the L{twisted.web.template.flatten} call fails. """ fobj.write(DOCTYPE) err = None def e(r: Failure) -> None: nonlocal err err = r.value flattenString(None, elem).addCallback(fobj.write).addErrback(e) if err: raise err class TemplateWriter(IWriter): """ HTML templates writer. """ @classmethod def __subclasshook__(cls, subclass: Type[object]) -> bool: for name in dir(cls): if not name.startswith('_'): if not hasattr(subclass, name): return False return True def __init__(self, build_directory: Path, template_lookup: TemplateLookup): """ @arg build_directory: Build directory. @arg template_lookup: L{TemplateLookup} object. """ self.build_directory = build_directory """Build directory""" self.template_lookup: TemplateLookup = template_lookup """Writer's L{TemplateLookup} object""" self.written_pages: int = 0 self.total_pages: int = 0 self.dry_run: bool = False def prepOutputDirectory(self) -> None: """ Write static CSS and JS files to build directory. """ self.build_directory.mkdir(exist_ok=True, parents=True) for template in self.template_lookup.templates: if isinstance(template, StaticTemplate): template.write(self.build_directory) def writeIndividualFiles(self, obs: Iterable[model.Documentable]) -> None: """ Iterate through C{obs} and call L{_writeDocsFor} method for each L{Documentable}. """ self.dry_run = True for ob in obs: self._writeDocsFor(ob) self.dry_run = False for ob in obs: self._writeDocsFor(ob) def writeSummaryPages(self, system: model.System) -> None: import time for pclass in summary.summarypages: system.msg('html', 'starting ' + pclass.__name__ + ' ...', nonl=True) T = time.time() page = pclass(system=system, template_lookup=self.template_lookup) with self.build_directory.joinpath(pclass.filename).open('wb') as fobj: flattenToFile(fobj, page) system.msg('html', "took %fs"%(time.time() - T), wantsnl=False) def _writeDocsFor(self, ob: model.Documentable) -> None: if not ob.isVisible: return if ob.documentation_location is model.DocLocation.OWN_PAGE: if self.dry_run: self.total_pages += 1 else: with self.build_directory.joinpath(f'{ob.fullName()}.html').open('wb') as fobj: self._writeDocsForOne(ob, fobj) for o in ob.contents.values(): self._writeDocsFor(o) def _writeDocsForOne(self, ob: model.Documentable, fobj: IO[bytes]) -> None: if not ob.isVisible: return pclass: Type[pages.CommonPage] = pages.CommonPage for parent in ob.__class__.__mro__: # This implementation relies on 'pages.commonpages' dict that ties # documentable class name (i.e. 'Class') with the # page class used for rendering: pages.ClassPage try: pclass = pages.commonpages[parent.__name__] except KeyError: continue else: break ob.system.msg('html', str(ob), thresh=1) page = pclass(ob=ob, template_lookup=self.template_lookup) self.written_pages += 1 ob.system.progress('html', self.written_pages, self.total_pages, 'pages written') flattenToFile(fobj, page) pydoctor-21.12.1/pydoctor/test/000077500000000000000000000000001416703725300163535ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/__init__.py000066400000000000000000000053511416703725300204700ustar00rootroot00000000000000"""PyDoctor's test suite.""" from logging import LogRecord from typing import Iterable, TYPE_CHECKING, Optional, Sequence import sys import pytest from pathlib import Path from twisted.web.template import Tag, tags from pydoctor import epydoc2stan, model from pydoctor.templatewriter import IWriter, TemplateLookup from pydoctor.epydoc.markup import DocstringLinker if TYPE_CHECKING: from twisted.web.template import Flattenable posonlyargs = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") typecomment = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") # Because pytest 6.1 does not yet export types for fixtures, we define # approximations that are good enough for our test cases: if TYPE_CHECKING: from typing_extensions import Protocol class CapLog(Protocol): records: Sequence[LogRecord] class CaptureResult(Protocol): out: str err: str class CapSys(Protocol): def readouterr(self) -> CaptureResult: ... from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.tmpdir import TempPathFactory else: CapLog = CaptureResult = CapSys = object FixtureRequest = MonkeyPatch = TempPathFactory = object class InMemoryWriter(IWriter): """ Minimal template writer that doesn't touches the filesystem but will trigger the rendering of epydoc for the targeted code. """ def __init__(self, build_directory: Path, template_lookup: 'TemplateLookup') -> None: pass def prepOutputDirectory(self) -> None: """ Does nothing. """ def writeIndividualFiles(self, obs: Iterable[model.Documentable]) -> None: """ Trigger in memory rendering for all objects. """ for ob in obs: self._writeDocsFor(ob) def writeSummaryPages(self, system: model.System) -> None: """ Rig the system to not created the inter sphinx inventory. """ system.options.makeintersphinx = False def _writeDocsFor(self, ob: model.Documentable) -> None: """ Trigger in memory rendering of the object. """ if not ob.isVisible: return epydoc2stan.format_docstring(ob) for o in ob.contents.values(): self._writeDocsFor(o) class NotFoundLinker(DocstringLinker): """A DocstringLinker implementation that cannot find any links.""" def link_to(self, target: str, label: "Flattenable") -> Tag: return tags.transparent(label) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: return tags.code(label) def resolve_identifier(self, identifier: str) -> Optional[str]: return None pydoctor-21.12.1/pydoctor/test/epydoc/000077500000000000000000000000001416703725300176365ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/epydoc/__init__.py000066400000000000000000000010231416703725300217430ustar00rootroot00000000000000# epydoc -- Regression testing # # Copyright (C) 2005 Edward Loper # Author: Edward Loper # URL: # from typing import List from pydoctor.epydoc.markup import ParseError, ParsedDocstring, get_parser_by_name def parse_docstring(doc: str, markup: str, processtypes: bool = False) -> ParsedDocstring: errors: List[ParseError] = [] parsed = get_parser_by_name(markup)(doc, errors, processtypes) assert not errors, [f"{e.linenum()}:{e.descr()}" for e in errors] return parsed pydoctor-21.12.1/pydoctor/test/epydoc/epytext.doctest000066400000000000000000000121441416703725300227310ustar00rootroot00000000000000Regression Testing for epytext ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These tests were taken pretty much verbatim out of the old unittests from epydoc 2.1. They could use some serious updating, when I get the time, esp. given that it's so much easier to write tests with doctest than it was with unittest. >>> from pydoctor.test.epydoc.test_epytext import parse >>> import re >>> def testparse(s): ... out = parse(s) ... # This is basically word-wrapping: ... out = re.sub(r'(()+)', r'\1\n', out).rstrip() ... out = re.sub(r'(?m)^(.{50,70}>)(.)', r'\1\n\2', out).rstrip() ... return out Paragraphs: >>> print(testparse(""" ... this is one paragraph. ... ... This is ... another. ... ... This is a third""")) this is one paragraph. This is another. This is a third Make sure that unindented fields are allowed: >>> print(testparse(""" ... This is a paragraph. ... ... @foo: This is a field.""")) This is a paragraph. foo This is a field. >>> print(testparse(""" ... This is a paragraph. ... @foo: This is a field.""")) This is a paragraph. foo This is a field. >>> print(testparse(""" ... This is a paragraph. ... @foo: This is a field. ... Hello.""")) This is a paragraph. foo This is a field. Hello. >>> print(testparse("""Paragraph\n@foo: field""")) Paragraph foo field >>> print(testparse("""Paragraph\n\n@foo: field""")) Paragraph foo field >>> print(testparse("""\nParagraph\n@foo: field""")) Paragraph foo field Make sure thta unindented lists are not allowed: >>> print(testparse(""" ... This is a paragraph. ... ... - This is a list item.""")) Traceback (most recent call last): StructuringError: Line 4: Lists must be indented. >>> print(testparse(""" ... This is a paragraph. ... - This is a list item.""")) Traceback (most recent call last): StructuringError: Line 3: Lists must be indented. >>> print(testparse(""" ... This is a paragraph. ... - This is a list item. ... Hello. ... - Sublist item""")) Traceback (most recent call last): StructuringError: Line 5: Lists must be indented. >>> print(testparse(""" ... This is a paragraph. ... - This is a list item. ... Hello. ... ... - Sublist item""")) Traceback (most recent call last): StructuringError: Line 6: Lists must be indented. >>> print(testparse("""Paragraph\n\n- list item""")) Traceback (most recent call last): StructuringError: Line 3: Lists must be indented. >>> print(testparse("""\nParagraph\n- list item""")) Traceback (most recent call last): StructuringError: Line 3: Lists must be indented. Special case if there's text on the same line as the opening quote: >>> print(testparse("""Paragraph\n- list item""")) Paragraph
  • list item
  • Make sure that indented lists are allowed: >>> print(testparse('This is a paragraph.\n - This is a list item.\n'+ ... 'This is a paragraph')) This is a paragraph.
  • This is a list item.
  • This is a paragraph >>> print(testparse('This is a paragraph.\n\n - This is a list item.'+ ... '\n\nThis is a paragraph')) This is a paragraph.
  • This is a list item.
  • This is a paragraph >>> print(testparse(""" ... This is a paragraph. ... ... - This is a list item. ... ... This is a paragraph""")) This is a paragraph.
  • This is a list item.
  • This is a paragraph >>> print(testparse(""" ... This is a paragraph. ... ... - This is a list item. ... This is a paragraph""")) This is a paragraph.
  • This is a list item.
  • This is a paragraph >>> print(testparse(""" ... - This is a list item."""))
  • This is a list item.
  • >>> print(testparse("""- This is a list item."""))
  • This is a list item.
  • >>> print(testparse("""\n- This is a list item."""))
  • This is a list item.
  • pydoctor-21.12.1/pydoctor/test/epydoc/restructuredtext.doctest000066400000000000000000000054451416703725300246750ustar00rootroot00000000000000Regression Testing for restructuredtext ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :RequireModule: docutils >>> from pydoctor.epydoc.markup import restructuredtext >>> from pydoctor.stanutils import flatten >>> def parse_and_print(s): ... errors = [] ... parsed = restructuredtext.parse_docstring(s, errors) ... for error in errors: ... print(f'ERROR: {error}') ... if parsed is None: ... print('EMPTY BODY') ... else: ... print(flatten(parsed.to_stan(None))) ... for field in parsed.fields: ... body = flatten(field.body().to_stan(None)) ... arg = field.arg() ... if arg is None: ... print(f'{field.tag()}: {body}') ... else: ... print(f'{field.tag()} "{arg}": {body}') Fields ====== >>> parse_and_print( ... """A test module ... ... :Version: 1.0 ... :Parameter i: integer ... """) A test module version: 1.0 parameter "i": integer >>> parse_and_print( ... """A test function ... ... :Parameters: a b c ... """) ERROR: Line 4: Unable to split consolidated field "Parameters" - does not contain a bulleted list or definition list. A test function newfield "parameters":

    Parameters

    parameters: a b c >>> parse_and_print( ... """A test function ... ... :exceptions: - `KeyError`: if the key is not found ... - `ValueError`: if the value is bad ... """) A test function except "KeyError": if the key is not found except "ValueError": if the value is bad >>> parse_and_print( ... """ ... Return the maximum speed for a fox. ... ... :Parameters: ... size ... The size of the fox (in meters) ... weight : float ... The weight of the fox (in stones) ... age : int ... The age of the fox (in years) ... """) Return the maximum speed for a fox. param "size": The size of the fox (in meters) param "weight": The weight of the fox (in stones) type "weight": float param "age": The age of the fox (in years) type "age": int Python code =========== reStructuredText markup defines a ``python`` directive to represent a block as colorized Python code. >>> err = [] >>> p = restructuredtext.parse_docstring( ... """A test module ... ... .. python:: ... ... # This is some Python code ... def foo(): ... pass ... ... class Foo: ... def __init__(self): ... pass ... """, err) >>> err [] >>> print(flatten(p.to_stan(None)))

    A test module

    # This is some Python code
    def foo():
        pass
    
    class Foo:
        def __init__(self):
            pass
    pydoctor-21.12.1/pydoctor/test/epydoc/test_epytext.py000066400000000000000000000067671416703725300227710ustar00rootroot00000000000000from typing import List from pydoctor.epydoc.markup import DocstringLinker, ParseError, epytext from pydoctor.test import NotFoundLinker from pydoctor.stanutils import flatten def epytext2html(s: str, linker: DocstringLinker = NotFoundLinker()) -> str: errs: List[ParseError] = [] v = flatten(epytext.parse_docstring(s, errs).to_stan(linker)) if errs: raise errs[0] return (v or '').rstrip() def parse(s: str) -> str: # this strips off the ... return ''.join(str(n) for n in epytext.parse(s).children) def test_basic_list() -> None: P1 = "This is a paragraph." P2 = "This is a \nparagraph." LI1 = " - This is a list item." LI2 = "\n - This is a list item." LI3 = " - This is a list\n item." LI4 = "\n - This is a list\n item." PARA = ('This is a paragraph.') ONELIST = ('
  • This is a ' 'list item.
  • ') TWOLIST = ('
  • This is a ' 'list item.
  • This is a ' 'list item.
  • ') for p in (P1, P2): for li1 in (LI1, LI2, LI3, LI4): assert parse(li1) == ONELIST assert parse(f'{p}\n{li1}') == PARA+ONELIST assert parse(f'{li1}\n{p}') == ONELIST+PARA assert parse(f'{p}\n{li1}\n{p}') == PARA+ONELIST+PARA for li2 in (LI1, LI2, LI3, LI4): assert parse(f'{li1}\n{li2}') == TWOLIST assert parse(f'{p}\n{li1}\n{li2}') == PARA+TWOLIST assert parse(f'{li1}\n{li2}\n{p}') == TWOLIST+PARA assert parse(f'{p}\n{li1}\n{li2}\n{p}') == PARA+TWOLIST+PARA LI5 = " - This is a list item.\n\n It contains two paragraphs." LI5LIST = ('
  • This is a list item.' 'It contains two paragraphs.
  • ') assert parse(LI5) == LI5LIST assert parse(f'{P1}\n{LI5}') == PARA+LI5LIST assert parse(f'{P2}\n{LI5}\n{P1}') == PARA+LI5LIST+PARA LI6 = (" - This is a list item with a literal block::\n" " hello\n there") LI6LIST = ('
  • This is a list item with a literal ' 'block: hello\n there' '
  • ') assert parse(LI6) == LI6LIST assert parse(f'{P1}\n{LI6}') == PARA+LI6LIST assert parse(f'{P2}\n{LI6}\n{P1}') == PARA+LI6LIST+PARA def test_item_wrap() -> None: LI = "- This is a list\n item." ONELIST = ('
  • This is a ' 'list item.
  • ') TWOLIST = ('
  • This is a ' 'list item.
  • This is a ' 'list item.
  • ') for indent in ('', ' '): for nl1 in ('', '\n'): assert parse(nl1+indent+LI) == ONELIST for nl2 in ('\n', '\n\n'): assert parse(nl1+indent+LI+nl2+indent+LI) == TWOLIST def test_literal_braces() -> None: """SF bug #1562530 reported some trouble with literal braces. This test makes sure that braces are getting rendered as desired. """ assert epytext2html("{1:{2:3}}") == '{1:{2:3}}' assert epytext2html("C{{1:{2:3}}}") == '{1:{2:3}}' assert epytext2html("{1:C{{2:3}}}") == '{1:{2:3}}' assert epytext2html("{{{}{}}{}}") == '{{{}{}}{}}' assert epytext2html("{{E{lb}E{lb}E{lb}}}") == '{{{{{}}' pydoctor-21.12.1/pydoctor/test/epydoc/test_epytext2html.py000066400000000000000000000225471416703725300237320ustar00rootroot00000000000000""" Test how epytext is transformed to HTML using L{ParsedDocstring.to_node()} and L{node2stan.node2stan()} functions. Many of these test cases are adapted examples from U{the epytext documentation}. """ from typing import List from pydoctor.epydoc.markup import ParseError, ParsedDocstring from pydoctor.stanutils import flatten from pydoctor.epydoc.markup.epytext import parse_docstring from pydoctor.node2stan import node2stan from pydoctor.test import NotFoundLinker from pydoctor.test.epydoc.test_restructuredtext import prettify from docutils import nodes def parse_epytext(s: str) -> ParsedDocstring: errors: List[ParseError] = [] parsed = parse_docstring(s, errors) assert not errors return parsed def epytext2node(s: str)-> nodes.document: return parse_epytext(s).to_node() def epytext2html(s: str) -> str: return squash(flatten(node2stan(epytext2node(s), NotFoundLinker()))) def squash(s: str) -> str: return ''.join(l.strip() for l in prettify(s).splitlines()) def test_epytext_paragraph() -> None: doc = ''' This is a paragraph. Paragraphs can span multiple lines, and can contain I{inline markup}. This is another paragraph. Paragraphs are separated by blank lines. ''' expected = '''

    This is a paragraph. Paragraphs can span multiple lines, and can contain inline markup .

    This is another paragraph. Paragraphs are separated by blank lines.

    ''' assert epytext2html(doc) == squash(expected) def test_epytext_ordered_list() -> None: doc = ''' 1. This is an ordered list item. 2. This is another ordered list item. 3. This is a third list item. Note that the paragraph may be indented more than the bullet. This ends the list. 4. This new list starts at four. ''' expected = '''
    1. This is an ordered list item.
    2. This is another ordered list item.
    3. This is a third list item. Note that the paragraph may be indented more than the bullet.

    This ends the list.

    1. This new list starts at four.
    ''' assert epytext2html(doc) == squash(expected) def test_epytext_nested_list() -> None: doc = ''' This is a paragraph. 1. This is a list item. 2. This is a second list item. - This is a sublist. ''' expected = '''

    This is a paragraph.

    1. This is a list item.
    2. This is a second list item.
      • This is a sublist.
    ''' assert epytext2html(doc) == squash(expected) def test_epytext_complex_list() -> None: doc = ''' This is a paragraph. 1. This is a list item. - This is a sublist. - The sublist contains two items. - The second item of the sublist has its own sublist. 2. This list item contains two paragraphs and a doctest block. >>> len('This is a doctest block') 23 This is the second paragraph. ''' expected = '''

    This is a paragraph.

    1. This is a list item.

      • This is a sublist.
      • The sublist contains two items.
        • The second item of the sublist has its own sublist.
    2. This list item contains two paragraphs and a doctest block.

      >>> 
              len('This is a doctest block')
              23

      This is the second paragraph.

    ''' assert epytext2html(doc) == squash(expected) def test_epytext_sections() -> None: doc = ''' This paragraph is not in any section. Section 1 ========= This is a paragraph in section 1. Section 1.1 ----------- This is a paragraph in section 1.1. Section 2 ========= This is a paragraph in section 2. ''' expected = '''

    This paragraph is not in any section.

    Section 1

    This is a paragraph in section 1.

    Section 1.1

    This is a paragraph in section 1.1.

    Section 2

    This is a paragraph in section 2.

    ''' assert epytext2html(doc) == squash(expected) def test_epytext_literal_block() -> None: doc = ''' The following is a literal block:: Literal / / Block This is a paragraph following the literal block. ''' expected = '''

    The following is a literal block:

        Literal /
               / Block
    

    This is a paragraph following the literal block.

    ''' assert epytext2html(doc) == squash(expected) def test_epytext_inline() -> None: doc = ''' I{B{Inline markup} may be nested; and it may span} multiple lines. - I{Italicized text} - B{Bold-faced text} - C{Source code} - Math: M{m*x+b} Without the capital letter, matching braces are not interpreted as markup: C{my_dict={1:2, 3:4}}. ''' expected = '''

    Inline markup may be nested; and it may span multiple lines.

    • Italicized text
    • Bold-faced text
    • Source code
    • Math: m * x + b

    Without the capital letter, matching braces are not interpreted as markup: my_dict={1:2, 3:4} .

    ''' assert epytext2html(doc) == squash(expected) def test_epytext_url() -> None: doc = ''' - U{www.python.org} - U{http://www.python.org} - U{The epydoc homepage} - U{The B{I{Python}} homepage } - U{Edward Loper} ''' expected = ''' ''' assert epytext2html(doc) == squash(expected) def test_epytext_symbol() -> None: doc = ''' Symbols can be used in equations: - S{sum}S{alpha}/x S{<=} S{beta} S{<-} and S{larr} both give left arrows. Some other arrows are S{rarr}, S{uarr}, and S{darr}. ''' expected = '''

    Symbols can be used in equations:

    • α /x β

    and both give left arrows. Some other arrows are , , and .

    ''' assert epytext2html(doc) == squash(expected) def test_nested_markup() -> None: """ The Epytext nested inline markup are correctly transformed to HTML. """ doc = ''' I{B{Inline markup} may be nested; and it may span} multiple lines. ''' expected = ''' Inline markup may be nested; and it may spanmultiple lines.''' assert epytext2html(doc) == squash(expected) doc = ''' It becomes a little bit complicated with U{B{custom} links } ''' expected = ''' It becomes a little bit complicated withcustomlinks ''' assert epytext2html(doc) == squash(expected) pydoctor-21.12.1/pydoctor/test/epydoc/test_epytext2node.py000066400000000000000000000025421416703725300237040ustar00rootroot00000000000000from pydoctor.test.epydoc.test_epytext2html import epytext2node def test_nested_markup() -> None: """ The Epytext nested inline markup are correctly transformed to L{docutils} nodes. """ doc = ''' I{B{Inline markup} may be nested; and it may span} multiple lines. ''' expected = ''' Inline markup may be nested; and it may span multiple lines. ''' assert epytext2node(doc).pformat() == expected doc = ''' It becomes a little bit complicated with U{B{custom} links } ''' expected = ''' It becomes a little bit complicated with custom links ''' assert epytext2node(doc).pformat() == expected doc = ''' It becomes a little bit complicated with L{B{custom} links } ''' expected = ''' It becomes a little bit complicated with custom links ''' assert epytext2node(doc).pformat() == expected pydoctor-21.12.1/pydoctor/test/epydoc/test_google_numpy.py000066400000000000000000000070141416703725300237550ustar00rootroot00000000000000from typing import List from pydoctor.epydoc.markup import ParseError from unittest import TestCase from pydoctor.test import NotFoundLinker from pydoctor.model import Attribute, System, Function from pydoctor.stanutils import flatten from pydoctor.epydoc.markup.google import get_parser as get_google_parser from pydoctor.epydoc.markup.numpy import get_parser as get_numpy_parser class TestGetParser(TestCase): def test_get_google_parser_attribute(self) -> None: obj = Attribute(system = System(), name='attr1') parse_docstring = get_google_parser(obj) docstring = """\ numpy.ndarray: super-dooper attribute""" errors: List[ParseError] = [] actual = flatten(parse_docstring(docstring, errors, False).fields[-1].body().to_stan(NotFoundLinker())) expected = """numpy.ndarray""" self.assertEqual(expected, actual) self.assertEqual(errors, []) def test_get_google_parser_not_attribute(self) -> None: obj = Function(system = System(), name='whatever') parse_docstring = get_google_parser(obj) docstring = """\ numpy.ndarray: super-dooper attribute""" errors: List[ParseError] = [] assert not parse_docstring(docstring, errors, False).fields # the numpy inline attribute parsing is the same as google-style # as shown in the example_numpy.py from Sphinx docs def test_get_numpy_parser_attribute(self) -> None: obj = Attribute(system = System(), name='attr1') parse_docstring = get_numpy_parser(obj) docstring = """\ numpy.ndarray: super-dooper attribute""" errors: List[ParseError] = [] actual = flatten(parse_docstring(docstring, errors, False).fields[-1].body().to_stan(NotFoundLinker())) expected = """numpy.ndarray""" self.assertEqual(expected, actual) self.assertEqual(errors, []) def test_get_numpy_parser_not_attribute(self) -> None: obj = Function(system = System(), name='whatever') parse_docstring = get_numpy_parser(obj) docstring = """\ numpy.ndarray: super-dooper attribute""" errors: List[ParseError] = [] assert not parse_docstring(docstring, errors, False).fields class TestWarnings(TestCase): def test_warnings(self) -> None: obj = Function(system = System(), name='func') parse_docstring = get_numpy_parser(obj) docstring = """ Description of the function. Some more text. Some more text. Some more text. Some more text. Args ---- my attr: 'bar or 'foo' super-dooper attribute a valid typed param: List[Union[str, bytes]] Description. other: {hello Choices. Returns ------- 'spam' or 'baz, optional A string. Note ---- Some more text. """ errors: List[ParseError] = [] parse_docstring(docstring, errors, False) self.assertEqual(len(errors), 3) self.assertIn("malformed string literal (missing closing quote)", errors[2].descr()) self.assertIn("invalid value set (missing closing brace)", errors[1].descr()) self.assertIn("malformed string literal (missing opening quote)", errors[0].descr()) self.assertEqual(errors[2].linenum(), 21) # #FIXME: It should be 23 actually... self.assertEqual(errors[1].linenum(), 18) self.assertEqual(errors[0].linenum(), 14) pydoctor-21.12.1/pydoctor/test/epydoc/test_parsed_docstrings.py000066400000000000000000000031741416703725300247710ustar00rootroot00000000000000""" Test generic features of ParsedDocstring. """ from typing import List from twisted.web.template import Tag from pydoctor.epydoc.markup import ParsedDocstring, ParseError from pydoctor.stanutils import flatten from pydoctor.epydoc.markup.plaintext import parse_docstring from pydoctor.test.epydoc.test_epytext2html import parse_epytext from pydoctor.test.epydoc.test_restructuredtext import parse_rst, prettify from pydoctor.test import NotFoundLinker def parse_plaintext(s: str) -> ParsedDocstring: errors: List[ParseError] = [] parsed = parse_docstring(s, errors) assert not errors return parsed def flatten_(stan: Tag) -> str: return ''.join(l.strip() for l in prettify(flatten(stan)).splitlines()) def test_to_node_to_stan_caching() -> None: """ Test if we get the same value again and again. """ epy = parse_epytext('Just some B{strings}') assert epy.to_node() == epy.to_node() == epy.to_node() assert flatten_(epy.to_stan(NotFoundLinker())) == flatten_(epy.to_stan(NotFoundLinker())) == flatten_(epy.to_stan(NotFoundLinker())) rst = parse_rst('Just some **strings**') assert rst.to_node() == rst.to_node() == rst.to_node() assert flatten_(rst.to_stan(NotFoundLinker())) == flatten_(rst.to_stan(NotFoundLinker())) == flatten_(rst.to_stan(NotFoundLinker())) plain = parse_plaintext('Just some **strings**') # ParsedPlaintextDocstring does not currently implement to_node() # assert plain.to_node() == plain.to_node() == plain.to_node() assert flatten_(plain.to_stan(NotFoundLinker())) == flatten_(plain.to_stan(NotFoundLinker())) == flatten_(plain.to_stan(NotFoundLinker())) pydoctor-21.12.1/pydoctor/test/epydoc/test_pyval_repr.py000066400000000000000000001140721416703725300234370ustar00rootroot00000000000000import ast import sys from textwrap import dedent from typing import Any, Union from pydoctor.epydoc.markup._pyval_repr import PyvalColorizer from pydoctor.test import NotFoundLinker from pydoctor.stanutils import flatten from pydoctor.node2stan import gettext def color(v: Any, linebreakok:bool=True, maxlines:int=5, linelen:int=40) -> str: colorizer = PyvalColorizer(linelen=linelen, linebreakok=linebreakok, maxlines=maxlines) parsed_doc = colorizer.colorize(v) return parsed_doc.to_node().pformat() #type: ignore def test_simple_types() -> None: """ Integers, floats, None, and complex numbers get printed using str, with no syntax highlighting. """ assert color(1) == """ 1\n""" assert color(0) == """ 0\n""" assert color(100) == """ 100\n""" assert color(1./4) == """ 0.25\n""" assert color(None) == """ None\n""" def test_long_numbers() -> None: """ Long ints will get wrapped if they're big enough. """ assert color(10000000) == """ 10000000\n""" assert color(10**90) == """ 1000000000000000000000000000000000000000 ↵ 0000000000000000000000000000000000000000 ↵ 00000000000\n""" def test_strings() -> None: """ Strings have their quotation marks tagged as 'quote'. Characters are escaped using the 'string-escape' encoding. """ assert color(bytes(range(255)), maxlines=9999) == r""" b ''' \x00\x01\x02\x03\x04\x05\x06\x07\x08 \t \x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x 15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x 1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCD EFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijk lmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\ x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\ x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\ x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\ xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\ xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\ xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\ xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\ xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\ xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\ xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\ xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\ xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\ xfc\xfd\xfe ''' """ def test_strings_quote() -> None: """ Currently, the "'" quote is always used, because that's what the 'string-escape' encoding expects. """ assert color('Hello') == """ ' Hello ' """ assert color('"Hello"') == """ ' "Hello" ' """ assert color("'Hello'") == r""" ' \'Hello\' ' """ def test_strings_special_chars() -> None: assert color("'abc \t\r\n\f\v \xff 😀'\x0c\x0b\t\r \\") == r""" ''' \'abc \t\r \f\v ÿ 😀\'\f\v\t\r \\ ''' """ def test_strings_multiline() -> None: """Strings containing newlines are automatically rendered as multiline strings.""" assert color("This\n is a multiline\n string!") == """ ''' This is a multiline string! '''\n""" # Unless we ask for them not to be: assert color("This\n is a multiline\n string!", linebreakok=False) == r""" ' This\n is a multiline\n string! ' """ def test_bytes_multiline() -> None: # The same should work also for binary strings (bytes): assert color(b"This\n is a multiline\n string!") == """ b ''' This is a multiline string! '''\n""" assert color(b"This\n is a multiline\n string!", linebreakok=False) == r""" b ' This\n is a multiline\n string! ' """ def test_unicode_str() -> None: """Unicode strings are handled properly. """ assert color("\uaaaa And \ubbbb") == """ ' ꪪ And 뮻 '\n""" assert color("ÉéèÈÜÏïü") == """ ' ÉéèÈÜÏïü '\n""" def test_bytes_str() -> None: """ Binary strings (bytes) are handled properly:""" assert color(b"Hello world") == """ b ' Hello world '\n""" assert color(b"\x00 And \xff") == r""" b ' \x00 And \xff ' """ def test_inline_list() -> None: """Lists, tuples, and sets are all colorized using the same method. The braces and commas are tagged with "op". If the value can fit on the current line, it is displayed on one line. Otherwise, each value is listed on a separate line, indented by the size of the open-bracket.""" assert color(list(range(10))) == """ [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]\n""" def test_multiline_list() -> None: assert color(list(range(100))) == """ [ 0 , 1 , 2 , 3 , 4 , ...\n""" def test_multiline_list2() -> None: assert color([1,2,[5,6,[(11,22,33),9],10],11]+[99,98,97,96,95]) == """ [ 1 , 2 , [ 5 , 6 , [ ( 11 , 22 , 33 ) , 9 ] , 10 ] , 11 , 99 , ...\n""" def test_multiline_set() -> None: assert color(set(range(20))) == """ set([ 0 , 1 , 2 , 3 , 4 , ...\n""" def test_frozenset() -> None: assert color(frozenset([1, 2, 3])) == """ frozenset([ 1 , 2 , 3 ])\n""" def test_custom_live_object() -> None: class Custom: def __repr__(self) -> str: return '123' assert color(Custom()) == """ 123\n""" def test_buggy_live_object() -> None: class Buggy: def __repr__(self) -> str: raise NotImplementedError() assert color(Buggy()) == """ ??\n""" def test_tuples_one_value() -> None: """Tuples that contains only one value need an ending comma.""" assert color((1,)) == """ ( 1 ,) """ def test_dictionaries() -> None: """Dicts are treated just like lists, except that the ":" is also tagged as "op".""" assert color({'1':33, '2':[1,2,3,{7:'oo'*20}]}) == """ { ' 1 ' : 33 , ' 2 ' : [ 1 , 2 , 3 , { 7 : ' oooooooooooooooooooooooooooo ...\n""" def extract_expr(_ast: ast.Module) -> ast.AST: elem = _ast.body[0] assert isinstance(elem, ast.Expr) return elem.value def test_ast_constants() -> None: assert color(extract_expr(ast.parse(dedent(""" 'Hello' """)))) == """ ' Hello '\n""" def test_ast_unary_op() -> None: assert color(extract_expr(ast.parse(dedent(""" not True """)))) == """ not True\n""" assert color(extract_expr(ast.parse(dedent(""" +3.0 """)))) == """ + 3.0\n""" assert color(extract_expr(ast.parse(dedent(""" -3.0 """)))) == """ - 3.0\n""" assert color(extract_expr(ast.parse(dedent(""" ~3.0 """)))) == """ ~ 3.0\n""" def test_ast_bin_op() -> None: assert color(extract_expr(ast.parse(dedent(""" 2.3*6 """)))) == """ 2.3 * 6\n""" assert color(extract_expr(ast.parse(dedent(""" (3-6)*2 """)))) == """ ( 3 - 6 ) * 2\n""" assert color(extract_expr(ast.parse(dedent(""" 101//4+101%4 """)))) == """ 101 // 4 + 101 % 4\n""" assert color(extract_expr(ast.parse(dedent(""" 1 & 0 """)))) == """ 1 & 0\n""" assert color(extract_expr(ast.parse(dedent(""" 1 | 0 """)))) == """ 1 | 0\n""" assert color(extract_expr(ast.parse(dedent(""" 1 ^ 0 """)))) == """ 1 ^ 0\n""" assert color(extract_expr(ast.parse(dedent(""" 1 << 0 """)))) == """ 1 << 0\n""" assert color(extract_expr(ast.parse(dedent(""" 1 >> 0 """)))) == """ 1 >> 0\n""" assert color(extract_expr(ast.parse(dedent(""" H @ beta """)))) == """ H @ beta\n""" def test_operator_precedences() -> None: assert color(extract_expr(ast.parse(dedent(""" (2 ** 3) ** 2 """)))) == """ ( 2 ** 3 ) ** 2\n""" assert color(extract_expr(ast.parse(dedent(""" 2 ** 3 ** 2 """)))) == """ 2 ** ( 3 ** 2 )\n""" assert color(extract_expr(ast.parse(dedent(""" (1 + 2) * 3 / 4 """)))) == """ ( ( 1 + 2 ) * 3 ) / 4\n""" assert color(extract_expr(ast.parse(dedent(""" ((1 + 2) * 3) / 4 """)))) == """ ( ( 1 + 2 ) * 3 ) / 4\n""" assert color(extract_expr(ast.parse(dedent(""" (1 + 2) * (3 / 4) """)))) == """ ( 1 + 2 ) * ( 3 / 4 )\n""" assert color(extract_expr(ast.parse(dedent(""" (1 + (2 * 3) / 4) - 1 """)))) == """ ( 1 + ( 2 * 3 ) / 4 ) - 1\n""" def test_ast_bool_op() -> None: assert color(extract_expr(ast.parse(dedent(""" True and 9 """)))) == """ True and 9\n""" assert color(extract_expr(ast.parse(dedent(""" 1 or 0 and 2 or 3 or 1 """)))) == """ 1 or 0 and 2 or 3 or 1\n""" def test_ast_list_tuple() -> None: assert color(extract_expr(ast.parse(dedent(""" [1,2,[5,6,[(11,22,33),9],10],11]+[99,98,97,96,95] """)))) == """ [ 1 , 2 , [ 5 , 6 , [ ( 11 , 22 , 33 ) , 9 ] , 10 ] , 11 ] + [ 99 , 98 , 97 , 96 , 95 ]\n""" assert color(extract_expr(ast.parse(dedent(""" (('1', 2, 3.14), (4, '5', 6.66)) """)))) == """ ( ( ' 1 ' , 2 , 3.14 ) , ( 4 , ' 5 ' , 6.66 ) )\n""" def test_ast_dict() -> None: assert color(extract_expr(ast.parse(dedent(""" {'1':33, '2':[1,2,3,{7:'oo'*20}]} """)))) == """ { ' 1 ' : 33 , ' 2 ' : [ 1 , 2 , 3 , { 7 : ' oo ' * 20 } ] }\n""" def test_ast_annotation() -> None: assert color(extract_expr(ast.parse(dedent(""" bar[typing.Sequence[dict[str, bytes]]] """))), linelen=999) == """ bar [ typing.Sequence [ dict [ str , bytes ] ] ]\n""" def test_ast_call() -> None: assert color(extract_expr(ast.parse(dedent(""" list(range(100)) """)))) == """ list ( range ( 100 ) )\n""" def test_ast_call_args() -> None: assert color(extract_expr(ast.parse(dedent(""" list(func(1, *two, three=2, **args)) """)))) == """ list ( func ( 1 , * two , three = 2 , ** args ) )\n""" def test_ast_ellipsis() -> None: assert color(extract_expr(ast.parse(dedent(""" ... """)))) == """ ...\n""" def test_ast_set() -> None: assert color(extract_expr(ast.parse(dedent(""" {1, 2} """)))) == """ set([ 1 , 2 ])\n""" assert color(extract_expr(ast.parse(dedent(""" set([1, 2]) """)))) == """ set ( [ 1 , 2 ] )\n""" def test_ast_slice() -> None: assert color(extract_expr(ast.parse(dedent(""" o[x:y] """)))) == """ o [ x:y ]\n""" assert color(extract_expr(ast.parse(dedent(""" o[x:y,z] """)))) == """ o [ x:y, (z) ]\n""" if sys.version_info < (3,9) else """ o [ x:y , z ]\n""" def test_ast_attribute() -> None: assert color(extract_expr(ast.parse(dedent(""" mod.attr """)))) == (""" mod.attr\n""") # ast.Attribute nodes that contains something else as ast.Name nodes are not handled explicitely. assert color(extract_expr(ast.parse(dedent(""" func().attr """)))) == (""" func().attr\n""") def test_ast_regex() -> None: # invalid arguments assert color(extract_expr(ast.parse(dedent(r""" re.compile(invalidarg='[A-Za-z0-9]+') """)))) == """ re.compile ( invalidarg = ' [A-Za-z0-9]+ ' )\n""" # invalid arguments 2 assert color(extract_expr(ast.parse(dedent(""" re.compile() """)))) == """ re.compile ( )\n""" # invalid arguments 3 assert color(extract_expr(ast.parse(dedent(""" re.compile(None) """)))) == """ re.compile ( None )\n""" # cannot colorize regex, be can't infer value assert color(extract_expr(ast.parse(dedent(""" re.compile(get_re()) """)))) == """ re.compile ( get_re ( ) )\n""" # cannot colorize regex, not a valid regex assert color(extract_expr(ast.parse(dedent(""" re.compile(r"[.*") """)))) == """ re.compile ( ' [.* ' )\n""" # actually colorize regex, with flags assert color(extract_expr(ast.parse(dedent(""" re.compile(r"[A-Za-z0-9]+", re.X) """)))) == """ re.compile ( r ' [ A - Z a - z 0 - 9 ] + ' , re.X )\n""" def color_re(s: Union[bytes, str], check_roundtrip:bool=True) -> str: colorizer = PyvalColorizer(linelen=55, maxlines=5) val = colorizer.colorize(extract_expr(ast.parse(f"re.compile({repr(s)})"))) if check_roundtrip: re_begin = 13 if isinstance(s, bytes): re_begin += 1 re_end = -2 round_trip: Union[bytes, str] = ''.join(gettext(val.to_node()))[re_begin:re_end] if isinstance(s, bytes): assert isinstance(round_trip, str) round_trip = bytes(round_trip, encoding='utf-8') assert round_trip == s, "%s != %s" % (repr(round_trip), repr(s)) return flatten(val.to_stan(NotFoundLinker()))[17:-8] def test_re_literals() -> None: # Literal characters assert color_re(r'abc \t\r\n\f\v \xff \uffff', False) == r"""r'abc \t\r\n\f\v \xff \uffff'""" assert color_re(r'\.\^\$\\\*\+\?\{\}\[\]\|\(\)\'') == r"""r'\.\^\$\\\*\+\?\{\}\[\]\|\(\)\''""" # Any character & character classes assert color_re(r".\d\D\s\S\w\W\A^$\b\B\Z") == r"""r'.\d\D\s\S\w\W\A^$\b\B\Z'""" def test_re_branching() -> None: # Branching assert color_re(r"foo|bar") == """r'foo|bar'""" def test_re_char_classes() -> None: # Character classes assert color_re(r"[abcd]") == """r'[abcd]'""" def test_re_repeats() -> None: # Repeats assert color_re(r"a*b+c{4,}d{,5}e{3,9}f?") == ("""r'a*""" """b+c{4,}""" """d{,5}e{3,9}""" """f?'""") assert color_re(r"a*?b+?c{4,}?d{,5}?e{3,9}?f??") == ("""r'a*?""" """b+?c{4,}?""" """d{,5}?e{3,9}?""" """f??'""") def test_re_subpatterns() -> None: # Subpatterns assert color_re(r"(foo (bar) | (baz))") == ("""r'(""" """foo (bar) """ """| (""" """baz))""" """'""") assert color_re(r"(?:foo (?:bar) | (?:baz))") == ("""r'(?:""" """foo (?:bar) | """ """(?:baz))'""") assert color_re(r"(<)?(\w+@\w+(?:\.\w+)+)") == ("""r'(<""" """)?""" r"""(\w+@\w""" r"""+(?:\.\w""" """+)+""" """)'""") assert color_re("(foo (?Pbar) | (?Pbaz))") == ("""r'(""" """foo (?P<""" """a>bar) """ """| (?P<""" """boop>""" """baz))""" """'""") def test_re_references() -> None: # Group References assert color_re(r"(...) and (\1)") == ("""r'(...""" """) and (""" r"""\1)""" """'""") def test_re_ranges() -> None: # Ranges assert color_re(r"[a-bp-z]") == ("""r'[a""" """-bp-z""" """]'""") assert color_re(r"[^a-bp-z]") == ("""r'[""" """^a-bp""" """-z]""" """'""") assert color_re(r"[^abc]") == ("""r'[""" """^abc]""" """'""") def test_re_lookahead_behinds() -> None: # Lookahead/behinds assert color_re(r"foo(?=bar)") == ("""r'foo(?=""" """bar)'""") assert color_re(r"foo(?!bar)") == ("""r'foo(?!""" """bar)'""") assert color_re(r"(?<=bar)foo") == ("""r'(?<=""" """bar)foo'""") assert color_re(r"(?'(?<!""" """bar)foo'""") def test_re_flags() -> None: # Flags assert color_re(r"(?imu)^Food") == """r'(?imu)^Food'""" assert color_re(b"(?Limsx)^Food") == """rb'(?Limsx)^Food'""" assert color_re(b"(?Limstx)^Food") == """rb'(?Limstx)^Food'""" assert color_re(r"(?imstux)^Food") == """r'(?imstux)^Food'""" assert color_re(r"(?x)This is verbose", False) == """r'(?ux)Thisisverbose'""" def test_re_not_literal() -> None: assert color_re(r"[^0-9]") == """r'[^0-9]'""" def test_re_named_groups() -> None: # This regex triggers some weird behaviour: it adds the ↵ element at the end where it should not be... # The regex is 42 caracters long, so more than 40, maybe that's why? # assert color_re(r'^<(?P.*) at (?P0x[0-9a-f]+)>$') == """""" assert color_re(r'^<(?P.*)>$') == """r'^<(?P<descr>.*)>$'""" def test_re_multiline() -> None: assert color(extract_expr(ast.parse(dedent(r'''re.compile(r"""\d + # the integral part \. # the decimal point \d * # some fractional digits""")''')))) == r""" re.compile ( ''' \\d + # the integral part \\. # the decimal point \\d * # some fractional digits ''' ↵ ) """ assert color(extract_expr(ast.parse(dedent(r'''re.compile(rb"""\d + # the integral part \. # the decimal point \d * # some fractional digits""")'''))), linelen=70) == r""" re.compile ( b ''' \\d + # the integral part \\. # the decimal point \\d * # some fractional digits ''' ) """ def test_line_wrapping() -> None: # If a line goes beyond linelen, it is wrapped using the ``↵`` element. # Check that the last line gets a ``↵`` when maxlines is exceeded: assert color('x'*1000) == """ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...\n""" # If linebreakok is False, then line wrapping gives an ellipsis instead: assert color('x'*100, linebreakok=False) == """ ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...\n""" def color2(v: Any) -> str: colorizer = PyvalColorizer(linelen=50, maxlines=5) colorized = colorizer.colorize(v) text = ''.join(gettext(colorized.to_node())) return text def test_repr_text() -> None: """Test a few representations, with a plain text version. """ class A: pass assert color2('hello') == "'hello'" assert color2(["hello", 123]) == "['hello', 123]" assert color2(A()) == ('.A object>') assert color2([A()]) == ('[.A object>]') assert color2([A(),1,2,3,4,5,6,7]) == ('[.A object>,\n' ' 1,\n' ' 2,\n' ' 3,\n' '...') def test_summary() -> None: """To generate summary-reprs, use maxlines=1 and linebreakok=False: """ summarizer = PyvalColorizer(linelen=60, maxlines=1, linebreakok=False) def summarize(v:Any) -> str: return(''.join(gettext(summarizer.colorize(v).to_node()))) assert summarize(list(range(100))) == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16..." assert summarize('hello\nworld') == r"'hello\nworld'" assert summarize('hello\nworld'*100) == r"'hello\nworldhello\nworldhello\nworldhello\nworldhello\nw..." pydoctor-21.12.1/pydoctor/test/epydoc/test_restructuredtext.py000066400000000000000000000152651416703725300247200ustar00rootroot00000000000000from typing import List from textwrap import dedent from pydoctor.epydoc.markup import DocstringLinker, ParseError, ParsedDocstring from pydoctor.epydoc.markup.restructuredtext import parse_docstring from pydoctor.test import NotFoundLinker from pydoctor.node2stan import node2stan from pydoctor.stanutils import flatten from docutils import nodes from bs4 import BeautifulSoup def prettify(html: str) -> str: return BeautifulSoup(html, features="html.parser").prettify() # type: ignore[no-any-return] def parse_rst(s: str) -> ParsedDocstring: errors: List[ParseError] = [] parsed = parse_docstring(s, errors) assert not errors return parsed def rst2html(docstring: str, linker: DocstringLinker = NotFoundLinker()) -> str: """ Render a docstring to HTML. """ return flatten(parse_rst(docstring).to_stan(linker)) def node2html(node: nodes.Node, oneline: bool = True) -> str: if oneline: return ''.join(prettify(flatten(node2stan(node, NotFoundLinker()))).splitlines()) else: return flatten(node2stan(node, NotFoundLinker())) def rst2node(s: str) -> nodes.document: return parse_rst(s).to_node() def test_rst_partial() -> None: """ The L{node2html()} function can convert fragment of a L{docutils} document, it's not restricted to actual L{docutils.nodes.document} object. Really, any nodes can be passed to that function, the only requirement is that the node's C{document} attribute is set to a valid L{docutils.nodes.document} object. """ doc = dedent(''' This is a paragraph. Paragraphs can span multiple lines, and can contain `inline markup`. This is another paragraph. Paragraphs are separated by blank lines. ''') expected = dedent('''

    This is another paragraph. Paragraphs are separated by blank lines.

    ''').lstrip() node = rst2node(doc) for child in node[:]: assert isinstance(child, nodes.paragraph) assert node2html(node[-1], oneline=False) == expected assert node[-1].parent == node def test_rst_body_empty() -> None: src = """ :return: a number :rtype: int """ errors: List[ParseError] = [] pdoc = parse_docstring(src, errors) assert not errors assert not pdoc.has_body assert len(pdoc.fields) == 2 def test_rst_body_nonempty() -> None: src = """ Only body text, no fields. """ errors: List[ParseError] = [] pdoc = parse_docstring(src, errors) assert not errors assert pdoc.has_body assert len(pdoc.fields) == 0 def test_rst_anon_link_target_missing() -> None: src = """ This link's target is `not defined anywhere`__. """ errors: List[ParseError] = [] parse_docstring(src, errors) assert len(errors) == 1 assert errors[0].descr().startswith("Anonymous hyperlink mismatch:") assert errors[0].is_fatal() def test_rst_anon_link_email() -> None: src = "``__" html = rst2html(src) assert html.startswith('
    mailto:postmaster@example.net') def test_rst_xref_with_target() -> None: src = "`mapping `" html = rst2html(src) assert html.startswith('mapping') def test_rst_xref_implicit_target() -> None: src = "`func()`" html = rst2html(src) assert html.startswith('func()') def test_rst_directive_adnomitions() -> None: expected_html_multiline="""

    {}

    this is the first line

    and this is the second line

    """ expected_html_single_line = """

    {}

    this is a single line

    """ admonition_map = { 'Attention': 'attention', 'Caution': 'caution', 'Danger': 'danger', 'Error': 'error', 'Hint': 'hint', 'Important': 'important', 'Note': 'note', 'Tip': 'tip', 'Warning': 'warning', } for title, admonition_name in admonition_map.items(): # Multiline docstring = (".. {}::\n" "\n" " this is the first line\n" " \n" " and this is the second line\n" ).format(admonition_name) expect = expected_html_multiline.format( admonition_name, title ) actual = rst2html(docstring) assert prettify(expect)==prettify(actual) # Single line docstring = (".. {}:: this is a single line\n" ).format(admonition_name) expect = expected_html_single_line.format( admonition_name, title ) actual = rst2html(docstring) assert prettify(expect)==prettify(actual) def test_rst_directive_versionadded() -> None: """ It renders the C{versionadded} RST directive using a custom markup with dedicated CSS classes. """ html = rst2html(".. versionadded:: 0.6") expected_html="""
    New in version 0.6.
    """ assert html==expected_html, html def test_rst_directive_versionchanged() -> None: """ It renders the C{versionchanged} RST directive with custom markup and supports an extra text besides the version information. """ html = rst2html(""".. versionchanged:: 0.7 Add extras""") expected_html="""
    Changed in version 0.7: Add extras
    """ assert html==expected_html, html def test_rst_directive_deprecated() -> None: """ It renders the C{deprecated} RST directive with custom markup and supports an extra text besides the version information. """ html = rst2html(""".. deprecated:: 0.2 For security reasons""") expected_html="""
    Deprecated since version 0.2: For security reasons
    """ assert html==expected_html, html def test_rst_directive_seealso() -> None: html = rst2html(".. seealso:: Hey") expected_html = """

    See Also

    Hey

    """ assert prettify(html).strip() == prettify(expected_html).strip(), html pydoctor-21.12.1/pydoctor/test/test_astbuilder.py000066400000000000000000001774021416703725300221350ustar00rootroot00000000000000from typing import Optional, Tuple, Type, overload, cast import ast import textwrap import astor from twisted.python._pydoctor import TwistedSystem from pydoctor import astbuilder, model from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring from pydoctor.stanutils import flatten, html2stan, flatten_text from pydoctor.epydoc.markup.epytext import Element, ParsedEpytextDocstring from pydoctor.epydoc2stan import format_summary, get_parsed_type from pydoctor.zopeinterface import ZopeInterfaceSystem from . import CapSys, NotFoundLinker, posonlyargs, typecomment import pytest systemcls_param = pytest.mark.parametrize( 'systemcls', (model.System, ZopeInterfaceSystem, TwistedSystem) ) def fromAST( ast: ast.Module, modname: str = '', is_package: bool = False, parent_name: Optional[str] = None, system: Optional[model.System] = None, buildercls: Optional[Type[astbuilder.ASTBuilder]] = None, systemcls: Type[model.System] = model.System ) -> model.Module: if system is None: _system = systemcls() else: _system = system if buildercls is None: buildercls = _system.defaultBuilder builder = buildercls(_system) if parent_name is None: full_name = modname else: full_name = f'{parent_name}.{modname}' # Set containing package as parent. builder.current = _system.allobjects[parent_name] factory = _system.Package if is_package else _system.Module mod: model.Module = builder._push(factory, modname, 0) builder._pop(factory) builder.processModuleAST(ast, mod) assert mod is _system.allobjects[full_name] mod.state = model.ProcessingState.PROCESSED if system is None: # Assume that an implicit system will only contain one module, # so post-process it as a convenience. _system.postProcess() return mod def fromText( text: str, *, modname: str = '', is_package: bool = False, parent_name: Optional[str] = None, system: Optional[model.System] = None, buildercls: Optional[Type[astbuilder.ASTBuilder]] = None, systemcls: Type[model.System] = model.System ) -> model.Module: ast = astbuilder._parse(textwrap.dedent(text)) return fromAST(ast, modname, is_package, parent_name, system, buildercls, systemcls) def unwrap(parsed_docstring: Optional[ParsedDocstring]) -> str: if parsed_docstring is None: raise TypeError("parsed_docstring cannot be None") if not isinstance(parsed_docstring, ParsedEpytextDocstring): raise TypeError(f"parsed_docstring must be a ParsedEpytextDocstring instance, not {parsed_docstring.__class__.__name__}") epytext = parsed_docstring._tree assert epytext is not None assert epytext.tag == 'epytext' assert len(epytext.children) == 1 para = epytext.children[0] assert isinstance(para, Element) assert para.tag == 'para' assert len(para.children) == 1 value = para.children[0] assert isinstance(value, str) return value def to_html( parsed_docstring: ParsedDocstring, linker: DocstringLinker = NotFoundLinker() ) -> str: return flatten(parsed_docstring.to_stan(linker)) @overload def type2str(type_expr: None) -> None: ... @overload def type2str(type_expr: ast.expr) -> str: ... def type2str(type_expr: Optional[ast.expr]) -> Optional[str]: if type_expr is None: return None else: src = astor.to_source(type_expr) assert isinstance(src, str) return src.strip() def type2html(obj: model.Documentable) -> str: parsed_type = get_parsed_type(obj) assert parsed_type is not None return to_html(parsed_type).replace('', '').replace('\n', '') def ann_str_and_line(obj: model.Documentable) -> Tuple[str, int]: """Return the textual representation and line number of an object's type annotation. @param obj: Documentable object with a type annotation. """ ann = obj.annotation # type: ignore[attr-defined] assert ann is not None return type2str(ann), ann.lineno def test_node2fullname() -> None: """The node2fullname() function finds the full (global) name for a name expression in the AST. """ mod = fromText(''' class session: from twisted.conch.interfaces import ISession ''', modname='test') def lookup(expr: str) -> Optional[str]: node = ast.parse(expr, mode='eval') assert isinstance(node, ast.Expression) return astbuilder.node2fullname(node.body, mod) # None is returned for non-name nodes. assert lookup('123') is None # Local names are returned with their full name. assert lookup('session') == 'test.session' # A name that has no match at the top level is returned as-is. assert lookup('nosuchname') == 'nosuchname' # Unknown names are resolved as far as possible. assert lookup('session.nosuchname') == 'test.session.nosuchname' # Aliases are resolved on local names. assert lookup('session.ISession') == 'twisted.conch.interfaces.ISession' # Aliases are resolved on global names. assert lookup('test.session.ISession') == 'twisted.conch.interfaces.ISession' @systemcls_param def test_no_docstring(systemcls: Type[model.System]) -> None: # Inheritance of the docstring of an overridden method depends on # methods with no docstring having None in their 'docstring' field. mod = fromText(''' def f(): pass class C: def m(self): pass ''', modname='test', systemcls=systemcls) f = mod.contents['f'] assert f.docstring is None m = mod.contents['C'].contents['m'] assert m.docstring is None @systemcls_param def test_function_simple(systemcls: Type[model.System]) -> None: src = ''' """ MOD DOC """ def f(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 1 func, = mod.contents.values() assert func.fullName() == '.f' assert func.docstring == """This is a docstring.""" assert isinstance(func, model.Function) assert func.is_async is False @systemcls_param def test_function_async(systemcls: Type[model.System]) -> None: src = ''' """ MOD DOC """ async def a(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 1 func, = mod.contents.values() assert func.fullName() == '.a' assert func.docstring == """This is a docstring.""" assert isinstance(func, model.Function) assert func.is_async is True @pytest.mark.parametrize('signature', ( '()', '(*, a, b=None)', '(*, a=(), b)', '(a, b=3, *c, **kw)', '(f=True)', '(x=0.1, y=-2)', r"(s='theory', t='con\'text')", )) @systemcls_param def test_function_signature(signature: str, systemcls: Type[model.System]) -> None: """ A round trip from source to inspect.Signature and back produces the original text. @note: Our inspect.Signature Paramters objects are now tweaked such that they might produce HTML tags, handled by the L{PyvalColorizer}. """ mod = fromText(f'def f{signature}: ...', systemcls=systemcls) docfunc, = mod.contents.values() assert isinstance(docfunc, model.Function) # This little trick makes it possible to back reproduce the original signature from the genrated HTML. text = flatten_text(html2stan(str(docfunc.signature))) assert text == signature @posonlyargs @pytest.mark.parametrize('signature', ( '(x, y, /)', '(x, y=0, /)', '(x, y, /, z, w)', '(x, y, /, z, w=42)', '(x, y, /, z=0, w=0)', '(x, y=3, /, z=5, w=7)', '(x, /, *v, a=1, b=2)', '(x, /, *, a=1, b=2, **kwargs)', )) @systemcls_param def test_function_signature_posonly(signature: str, systemcls: Type[model.System]) -> None: test_function_signature(signature, systemcls) @pytest.mark.parametrize('signature', ( '(a, a)', )) @systemcls_param def test_function_badsig(signature: str, systemcls: Type[model.System], capsys: CapSys) -> None: """When a function has an invalid signature, an error is logged and the empty signature is returned. Note that most bad signatures lead to a SyntaxError, which we cannot recover from. This test checks what happens if the AST can be produced but inspect.Signature() rejects the parsed parameters. """ mod = fromText(f'def f{signature}: ...', systemcls=systemcls, modname='mod') docfunc, = mod.contents.values() assert isinstance(docfunc, model.Function) assert str(docfunc.signature) == '()' captured = capsys.readouterr().out assert captured.startswith("mod:1: mod.f has invalid parameters: ") @systemcls_param def test_class(systemcls: Type[model.System]) -> None: src = ''' class C: def f(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 1 cls, = mod.contents.values() assert cls.fullName() == '.C' assert cls.docstring == None assert len(cls.contents) == 1 func, = cls.contents.values() assert func.fullName() == '.C.f' assert func.docstring == """This is a docstring.""" @systemcls_param def test_class_with_base(systemcls: Type[model.System]) -> None: src = ''' class C: def f(): """This is a docstring.""" class D(C): def f(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 2 clsC, clsD = mod.contents.values() assert clsC.fullName() == '.C' assert clsC.docstring == None assert len(clsC.contents) == 1 assert clsD.fullName() == '.D' assert clsD.docstring == None assert len(clsD.contents) == 1 assert isinstance(clsD, model.Class) assert len(clsD.bases) == 1 base, = clsD.bases assert base == '.C' @systemcls_param def test_follow_renaming(systemcls: Type[model.System]) -> None: src = ''' class C: pass D = C class E(D): pass ''' mod = fromText(src, systemcls=systemcls) C = mod.contents['C'] E = mod.contents['E'] assert isinstance(C, model.Class) assert isinstance(E, model.Class) assert E.baseobjects == [C], E.baseobjects @systemcls_param def test_relative_import_in_package(systemcls: Type[model.System]) -> None: """Relative imports in a package must be resolved by going up one level less, since we don't count "__init__.py" as a level. Hierarchy:: top: def f - pkg: imports f and g - mod: def g """ top_src = ''' def f(): pass ''' mod_src = ''' def g(): pass ''' pkg_src = ''' from .. import f from .mod import g ''' system = systemcls() top = fromText(top_src, modname='top', is_package=True, system=system) mod = fromText(mod_src, modname='top.pkg.mod', system=system) pkg = fromText(pkg_src, modname='pkg', parent_name='top', is_package=True, system=system) assert pkg.resolveName('f') is top.contents['f'] assert pkg.resolveName('g') is mod.contents['g'] @systemcls_param @pytest.mark.parametrize('level', (1, 2, 3, 4)) def test_relative_import_past_top( systemcls: Type[model.System], level: int, capsys: CapSys ) -> None: """A warning is logged when a relative import goes beyond the top-level package. """ system = systemcls() fromText('', modname='pkg', is_package=True, system=system) fromText(f''' from {'.' * level + 'X'} import A ''', modname='mod', parent_name='pkg', system=system) captured = capsys.readouterr().out if level == 1: assert not captured else: assert f'pkg.mod:2: relative import level ({level}) too high\n' == captured @systemcls_param def test_class_with_base_from_module(systemcls: Type[model.System]) -> None: src = ''' from X.Y import A from Z import B as C class D(A, C): def f(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 1 clsD, = mod.contents.values() assert clsD.fullName() == '.D' assert clsD.docstring == None assert len(clsD.contents) == 1 assert isinstance(clsD, model.Class) assert len(clsD.bases) == 2 base1, base2 = clsD.bases assert base1 == 'X.Y.A' assert base2 == 'Z.B' src = ''' import X import Y.Z as M class D(X.A, X.B.C, M.C): def f(): """This is a docstring.""" ''' mod = fromText(src, systemcls=systemcls) assert len(mod.contents) == 1 clsD, = mod.contents.values() assert clsD.fullName() == '.D' assert clsD.docstring == None assert len(clsD.contents) == 1 assert isinstance(clsD, model.Class) assert len(clsD.bases) == 3 base1, base2, base3 = clsD.bases assert base1 == 'X.A', base1 assert base2 == 'X.B.C', base2 assert base3 == 'Y.Z.C', base3 @systemcls_param def test_aliasing(systemcls: Type[model.System]) -> None: def addsrc(system: model.System) -> None: src_private = ''' class A: pass ''' src_export = ''' from _private import A as B __all__ = ['B'] ''' src_user = ''' from public import B class C(B): pass ''' fromText(src_private, modname='_private', system=system) fromText(src_export, modname='public', system=system) fromText(src_user, modname='app', system=system) system = systemcls() addsrc(system) C = system.allobjects['app.C'] assert isinstance(C, model.Class) # An older version of this test expected _private.A as the result. # The expected behavior was changed because: # - relying on on-demand processing of other modules is unreliable when # there are cyclic imports: expandName() on a module that is still being # processed can return the not-found result for a name that does exist # - code should be importing names from their official home, so if we # import public.B then for the purposes of documentation public.B is # the name we should use assert C.bases == ['public.B'] @systemcls_param def test_more_aliasing(systemcls: Type[model.System]) -> None: def addsrc(system: model.System) -> None: src_a = ''' class A: pass ''' src_b = ''' from a import A as B ''' src_c = ''' from b import B as C ''' src_d = ''' from c import C class D(C): pass ''' fromText(src_a, modname='a', system=system) fromText(src_b, modname='b', system=system) fromText(src_c, modname='c', system=system) fromText(src_d, modname='d', system=system) system = systemcls() addsrc(system) D = system.allobjects['d.D'] assert isinstance(D, model.Class) # An older version of this test expected a.A as the result. # Read the comment in test_aliasing() to learn why this was changed. assert D.bases == ['c.C'] @systemcls_param def test_aliasing_recursion(systemcls: Type[model.System]) -> None: system = systemcls() src = ''' class C: pass from mod import C class D(C): pass ''' mod = fromText(src, modname='mod', system=system) D = mod.contents['D'] assert isinstance(D, model.Class) assert D.bases == ['mod.C'], D.bases @systemcls_param def test_documented_no_alias(systemcls: Type[model.System]) -> None: """A variable that is documented should not be considered an alias.""" # TODO: We should also verify this for inline docstrings, but the code # currently doesn't support that. We should perhaps store aliases # as Documentables as well, so we can change their 'kind' when # an inline docstring follows the assignment. mod = fromText(''' class SimpleClient: pass class Processor: """ @ivar clientFactory: Callable that returns a client. """ clientFactory = SimpleClient ''', systemcls=systemcls) P = mod.contents['Processor'] f = P.contents['clientFactory'] assert unwrap(f.parsed_docstring) == """Callable that returns a client.""" assert f.privacyClass is model.PrivacyClass.VISIBLE assert f.kind is model.DocumentableKind.INSTANCE_VARIABLE assert f.linenumber @systemcls_param def test_subclasses(systemcls: Type[model.System]) -> None: src = ''' class A: pass class B(A): pass ''' system = fromText(src, systemcls=systemcls).system A = system.allobjects['.A'] assert isinstance(A, model.Class) assert A.subclasses == [system.allobjects['.B']] @systemcls_param def test_inherit_names(systemcls: Type[model.System]) -> None: src = ''' class A: pass class A(A): pass ''' mod = fromText(src, systemcls=systemcls) A = mod.contents['A'] assert isinstance(A, model.Class) assert [b.name for b in A.allbases()] == ['A 0'] @systemcls_param def test_nested_class_inheriting_from_same_module(systemcls: Type[model.System]) -> None: src = ''' class A: pass class B: class C(A): pass ''' fromText(src, systemcls=systemcls) @systemcls_param def test_all_recognition(systemcls: Type[model.System]) -> None: """The value assigned to __all__ is parsed to Module.all.""" mod = fromText(''' def f(): pass __all__ = ['f'] ''', systemcls=systemcls) assert mod.all == ['f'] assert '__all__' not in mod.contents @systemcls_param def test_docformat_recognition(systemcls: Type[model.System]) -> None: """The value assigned to __docformat__ is parsed to Module.docformat.""" mod = fromText(''' __docformat__ = 'Epytext en' def f(): pass ''', systemcls=systemcls) assert mod.docformat == 'epytext' assert '__docformat__' not in mod.contents @systemcls_param def test_docformat_warn_not_str(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' __docformat__ = [i for i in range(3)] def f(): pass ''', systemcls=systemcls, modname='mod') captured = capsys.readouterr().out assert captured == 'mod:2: Cannot parse value assigned to "__docformat__": not a string\n' assert mod.docformat is None assert '__docformat__' not in mod.contents @systemcls_param def test_docformat_warn_not_str2(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' __docformat__ = 3.14 def f(): pass ''', systemcls=systemcls, modname='mod') captured = capsys.readouterr().out assert captured == 'mod:2: Cannot parse value assigned to "__docformat__": not a string\n' assert mod.docformat == None assert '__docformat__' not in mod.contents @systemcls_param def test_docformat_warn_empty(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' __docformat__ = ' ' def f(): pass ''', systemcls=systemcls, modname='mod') captured = capsys.readouterr().out assert captured == 'mod:2: Cannot parse value assigned to "__docformat__": empty value\n' assert mod.docformat == None assert '__docformat__' not in mod.contents @systemcls_param def test_docformat_warn_overrides(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' __docformat__ = 'numpy' def f(): pass __docformat__ = 'restructuredtext' ''', systemcls=systemcls, modname='mod') captured = capsys.readouterr().out assert captured == 'mod:7: Assignment to "__docformat__" overrides previous assignment\n' assert mod.docformat == 'restructuredtext' assert '__docformat__' not in mod.contents @systemcls_param def test_all_in_class_non_recognition(systemcls: Type[model.System]) -> None: """A class variable named __all__ is just an ordinary variable and does not affect Module.all. """ mod = fromText(''' class C: __all__ = ['f'] ''', systemcls=systemcls) assert mod.all is None assert '__all__' not in mod.contents assert '__all__' in mod.contents['C'].contents @systemcls_param def test_all_multiple(systemcls: Type[model.System], capsys: CapSys) -> None: """If there are multiple assignments to __all__, a warning is logged and the last assignment takes effect. """ mod = fromText(''' __all__ = ['f'] __all__ = ['g'] ''', modname='mod', systemcls=systemcls) captured = capsys.readouterr().out assert captured == 'mod:3: Assignment to "__all__" overrides previous assignment\n' assert mod.all == ['g'] @systemcls_param def test_all_bad_sequence(systemcls: Type[model.System], capsys: CapSys) -> None: """Values other than lists and tuples assigned to __all__ have no effect and a warning is logged. """ mod = fromText(''' __all__ = {} ''', modname='mod', systemcls=systemcls) captured = capsys.readouterr().out assert captured == 'mod:2: Cannot parse value assigned to "__all__"\n' assert mod.all is None @systemcls_param def test_all_nonliteral(systemcls: Type[model.System], capsys: CapSys) -> None: """Non-literals in __all__ are ignored.""" mod = fromText(''' __all__ = ['a', 'b', '.'.join(['x', 'y']), 'c'] ''', modname='mod', systemcls=systemcls) captured = capsys.readouterr().out assert captured == 'mod:2: Cannot parse element 2 of "__all__"\n' assert mod.all == ['a', 'b', 'c'] @systemcls_param def test_all_nonstring(systemcls: Type[model.System], capsys: CapSys) -> None: """Non-string literals in __all__ are ignored.""" mod = fromText(''' __all__ = ('a', 'b', 123, 'c', True) ''', modname='mod', systemcls=systemcls) captured = capsys.readouterr().out assert captured == ( 'mod:2: Element 2 of "__all__" has type "int", expected "str"\n' 'mod:2: Element 4 of "__all__" has type "bool", expected "str"\n' ) assert mod.all == ['a', 'b', 'c'] @systemcls_param def test_all_allbad(systemcls: Type[model.System], capsys: CapSys) -> None: """If no value in __all__ could be parsed, the result is an empty list.""" mod = fromText(''' __all__ = (123, True) ''', modname='mod', systemcls=systemcls) captured = capsys.readouterr().out assert captured == ( 'mod:2: Element 0 of "__all__" has type "int", expected "str"\n' 'mod:2: Element 1 of "__all__" has type "bool", expected "str"\n' ) assert mod.all == [] @systemcls_param def test_classmethod(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: @classmethod def f(klass): pass ''', systemcls=systemcls) assert mod.contents['C'].contents['f'].kind is model.DocumentableKind.CLASS_METHOD mod = fromText(''' class C: def f(klass): pass f = classmethod(f) ''', systemcls=systemcls) assert mod.contents['C'].contents['f'].kind is model.DocumentableKind.CLASS_METHOD @systemcls_param def test_classdecorator(systemcls: Type[model.System]) -> None: mod = fromText(''' def cd(cls): pass @cd class C: pass ''', modname='mod', systemcls=systemcls) C = mod.contents['C'] assert isinstance(C, model.Class) assert C.decorators == [('mod.cd', None)] @systemcls_param def test_classdecorator_with_args(systemcls: Type[model.System]) -> None: mod = fromText(''' def cd(): pass class A: pass @cd(A) class C: pass ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert isinstance(C, model.Class) assert len(C.decorators) == 1 (name, args), = C.decorators assert name == 'test.cd' assert args is not None assert len(args) == 1 arg, = args assert astbuilder.node2fullname(arg, mod) == 'test.A' @systemcls_param def test_methoddecorator(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' class C: def method_undecorated(): pass @staticmethod def method_static(): pass @classmethod def method_class(cls): pass @staticmethod @classmethod def method_both(): pass ''', modname='mod', systemcls=systemcls) C = mod.contents['C'] assert C.contents['method_undecorated'].kind is model.DocumentableKind.METHOD assert C.contents['method_static'].kind is model.DocumentableKind.STATIC_METHOD assert C.contents['method_class'].kind is model.DocumentableKind.CLASS_METHOD captured = capsys.readouterr().out assert captured == "mod:14: mod.C.method_both is both classmethod and staticmethod\n" @systemcls_param def test_assignment_to_method_in_class(systemcls: Type[model.System]) -> None: """An assignment to a method in a class body does not change the type of the documentable. If the name we assign to exists and it does not belong to an Attribute (it's a Function instead, in this test case), the assignment will be ignored. """ mod = fromText(''' class Base: def base_method(): """Base method docstring.""" class Sub(Base): base_method = wrap_method(base_method) """Overriding the docstring is not supported.""" def sub_method(): """Sub method docstring.""" sub_method = wrap_method(sub_method) """Overriding the docstring is not supported.""" ''', systemcls=systemcls) assert isinstance(mod.contents['Base'].contents['base_method'], model.Function) assert mod.contents['Sub'].contents.get('base_method') is None sub_method = mod.contents['Sub'].contents['sub_method'] assert isinstance(sub_method, model.Function) assert sub_method.docstring == """Sub method docstring.""" @systemcls_param def test_assignment_to_method_in_init(systemcls: Type[model.System]) -> None: """An assignment to a method inside __init__() does not change the type of the documentable. If the name we assign to exists and it does not belong to an Attribute (it's a Function instead, in this test case), the assignment will be ignored. """ mod = fromText(''' class Base: def base_method(): """Base method docstring.""" class Sub(Base): def sub_method(): """Sub method docstring.""" def __init__(self): self.base_method = wrap_method(self.base_method) """Overriding the docstring is not supported.""" self.sub_method = wrap_method(self.sub_method) """Overriding the docstring is not supported.""" ''', systemcls=systemcls) assert isinstance(mod.contents['Base'].contents['base_method'], model.Function) assert mod.contents['Sub'].contents.get('base_method') is None sub_method = mod.contents['Sub'].contents['sub_method'] assert isinstance(sub_method, model.Function) assert sub_method.docstring == """Sub method docstring.""" @systemcls_param def test_import_star(systemcls: Type[model.System]) -> None: mod_a = fromText(''' def f(): pass ''', modname='a', systemcls=systemcls) mod_b = fromText(''' from a import * ''', modname='b', system=mod_a.system) assert mod_b.resolveName('f') == mod_a.contents['f'] @systemcls_param def test_import_func_from_package(systemcls: Type[model.System]) -> None: """Importing a function from a package should look in the C{__init__} module. In this test the following hierarchy is constructed:: package a module __init__ defines function 'f' module c imports function 'f' module b imports function 'f' We verify that when module C{b} and C{c} import the name C{f} from package C{a}, they import the function C{f} from the module C{a.__init__}. """ system = systemcls() mod_a = fromText(''' def f(): pass ''', modname='a', is_package=True, system=system) mod_b = fromText(''' from a import f ''', modname='b', system=system) mod_c = fromText(''' from . import f ''', modname='c', parent_name='a', system=system) assert mod_b.resolveName('f') == mod_a.contents['f'] assert mod_c.resolveName('f') == mod_a.contents['f'] @systemcls_param def test_import_module_from_package(systemcls: Type[model.System]) -> None: """Importing a module from a package should not look in C{__init__} module. In this test the following hierarchy is constructed:: package a module __init__ module b defines function 'f' module c imports module 'a.b' We verify that when module C{c} imports the name C{b} from package C{a}, it imports the module C{a.b} which contains C{f}. """ system = systemcls() fromText(''' # This module intentionally left blank. ''', modname='a', system=system) mod_b = fromText(''' def f(): pass ''', modname='b', parent_name='a', system=system) mod_c = fromText(''' from a import b f = b.f ''', modname='c', system=system) assert mod_c.resolveName('f') == mod_b.contents['f'] @systemcls_param def test_inline_docstring_modulevar(systemcls: Type[model.System]) -> None: mod = fromText(''' """regular module docstring @var b: doc for b """ """not a docstring""" a = 1 """inline doc for a""" b = 2 def f(): pass """not a docstring""" ''', modname='test', systemcls=systemcls) assert sorted(mod.contents.keys()) == ['a', 'b', 'f'] a = mod.contents['a'] assert a.docstring == """inline doc for a""" b = mod.contents['b'] assert unwrap(b.parsed_docstring) == """doc for b""" f = mod.contents['f'] assert not f.docstring @systemcls_param def test_inline_docstring_classvar(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """regular class docstring""" def f(self): pass """not a docstring""" a = 1 """inline doc for a""" """not a docstring""" _b = 2 """inline doc for _b""" None """not a docstring""" ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert sorted(C.contents.keys()) == ['_b', 'a', 'f'] f = C.contents['f'] assert not f.docstring a = C.contents['a'] assert a.docstring == """inline doc for a""" assert a.privacyClass is model.PrivacyClass.VISIBLE b = C.contents['_b'] assert b.docstring == """inline doc for _b""" assert b.privacyClass is model.PrivacyClass.PRIVATE @systemcls_param def test_inline_docstring_annotated_classvar(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """regular class docstring""" a: int """inline doc for a""" _b: int = 4 """inline doc for _b""" ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert sorted(C.contents.keys()) == ['_b', 'a'] a = C.contents['a'] assert a.docstring == """inline doc for a""" assert a.privacyClass is model.PrivacyClass.VISIBLE b = C.contents['_b'] assert b.docstring == """inline doc for _b""" assert b.privacyClass is model.PrivacyClass.PRIVATE @systemcls_param def test_inline_docstring_instancevar(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """regular class docstring""" d = None """inline doc for d""" f = None """inline doc for f""" def __init__(self): self.a = 1 """inline doc for a""" """not a docstring""" self._b = 2 """inline doc for _b""" x = -1 """not a docstring""" self.c = 3 """inline doc for c""" self.d = 4 self.e = 5 """not a docstring""" def set_f(self, value): self.f = value ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert sorted(C.contents.keys()) == [ '__init__', '_b', 'a', 'c', 'd', 'e', 'f', 'set_f' ] a = C.contents['a'] assert a.docstring == """inline doc for a""" assert a.privacyClass is model.PrivacyClass.VISIBLE assert a.kind is model.DocumentableKind.INSTANCE_VARIABLE b = C.contents['_b'] assert b.docstring == """inline doc for _b""" assert b.privacyClass is model.PrivacyClass.PRIVATE assert b.kind is model.DocumentableKind.INSTANCE_VARIABLE c = C.contents['c'] assert c.docstring == """inline doc for c""" assert c.privacyClass is model.PrivacyClass.VISIBLE assert c.kind is model.DocumentableKind.INSTANCE_VARIABLE d = C.contents['d'] assert d.docstring == """inline doc for d""" assert d.privacyClass is model.PrivacyClass.VISIBLE assert d.kind is model.DocumentableKind.INSTANCE_VARIABLE e = C.contents['e'] assert not e.docstring f = C.contents['f'] assert f.docstring == """inline doc for f""" assert f.privacyClass is model.PrivacyClass.VISIBLE assert f.kind is model.DocumentableKind.INSTANCE_VARIABLE @systemcls_param def test_inline_docstring_annotated_instancevar(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """regular class docstring""" a: int def __init__(self): self.a = 1 """inline doc for a""" self.b: int = 2 """inline doc for b""" ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert sorted(C.contents.keys()) == ['__init__', 'a', 'b'] a = C.contents['a'] assert a.docstring == """inline doc for a""" b = C.contents['b'] assert b.docstring == """inline doc for b""" @systemcls_param def test_docstring_assignment(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(r''' def fun(): pass class CLS: def method1(): """Temp docstring.""" pass def method2(): pass method1.__doc__ = "Updated docstring #1" fun.__doc__ = "Happy Happy Joy Joy" CLS.__doc__ = "Clears the screen" CLS.method2.__doc__ = "Updated docstring #2" None.__doc__ = "Free lunch!" real.__doc__ = "Second breakfast" fun.__doc__ = codecs.encode('Pnrfne fnynq', 'rot13') CLS.method1.__doc__ = 4 def mark_unavailable(func): # No warning: docstring updates in functions are ignored. func.__doc__ = func.__doc__ + '\n\nUnavailable on this system.' ''', systemcls=systemcls) fun = mod.contents['fun'] assert fun.kind is model.DocumentableKind.FUNCTION assert fun.docstring == """Happy Happy Joy Joy""" CLS = mod.contents['CLS'] assert CLS.kind is model.DocumentableKind.CLASS assert CLS.docstring == """Clears the screen""" method1 = CLS.contents['method1'] assert method1.kind is model.DocumentableKind.METHOD assert method1.docstring == "Updated docstring #1" method2 = CLS.contents['method2'] assert method2.kind is model.DocumentableKind.METHOD assert method2.docstring == "Updated docstring #2" captured = capsys.readouterr() lines = captured.out.split('\n') assert len(lines) > 0 and lines[0] == \ ":20: Unable to figure out target for __doc__ assignment" assert len(lines) > 1 and lines[1] == \ ":21: Unable to figure out target for __doc__ assignment: " \ "computed full name not found: real" assert len(lines) > 2 and lines[2] == \ ":22: Unable to figure out value for __doc__ assignment, " \ "maybe too complex" assert len(lines) > 3 and lines[3] == \ ":23: Ignoring value assigned to __doc__: not a string" assert len(lines) == 5 and lines[-1] == '' @systemcls_param def test_docstring_assignment_detuple(systemcls: Type[model.System], capsys: CapSys) -> None: """We currently don't trace values for detupling assignments, so when assigning to __doc__ we get a warning about the unknown value. """ fromText(''' def fun(): pass fun.__doc__, other = 'Detupling to __doc__', 'is not supported' ''', modname='test', systemcls=systemcls) captured = capsys.readouterr().out assert captured == ( "test:5: Unable to figure out value for __doc__ assignment, maybe too complex\n" ) @systemcls_param def test_variable_scopes(systemcls: Type[model.System]) -> None: mod = fromText(''' l = 1 """module-level l""" m = 1 """module-level m""" class C: """class docstring @ivar k: class level doc for k """ a = None k = 640 m = 2 """class-level m""" def __init__(self): self.a = 1 """inline doc for a""" self.l = 2 """instance l""" ''', modname='test', systemcls=systemcls) l1 = mod.contents['l'] assert l1.kind is model.DocumentableKind.VARIABLE assert l1.docstring == """module-level l""" m1 = mod.contents['m'] assert m1.kind is model.DocumentableKind.VARIABLE assert m1.docstring == """module-level m""" C = mod.contents['C'] assert sorted(C.contents.keys()) == ['__init__', 'a', 'k', 'l', 'm'] a = C.contents['a'] assert a.kind is model.DocumentableKind.INSTANCE_VARIABLE assert a.docstring == """inline doc for a""" k = C.contents['k'] assert k.kind is model.DocumentableKind.INSTANCE_VARIABLE assert unwrap(k.parsed_docstring) == """class level doc for k""" l2 = C.contents['l'] assert l2.kind is model.DocumentableKind.INSTANCE_VARIABLE assert l2.docstring == """instance l""" m2 = C.contents['m'] assert m2.kind is model.DocumentableKind.CLASS_VARIABLE assert m2.docstring == """class-level m""" @systemcls_param def test_variable_types(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """class docstring @cvar a: first @type a: string @type b: string @cvar b: second @type c: string @ivar d: fourth @type d: string @type e: string @ivar e: fifth @type f: string @type g: string """ a = "A" b = "B" c = "C" """third""" def __init__(self): self.d = "D" self.e = "E" self.f = "F" """sixth""" self.g = g = "G" """seventh""" ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert sorted(C.contents.keys()) == [ '__init__', 'a', 'b', 'c', 'd', 'e', 'f', 'g' ] a = C.contents['a'] assert unwrap(a.parsed_docstring) == """first""" assert str(unwrap(a.parsed_type)) == 'string' assert a.kind is model.DocumentableKind.CLASS_VARIABLE b = C.contents['b'] assert unwrap(b.parsed_docstring) == """second""" assert str(unwrap(b.parsed_type)) == 'string' assert b.kind is model.DocumentableKind.CLASS_VARIABLE c = C.contents['c'] assert c.docstring == """third""" assert str(unwrap(c.parsed_type)) == 'string' assert c.kind is model.DocumentableKind.CLASS_VARIABLE d = C.contents['d'] assert unwrap(d.parsed_docstring) == """fourth""" assert str(unwrap(d.parsed_type)) == 'string' assert d.kind is model.DocumentableKind.INSTANCE_VARIABLE e = C.contents['e'] assert unwrap(e.parsed_docstring) == """fifth""" assert str(unwrap(e.parsed_type)) == 'string' assert e.kind is model.DocumentableKind.INSTANCE_VARIABLE f = C.contents['f'] assert f.docstring == """sixth""" assert str(unwrap(f.parsed_type)) == 'string' assert f.kind is model.DocumentableKind.INSTANCE_VARIABLE g = C.contents['g'] assert g.docstring == """seventh""" assert str(unwrap(g.parsed_type)) == 'string' assert g.kind is model.DocumentableKind.INSTANCE_VARIABLE @systemcls_param def test_annotated_variables(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: """class docstring @cvar a: first @type a: string @type b: string @cvar b: second """ a: str = "A" b: str c: str = "C" """third""" d: str """fourth""" e: List['C'] """fifth""" f: 'List[C]' """sixth""" g: 'List["C"]' """seventh""" def __init__(self): self.s: List[str] = [] """instance""" m: bytes = b"M" """module-level""" ''', modname='test', systemcls=systemcls) C = mod.contents['C'] a = C.contents['a'] assert unwrap(a.parsed_docstring) == """first""" assert type2html(a) == 'string' b = C.contents['b'] assert unwrap(b.parsed_docstring) == """second""" assert type2html(b) == 'string' c = C.contents['c'] assert c.docstring == """third""" assert type2html(c) == 'str' d = C.contents['d'] assert d.docstring == """fourth""" assert type2html(d) == 'str' e = C.contents['e'] assert e.docstring == """fifth""" assert type2html(e) == 'List[C]' f = C.contents['f'] assert f.docstring == """sixth""" assert type2html(f) == 'List[C]' g = C.contents['g'] assert g.docstring == """seventh""" assert type2html(g) == 'List[C]' s = C.contents['s'] assert s.docstring == """instance""" assert type2html(s) == 'List[str]' m = mod.contents['m'] assert m.docstring == """module-level""" assert type2html(m) == 'bytes' @typecomment @systemcls_param def test_type_comment(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' d = {} # type: dict[str, int] i = [] # type: ignore[misc] ''', systemcls=systemcls) assert type2str(cast(model.Attribute, mod.contents['d']).annotation) == 'dict[str, int]' # We don't use ignore comments for anything at the moment, # but do verify that their presence doesn't break things. assert type2str(cast(model.Attribute, mod.contents['i']).annotation) == 'list' assert not capsys.readouterr().out @systemcls_param def test_unstring_annotation(systemcls: Type[model.System]) -> None: """Annotations or parts thereof that are strings are parsed and line number information is preserved. """ mod = fromText(''' a: "int" b: 'str' = 'B' c: list["Thingy"] ''', systemcls=systemcls) assert ann_str_and_line(mod.contents['a']) == ('int', 2) assert ann_str_and_line(mod.contents['b']) == ('str', 3) assert ann_str_and_line(mod.contents['c']) == ('list[Thingy]', 4) @pytest.mark.parametrize('annotation', ("[", "pass", "1 ; 2")) @systemcls_param def test_bad_string_annotation( annotation: str, systemcls: Type[model.System], capsys: CapSys ) -> None: """Invalid string annotations must be reported as syntax errors.""" mod = fromText(f''' x: "{annotation}" ''', modname='test', systemcls=systemcls) assert isinstance(cast(model.Attribute, mod.contents['x']).annotation, ast.expr) assert "syntax error in annotation" in capsys.readouterr().out @pytest.mark.parametrize('annotation,expected', ( ("Literal['[', ']']", "Literal['[', ']']"), ("typing.Literal['pass', 'raise']", "typing.Literal['pass', 'raise']"), ("Optional[Literal['1 ; 2']]", "Optional[Literal['1 ; 2']]"), ("'Literal'['!']", "Literal['!']"), (r"'Literal[\'if\', \'while\']'", "Literal['if', 'while']"), )) def test_literal_string_annotation(annotation: str, expected: str) -> None: """Strings inside Literal annotations must not be recursively parsed.""" stmt, = ast.parse(annotation).body assert isinstance(stmt, ast.Expr) unstringed = astbuilder._AnnotationStringParser().visit(stmt.value) assert astor.to_source(unstringed).strip() == expected @systemcls_param def test_inferred_variable_types(systemcls: Type[model.System]) -> None: mod = fromText(''' class C: a = "A" b = 2 c = ['a', 'b', 'c'] d = {'a': 1, 'b': 2} e = (True, False, True) f = 1.618 g = {2, 7, 1, 8} h = [] i = ['r', 2, 'd', 2] j = ((), ((), ())) n = None x = list(range(10)) y = [n for n in range(10) if n % 2] def __init__(self): self.s = ['S'] self.t = t = 'T' m = b'octets' ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert ann_str_and_line(C.contents['a']) == ('str', 3) assert ann_str_and_line(C.contents['b']) == ('int', 4) assert ann_str_and_line(C.contents['c']) == ('list[str]', 5) assert ann_str_and_line(C.contents['d']) == ('dict[str, int]', 6) assert ann_str_and_line(C.contents['e']) == ('tuple[bool, ...]', 7) assert ann_str_and_line(C.contents['f']) == ('float', 8) assert ann_str_and_line(C.contents['g']) == ('set[int]', 9) # Element type is unknown, not uniform or too complex. assert ann_str_and_line(C.contents['h']) == ('list', 10) assert ann_str_and_line(C.contents['i']) == ('list', 11) assert ann_str_and_line(C.contents['j']) == ('tuple', 12) # It is unlikely that a variable actually will contain only None, # so we should treat this as not be able to infer the type. assert cast(model.Attribute, C.contents['n']).annotation is None # These expressions are considered too complex for pydoctor. # Maybe we can use an external type inferrer at some point. assert cast(model.Attribute, C.contents['x']).annotation is None assert cast(model.Attribute, C.contents['y']).annotation is None # Type inference isn't different for module and instance variables, # so we don't need to re-test everything. assert ann_str_and_line(C.contents['s']) == ('list[str]', 17) # Check that type is inferred on assignments with multiple targets. assert ann_str_and_line(C.contents['t']) == ('str', 18) assert ann_str_and_line(mod.contents['m']) == ('bytes', 19) @systemcls_param def test_attrs_attrib_type(systemcls: Type[model.System]) -> None: """An attr.ib's "type" or "default" argument is used as an alternative type annotation. """ mod = fromText(''' import attr from attr import attrib @attr.s class C: a = attr.ib(type=int) b = attrib(type=int) c = attr.ib(type='C') d = attr.ib(default=True) e = attr.ib(123) ''', modname='test', systemcls=systemcls) C = mod.contents['C'] A = C.contents['a'] B = C.contents['b'] _C = C.contents['c'] D = C.contents['d'] E = C.contents['e'] assert isinstance(A, model.Attribute) assert isinstance(B, model.Attribute) assert isinstance(_C, model.Attribute) assert isinstance(D, model.Attribute) assert isinstance(E, model.Attribute) assert type2str(A.annotation) == 'int' assert type2str(B.annotation) == 'int' assert type2str(_C.annotation) == 'C' assert type2str(D.annotation) == 'bool' assert type2str(E.annotation) == 'int' @systemcls_param def test_attrs_attrib_instance(systemcls: Type[model.System]) -> None: """An attr.ib attribute is classified as an instance variable.""" mod = fromText(''' import attr @attr.s class C: a = attr.ib(type=int) ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert C.contents['a'].kind is model.DocumentableKind.INSTANCE_VARIABLE @systemcls_param def test_attrs_attrib_badargs(systemcls: Type[model.System], capsys: CapSys) -> None: """.""" fromText(''' import attr @attr.s class C: a = attr.ib(nosuchargument='bad') ''', modname='test', systemcls=systemcls) captured = capsys.readouterr().out assert captured == ( 'test:5: Invalid arguments for attr.ib(): got an unexpected keyword argument "nosuchargument"\n' ) @systemcls_param def test_attrs_auto_instance(systemcls: Type[model.System]) -> None: """Attrs auto-attributes are classified as instance variables.""" mod = fromText(''' from typing import ClassVar import attr @attr.s(auto_attribs=True) class C: a: int b: bool = False c: ClassVar[str] # explicit class variable d = 123 # ignored by auto_attribs because no annotation ''', modname='test', systemcls=systemcls) C = mod.contents['C'] assert C.contents['a'].kind is model.DocumentableKind.INSTANCE_VARIABLE assert C.contents['b'].kind is model.DocumentableKind.INSTANCE_VARIABLE assert C.contents['c'].kind is model.DocumentableKind.CLASS_VARIABLE assert C.contents['d'].kind is model.DocumentableKind.CLASS_VARIABLE @systemcls_param def test_attrs_args(systemcls: Type[model.System], capsys: CapSys) -> None: """Non-existing arguments and invalid values to recognized arguments are rejected with a warning. """ fromText(''' import attr @attr.s() class C0: ... @attr.s(repr=False) class C1: ... @attr.s(auto_attribzzz=True) class C2: ... @attr.s(auto_attribs=not False) class C3: ... @attr.s(auto_attribs=1) class C4: ... ''', modname='test', systemcls=systemcls) captured = capsys.readouterr().out assert captured == ( 'test:10: Invalid arguments for attr.s(): got an unexpected keyword argument "auto_attribzzz"\n' 'test:13: Unable to figure out value for "auto_attribs" argument to attr.s(), maybe too complex\n' 'test:16: Value for "auto_attribs" argument to attr.s() has type "int", expected "bool"\n' ) @systemcls_param def test_detupling_assignment(systemcls: Type[model.System]) -> None: mod = fromText(''' a, b, c = range(3) ''', modname='test', systemcls=systemcls) assert sorted(mod.contents.keys()) == ['a', 'b', 'c'] @systemcls_param def test_property_decorator(systemcls: Type[model.System]) -> None: """A function decorated with '@property' is documented as an attribute.""" mod = fromText(''' class C: @property def prop(self) -> str: """For sale.""" return 'seaside' @property def oldschool(self): """ @return: For rent. @rtype: string @see: U{https://example.com/} """ return 'downtown' ''', modname='test', systemcls=systemcls) C = mod.contents['C'] prop = C.contents['prop'] assert isinstance(prop, model.Attribute) assert prop.kind is model.DocumentableKind.PROPERTY assert prop.docstring == """For sale.""" assert type2str(prop.annotation) == 'str' oldschool = C.contents['oldschool'] assert isinstance(oldschool, model.Attribute) assert oldschool.kind is model.DocumentableKind.PROPERTY assert isinstance(oldschool.parsed_docstring, ParsedEpytextDocstring) assert unwrap(oldschool.parsed_docstring) == """For rent.""" assert flatten(format_summary(oldschool)) == 'For rent.' assert isinstance(oldschool.parsed_type, ParsedEpytextDocstring) assert str(unwrap(oldschool.parsed_type)) == 'string' fields = oldschool.parsed_docstring.fields assert len(fields) == 1 assert fields[0].tag() == 'see' @systemcls_param def test_property_setter(systemcls: Type[model.System], capsys: CapSys) -> None: """Property setter and deleter methods are renamed, so they don't replace the property itself. """ mod = fromText(''' class C: @property def prop(self): """Getter.""" @prop.setter def prop(self, value): """Setter.""" @prop.deleter def prop(self): """Deleter.""" ''', modname='mod', systemcls=systemcls) C = mod.contents['C'] getter = C.contents['prop'] assert isinstance(getter, model.Attribute) assert getter.kind is model.DocumentableKind.PROPERTY assert getter.docstring == """Getter.""" setter = C.contents['prop.setter'] assert isinstance(setter, model.Function) assert setter.kind is model.DocumentableKind.METHOD assert setter.docstring == """Setter.""" deleter = C.contents['prop.deleter'] assert isinstance(deleter, model.Function) assert deleter.kind is model.DocumentableKind.METHOD assert deleter.docstring == """Deleter.""" @systemcls_param def test_property_custom(systemcls: Type[model.System], capsys: CapSys) -> None: """Any custom decorator with a name ending in 'property' makes a method into a property getter. """ mod = fromText(''' class C: @deprecate.deprecatedProperty(incremental.Version("Twisted", 18, 7, 0)) def processes(self): return {} @async_property async def remote_value(self): return await get_remote_value() @abc.abstractproperty def name(self): raise NotImplementedError ''', modname='mod', systemcls=systemcls) C = mod.contents['C'] deprecated = C.contents['processes'] assert isinstance(deprecated, model.Attribute) assert deprecated.kind is model.DocumentableKind.PROPERTY async_prop = C.contents['remote_value'] assert isinstance(async_prop, model.Attribute) assert async_prop.kind is model.DocumentableKind.PROPERTY abstract_prop = C.contents['name'] assert isinstance(abstract_prop, model.Attribute) assert abstract_prop.kind is model.DocumentableKind.PROPERTY @pytest.mark.parametrize('decoration', ('classmethod', 'staticmethod')) @systemcls_param def test_property_conflict( decoration: str, systemcls: Type[model.System], capsys: CapSys ) -> None: """Warn when a method is decorated as both property and class/staticmethod. These decoration combinations do not create class/static properties. """ mod = fromText(f''' class C: @{decoration} @property def prop(): raise NotImplementedError ''', modname='mod', systemcls=systemcls) C = mod.contents['C'] assert C.contents['prop'].kind is model.DocumentableKind.PROPERTY captured = capsys.readouterr().out assert captured == f"mod:3: mod.C.prop is both property and {decoration}\n" @systemcls_param def test_ignore_function_contents(systemcls: Type[model.System]) -> None: mod = fromText(''' def outer(): """Outer function.""" class Clazz: """Inner class.""" def func(): """Inner function.""" var = 1 """Local variable.""" ''', systemcls=systemcls) outer = mod.contents['outer'] assert not outer.contents @systemcls_param def test_constant_module(systemcls: Type[model.System]) -> None: """ Module variables with all-uppercase names are recognized as constants. """ mod = fromText(''' LANG = 'FR' ''', systemcls=systemcls) lang = mod.contents['LANG'] assert isinstance(lang, model.Attribute) assert lang.kind is model.DocumentableKind.CONSTANT assert ast.literal_eval(getattr(mod.resolveName('LANG'), 'value')) == 'FR' @systemcls_param def test_constant_module_with_final(systemcls: Type[model.System]) -> None: """ Module variables annotated with typing.Final are recognized as constants. """ mod = fromText(''' from typing import Final lang: Final = 'fr' ''', systemcls=systemcls) attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'fr' @systemcls_param def test_constant_module_with_typing_extensions_final(systemcls: Type[model.System]) -> None: """ Module variables annotated with typing_extensions.Final are recognized as constants. """ mod = fromText(''' from typing_extensions import Final lang: Final = 'fr' ''', systemcls=systemcls) attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'fr' @systemcls_param def test_constant_module_with_final_subscript1(systemcls: Type[model.System]) -> None: """ It can recognize constants defined with typing.Final[something] """ mod = fromText(''' from typing import Final lang: Final[Sequence[str]] = ('fr', 'en') ''', systemcls=systemcls) attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == ('fr', 'en') assert astor.to_source(attr.annotation).strip() == "Sequence[str]" @systemcls_param def test_constant_module_with_final_subscript2(systemcls: Type[model.System]) -> None: """ It can recognize constants defined with typing.Final[something]. And it automatically remove the Final part from the annotation. """ mod = fromText(''' import typing lang: typing.Final[tuple] = ('fr', 'en') ''', systemcls=systemcls) attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == ('fr', 'en') assert astbuilder.node2fullname(attr.annotation, attr) == "tuple" @systemcls_param def test_constant_module_with_final_subscript_invalid_warns(systemcls: Type[model.System], capsys: CapSys) -> None: """ It warns if there is an invalid Final annotation. """ mod = fromText(''' from typing import Final lang: Final[tuple, 12:13] = ('fr', 'en') ''', systemcls=systemcls, modname='mod') attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == ('fr', 'en') captured = capsys.readouterr().out assert "mod:3: Annotation is invalid, it should not contain slices.\n" == captured assert astor.to_source(attr.annotation).strip() == "tuple[str, ...]" @systemcls_param def test_constant_module_with_final_subscript_invalid_warns2(systemcls: Type[model.System], capsys: CapSys) -> None: """ It warns if there is an invalid Final annotation. """ mod = fromText(''' import typing lang: typing.Final[12:13] = ('fr', 'en') ''', systemcls=systemcls, modname='mod') attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == ('fr', 'en') captured = capsys.readouterr().out assert "mod:3: Annotation is invalid, it should not contain slices.\n" == captured assert astor.to_source(attr.annotation).strip() == "tuple[str, ...]" @systemcls_param def test_constant_module_with_final_annotation_gets_infered(systemcls: Type[model.System]) -> None: """ It can recognize constants defined with typing.Final. It will infer the type of the constant if Final do not use subscripts. """ mod = fromText(''' import typing lang: typing.Final = 'fr' ''', systemcls=systemcls) attr = mod.resolveName('lang') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'fr' assert astbuilder.node2fullname(attr.annotation, attr) == "str" @systemcls_param def test_constant_class(systemcls: Type[model.System]) -> None: """ Class variables with all-uppercase names are recognized as constants. """ mod = fromText(''' class Clazz: """Class.""" LANG = 'FR' ''', systemcls=systemcls) attr = mod.resolveName('Clazz.LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'FR' @systemcls_param def test_all_caps_variable_in_instance_is_not_a_constant(systemcls: Type[model.System], capsys: CapSys) -> None: """ Currently, it does not mark instance members as constants, never. """ mod = fromText(''' from typing import Final class Clazz: """Class.""" def __init__(**args): self.LANG: Final = 'FR' ''', systemcls=systemcls) attr = mod.resolveName('Clazz.LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.INSTANCE_VARIABLE assert attr.value is not None assert ast.literal_eval(attr.value) == 'FR' captured = capsys.readouterr().out assert not captured @systemcls_param def test_constant_override_in_instace_warns(systemcls: Type[model.System], capsys: CapSys) -> None: """ It warns when a constant is beeing re defined in instance. But it ignores it's value. """ mod = fromText(''' class Clazz: """Class.""" LANG = 'EN' def __init__(self, **args): self.LANG = 'FR' ''', systemcls=systemcls, modname="mod") attr = mod.resolveName('Clazz.LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'EN' captured = capsys.readouterr().out assert "mod:6: Assignment to constant \"LANG\" inside an instance is ignored, this value will not be part of the docs.\n" == captured @systemcls_param def test_constant_override_in_instace_warns2(systemcls: Type[model.System], capsys: CapSys) -> None: """ It warns when a constant is beeing re defined in instance. But it ignores it's value. Even if the actual constant definition is detected after the instance variable of the same name. """ mod = fromText(''' class Clazz: """Class.""" def __init__(self, **args): self.LANG = 'FR' LANG = 'EN' ''', systemcls=systemcls, modname="mod") attr = mod.resolveName('Clazz.LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 'EN' captured = capsys.readouterr().out assert "mod:5: Assignment to constant \"LANG\" inside an instance is ignored, this value will not be part of the docs.\n" == captured @systemcls_param def test_constant_override_in_module_warns(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' """Mod.""" import sys IS_64BITS = False if sys.maxsize > 2**32: IS_64BITS = True ''', systemcls=systemcls, modname="mod") attr = mod.resolveName('IS_64BITS') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == True captured = capsys.readouterr().out assert "mod:6: Assignment to constant \"IS_64BITS\" overrides previous assignment at line 4, the original value will not be part of the docs.\n" == captured @systemcls_param def test_constant_override_do_not_warns_when_defined_in_class_docstring(systemcls: Type[model.System], capsys: CapSys) -> None: """ Constant can be documented as variables at docstring level without any warnings. """ mod = fromText(''' class Clazz: """ @cvar LANG: French. """ LANG = 99 ''', systemcls=systemcls, modname="mod") attr = mod.resolveName('Clazz.LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 99 captured = capsys.readouterr().out assert not captured @systemcls_param def test_constant_override_do_not_warns_when_defined_in_module_docstring(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' """ @var LANG: French. """ LANG = 99 ''', systemcls=systemcls, modname="mod") attr = mod.resolveName('LANG') assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.CONSTANT assert attr.value is not None assert ast.literal_eval(attr.value) == 99 captured = capsys.readouterr().out assert not captured pydoctor-21.12.1/pydoctor/test/test_colorize.py000066400000000000000000000066651416703725300216270ustar00rootroot00000000000000from pydoctor.epydoc.doctest import colorize_codeblock, colorize_doctest from pydoctor.stanutils import flatten def test_colorize_codeblock() -> None: src = ''' def foo(): """A multi-line docstring. The "doc" part doesn't matter for this test, but the "string" part does. """ return list({1, 2, 3}) class Foo: def __init__(self): # Nothing to do. pass '''.lstrip() expected = '''
    def foo():
        """A multi-line docstring.
    
        The "doc" part doesn't matter for this test,
        but the "string" part does.
        """
        return list({1, 2, 3})
    
    class Foo:
        def __init__(self):
            # Nothing to do.
            pass
    
    '''.strip() assert flatten(colorize_codeblock(src)) == expected def test_colorize_doctest_more_string() -> None: src = ''' Test multi-line string: >>> """A ... B ... C""" 'A\\nB\\nC' '''.lstrip() expected = '''
    Test multi-line string:
    
        >>> """A
        ... B
        ... C"""
        'A\\nB\\nC'
    
    '''.strip() assert flatten(colorize_doctest(src)) == expected def test_colorize_doctest_more_input() -> None: src = ''' Test multi-line expression: >>> [chr(i + 65) ... for i in range(26) ... if i % 2 == 0] ['A', 'C', 'E', 'G', 'I', 'K', 'M', 'O', 'Q', 'S', 'U', 'W', 'Y'] '''.lstrip() expected = '''
    Test multi-line expression:
    
        >>> [chr(i + 65)
        ...  for i in range(26)
        ...  if i % 2 == 0]
        ['A', 'C', 'E', 'G', 'I', 'K', 'M', 'O', 'Q', 'S', 'U', 'W', 'Y']
    
    '''.strip() assert flatten(colorize_doctest(src)) == expected def test_colorize_doctest_exception() -> None: src = ''' Test division by zero: >>> 1/0 Traceback (most recent call last): ZeroDivisionError: integer division or modulo by zero '''.lstrip() expected = '''
    Test division by zero:
    
        >>> 1/0
        Traceback (most recent call last):
        ZeroDivisionError: integer division or modulo by zero
    
    '''.strip() assert flatten(colorize_doctest(src)) == expected def test_colorize_doctest_no_output() -> None: src = ''' Test expecting no output: >>> None '''.lstrip() expected = '''
    Test expecting no output:
    
        >>> None
    
    '''.strip() assert flatten(colorize_doctest(src)) == expected pydoctor-21.12.1/pydoctor/test/test_commandline.py000066400000000000000000000173541416703725300222640ustar00rootroot00000000000000from contextlib import redirect_stdout from io import StringIO from pathlib import Path import sys from pytest import raises from pydoctor import driver from . import CapSys def geterrtext(*options: str) -> str: """ Run CLI with options and return the output triggered by system exit. """ se = sys.stderr f = StringIO() print(options) sys.stderr = f try: try: driver.main(list(options)) except SystemExit: pass else: assert False, "did not fail" finally: sys.stderr = se return f.getvalue() def test_invalid_option() -> None: err = geterrtext('--no-such-option') assert 'no such option' in err def test_cannot_advance_blank_system() -> None: err = geterrtext('--make-html') assert 'No source paths given' in err def test_no_systemclasses_py3() -> None: err = geterrtext('--system-class') assert 'requires 1 argument' in err def test_invalid_systemclasses() -> None: err = geterrtext('--system-class=notdotted') assert 'dotted name' in err err = geterrtext('--system-class=no-such-module.System') assert 'could not import module' in err err = geterrtext('--system-class=pydoctor.model.Class') assert 'is not a subclass' in err def test_projectbasedir_absolute(tmp_path: Path) -> None: """ The --project-base-dir option, when given an absolute path, should set that path as the projectbasedirectory attribute on the options object. Previous versions of this test tried using non-existing paths and compared the string representations, but that was unreliable, since the input path might contain a symlink that will be resolved, such as "/home" on macOS. Using L{Path.samefile()} is reliable, but requires an existing path. """ assert tmp_path.is_absolute() options, args = driver.parse_args(["--project-base-dir", str(tmp_path)]) assert options.projectbasedirectory.samefile(tmp_path) assert options.projectbasedirectory.is_absolute() def test_projectbasedir_symlink(tmp_path: Path) -> None: """ The --project-base-dir option, when given a path containing a symbolic link, should resolve the path to the target directory. """ target = tmp_path / 'target' target.mkdir() link = tmp_path / 'link' link.symlink_to('target', target_is_directory=True) assert link.samefile(target) options, args = driver.parse_args(["--project-base-dir", str(link)]) assert options.projectbasedirectory.samefile(target) assert options.projectbasedirectory.is_absolute() def test_projectbasedir_relative() -> None: """ The --project-base-dir option, when given a relative path, should convert that path to absolute and set it as the projectbasedirectory attribute on the options object. """ relative = "projbasedirvalue" options, args = driver.parse_args(["--project-base-dir", relative]) assert options.projectbasedirectory.is_absolute() assert options.projectbasedirectory.name == relative assert options.projectbasedirectory.parent == Path.cwd() def test_cache_enabled_by_default() -> None: """ Intersphinx object caching is enabled by default. """ parser = driver.getparser() (options, _) = parser.parse_args([]) assert options.enable_intersphinx_cache def test_cli_warnings_on_error() -> None: """ The --warnings-as-errors option is disabled by default. This is the test for the long form of the CLI option. """ options, args = driver.parse_args([]) assert options.warnings_as_errors == False options, args = driver.parse_args(['--warnings-as-errors']) assert options.warnings_as_errors == True def test_project_version_default() -> None: """ When no --project-version is provided, it will default empty string. """ options, args = driver.parse_args([]) assert options.projectversion == '' def test_project_version_string() -> None: """ --project-version can be passed as a simple string. """ options, args = driver.parse_args(['--project-version', '1.2.3.rc1']) assert options.projectversion == '1.2.3.rc1' def test_main_project_name_guess(capsys: CapSys) -> None: """ When no project name is provided in the CLI arguments, a default name is used and logged. """ exit_code = driver.main(args=[ '-v', '--testing', 'pydoctor/test/testpackages/basic/' ]) assert exit_code == 0 assert "Guessing 'basic' for project name." in capsys.readouterr().out def test_main_project_name_option(capsys: CapSys) -> None: """ When a project name is provided in the CLI arguments nothing is logged. """ exit_code = driver.main(args=[ '-v', '--testing', '--project-name=some-name', 'pydoctor/test/testpackages/basic/' ]) assert exit_code == 0 assert 'Guessing ' not in capsys.readouterr().out def test_main_return_zero_on_warnings() -> None: """ By default it will return 0 as exit code even when there are warnings. """ stream = StringIO() with redirect_stdout(stream): exit_code = driver.main(args=[ '--html-writer=pydoctor.test.InMemoryWriter', 'pydoctor/test/testpackages/report_trigger/' ]) assert exit_code == 0 assert "__init__.py:8: Unknown field 'bad_field'" in stream.getvalue() assert 'report_module.py:9: Cannot find link target for "BadLink"' in stream.getvalue() def test_main_return_non_zero_on_warnings() -> None: """ When `-W` is used it returns 3 as exit code when there are warnings. """ stream = StringIO() with redirect_stdout(stream): exit_code = driver.main(args=[ '-W', '--html-writer=pydoctor.test.InMemoryWriter', 'pydoctor/test/testpackages/report_trigger/' ]) assert exit_code == 3 assert "__init__.py:8: Unknown field 'bad_field'" in stream.getvalue() assert 'report_module.py:9: Cannot find link target for "BadLink"' in stream.getvalue() def test_main_symlinked_paths(tmp_path: Path) -> None: """ The project base directory and package/module directories are normalized in the same way, such that System.setSourceHref() can call Path.relative_to() on them. """ link = tmp_path / 'src' link.symlink_to(Path.cwd(), target_is_directory=True) exit_code = driver.main(args=[ '--project-base-dir=.', '--html-viewsource-base=http://example.com', f'{link}/pydoctor/test/testpackages/basic/' ]) assert exit_code == 0 def test_main_source_outside_basedir(capsys: CapSys) -> None: """ If a --project-base-dir is given, all package and module paths must be located inside that base directory. """ with raises(SystemExit): driver.main(args=[ '--project-base-dir=docs', 'pydoctor/test/testpackages/basic/' ]) assert "Source path lies outside base directory:" in capsys.readouterr().err def test_make_intersphix(tmp_path: Path) -> None: """ --make-intersphinx without --make-html will only produce the Sphinx inventory object. This is also an integration test for the Sphinx inventory writer. """ inventory = tmp_path / 'objects.inv' exit_code = driver.main(args=[ '--project-base-dir=.', '--make-intersphinx', '--project-name=acme-lib', '--project-version=20.12.0-dev123', '--html-output', str(tmp_path), 'pydoctor/test/testpackages/basic/' ]) assert exit_code == 0 # No other files are created, other than the inventory. assert [p.name for p in tmp_path.iterdir()] == ['objects.inv'] assert inventory.is_file() assert b'Project: acme-lib\n# Version: 20.12.0-dev123\n' in inventory.read_bytes() pydoctor-21.12.1/pydoctor/test/test_epydoc2stan.py000066400000000000000000001144361416703725300222300ustar00rootroot00000000000000from typing import List, Optional, cast, TYPE_CHECKING import re from pytest import mark, raises import pytest from twisted.web.template import Tag, tags from pydoctor import epydoc2stan, model from pydoctor.epydoc.markup import DocstringLinker from pydoctor.stanutils import flatten, flatten_text from pydoctor.epydoc.markup.epytext import ParsedEpytextDocstring from pydoctor.sphinx import SphinxInventory from pydoctor.test.test_astbuilder import fromText, unwrap from pydoctor.test import CapSys if TYPE_CHECKING: from twisted.web.template import Flattenable def test_multiple_types() -> None: mod = fromText(''' def f(a): """ @param a: it\'s a parameter! @type a: a pink thing! @type a: no, blue! aaaargh! """ class C: """ @ivar a: it\'s an instance var @type a: a pink thing! @type a: no, blue! aaaargh! """ class D: """ @cvar a: it\'s an instance var @type a: a pink thing! @type a: no, blue! aaaargh! """ class E: """ @cvar: missing name @type: name still missing """ ''') # basically "assert not fail": epydoc2stan.format_docstring(mod.contents['f']) epydoc2stan.format_docstring(mod.contents['C']) epydoc2stan.format_docstring(mod.contents['D']) epydoc2stan.format_docstring(mod.contents['E']) def docstring2html(obj: model.Documentable, docformat: Optional[str] = None) -> str: if docformat: obj.module.docformat = docformat stan = epydoc2stan.format_docstring(obj) assert stan.tagName == 'div', stan # We strip off break lines for the sake of simplicity. return flatten(stan).replace('><', '>\n<').replace('', '').replace('\n', '') def summary2html(obj: model.Documentable) -> str: stan = epydoc2stan.format_summary(obj) if stan.attributes.get('class') == 'undocumented': assert stan.tagName == 'span', stan else: # Summaries are now generated without englobing when we don't need one. assert stan.tagName == '', stan return flatten(stan.children) def test_html_empty_module() -> None: mod = fromText(''' """Empty module.""" ''') assert docstring2html(mod) == "
    Empty module.
    " def test_xref_link_not_found() -> None: """A linked name that is not found is output as text.""" mod = fromText(''' """This link leads L{nowhere}.""" ''', modname='test') html = docstring2html(mod) assert 'nowhere' in html def test_xref_link_same_page() -> None: """A linked name that is documented on the same page is linked using only a fragment as the URL. """ mod = fromText(''' """The home of L{local_func}.""" def local_func(): pass ''', modname='test') html = docstring2html(mod) assert 'href="#local_func"' in html def test_xref_link_other_page() -> None: """A linked name that is documented on a different page but within the same project is linked using a relative URL. """ mod1 = fromText(''' def func(): """This is not L{test2.func}.""" ''', modname='test1') fromText(''' def func(): pass ''', modname='test2', system=mod1.system) html = docstring2html(mod1.contents['func']) assert 'href="test2.html#func"' in html def test_xref_link_intersphinx() -> None: """A linked name that is documented in another project is linked using an absolute URL (retrieved via Intersphinx). """ mod = fromText(''' def func(): """This is a thin wrapper around L{external.func}.""" ''', modname='test') system = mod.system inventory = SphinxInventory(system.msg) inventory._links['external.func'] = ('https://example.net', 'lib.html#func') system.intersphinx = inventory html = docstring2html(mod.contents['func']) assert 'href="https://example.net/lib.html#func"' in html def test_func_undocumented_return_nothing() -> None: """When the returned value is undocumented (no 'return' field) and its type annotation is None, omit the "Returns" entry from the output. """ mod = fromText(''' def nop() -> None: pass ''') func = mod.contents['nop'] lines = docstring2html(func).split('\n') assert 'Returns' not in lines def test_func_undocumented_return_something() -> None: """When the returned value is undocumented (no 'return' field) and its type annotation is not None, include the "Returns" entry in the output. """ mod = fromText(''' def get_answer() -> int: return 42 ''') func = mod.contents['get_answer'] lines = docstring2html(func).splitlines() expected_html = [ '
    ', '

    Undocumented

    ', '', '', '', '', '', '', '', '', '
    Returns
    ', 'int', '', 'Undocumented', '
    ', '
    ' ] assert lines == expected_html, str(lines) # These 3 tests fails because AnnotationDocstring is not using node2stan() yet. @pytest.mark.xfail def test_func_arg_and_ret_annotation() -> None: annotation_mod = fromText(''' def f(a: List[str], b: "List[str]") -> bool: """ @param a: an arg, a the best of args @param b: a param to follow a @return: the best that we can do """ ''') classic_mod = fromText(''' def f(a, b): """ @param a: an arg, a the best of args @type a: C{List[str]} @param b: a param to follow a @type b: C{List[str]} @return: the best that we can do @rtype: C{bool} """ ''') annotation_fmt = docstring2html(annotation_mod.contents['f']) classic_fmt = docstring2html(classic_mod.contents['f']) assert annotation_fmt == classic_fmt @pytest.mark.xfail def test_func_arg_and_ret_annotation_with_override() -> None: annotation_mod = fromText(''' def f(a: List[str], b: List[str]) -> bool: """ @param a: an arg, a the best of args @param b: a param to follow a @type b: C{List[awesome]} @return: the best that we can do """ ''') classic_mod = fromText(''' def f(a, b): """ @param a: an arg, a the best of args @type a: C{List[str]} @param b: a param to follow a @type b: C{List[awesome]} @return: the best that we can do @rtype: C{bool} """ ''') annotation_fmt = docstring2html(annotation_mod.contents['f']) classic_fmt = docstring2html(classic_mod.contents['f']) assert annotation_fmt == classic_fmt @pytest.mark.xfail def test_func_arg_when_doc_missing() -> None: annotation_mod = fromText(''' def f(a: List[str], b: int) -> bool: """ Today I will not document details """ ''') classic_mod = fromText(''' def f(a): """ Today I will not document details @type a: C{List[str]} @type b: C{int} @rtype: C{bool} """ ''') annotation_fmt = docstring2html(annotation_mod.contents['f']) classic_fmt = docstring2html(classic_mod.contents['f']) assert annotation_fmt == classic_fmt def test_func_param_duplicate(capsys: CapSys) -> None: """Warn when the same parameter is documented more than once.""" mod = fromText(''' def f(x, y): """ @param x: Actual documentation. @param x: Likely typo or copy-paste error. """ ''') epydoc2stan.format_docstring(mod.contents['f']) captured = capsys.readouterr().out assert captured == ':5: Parameter "x" was already documented\n' @mark.parametrize('field', ('param', 'type')) def test_func_no_such_arg(field: str, capsys: CapSys) -> None: """Warn about documented parameters that don't exist in the definition.""" mod = fromText(f''' def f(): """ This function takes no arguments... @{field} x: ...but it does document one. """ ''') epydoc2stan.format_docstring(mod.contents['f']) captured = capsys.readouterr().out assert captured == ':6: Documented parameter "x" does not exist\n' def test_func_no_such_arg_warn_once(capsys: CapSys) -> None: """Warn exactly once about a param/type combination not existing.""" mod = fromText(''' def f(): """ @param x: Param first. @type x: Param first. @type y: Type first. @param y: Type first. """ ''') epydoc2stan.format_docstring(mod.contents['f']) captured = capsys.readouterr().out assert captured == ( ':4: Documented parameter "x" does not exist\n' ':6: Documented parameter "y" does not exist\n' ) def test_func_arg_not_inherited(capsys: CapSys) -> None: """Do not warn when a subclass method lacks parameters that are documented in an inherited docstring. """ mod = fromText(''' class Base: def __init__(self, value): """ @param value: Preciousss. @type value: Gold. """ class Sub(Base): def __init__(self): super().__init__(1) ''') epydoc2stan.format_docstring(mod.contents['Base'].contents['__init__']) assert capsys.readouterr().out == '' epydoc2stan.format_docstring(mod.contents['Sub'].contents['__init__']) assert capsys.readouterr().out == '' def test_func_param_as_keyword(capsys: CapSys) -> None: """Warn when a parameter is documented as a @keyword.""" mod = fromText(''' def f(p, **kwargs): """ @keyword a: Advanced. @keyword b: Basic. @type b: Type for previously introduced keyword. @keyword p: A parameter, not a keyword. """ ''') epydoc2stan.format_docstring(mod.contents['f']) assert capsys.readouterr().out == ':7: Parameter "p" is documented as keyword\n' def test_func_missing_param_name(capsys: CapSys) -> None: """Param and type fields must include the name of the parameter.""" mod = fromText(''' def f(a, b): """ @param a: The first parameter. @param: The other one. @type: C{str} """ ''') epydoc2stan.format_docstring(mod.contents['f']) captured = capsys.readouterr().out assert captured == ( ':5: Parameter name missing\n' ':6: Parameter name missing\n' ) def test_missing_param_computed_base(capsys: CapSys) -> None: """Do not warn if a parameter might be added by a computed base class.""" mod = fromText(''' from twisted.python import components import zope.interface class IFoo(zope.interface.Interface): pass class Proxy(components.proxyForInterface(IFoo)): """ @param original: The wrapped instance. """ ''') html = ''.join(docstring2html(mod.contents['Proxy']).splitlines()) assert 'The wrapped instance.' in html captured = capsys.readouterr().out assert captured == '' def test_constructor_param_on_class(capsys: CapSys) -> None: """Constructor parameters can be documented on the class.""" mod = fromText(''' class C: """ @param p: Constructor parameter. @param q: Not a constructor parameter. """ def __init__(self, p): pass ''') html = ''.join(docstring2html(mod.contents['C']).splitlines()) assert 'Constructor parameter.' in html # Non-existing parameters should still end up in the output, because: # - pydoctor might be wrong about them not existing # - the documentation may still be useful, for example if belongs to # an existing parameter but the name in the @param field has a typo assert 'Not a constructor parameter.' in html captured = capsys.readouterr().out assert captured == ':5: Documented parameter "q" does not exist\n' def test_func_raise_linked() -> None: """Raise fields are formatted by linking the exception type.""" mod = fromText(''' class SpanishInquisition(Exception): pass def f(): """ @raise SpanishInquisition: If something unexpected happens. """ ''', modname='test') html = docstring2html(mod.contents['f']).split('\n') assert 'SpanishInquisition' in html def test_func_raise_missing_exception_type(capsys: CapSys) -> None: """When a C{raise} field is missing the exception type, a warning is logged and the HTML will list the exception type as unknown. """ mod = fromText(''' def f(x): """ @raise ValueError: If C{x} is rejected. @raise: On a blue moon. """ ''') func = mod.contents['f'] epydoc2stan.format_docstring(func) captured = capsys.readouterr().out assert captured == ':5: Exception type missing\n' html = docstring2html(func).split('\n') assert 'Unknown exception' in html def test_unexpected_field_args(capsys: CapSys) -> None: """Warn when field arguments that should be empty aren't.""" mod = fromText(''' def get_it(): """ @return value: The thing you asked for, probably. @rtype value: Not a clue. """ ''') epydoc2stan.format_docstring(mod.contents['get_it']) captured = capsys.readouterr().out assert captured == ":4: Unexpected argument in return field\n" \ ":5: Unexpected argument in rtype field\n" def test_func_starargs(capsys: CapSys) -> None: """ Var-args can be named in fields with or without asterixes. Constructor parameters can be documented on the class. @note: Asterixes need to be escaped with reStructuredText. """ mod_epy_star = fromText(''' class f: """ Do something with var-positional and var-keyword arguments. @param *args: var-positional arguments @param **kwargs: var-keyword arguments @type **kwargs: str """ def __init__(*args: int, **kwargs) -> None: ... ''', modname='') mod_epy_no_star = fromText(''' class f: """ Do something with var-positional and var-keyword arguments. @param args: var-positional arguments @param kwargs: var-keyword arguments @type kwargs: str """ def __init__(*args: int, **kwargs) -> None: ... ''', modname='') mod_rst_star = fromText(r''' __docformat__='restructuredtext' class f: r""" Do something with var-positional and var-keyword arguments. :param \*args: var-positional arguments :param \*\*kwargs: var-keyword arguments :type \*\*kwargs: str """ def __init__(*args: int, **kwargs) -> None: ... ''', modname='') mod_rst_no_star = fromText(''' __docformat__='restructuredtext' class f: """ Do something with var-positional and var-keyword arguments. :param args: var-positional arguments :param kwargs: var-keyword arguments :type kwargs: str """ def __init__(*args: int, **kwargs) -> None: ... ''', modname='') mod_epy_star_fmt = docstring2html(mod_epy_star.contents['f']) mod_epy_no_star_fmt = docstring2html(mod_epy_no_star.contents['f']) mod_rst_star_fmt = docstring2html(mod_rst_star.contents['f']) mod_rst_no_star_fmt = docstring2html(mod_rst_no_star.contents['f']) assert mod_rst_star_fmt == mod_rst_no_star_fmt == mod_epy_star_fmt == mod_epy_no_star_fmt expected_parts = ['*args', '**kwargs',] for part in expected_parts: assert part in mod_epy_star_fmt captured = capsys.readouterr().out assert not captured def test_func_starargs_more(capsys: CapSys) -> None: """ Star arguments, even if there are not named 'args' or 'kwargs', are recognized. """ mod_epy_with_asterixes = fromText(''' def f(args, kwargs, *a, **kwa) -> None: """ Do something with var-positional and var-keyword arguments. @param args: some regular argument @param kwargs: some regular argument @param *a: var-positional arguments @param **kwa: var-keyword arguments """ ''', modname='') mod_rst_with_asterixes = fromText(r''' def f(args, kwargs, *a, **kwa) -> None: r""" Do something with var-positional and var-keyword arguments. :param args: some regular argument :param kwargs: some regular argument :param \*a: var-positional arguments :param \*\*kwa: var-keyword arguments """ ''', modname='') mod_rst_without_asterixes = fromText(''' def f(args, kwargs, *a, **kwa) -> None: """ Do something with var-positional and var-keyword arguments. :param args: some regular argument :param kwargs: some regular argument :param a: var-positional arguments :param kwa: var-keyword arguments """ ''', modname='') mod_epy_without_asterixes = fromText(''' def f(args, kwargs, *a, **kwa) -> None: """ Do something with var-positional and var-keyword arguments. @param args: some regular argument @param kwargs: some regular argument @param a: var-positional arguments @param kwa: var-keyword arguments """ ''', modname='') epy_with_asterixes_fmt = docstring2html(mod_epy_with_asterixes.contents['f']) rst_with_asterixes_fmt = docstring2html(mod_rst_with_asterixes.contents['f'], docformat='restructuredtext') rst_without_asterixes_fmt = docstring2html(mod_rst_without_asterixes.contents['f'], docformat='restructuredtext') epy_without_asterixes_fmt = docstring2html(mod_epy_without_asterixes.contents['f']) assert epy_with_asterixes_fmt == rst_with_asterixes_fmt == rst_without_asterixes_fmt == epy_without_asterixes_fmt expected_parts = ['args', 'kwargs', '*a', '**kwa',] for part in expected_parts: assert part in epy_with_asterixes_fmt captured = capsys.readouterr().out assert not captured def test_func_starargs_no_docstring(capsys: CapSys) -> None: """ Star arguments, even if there are not docstring attached, will be rendered with stars. @note: This test might not pass anymore when we include the annotations inside the signatures. """ mod = fromText(''' def f(args:str, kwargs:str, *a:Any, **kwa:Any) -> None: """ Do something with var-positional and var-keyword arguments. """ ''', modname='') mod_fmt = docstring2html(mod.contents['f']) expected_parts = ['args:', 'kwargs:', '*a:', '**kwa:',] for part in expected_parts: assert part in mod_fmt, mod_fmt captured = capsys.readouterr().out assert not captured def test_summary() -> None: mod = fromText(''' def single_line_summary(): """ Lorem Ipsum Ipsum Lorem """ def no_summary(): """ Foo Bar Baz Qux """ def three_lines_summary(): """ Foo Bar Baz Lorem Ipsum """ ''') assert 'Lorem Ipsum' == summary2html(mod.contents['single_line_summary']) assert 'Foo Bar Baz' == summary2html(mod.contents['three_lines_summary']) assert 'No summary' == summary2html(mod.contents['no_summary']) def test_ivar_overriding_attribute() -> None: """An 'ivar' field in a subclass overrides a docstring for the same attribute set in the base class. The 'a' attribute in the test code reproduces a regression introduced in pydoctor 20.7.0, where the summary would be constructed from the base class documentation instead. The problem was in the fact that a split field's docstring is stored in 'parsed_docstring', while format_summary() looked there only if no unparsed docstring could be found. The 'b' attribute in the test code is there to make sure that in the absence of an 'ivar' field, the docstring is inherited. """ mod = fromText(''' class Base: a: str """base doc details """ b: object """not overridden details """ class Sub(Base): """ @ivar a: sub doc @type b: sub type """ ''') base = mod.contents['Base'] base_a = base.contents['a'] assert isinstance(base_a, model.Attribute) assert summary2html(base_a) == "base doc" assert docstring2html(base_a) == "
    \n

    base doc

    \n

    details

    \n
    " base_b = base.contents['b'] assert isinstance(base_b, model.Attribute) assert summary2html(base_b) == "not overridden" assert docstring2html(base_b) == "
    \n

    not overridden

    \n

    details

    \n
    " sub = mod.contents['Sub'] sub_a = sub.contents['a'] assert isinstance(sub_a, model.Attribute) assert summary2html(sub_a) == 'sub doc' assert docstring2html(sub_a) == "
    sub doc
    " sub_b = sub.contents['b'] assert isinstance(sub_b, model.Attribute) assert summary2html(sub_b) == 'not overridden' assert docstring2html(sub_b) == "
    \n

    not overridden

    \n

    details

    \n
    " def test_missing_field_name(capsys: CapSys) -> None: mod = fromText(''' """ A test module. @ivar: Mystery variable. @type: str """ ''', modname='test') epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out assert captured == "test:5: Missing field name in @ivar\n" \ "test:6: Missing field name in @type\n" def test_unknown_field_name(capsys: CapSys) -> None: mod = fromText(''' """ A test module. @zap: No such field. """ ''', modname='test') epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out assert captured == "test:5: Unknown field 'zap'\n" def test_inline_field_type(capsys: CapSys) -> None: """The C{type} field in a variable docstring updates the C{parsed_type} of the Attribute it documents. """ mod = fromText(''' a = 2 """ Variable documented by inline docstring. @type: number """ ''', modname='test') a = mod.contents['a'] assert isinstance(a, model.Attribute) epydoc2stan.format_docstring(a) assert isinstance(a.parsed_type, ParsedEpytextDocstring) assert str(unwrap(a.parsed_type)) == 'number' assert not capsys.readouterr().out def test_inline_field_name(capsys: CapSys) -> None: """Warn if a name is given for a C{type} field in a variable docstring. A variable docstring only documents a single variable, so the name is redundant at best and misleading at worst. """ mod = fromText(''' a = 2 """ Variable documented by inline docstring. @type a: number """ ''', modname='test') a = mod.contents['a'] assert isinstance(a, model.Attribute) epydoc2stan.format_docstring(a) captured = capsys.readouterr().out assert captured == "test:5: Field in variable docstring should not include a name\n" def test_EpydocLinker_look_for_intersphinx_no_link() -> None: """ Return None if inventory had no link for our markup. """ system = model.System() target = model.Module(system, 'ignore-name') sut = epydoc2stan._EpydocLinker(target) result = sut.look_for_intersphinx('base.module') assert None is result def test_EpydocLinker_look_for_intersphinx_hit() -> None: """ Return the link from inventory based on first package name. """ system = model.System() inventory = SphinxInventory(system.msg) inventory._links['base.module.other'] = ('http://tm.tld', 'some.html') system.intersphinx = inventory target = model.Module(system, 'ignore-name') sut = epydoc2stan._EpydocLinker(target) result = sut.look_for_intersphinx('base.module.other') assert 'http://tm.tld/some.html' == result def test_EpydocLinker_resolve_identifier_xref_intersphinx_absolute_id() -> None: """ Returns the link from Sphinx inventory based on a cross reference ID specified in absolute dotted path and with a custom pretty text for the URL. """ system = model.System() inventory = SphinxInventory(system.msg) inventory._links['base.module.other'] = ('http://tm.tld', 'some.html') system.intersphinx = inventory target = model.Module(system, 'ignore-name') sut = epydoc2stan._EpydocLinker(target) url = sut.resolve_identifier('base.module.other') url_xref = sut._resolve_identifier_xref('base.module.other', 0) assert "http://tm.tld/some.html" == url assert "http://tm.tld/some.html" == url_xref def test_EpydocLinker_resolve_identifier_xref_intersphinx_relative_id() -> None: """ Return the link from inventory using short names, by resolving them based on the imports done in the module. """ system = model.System() inventory = SphinxInventory(system.msg) inventory._links['ext_package.ext_module'] = ('http://tm.tld', 'some.html') system.intersphinx = inventory target = model.Module(system, 'ignore-name') # Here we set up the target module as it would have this import. # from ext_package import ext_module ext_package = model.Module(system, 'ext_package') target.contents['ext_module'] = model.Module( system, 'ext_module', parent=ext_package) sut = epydoc2stan._EpydocLinker(target) # This is called for the L{ext_module} markup. url = sut.resolve_identifier('ext_module') url_xref = sut._resolve_identifier_xref('ext_module', 0) assert "http://tm.tld/some.html" == url assert "http://tm.tld/some.html" == url_xref def test_EpydocLinker_resolve_identifier_xref_intersphinx_link_not_found(capsys: CapSys) -> None: """ A message is sent to stdout when no link could be found for the reference, while returning the reference name without an A link tag. The message contains the full name under which the reference was resolved. FIXME: Use a proper logging system instead of capturing stdout. https://github.com/twisted/pydoctor/issues/112 """ system = model.System() target = model.Module(system, 'ignore-name') # Here we set up the target module as it would have this import. # from ext_package import ext_module ext_package = model.Module(system, 'ext_package') target.contents['ext_module'] = model.Module( system, 'ext_module', parent=ext_package) sut = epydoc2stan._EpydocLinker(target) # This is called for the L{ext_module} markup. assert sut.resolve_identifier('ext_module') is None assert not capsys.readouterr().out with raises(LookupError): sut._resolve_identifier_xref('ext_module', 0) captured = capsys.readouterr().out expected = ( 'ignore-name:???: Cannot find link target for "ext_package.ext_module", ' 'resolved from "ext_module" ' '(you can link to external docs with --intersphinx)\n' ) assert expected == captured class InMemoryInventory: """ A simple inventory implementation which has an in-memory API link mapping. """ INVENTORY = { 'socket.socket': 'https://docs.python.org/3/library/socket.html#socket.socket', } def getLink(self, name: str) -> Optional[str]: return self.INVENTORY.get(name) def test_EpydocLinker_resolve_identifier_xref_order(capsys: CapSys) -> None: """ Check that the best match is picked when there are multiple candidates. """ mod = fromText(''' class C: socket = None ''') mod.system.intersphinx = cast(SphinxInventory, InMemoryInventory()) linker = epydoc2stan._EpydocLinker(mod) url = linker.resolve_identifier('socket.socket') url_xref = linker._resolve_identifier_xref('socket.socket', 0) assert 'https://docs.python.org/3/library/socket.html#socket.socket' == url assert 'https://docs.python.org/3/library/socket.html#socket.socket' == url_xref assert not capsys.readouterr().out def test_EpydocLinker_resolve_identifier_xref_internal_full_name() -> None: """Link to an internal object referenced by its full name.""" # Object we want to link to. int_mod = fromText(''' class C: pass ''', modname='internal_module') system = int_mod.system # Dummy module that we want to link from. target = model.Module(system, 'ignore-name') sut = epydoc2stan._EpydocLinker(target) url = sut.resolve_identifier('internal_module.C') xref = sut._resolve_identifier_xref('internal_module.C', 0) assert "internal_module.C.html" == url assert int_mod.contents['C'] is xref def test_xref_not_found_epytext(capsys: CapSys) -> None: """ When a link in an epytext docstring cannot be resolved, the reference and the line number of the link should be reported. """ mod = fromText(''' """ A test module. Link to limbo: L{NoSuchName}. """ ''', modname='test') epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out assert captured == 'test:5: Cannot find link target for "NoSuchName"\n' def test_xref_not_found_restructured(capsys: CapSys) -> None: """ When a link in an reStructedText docstring cannot be resolved, the reference and the line number of the link should be reported. However, currently the best we can do is report the starting line of the docstring instead. """ system = model.System() system.options.docformat = 'restructuredtext' mod = fromText(''' """ A test module. Link to limbo: `NoSuchName`. """ ''', modname='test', system=system) epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out # TODO: Should actually be line 5, but I can't get docutils to fill in # the line number when it calls visit_title_reference(). # https://github.com/twisted/pydoctor/issues/237 assert captured == 'test:3: Cannot find link target for "NoSuchName"\n' class RecordingAnnotationLinker(DocstringLinker): """A DocstringLinker implementation that cannot find any links, but does record which identifiers it was asked to link. """ def __init__(self) -> None: self.requests: List[str] = [] def link_to(self, target: str, label: "Flattenable") -> Tag: self.resolve_identifier(target) return tags.transparent(label) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: assert False def resolve_identifier(self, identifier: str) -> Optional[str]: self.requests.append(identifier) return None @mark.parametrize('annotation', ( '', '', '[]', '[]', '[, ]', '[, ]', '[, ...]', '[[, ], ]', )) def test_annotation_formatting(annotation: str) -> None: """ Perform two checks on the annotation formatting: - all type names in the annotation are passed to the linker - the plain text version of the output matches the input @note: The annotation formatting is now handled by L{PyvalColorizer}. We use the function C{flatten_text} in order to back reproduce the original text annotations. """ expected_lookups = [found[1:-1] for found in re.findall('<[^>]*>', annotation)] expected_text = annotation.replace('<', '').replace('>', '') mod = fromText(f''' value: {expected_text} ''') obj = mod.contents['value'] parsed = epydoc2stan.get_parsed_type(obj) assert parsed is not None linker = RecordingAnnotationLinker() stan = parsed.to_stan(linker) assert linker.requests == expected_lookups html = flatten(stan) assert html.startswith('') assert html.endswith('') text = flatten_text(stan) assert text == expected_text def test_module_docformat(capsys: CapSys) -> None: """ Test if Module.docformat effectively override System.options.docformat """ system = model.System() system.options.docformat = 'plaintext' mod = fromText(''' """ Link to pydoctor: U{pydoctor }. """ __docformat__ = "epytext" ''', modname='test_epy', system=system) epytext_output = epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out assert not captured mod = fromText(''' """ Link to pydoctor: `pydoctor `_. """ __docformat__ = "restructuredtext en" ''', modname='test_rst', system=system) restructuredtext_output = epydoc2stan.format_docstring(mod) captured = capsys.readouterr().out assert not captured assert ('Link to pydoctor: pydoctor' in flatten(epytext_output)) assert ('Link to pydoctor: pydoctor' in flatten(restructuredtext_output)) def test_module_docformat_inheritence(capsys: CapSys) -> None: top_src = ''' def f(a: str, b: int): """ :param a: string :param b: integer """ pass ''' mod_src = ''' def f(a: str, b: int): """ @param a: string @param b: integer """ pass ''' pkg_src = ''' __docformat__ = 'epytext' ''' system = model.System() system.options.docformat = 'restructuredtext' top = fromText(top_src, modname='top', is_package=True, system=system) fromText(pkg_src, modname='pkg', parent_name='top', is_package=True, system=system) mod = fromText(mod_src, modname='top.pkg.mod', parent_name='top.pkg', system=system) captured = capsys.readouterr().out assert not captured assert ''.join(docstring2html(top.contents['f']).splitlines()) == ''.join(docstring2html(mod.contents['f']).splitlines()) def test_module_docformat_with_docstring_inheritence(capsys: CapSys) -> None: mod_src = ''' __docformat__ = "restructuredtext" class A: def f(self, a: str, b: int): """ .. note:: Note. """ ''' mod2_src = ''' from mod import A __docformat__ = "epytext" class B(A): def f(self, a: str, b: int): pass ''' system = model.System() system.options.docformat = 'epytext' mod = fromText(mod_src, modname='mod', system=system) mod2 = fromText(mod2_src, modname='mod2', system=system) captured = capsys.readouterr().out assert not captured B_f = mod2.resolveName('B.f') A_f = mod.resolveName('A.f') assert B_f assert A_f assert ''.join(docstring2html(B_f).splitlines()) == ''.join(docstring2html(A_f).splitlines()) def test_constant_values_rst(capsys: CapSys) -> None: """ Test epydoc2stan.format_constant_value(). """ mod1 = ''' def f(a, b): pass ''' mod2 = ''' from .mod1 import f CONST = (f,) ''' system = model.System() system.options.docformat = 'restructuredtext' fromText("", modname='pack', system=system, is_package=True) fromText(mod1, modname='mod1', system=system, parent_name='pack') mod = fromText(mod2, modname='mod2', system=system, parent_name='pack') captured = capsys.readouterr().out assert not captured expected = ('' '
    Value
    ' '
    ('
                    'f)
    ') attr = mod.contents['CONST'] assert isinstance(attr, model.Attribute) docstring2html(attr) assert ''.join(flatten(epydoc2stan.format_constant_value(attr)).splitlines()) == expected def test_warns_field(capsys: CapSys) -> None: """Test if the :warns: field is correctly recognized.""" mod = fromText(''' def func(): """ @warns: If there is an issue. """ pass ''') html = ''.join(docstring2html(mod.contents['func']).splitlines()) assert ('
    ' '' '
    Warns
    If there is an issue.
    ') == html captured = capsys.readouterr().out assert captured == '' mod = fromText(''' def func(): """ @warns RuntimeWarning: If there is an issue. """ pass ''') html = ''.join(docstring2html(mod.contents['func']).splitlines()) assert ('
    ' '' '' '
    Warns
    RuntimeWarningIf there is an issue.
    ') == html captured = capsys.readouterr().out assert captured == '' def test_yields_field(capsys: CapSys) -> None: """Test if the :warns: field is correctly recognized.""" mod = fromText(''' def func(): """ @yields: Each member of the sequence. @ytype: str """ pass ''') html = ''.join(docstring2html(mod.contents['func']).splitlines()) assert html == ('
    ' '' '' '
    Yields
    strEach member of the sequence.' '
    ') captured = capsys.readouterr().out assert captured == '' pydoctor-21.12.1/pydoctor/test/test_model.py000066400000000000000000000156211416703725300210710ustar00rootroot00000000000000""" Unit tests for model. """ from optparse import Values from pathlib import Path, PurePosixPath, PureWindowsPath from typing import cast import zlib import pytest from pydoctor import model from pydoctor.driver import parse_args from pydoctor.sphinx import CacheT from pydoctor.test.test_astbuilder import fromText class FakeOptions: """ A fake options object as if it came from that stupid optparse thing. """ sourcehref = None projectbasedirectory: Path docformat = 'epytext' class FakeDocumentable: """ A fake of pydoctor.model.Documentable that provides a system and sourceHref attribute. """ system: model.System sourceHref = None filepath: str @pytest.mark.parametrize('projectBaseDir', [ PurePosixPath("/foo/bar/ProjectName"), PureWindowsPath("C:\\foo\\bar\\ProjectName")] ) def test_setSourceHrefOption(projectBaseDir: Path) -> None: """ Test that the projectbasedirectory option sets the model.sourceHref properly. """ mod = cast(model.Module, FakeDocumentable()) options = FakeOptions() options.projectbasedirectory = projectBaseDir system = model.System() system.sourcebase = "http://example.org/trac/browser/trunk" system.options = cast(Values, options) mod.system = system system.setSourceHref(mod, projectBaseDir / "package" / "module.py") assert mod.sourceHref == "http://example.org/trac/browser/trunk/package/module.py" def test_initialization_default() -> None: """ When initialized without options, will use default options and default verbosity. """ sut = model.System() assert None is sut.options.projectname assert 3 == sut.options.verbosity def test_initialization_options() -> None: """ Can be initialized with options. """ options = cast(Values, object()) sut = model.System(options=options) assert options is sut.options def test_fetchIntersphinxInventories_empty() -> None: """ Convert option to empty dict. """ options, _ = parse_args([]) options.intersphinx = [] sut = model.System(options=options) sut.fetchIntersphinxInventories({}) # Use internal state since I don't know how else to # check for SphinxInventory state. assert {} == sut.intersphinx._links def test_fetchIntersphinxInventories_content() -> None: """ Download and parse intersphinx inventories for each configured intersphix. """ options, _ = parse_args([]) options.intersphinx = [ 'http://sphinx/objects.inv', 'file:///twisted/index.inv', ] url_content = { 'http://sphinx/objects.inv': zlib.compress( b'sphinx.module py:module -1 sp.html -'), 'file:///twisted/index.inv': zlib.compress( b'twisted.package py:module -1 tm.html -'), } sut = model.System(options=options) log = [] def log_msg(part: str, msg: str) -> None: log.append((part, msg)) sut.msg = log_msg # type: ignore[assignment] class Cache(CacheT): """Avoid touching the network.""" def get(self, url: str) -> bytes: return url_content[url] sut.fetchIntersphinxInventories(Cache()) assert [] == log assert ( 'http://sphinx/sp.html' == sut.intersphinx.getLink('sphinx.module') ) assert ( 'file:///twisted/tm.html' == sut.intersphinx.getLink('twisted.package') ) def test_docsources_class_attribute() -> None: src = ''' class Base: attr = False """documentation""" class Sub(Base): attr = True ''' mod = fromText(src) base_attr = mod.contents['Base'].contents['attr'] sub_attr = mod.contents['Sub'].contents['attr'] assert base_attr in list(sub_attr.docsources()) def test_constructor_params_empty() -> None: src = ''' class C: pass ''' mod = fromText(src) C = mod.contents['C'] assert isinstance(C, model.Class) assert C.constructor_params == {} def test_constructor_params_simple() -> None: src = ''' class C: def __init__(self, a: int, b: str): pass ''' mod = fromText(src) C = mod.contents['C'] assert isinstance(C, model.Class) assert C.constructor_params.keys() == {'self', 'a', 'b'} def test_constructor_params_inherited() -> None: src = ''' class A: def __init__(self, a: int, b: str): pass class B: def __init__(self): pass class C(A, B): pass ''' mod = fromText(src) C = mod.contents['C'] assert isinstance(C, model.Class) assert C.constructor_params.keys() == {'self', 'a', 'b'} def test_docstring_lineno() -> None: src = ''' def f(): """ This is a long docstring. Somewhat long, anyway. This should be enough. """ ''' mod = fromText(src) func = mod.contents['f'] assert func.linenumber == 2 assert func.docstring_lineno == 4 # first non-blank line class Dummy: def crash(self) -> None: """Mmm""" def test_introspection_python() -> None: """Find docstrings from this test using introspection on pure Python.""" system = model.System() system.introspectModule(Path(__file__), __name__, None) module = system.objForFullName(__name__) assert module is not None assert module.docstring == __doc__ func = module.contents['test_introspection_python'] assert func.docstring == "Find docstrings from this test using introspection on pure Python." method = system.objForFullName(__name__ + '.Dummy.crash') assert method is not None assert method.docstring == "Mmm" def test_introspection_extension() -> None: """Find docstrings from this test using introspection of an extension.""" try: import cython_test_exception_raiser.raiser except ImportError: pytest.skip("cython_test_exception_raiser not installed") system = model.System() package = system.introspectModule( Path(cython_test_exception_raiser.__file__), 'cython_test_exception_raiser', None) assert isinstance(package, model.Package) module = system.introspectModule( Path(cython_test_exception_raiser.raiser.__file__), 'raiser', package) assert not isinstance(module, model.Package) assert system.objForFullName('cython_test_exception_raiser') is package assert system.objForFullName('cython_test_exception_raiser.raiser') is module assert module.docstring is not None assert module.docstring.strip().split('\n')[0] == "A trivial extension that just raises an exception." cls = module.contents['RaiserException'] assert cls.docstring is not None assert cls.docstring.strip() == "A speficic exception only used to be identified in tests." func = module.contents['raiseException'] assert func.docstring is not None assert func.docstring.strip() == "Raise L{RaiserException}." pydoctor-21.12.1/pydoctor/test/test_napoleon_docstring.py000066400000000000000000002432461416703725300236660ustar00rootroot00000000000000 """ Forked from the tests for ``sphinx.ext.napoleon.docstring`` module. :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from typing import Type, Union, Any from unittest import TestCase from textwrap import dedent import functools from pydoctor.napoleon.docstring import (GoogleDocstring as _GoogleDocstring, NumpyDocstring as _NumpyDocstring, TokenType, TypeDocstring, is_type, is_google_typed_arg) import sphinx.ext.napoleon as sphinx_napoleon __docformat__ = "restructuredtext" def partialclass(cls: Type[Any], *args: Any, **kwds: Any) -> Type[Any]: # mypy gets errors: - Variable "cls" is not valid as a type # - Invalid base class "cls" class NewCls(cls): #type: ignore __init__ = functools.partialmethod(cls.__init__, *args, **kwds) #type: ignore __class__ = cls assert isinstance(NewCls, type) return NewCls sphinx_napoleon_config = sphinx_napoleon.Config( napoleon_use_admonition_for_examples=True, napoleon_use_admonition_for_notes=True, napoleon_use_admonition_for_references=True, napoleon_use_ivar=True, napoleon_use_param=True, napoleon_use_keyword=True, napoleon_use_rtype=True, napoleon_preprocess_types=True) # Adapters for upstream Sphinx napoleon classes SphinxGoogleDocstring = partialclass(sphinx_napoleon.docstring.GoogleDocstring, config=sphinx_napoleon_config, what='function') SphinxNumpyDocstring = partialclass(sphinx_napoleon.docstring.NumpyDocstring, config=sphinx_napoleon_config, what='function') # Create adapter classes that uses process_type_fields=True for the testing purposes GoogleDocstring = partialclass(_GoogleDocstring, process_type_fields=True) NumpyDocstring = partialclass(_NumpyDocstring, process_type_fields=True) class BaseDocstringTest(TestCase): maxDiff = None # mypy get error: # Variable "pydoctor.test.test_napoleon_docstring.SphinxGoogleDocstring" is not valid as a type def assertAlmostEqualSphinxDocstring(self, expected: str, docstring: str, type_: Type[Union[SphinxGoogleDocstring, SphinxNumpyDocstring]]) -> None: #type: ignore[valid-type] """ Check if the upstream version of the parser class (from `sphinx.ext.napoleon`) parses the docstring as expected. This is used as a supplementary manner of testing the parser behaviour. Some approximation are applied with `re.sub` to the ``expected`` string and the reST docstring generated by `sphinx.ext.napoleon` classes. This is done in order to use the expected reST strings designed for `pydoctor.napoleon` and apply them to `sphinx.ext.napoleon` in the same test. Tho, not all tests cases can be adapted to pass this check. :param expected: The exact expected reST docstring generated by `pydoctor.napoleon` classes (trailling whitespaces ignored) """ expected_sphinx_output = re.sub( r"(`|\\\s|\\|:mod:|:func:|:class:|:obj:)", "", expected) # mypy error: Cannot instantiate type "Type[SphinxGoogleDocstring?] sphinx_docstring_output = re.sub( r"(`|\\|:mod:|:func:|:class:|:obj:|\s)", "", str(type_(docstring)).replace( #type: ignore[misc] ":kwtype", ":type").replace(":vartype", ":type").replace(" -- ", " - ").replace(':rtype:', ':returntype:').rstrip()) self.assertEqual(expected_sphinx_output.rstrip(), sphinx_docstring_output) class TypeDocstringTest(BaseDocstringTest): def test_is_type(self): self.assertFalse(is_type("Random words are not a type spec")) self.assertFalse(is_type("List of string or any kind fo sequences of strings")) self.assertTrue(is_type("Sequence(str), optional")) self.assertTrue(is_type("Sequence(str) or str")) self.assertTrue(is_type("List[str] or list(bytes), optional")) self.assertTrue(is_type('{"F", "C", "N"}, optional')) self.assertTrue(is_type("list of int or float or None, default: None")) self.assertTrue(is_type("`complicated string` or `strIO `")) def test_is_google_typed_arg(self): self.assertFalse(is_google_typed_arg("Random words are not a type spec")) self.assertFalse(is_google_typed_arg("List of string or any kind fo sequences of strings")) self.assertTrue(is_google_typed_arg("Sequence(str), optional")) self.assertTrue(is_google_typed_arg("Sequence(str) or str")) self.assertTrue(is_google_typed_arg("List[str] or list(bytes), optional")) self.assertTrue(is_google_typed_arg('{"F", "C", "N"}, optional')) self.assertTrue(is_google_typed_arg("list of int or float or None, default: None")) self.assertTrue(is_google_typed_arg("`complicated string` or `strIO `")) # Google-style specific self.assertFalse(is_google_typed_arg("foo (Random words are not a type spec)")) self.assertFalse(is_google_typed_arg("foo (List of string or any kind fo sequences of strings)")) self.assertTrue(is_google_typed_arg("foo (Sequence(str), optional)")) self.assertTrue(is_google_typed_arg("foo (Sequence[str] or str)")) self.assertTrue(is_google_typed_arg("foo (List[str] or list(bytes), optional)")) self.assertTrue(is_google_typed_arg('foo ({"F", "C", "N"}, optional)')) self.assertTrue(is_google_typed_arg("foo (list of int or float or None, default: None)")) self.assertTrue(is_google_typed_arg("foo (`complicated string` or `strIO `)")) self.assertTrue(is_google_typed_arg("Random words are not a type spec (List[str] or list(bytes), optional)")) self.assertTrue(is_google_typed_arg("Random words are not a type spec (list of int or float or None, default: None)")) self.assertTrue(is_google_typed_arg("Random words are not a type spec (`complicated string` or `strIO `, optional)")) def test_token_type(self): tokens = ( ("1", TokenType.LITERAL), ("-4.6", TokenType.LITERAL), ("2j", TokenType.LITERAL), ("'string'", TokenType.LITERAL), ('"another_string"', TokenType.LITERAL), ("{1, 2}", TokenType.LITERAL), ("{'va{ue', 'set'}", TokenType.LITERAL), ("optional", TokenType.CONTROL), ("default", TokenType.CONTROL), (", ", TokenType.DELIMITER), (" of ", TokenType.DELIMITER), (" or ", TokenType.DELIMITER), (": ", TokenType.DELIMITER), ("]", TokenType.DELIMITER), ("[", TokenType.DELIMITER), (")", TokenType.DELIMITER), ("(", TokenType.DELIMITER), ("True", TokenType.OBJ), ("None", TokenType.OBJ), ("name", TokenType.OBJ), (":py:class:`Enum`", TokenType.REFERENCE), ("`a complicated string`", TokenType.REFERENCE), ("just a string", TokenType.UNKNOWN), (len("not a string"), TokenType.ANY), ) type_spec = TypeDocstring('', 0) for token, _type in tokens: actual = type_spec._token_type(token) self.assertEqual(_type, actual) def test_tokenize_type_spec(self): specs = ( "str", "defaultdict", "int, float, or complex", "int or float or None, optional", '{"F", "C", "N"}', "{'F', 'C', 'N'}, default: 'F'", "{'F', 'C', 'N or C'}, default 'F'", "str, default: 'F or C'", "int, default: None", "int, default None", "int, default :obj:`None`", '"ma{icious"', r"'with \'quotes\''", ) tokens = ( ["str"], ["defaultdict"], ["int", ", ", "float", ", or ", "complex"], ["int", " or ", "float", " or ", "None", ", ", "optional"], ["{", '"F"', ", ", '"C"', ", ", '"N"', "}"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "'F'"], ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"], ["str", ", ", "default", ": ", "'F or C'"], ["int", ", ", "default", ": ", "None"], ["int", ", ", "default", " ", "None"], ["int", ", ", "default", " ", ":obj:`None`"], ['"ma{icious"'], [r"'with \'quotes\''"], ) for spec, expected in zip(specs, tokens): actual = TypeDocstring._tokenize_type_spec(spec) self.assertEqual(expected, actual) def test_recombine_set_tokens(self): tokens = ( ["{", "1", ", ", "2", "}"], ["{", '"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", ": ", "None"], ["{", "'F'", ", ", "'C'", ", ", "'N'", "}", ", ", "default", " ", "None"], ) combined_tokens = ( ["{1, 2}"], ['{"F", "C", "N"}', ", ", "optional"], ["{'F', 'C', 'N'}", ", ", "default", ": ", "None"], ["{'F', 'C', 'N'}", ", ", "default", " ", "None"], ) for tokens_, expected in zip(tokens, combined_tokens): actual = TypeDocstring._recombine_set_tokens(tokens_) self.assertEqual(expected, actual) def test_recombine_set_tokens_invalid(self): tokens = ( ["{", "1", ", ", "2"], ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{", "1", ", ", "2", ", ", "default", ": ", "None"], ) combined_tokens = ( ["{1, 2"], ['"F"', ", ", '"C"', ", ", '"N"', "}", ", ", "optional"], ["{1, 2", ", ", "default", ": ", "None"], ) for tokens_, expected in zip(tokens, combined_tokens): actual = TypeDocstring._recombine_set_tokens(tokens_) self.assertEqual(expected, actual) def test_convert_numpy_type_spec(self): specs = ( "", "optional", "str, optional", "int or float or None, default: None", "int, default None", '{"F", "C", "N"}', "{'F', 'C', 'N'}, default: 'N'", "{'F', 'C', 'N'}, default 'N'", "DataFrame, optional", "default[str]", # corner cases... "optional[str]", ",[str]", ", [str]", " of [str]", " or [str]", ": [str]", " and [str]", "'hello'[str]", '"hello"[str]', "`hello`[str]", "`hello `_[str]", "**hello**[str]", ) converted = ( "", "*optional*", "`str`, *optional*", "`int` or `float` or `None`, *default*: `None`", "`int`, *default* `None`", '``{"F", "C", "N"}``', "``{'F', 'C', 'N'}``, *default*: ``'N'``", "``{'F', 'C', 'N'}``, *default* ``'N'``", "`DataFrame`, *optional*", r"*default*\ [`str`]", r"*optional*\ [`str`]", ", [`str`]", ", [`str`]", " of [`str`]", " or [`str`]", ": [`str`]", " and [`str`]", r"``'hello'``\ [`str`]", r'``"hello"``\ [`str`]', r"`hello`\ [`str`]", r"`hello `_\ [`str`]", r"**hello**\ [`str`]", ) for spec, expected in zip(specs, converted): actual = str(TypeDocstring(spec)) self.assertEqual(expected, actual) def test_token_type_invalid(self): tokens = ( "{1, 2", "}", "'abc", "def'", '"ghi', 'jkl"', ) errors = ( r"invalid value set \(missing closing brace\):", r"invalid value set \(missing opening brace\):", r"malformed string literal \(missing closing quote\):", r"malformed string literal \(missing opening quote\):", r"malformed string literal \(missing closing quote\):", r"malformed string literal \(missing opening quote\):", ) for token, error in zip(tokens, errors): type_spec = TypeDocstring('') type_spec._token_type(token) match_re = re.compile(error) assert len(type_spec.warnings) == 1, type_spec.warnings assert match_re.match(str(type_spec.warnings.pop())) def test_unbalanced_parenthesis(self): strings = ( "list[union[str, bytes]", "list(union[str, bytes)", "list[union(str, bytes]", ) errors = ( r"unbalanced square braces", r"unbalanced square braces", r"unbalanced parenthesis", ) for string, error in zip(strings, errors): type_spec = TypeDocstring(string) match_re = re.compile(error) assert len(type_spec.warnings) == 1, type_spec.warnings assert match_re.match(str(type_spec.warnings.pop())) class InlineAttributeTest(BaseDocstringTest): def test_class_data_member(self): docstring = """\ data member description: - a: b """ actual = str(GoogleDocstring(docstring, is_attribute=True)) expected = """\ data member description: - a: b""" self.assertEqual(expected.rstrip(), actual) def test_class_data_member_inline(self): docstring = """b: data member description with :ref:`reference`""" actual = str(GoogleDocstring(docstring, is_attribute=True)) expected = ("""\ data member description with :ref:`reference` :type: `b`""") self.assertEqual(expected.rstrip(), actual) def test_class_data_member_inline_no_type(self): docstring = """data with ``a : in code`` and :ref:`reference` and no type""" actual = str(GoogleDocstring(docstring, is_attribute=True)) expected = """data with ``a : in code`` and :ref:`reference` and no type""" self.assertEqual(expected.rstrip(), actual) def test_class_data_member_inline_ref_in_type(self): docstring = """:class:`int`: data member description""" actual = str(GoogleDocstring(docstring, is_attribute=True)) expected = ("""\ data member description :type: :class:`int`""") self.assertEqual(expected.rstrip(), actual) class GoogleDocstringTest(BaseDocstringTest): docstrings = [( """Single line summary""", """Single line summary""" ), ( """ Single line summary Extended description """, """ Single line summary Extended description """ ), ( """ Single line summary Args: arg1(str):Extended description of arg1 """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` """ ), ( """ Single line summary Args: arg1(str):Extended description of arg1 arg2 ( int ) : Extended description of arg2 Keyword Args: kwarg1(str):Extended description of kwarg1 kwarg2 ( int ) : Extended description of kwarg2""", """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param arg2: Extended description of arg2 :type arg2: `int` :keyword kwarg1: Extended description of kwarg1 :type kwarg1: `str` :keyword kwarg2: Extended description of kwarg2 :type kwarg2: `int` """ ), ( """ Single line summary Arguments: arg1(str):Extended description of arg1 arg2 ( int ) : Extended description of arg2 Keyword Arguments: kwarg1(str):Extended description of kwarg1 kwarg2 ( int ) : Extended description of kwarg2""", """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param arg2: Extended description of arg2 :type arg2: `int` :keyword kwarg1: Extended description of kwarg1 :type kwarg1: `str` :keyword kwarg2: Extended description of kwarg2 :type kwarg2: `int` """ ), ( """ Single line summary Return: str:Extended description of return value """, """ Single line summary :returns: Extended description of return value :returntype: `str` """ ), ( """ Single line summary Returns: str:Extended description of return value """, """ Single line summary :returns: Extended description of return value :returntype: `str` """ ), ( """ Single line summary Returns: Extended description of return value """, """ Single line summary :returns: Extended description of return value """ ), ( """ Single line summary Args: arg1(str):Extended description of arg1 *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. """, r""" Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param \*args: Variable length argument list. :param \*\*kwargs: Arbitrary keyword arguments. """ ), ( """ Single line summary Args: arg1 (list(int)): Description arg2 (list[int]): Description arg3 (dict(str, int)): Description arg4 (dict[str, int]): Description """, r""" Single line summary :param arg1: Description :type arg1: `list`\ (`int`) :param arg2: Description :type arg2: `list`\ [`int`] :param arg3: Description :type arg3: `dict`\ (`str`, `int`) :param arg4: Description :type arg4: `dict`\ [`str`, `int`] """ ), ( """ Single line summary Receive: arg1 (list(int)): Description arg2 (list[int]): Description """, r""" Single line summary :param arg1: Description :type arg1: `list`\ (`int`) :param arg2: Description :type arg2: `list`\ [`int`] """ ), ( """ Single line summary Receives: arg1 (list(int)): Description arg2 (list[int]): Description """, r""" Single line summary :param arg1: Description :type arg1: `list`\ (`int`) :param arg2: Description :type arg2: `list`\ [`int`] """ ), ( """ Single line summary Yield: str:Extended description of yielded value """, """ Single line summary :yields: Extended description of yielded value :yieldtype: `str` """ ), ( """ Single line summary Yields: Extended description of yielded value """, """ Single line summary :yields: Extended description of yielded value """ ), ( """ Single line summary Args: arg1 (list(int)): desc arg1. arg2 (list[int]): desc arg2. """, r""" Single line summary :param arg1: desc arg1. :type arg1: `list`\ (`int`) :param arg2: desc arg2. :type arg2: `list`\ [`int`] """ ),( """ Single line summary Args: my first argument (list(int)): desc arg1. my second argument (list[int]): desc arg2. """, r""" Single line summary :param my first argument: desc arg1. :type my first argument: `list`\ (`int`) :param my second argument: desc arg2. :type my second argument: `list`\ [`int`] """ ), (""" Single line summary Usage: import stuff stuff.do() """, # nothing special about the headings that are not recognized as a section """ Single line summary Usage: import stuff stuff.do()"""),( """ Single line summary Todo: stuff """, """ Single line summary .. admonition:: Todo stuff """ ),( """ Single line summary Todo: """, """ Single line summary Todo: """),(""" Single line summary References: stuff """, """ Single line summary .. admonition:: References stuff """),(""" Single line summary See also: my thing """, """ Single line summary .. seealso:: my thing """)] def test_docstrings(self): for docstring, expected in self.docstrings: actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) if not 'Yield' in docstring and not 'Todo' in docstring: # The yield and todo sections are very different from sphinx's. self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_returns_section_type_only(self): docstring=""" Single line summary Returns: str: """ expected=""" Single line summary :returns: str """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.strip(), actual.strip()) docstring=""" Single line summary Returns: str """ expected=""" Single line summary :returns: str """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.strip(), actual.strip()) def test_sphinx_admonitions(self): admonition_map = { 'Attention': 'attention', 'Caution': 'caution', 'Danger': 'danger', 'Error': 'error', 'Hint': 'hint', 'Important': 'important', 'Note': 'note', 'Tip': 'tip', 'Warning': 'warning', 'Warnings': 'warning', } for section, admonition in admonition_map.items(): # Multiline actual = str(GoogleDocstring(("{}:\n" " this is the first line\n" "\n" " and this is the second line\n" ).format(section))) expect = (".. {}::\n" "\n" " this is the first line\n" " \n" " and this is the second line\n" ).format(admonition) self.assertEqual(expect.rstrip(), actual) # Single line actual = str(GoogleDocstring(("{}:\n" " this is a single line\n" ).format(section))) expect = (".. {}:: this is a single line\n" ).format(admonition) self.assertEqual(expect.rstrip(), actual) def test_parameters_with_class_reference(self): # mot sure why this test include back slash in the type spec... # users should not write type like that in pydoctor anyway. docstring = r"""Construct a new XBlock. This class should only be used by runtimes. Arguments: runtime (:class:`~typing.Dict`[:class:`int`, :class:`str`]): Use it to access the environment. It is available in XBlock code as ``self.runtime``. field_data (:class:`FieldData`): Interface used by the XBlock fields to access their data from wherever it is persisted. scope_ids (:class:`ScopeIds`): Identifiers needed to resolve scopes. """ actual = str(GoogleDocstring(docstring)) expected = r"""Construct a new XBlock. This class should only be used by runtimes. :param runtime: Use it to access the environment. It is available in XBlock code as ``self.runtime``. :type runtime: :class:`~typing.Dict`\ [:class:`int`, :class:`str`] :param field_data: Interface used by the XBlock fields to access their data from wherever it is persisted. :type field_data: :class:`FieldData` :param scope_ids: Identifiers needed to resolve scopes. :type scope_ids: :class:`ScopeIds` """ self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_attributes_with_class_reference(self): docstring = """\ Attributes: in_attr(:class:`numpy.ndarray`): super-dooper attribute """ actual = str(GoogleDocstring(docstring)) expected = """\ :ivar in_attr: super-dooper attribute :type in_attr: :class:`numpy.ndarray` """ self.assertEqual(expected.rstrip(), actual) docstring = """\ Attributes: in_attr(numpy.ndarray): super-dooper attribute """ actual = str(GoogleDocstring(docstring)) expected = """\ :ivar in_attr: super-dooper attribute :type in_attr: `numpy.ndarray` """ self.assertEqual(expected.rstrip(), actual) def test_code_block_in_returns_section(self): docstring = """ Returns: foobar: foo:: codecode codecode """ expected = """ :returns: foo:: codecode codecode :returntype: `foobar` """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_colon_in_return_type(self): docstring = """Example property. Returns: :py:class:`~.module.submodule.SomeClass`: an example instance if available, None if not available. """ expected = """Example property. :returns: an example instance if available, None if not available. :returntype: :py:class:`~.module.submodule.SomeClass` """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_xrefs_in_return_type(self): docstring = """Example Function Returns: :class:`numpy.ndarray`: A :math:`n \\times 2` array containing a bunch of math items """ expected = """Example Function :returns: A :math:`n \\times 2` array containing a bunch of math items :returntype: :class:`numpy.ndarray` """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_raises_types(self): docstrings = [(""" Example Function Raises: RuntimeError: A setting wasn't specified, or was invalid. ValueError: Something something value error. :py:class:`AttributeError` errors for missing attributes. ~InvalidDimensionsError If the dimensions couldn't be parsed. `InvalidArgumentsError` If the arguments are invalid. :exc:`~ValueError` If the arguments are wrong. """, """ Example Function :raises RuntimeError: A setting wasn't specified, or was invalid. :raises ValueError: Something something value error. :raises AttributeError: errors for missing attributes. :raises ~InvalidDimensionsError: If the dimensions couldn't be parsed. :raises InvalidArgumentsError: If the arguments are invalid. :raises ~ValueError: If the arguments are wrong. """), ################################ (""" Example Function Raises: InvalidDimensionsError """, """ Example Function :raises InvalidDimensionsError: """), ################################ (""" Example Function Raises: Invalid Dimensions Error """, """ Example Function :raises Invalid Dimensions Error: """), ################################ (""" Example Function Raises: Invalid Dimensions Error: With description """, """ Example Function :raises Invalid Dimensions Error: With description """), ################################ (""" Example Function Raises: InvalidDimensionsError: If the dimensions couldn't be parsed. """, """ Example Function :raises InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: Invalid Dimensions Error: If the dimensions couldn't be parsed. """, """ Example Function :raises Invalid Dimensions Error: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: If the dimensions couldn't be parsed. """, """ Example Function :raises If the dimensions couldn't be parsed.: """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError` """, """ Example Function :raises exc.InvalidDimensionsError: """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed. :class:`exc.InvalidArgumentsError`: If the arguments are invalid. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. :raises exc.InvalidArgumentsError: If the arguments are invalid. """), ################################ (""" Example Function Raises: :class:`exc.InvalidDimensionsError` :class:`exc.InvalidArgumentsError` """, """ Example Function :raises exc.InvalidDimensionsError: :raises exc.InvalidArgumentsError: """)] for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_kwargs_in_arguments(self): docstring = """Allows to create attributes binded to this device. Some other paragraph. Code sample for usage:: dev.bind(loopback=Loopback) dev.loopback.configure() Arguments: **kwargs: name/class pairs that will create resource-managers bound as instance attributes to this instance. See code example above. """ expected = """Allows to create attributes binded to this device. Some other paragraph. Code sample for usage:: dev.bind(loopback=Loopback) dev.loopback.configure() :param \\*\\*kwargs: name/class pairs that will create resource-managers bound as instance attributes to this instance. See code example above. """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_section_header_formatting(self): docstrings = [(""" Summary line Example: Multiline reStructuredText literal code block """, """ Summary line .. admonition:: Example Multiline reStructuredText literal code block """), ################################ (""" Summary line Example:: Multiline reStructuredText literal code block """, """ Summary line Example:: Multiline reStructuredText literal code block """), ################################ (""" Summary line :Example: Multiline reStructuredText literal code block """, """ Summary line :Example: Multiline reStructuredText literal code block """)] for docstring, expected in docstrings: actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_list_in_parameter_description(self): docstring = """One line summary. Parameters: no_list (int): one_bullet_empty (int): * one_bullet_single_line (int): - first line one_bullet_two_lines (int): + first line continued two_bullets_single_line (int): - first line - second line two_bullets_two_lines (int): * first line continued * second line continued one_enumeration_single_line (int): 1. first line one_enumeration_two_lines (int): 1) first line continued two_enumerations_one_line (int): (iii) first line (iv) second line two_enumerations_two_lines (int): a. first line continued b. second line continued one_definition_one_line (int): item 1 first line one_definition_two_lines (int): item 1 first line continued two_definitions_one_line (int): item 1 first line item 2 second line two_definitions_two_lines (int): item 1 first line continued item 2 second line continued one_definition_blank_line (int): item 1 first line extra first line two_definitions_blank_lines (int): item 1 first line extra first line item 2 second line extra second line definition_after_inline_text (int): text line item 1 first line definition_after_normal_text (int): text line item 1 first line """ expected = """One line summary. :param no_list: :type no_list: `int` :param one_bullet_empty: * :type one_bullet_empty: `int` :param one_bullet_single_line: - first line :type one_bullet_single_line: `int` :param one_bullet_two_lines: + first line continued :type one_bullet_two_lines: `int` :param two_bullets_single_line: - first line - second line :type two_bullets_single_line: `int` :param two_bullets_two_lines: * first line continued * second line continued :type two_bullets_two_lines: `int` :param one_enumeration_single_line: 1. first line :type one_enumeration_single_line: `int` :param one_enumeration_two_lines: 1) first line continued :type one_enumeration_two_lines: `int` :param two_enumerations_one_line: (iii) first line (iv) second line :type two_enumerations_one_line: `int` :param two_enumerations_two_lines: a. first line continued b. second line continued :type two_enumerations_two_lines: `int` :param one_definition_one_line: item 1 first line :type one_definition_one_line: `int` :param one_definition_two_lines: item 1 first line continued :type one_definition_two_lines: `int` :param two_definitions_one_line: item 1 first line item 2 second line :type two_definitions_one_line: `int` :param two_definitions_two_lines: item 1 first line continued item 2 second line continued :type two_definitions_two_lines: `int` :param one_definition_blank_line: item 1 first line extra first line :type one_definition_blank_line: `int` :param two_definitions_blank_lines: item 1 first line extra first line item 2 second line extra second line :type two_definitions_blank_lines: `int` :param definition_after_inline_text: text line item 1 first line :type definition_after_inline_text: `int` :param definition_after_normal_text: text line item 1 first line :type definition_after_normal_text: `int` """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_attr_with_method(self): docstring = """ Attributes: arg : description Methods: func(abc, def): description """ expected = r""" :ivar arg: description .. admonition:: Methods `func`\ (`abc`, `def`) description """ # NOQA actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_return_formatting_indentation(self): docstring = """ Returns: bool: True if successful, False otherwise. The return type is optional and may be specified at the beginning of the ``Returns`` section followed by a colon. The ``Returns`` section may span multiple lines and paragraphs. Following lines should be indented to match the first line. The ``Returns`` section supports any reStructuredText formatting, including literal blocks:: { 'param1': param1, 'param2': param2 } """ expected = """ :returns: True if successful, False otherwise. The return type is optional and may be specified at the beginning of the ``Returns`` section followed by a colon. The ``Returns`` section may span multiple lines and paragraphs. Following lines should be indented to match the first line. The ``Returns`` section supports any reStructuredText formatting, including literal blocks:: { 'param1': param1, 'param2': param2 } :returntype: `bool` """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) def test_column_summary_lines_sphinx_issue_4016(self): # test https://github.com/sphinx-doc/sphinx/issues/4016 docstring = """Get time formated as ``HH:MM:SS``.""" expected = """Get time formated as ``HH:MM:SS``.""" actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxGoogleDocstring) actual = str(GoogleDocstring(docstring, is_attribute=True)) self.assertEqual(expected.rstrip(), actual) docstring2 = """Put *key* and *value* into a dictionary. Returns: A dictionary ``{key: value}`` """ expected2 = """Put *key* and *value* into a dictionary. :returns: A dictionary ``{key: value}`` """ actual = str(GoogleDocstring(docstring2)) self.assertEqual(expected2.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected2, docstring2, type_=SphinxGoogleDocstring) actual = str(GoogleDocstring(docstring2, is_attribute=True)) self.assertEqual(expected2.rstrip(), actual) def test_multiline_types(self): # Real life example from # https://googleapis.github.io/google-api-python-client/docs/epy/index.html docstring = """ Scopes the credentials if necessary. Args: credentials (Union[ google.auth.credentials.Credentials, oauth2client.client.Credentials]): The credentials to scope. scopes (Sequence[str]): The list of scopes. errors (Sequence[Union[ParseError, ParseWarning, ParseInfo, ...]]): The list of errors, warnings or other informations. Returns: Union[google.auth.credentials.Credentials, oauth2client.client.Credentials]: The scoped credentials. """ expected = r""" Scopes the credentials if necessary. :param credentials: The credentials to scope. :type credentials: `Union`\ [`google.auth.credentials.Credentials`, `oauth2client.client.Credentials`] :param scopes: The list of scopes. :type scopes: `Sequence`\ [`str`] :param errors: The list of errors, warnings or other informations. :type errors: `Sequence`\ [`Union`\ [`ParseError`, `ParseWarning`, `ParseInfo`, `...`]] :returns: The scoped credentials. :returntype: `Union`\ [`google.auth.credentials.Credentials`, `oauth2client.client.Credentials`] """ actual = str(GoogleDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_multiline_types_invalid_log_warning(self): # test robustness with invalid arg syntax + log warning docstring = """ Description... Args: docformat Can be one of: - "numpy" - "google" scopes (Sequence[str]): The list of scopes. """ expected = r""" Description... :param docformat: Can be one of: - "numpy" - "google" :param scopes: The list of scopes. :type scopes: `Sequence`\ [`str`] """ doc = GoogleDocstring(docstring) actual = str(doc) self.assertEqual(expected.rstrip(), actual) self.assertEqual(1, len(doc.warnings)) warning = doc.warnings.pop() self.assertIn("invalid type: 'docformatCan be one of'", warning[0]) self.assertEqual(5, warning[1]) docstring = """ Description... Args: docformat (Can be "numpy" or "google"): Desc scopes (Sequence[str]): The list of scopes. """ expected = r""" Description... :param docformat (Can be "numpy": or "google"): Desc :param scopes: The list of scopes. :type scopes: `Sequence`\ [`str`] """ doc = GoogleDocstring(docstring) actual = str(doc) self.assertEqual(expected.rstrip(), actual) self.assertEqual(1, len(doc.warnings)) warning = doc.warnings.pop() self.assertIn("invalid type: 'docformat (Can be \"numpy\"or \"google\")'", warning[0]) self.assertEqual(5, warning[1]) class NumpyDocstringTest(BaseDocstringTest): docstrings = [( """Single line summary""", """Single line summary""" ), ( """ Single line summary Extended description """, """ Single line summary Extended description """ ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` """ ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 Keyword Arguments ----------------- kwarg1:str Extended description of kwarg1 kwarg2 : int Extended description of kwarg2 """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param arg2: Extended description of arg2 :type arg2: `int` :keyword kwarg1: Extended description of kwarg1 :type kwarg1: `str` :keyword kwarg2: Extended description of kwarg2 :type kwarg2: `int` """ ), ( """ Single line summary Return ------ str Extended description of return value """, """ Single line summary :returns: Extended description of return value :returntype: `str` """ ),( """ Single line summary Return ------ the string of your life: str """, """ Single line summary :returns: **the string of your life** :returntype: `str` """ ),( """ Single line summary Return ------ """, """ Single line summary """ ), ( """ Single line summary Returns ------- str Extended description of return value """, """ Single line summary :returns: Extended description of return value :returntype: `str` """ ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 *args: Variable length argument list. **kwargs: Arbitrary keyword arguments. """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param \\*args: Variable length argument list. :param \\*\\*kwargs: Arbitrary keyword arguments. """ ), ( """ Single line summary Parameters ---------- arg1:str Extended description of arg1 *args, **kwargs: Variable length argument list and arbitrary keyword arguments. """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param \\*args: Variable length argument list and arbitrary keyword arguments. :param \\*\\*kwargs: Variable length argument list and arbitrary keyword arguments. """ ), ( """ Single line summary Receive ------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param arg2: Extended description of arg2 :type arg2: `int` """ ), ( """ Single line summary Receives -------- arg1:str Extended description of arg1 arg2 : int Extended description of arg2 """, """ Single line summary :param arg1: Extended description of arg1 :type arg1: `str` :param arg2: Extended description of arg2 :type arg2: `int` """ ), ( """ Single line summary Yield ----- str Extended description of yielded value """, """ Single line summary :yields: Extended description of yielded value :yieldtype: `str` """ ), ( """ Single line summary Yields ------ str Extended description of yielded value """, """ Single line summary :yields: Extended description of yielded value :yieldtype: `str` """ ), (""" Derived from the NumpyDoc implementation of _parse_see_also:: See Also -------- func_name : Descriptive text continued text another_func_name : Descriptive text func_name1, func_name2, :meth:`func_name`, func_name3 """, """ Derived from the NumpyDoc implementation of _parse_see_also:: See Also -------- func_name : Descriptive text continued text another_func_name : Descriptive text func_name1, func_name2, :meth:`func_name`, func_name3 """),( """ Single line summary Args ---- my first argument: list(int) desc arg1. my second argument: list[int] desc arg2. """, r""" Single line summary :param my first argument: desc arg1. :type my first argument: `list`\ (`int`) :param my second argument: desc arg2. :type my second argument: `list`\ [`int`] """),(""" Single line summary Usage ----- import stuff stuff.do() """, """ Single line summary Usage ----- import stuff stuff.do() """), (""" Single line summary Generic admonition ------------------ """, # nothing special about the headings that are not recognized as a section """ Single line summary Generic admonition ------------------ """),( """ Single line summary Todo ---- stuff """, """ Single line summary .. admonition:: Todo stuff """),( """ Single line summary Todo ---- """, """ Single line summary .. admonition:: Todo """) ,( """ Single line summary References ---------- stuff """, """ Single line summary .. admonition:: References stuff """) ] def test_docstrings(self): for docstring, expected in self.docstrings: actual = str(NumpyDocstring(dedent(docstring))) expected = dedent(expected) self.assertEqual(actual, expected.rstrip()) if not 'Yield' in docstring and not 'Todo' in docstring: # The yield and todo sections are very different from sphinx's. self.assertAlmostEqualSphinxDocstring(expected, dedent(docstring), type_=SphinxNumpyDocstring) def test_sphinx_admonitions(self): admonition_map = { 'Attention': 'attention', 'Caution': 'caution', 'Danger': 'danger', 'Error': 'error', 'Hint': 'hint', 'Important': 'important', 'Note': 'note', 'Tip': 'tip', 'Warning': 'warning', 'Warnings': 'warning', } for section, admonition in admonition_map.items(): # Multiline actual = str(NumpyDocstring(("{}\n" "{}\n" " this is the first line\n" "\n" " and this is the second line\n" ).format(section, '-' * len(section)))) expected = (".. {}::\n" "\n" " this is the first line\n" " \n" " and this is the second line\n" ).format(admonition) self.assertEqual(expected.rstrip(), actual) # Single line actual = str(NumpyDocstring(("{}\n" "{}\n" " this is a single line\n" ).format(section, '-' * len(section)))) expected = (".. {}:: this is a single line\n" ).format(admonition) self.assertEqual(expected.rstrip(), actual) def test_parameters_with_class_reference(self): docstring = """\ Parameters ---------- param1 : :class:`MyClass ` instance """ actual = str(NumpyDocstring(docstring)) expected = """\ :param param1: :type param1: :class:`MyClass ` instance """ self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_multiple_parameters(self): docstring = """\ Parameters ---------- x1, x2 : array_like Input arrays, description of ``x1``, ``x2``. """ actual = str(NumpyDocstring(dedent(docstring))) expected = """\ :param x1: Input arrays, description of ``x1``, ``x2``. :type x1: `array_like` :param x2: Input arrays, description of ``x1``, ``x2``. :type x2: `array_like` """ self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_parameters_without_class_reference(self): docstring = """\ Parameters ---------- param1 : MyClass instance """ actual = str(NumpyDocstring(dedent(docstring))) expected = """\ :param param1: :type param1: MyClass instance """ self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_parameter_types(self): docstring = dedent("""\ Parameters ---------- param1 : DataFrame the data to work on param2 : int or float or None, optional a parameter with different types param3 : dict-like, optional a optional mapping param4 : int or float or None, optional a optional parameter with different types param5 : {"F", "C", "N"}, optional a optional parameter with fixed values param6 : int, default None different default format param7 : mapping of hashable to str, optional a optional mapping param8 : ... or Ellipsis ellipsis """) expected = dedent("""\ :param param1: the data to work on :type param1: `DataFrame` :param param2: a parameter with different types :type param2: `int` or `float` or `None`, *optional* :param param3: a optional mapping :type param3: `dict-like`, *optional* :param param4: a optional parameter with different types :type param4: `int` or `float` or `None`, *optional* :param param5: a optional parameter with fixed values :type param5: ``{"F", "C", "N"}``, *optional* :param param6: different default format :type param6: `int`, *default* `None` :param param7: a optional mapping :type param7: `mapping` of `hashable` to `str`, *optional* :param param8: ellipsis :type param8: `...` or `Ellipsis` """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_see_also_refs_invalid(self): docstring = """\ See Also -------- $funcs 123 """ expected = """\ .. seealso:: $funcs 123 """ self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring))) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_see_also_refs(self): docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ docstring2 = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, :func:`funcs` otherfunc : relationship """ expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: `some`, `other`, `funcs` `otherfunc` relationship """ self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring))) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring2))) self.assertAlmostEqualSphinxDocstring(expected, docstring2, type_=SphinxNumpyDocstring) docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, funcs otherfunc : relationship """ expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: `some`, `other`, `funcs` `otherfunc` relationship """ self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring))) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) docstring = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) See Also -------- some, other, :func:`funcs` otherfunc : relationship """ expected = """\ numpy.multivariate_normal(mean, cov, shape=None, spam=None) .. seealso:: `some`, `other`, `funcs` `otherfunc` relationship """ self.assertEqual(expected.rstrip(), str(NumpyDocstring(docstring))) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_colon_in_return_type(self): docstring = """ Summary Returns ------- :py:class:`~my_mod.my_class` an instance of :py:class:`~my_mod.my_class` """ expected = """ Summary :returns: an instance of :py:class:`~my_mod.my_class` :returntype: :py:class:`~my_mod.my_class` """ actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_underscore_in_attribute(self): docstring = """ Attributes ---------- arg_ : type some description """ expected = """ :ivar arg_: some description :type arg_: `type` """ actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_return_types(self): docstring = dedent(""" Returns ------- pandas.DataFrame a dataframe """) expected = dedent(""" :returns: a dataframe :returntype: `pandas.DataFrame` """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_yield_types(self): docstring = dedent(""" Example Function Yields ------ scalar or array-like The result of the computation """) expected = dedent(""" Example Function :yields: The result of the computation :yieldtype: `scalar` or `array-like` """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_raises_types(self): docstrings = [(""" Example Function Raises ------ RuntimeError A setting wasn't specified, or was invalid. ValueError Something something value error. """, """ Example Function :raises RuntimeError: A setting wasn't specified, or was invalid. :raises ValueError: Something something value error. """), ################################ (""" Example Function Raises ------ InvalidDimensionsError """, """ Example Function :raises InvalidDimensionsError: """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error """, """ Example Function :raises Invalid Dimensions Error: """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error With description """, """ Example Function :raises Invalid Dimensions Error: With description """), ################################ (""" Example Function Raises ------ InvalidDimensionsError If the dimensions couldn't be parsed. """, """ Example Function :raises InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ Invalid Dimensions Error If the dimensions couldn't be parsed. """, """ Example Function :raises Invalid Dimensions Error: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ If the dimensions couldn't be parsed. """, """ Example Function :raises If the dimensions couldn't be parsed.: """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` """, """ Example Function :raises exc.InvalidDimensionsError: """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed, then a :class:`exc.InvalidDimensionsError` will be raised. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` If the dimensions couldn't be parsed. :class:`exc.InvalidArgumentsError` If the arguments are invalid. """, """ Example Function :raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed. :raises exc.InvalidArgumentsError: If the arguments are invalid. """), ################################ (""" Example Function Raises ------ CustomError If the dimensions couldn't be parsed. """, """ Example Function :raises CustomError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ AnotherError If the dimensions couldn't be parsed. """, """ Example Function :raises AnotherError: If the dimensions couldn't be parsed. """), ################################ (""" Example Function Raises ------ :class:`exc.InvalidDimensionsError` :class:`exc.InvalidArgumentsError` """, """ Example Function :raises exc.InvalidDimensionsError: :raises exc.InvalidArgumentsError: """)] for docstring, expected in docstrings: actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_xrefs_in_return_type(self): docstring = """ Example Function Returns ------- :class:`numpy.ndarray` A :math:`n \\times 2` array containing a bunch of math items """ expected = """ Example Function :returns: A :math:`n \\times 2` array containing a bunch of math items :returntype: :class:`numpy.ndarray` """ actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_section_header_underline_length(self): docstrings = [(""" Summary line Example - Multiline example body """, """ Summary line Example - Multiline example body """), ################################ (""" Summary line Example -- Multiline example body """, """ Summary line .. admonition:: Example Multiline example body """), ################################ (""" Summary line Example ------- Multiline example body """, """ Summary line .. admonition:: Example Multiline example body """), ################################ (""" Summary line Example ------------ Multiline example body """, """ Summary line .. admonition:: Example Multiline example body """)] for docstring, expected in docstrings: actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_list_in_parameter_description(self): docstring = """One line summary. Parameters ---------- no_list : int one_bullet_empty : int * one_bullet_single_line : int - first line one_bullet_two_lines : int + first line continued two_bullets_single_line : int - first line - second line two_bullets_two_lines : int * first line continued * second line continued one_enumeration_single_line : int 1. first line one_enumeration_two_lines : int 1) first line continued two_enumerations_one_line : int (iii) first line (iv) second line two_enumerations_two_lines : int a. first line continued b. second line continued one_definition_one_line : int item 1 first line one_definition_two_lines : int item 1 first line continued two_definitions_one_line : int item 1 first line item 2 second line two_definitions_two_lines : int item 1 first line continued item 2 second line continued one_definition_blank_line : int item 1 first line extra first line two_definitions_blank_lines : int item 1 first line extra first line item 2 second line extra second line definition_after_normal_text : int text line item 1 first line """ expected = """One line summary. :param no_list: :type no_list: `int` :param one_bullet_empty: * :type one_bullet_empty: `int` :param one_bullet_single_line: - first line :type one_bullet_single_line: `int` :param one_bullet_two_lines: + first line continued :type one_bullet_two_lines: `int` :param two_bullets_single_line: - first line - second line :type two_bullets_single_line: `int` :param two_bullets_two_lines: * first line continued * second line continued :type two_bullets_two_lines: `int` :param one_enumeration_single_line: 1. first line :type one_enumeration_single_line: `int` :param one_enumeration_two_lines: 1) first line continued :type one_enumeration_two_lines: `int` :param two_enumerations_one_line: (iii) first line (iv) second line :type two_enumerations_one_line: `int` :param two_enumerations_two_lines: a. first line continued b. second line continued :type two_enumerations_two_lines: `int` :param one_definition_one_line: item 1 first line :type one_definition_one_line: `int` :param one_definition_two_lines: item 1 first line continued :type one_definition_two_lines: `int` :param two_definitions_one_line: item 1 first line item 2 second line :type two_definitions_one_line: `int` :param two_definitions_two_lines: item 1 first line continued item 2 second line continued :type two_definitions_two_lines: `int` :param one_definition_blank_line: item 1 first line extra first line :type one_definition_blank_line: `int` :param two_definitions_blank_lines: item 1 first line extra first line item 2 second line extra second line :type two_definitions_blank_lines: `int` :param definition_after_normal_text: text line item 1 first line :type definition_after_normal_text: `int` """ actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) def test_docstring_token_type_invalid_warnings_with_linenum(self): docstring = """ Description of the function. Args ---- param1: {1,2 param2: } param3: 'abc param4: def' Returns ------- list of int """ errors = ( r"invalid value set \(missing closing brace\):", r"invalid value set \(missing opening brace\):", r"malformed string literal \(missing closing quote\):", r"malformed string literal \(missing opening quote\):", ) numpy_docstring = NumpyDocstring(docstring) numpy_warnings = numpy_docstring.warnings self.assertEqual(len(numpy_warnings), 4, numpy_warnings) for i, error in enumerate(errors): warn = numpy_warnings.pop(0) match_re = re.compile(error) self.assertTrue(bool(match_re.match(warn[0])), f"{error} \n do not match \n {warn[0]}") self.assertEqual(i+6, warn[1], msg=f"msg={warn[0]}, docstring='{str(numpy_docstring)}'") # FIXME: The offset should be 5 actually, no big deal and it looks like an really painful issue to # fix due to the fact that the changes in the docstring line numbers are happening at the level of napoleon. # name, expected escape_kwargs_tests_cases = [("x, y, z", "x, y, z"), ("*args, **kwargs", r"\*args, \*\*kwargs"), ("*x, **y", r"\*x, \*\*y") ] def test_escape_args_and_kwargs(self): for name, expected in self.escape_kwargs_tests_cases: numpy_docstring = NumpyDocstring("") actual = numpy_docstring._escape_args_and_kwargs(name) assert actual == expected # test docstrings for the free form text in the return secion. # this feature is always enabled # see https://github.com/sphinx-doc/sphinx/issues/7077 docstrings_returns = [( """ Single line summary Return ------ the string of your life: `a complicated string` the strings of your life: list of `complicated string` or str, default: ["you", "me"] the str of your life: {"foo", "bob", "bar"} the int of your life: int the tuple of your life: tuple """, """ Single line summary :returns: * **the string of your life**: `a complicated string` * **the strings of your life**: `list` of `complicated string` or `str`, *default*: [``"you"``, ``"me"``] * **the str of your life**: ``{"foo", "bob", "bar"}`` * **the int of your life**: `int` * **the tuple of your life**: `tuple` """ ), (""" Summary line. Returns ------- list of strings Sequence of arguments, in the order in which they should be called. """, """ Summary line. :returns: Sequence of arguments, in the order in which they should be called. :returntype: `list` of `strings` """), (""" Summary line. Returns ------- Sequence of arguments, in the order in which they should be called. """, """ Summary line. :returns: Sequence of arguments, in the order in which they should be called. """), (""" Summary line. Returns ------- str """, """ Summary line. :returntype: `str` """),( """ Summary line. Returns ------- str A URL string """, """ Summary line. :returns: A URL string :returntype: `str` """ ), ( """ Summary line. Returns ------- a string, can you believe it? """, """ Summary line. :returns: a string, can you believe it? """ ),( """ Single line summary Return ------ the string of your life """, """ Single line summary :returns: the string of your life """ ) ,( """ Summary line. Returns ------- a string, can you believe it? Raises -- UserError """, """ Summary line. :returns: a string, can you believe it? :raises UserError: """ ),( """ Summary line. Returns ------- str Raises -- UserError Warns --- RuntimeWarning """, """ Summary line. :returntype: `str` :raises UserError: :warns: RuntimeWarning """ ),( """ Summary line. Returns ------- str Description of return value Raises -- UserError Description of raised exception Warns -------- RuntimeWarning Description of raised warnings """, """ Summary line. :returns: Description of return value :returntype: `str` :raises UserError: Description of raised exception :warns RuntimeWarning: Description of raised warnings """ ), ( """ Summary line. Returns ------- list(str) The lines of the docstring in a list. Note ---- Nested markup works. """, r""" Summary line. :returns: The lines of the docstring in a list. .. note:: Nested markup works. :returntype: `list`\ (`str`) """ ), ( """ Summary line. Returns ------- List[str] The lines of the docstring in a list. Note ---- Nested markup works. """, r""" Summary line. :returns: The lines of the docstring in a list. .. note:: Nested markup works. :returntype: `List`\ [`str`] """ ), ( """ Summary line. Methods ------- __str__() The lines of the docstring in a list. Note ---- Nested markup works. """, """ Summary line. .. admonition:: Methods `__str__`() The lines of the docstring in a list. .. note:: Nested markup works. """ ), ( """ Single line summary Return ------ a complicated string Extended description of return value int Extended description of return value the tuple of your life: tuple Extended description of return value """, """ Single line summary :returns: * a complicated string - Extended description of return value * `int` - Extended description of return value * **the tuple of your life**: `tuple` - Extended description of return value """ ),] # https://github.com/sphinx-contrib/napoleon/issues/12 # https://github.com/sphinx-doc/sphinx/issues/7077 def test_return_no_type_sphinx_issue_7077(self): for docstring, expected in self.docstrings_returns: actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_return_type_annotation_style(self): docstring = dedent(""" Summary line. Returns ------- List[Union[str, bytes, typing.Pattern]] """) expected = dedent(r""" Summary line. :returntype: `List`\ [`Union`\ [`str`, `bytes`, `typing.Pattern`]] """) actual = str(NumpyDocstring(docstring, )) self.assertEqual(expected.rstrip(), actual) def test_issue_with_link_end_of_section(self): # section breaks needs two white spaces with numpy-style docstrings, # even if footnotes are following-up docstring = """`PEP 484`_ type annotations are supported. Returns ------- bool True if successful, False otherwise. .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ """ expected = """`PEP 484`_ type annotations are supported. :returns: True if successful, False otherwise. :returntype: `bool` .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ """ actual = str(NumpyDocstring(docstring, )) self.assertEqual(expected.rstrip(), actual, str(actual)) self.assertAlmostEqualSphinxDocstring(expected, docstring, type_=SphinxNumpyDocstring) # test that Sphinx also cannot parse correctly the docstring # without two blank lines before new section # if no section header is provided bogus = """`PEP 484`_ type annotations are supported. Returns ------- bool True if successful, False otherwise. .. _PEP 484: https://www.python.org/dev/peps/pep-0484/ """ expected_bogus = """`PEP 484`_ type annotations are supported. :returns: * `bool` - True if successful, False otherwise. * .. _PEP 484 - https://www.python.org/dev/peps/pep-0484/ """ actual = str(NumpyDocstring(bogus, )) self.assertEqual(expected_bogus.rstrip(), actual, str(actual)) # test that we have the same interpretation with sphinx self.assertAlmostEqualSphinxDocstring(str(NumpyDocstring(bogus, )), bogus, type_=SphinxNumpyDocstring) def test_return_type_list_free_style_do_desc(self): docstring = dedent(""" Return ------ the list of your life: list of str the str of your life: {"foo", "bob", "bar"} the int of your life: int the tuple of your life: tuple """) expected = dedent(""" :returns: * **the list of your life**: `list` of `str` * **the str of your life**: ``{"foo", "bob", "bar"}`` * **the int of your life**: `int` * **the tuple of your life**: `tuple` """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) docstring = dedent(""" Yields ------ the list of your life: list of str the str of your life: {"foo", "bob", "bar"} the int of your life: int the tuple of your life: tuple """) expected = dedent(""" :yields: * **the list of your life**: `list` of `str` * **the str of your life**: ``{"foo", "bob", "bar"}`` * **the int of your life**: `int` * **the tuple of your life**: `tuple` """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_fields_blank_lines(self): """ Test for issue https://github.com/twisted/pydoctor/issues/366 """ docstring = dedent(""" Made my day Parameters ---------- foo: str a string bob: list of str Returns ------- bool: The lines of the docstring in a list. Note ---- Markup works. It is strong Yields ------ tuple(ice, cream) Yes""") expected = dedent(r""" Made my day :param foo: a string :type foo: `str` :param bob: :type bob: `list` of `str` :returns: The lines of the docstring in a list. :returntype: `bool` .. note:: Markup works. It is strong :yields: Yes :yieldtype: `tuple`\ (`ice`, `cream`) """) actual = str(NumpyDocstring(docstring)) self.assertEqual(expected.rstrip(), actual) def test_fields_blank_lines_sphinx_upstream(self): """ Test that sphinx napoleon upstream version of NumpyDocstring is actually generating wrong reST text (for now)... """ docstring = dedent(""" Made my day Parameters ---------- foo: str a string bob: list of str Returns ------- bool: The lines of the docstring in a list. Note ---- Markup works. It is strong Yields ------ tuple(ice, cream) Yes""") expected_wrong = dedent(r""" Made my day :param foo: a string :type foo: `str` :param bob: :type bob: `list` of `str` :returns: The lines of the docstring in a list. :returntype: `bool` .. note:: Markup works. It is strong :Yields: `tuple`\ (`ice`, `cream`) - Yes """) self.assertAlmostEqualSphinxDocstring(expected_wrong, docstring, type_=SphinxNumpyDocstring) pydoctor-21.12.1/pydoctor/test/test_napoleon_iterators.py000066400000000000000000000272511416703725300237020ustar00rootroot00000000000000""" Tests for :mod:`pydoctor.napoleon.iterators` module. :: :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ __docformat__ = "restructuredtext" from unittest import TestCase from pydoctor.napoleon.iterators import modify_iter, peek_iter class BaseIteratorsTest(TestCase): def assertEqualTwice(self, expected, func, *args): self.assertEqual(expected, func(*args)) self.assertEqual(expected, func(*args)) def assertFalseTwice(self, func, *args): self.assertFalse(func(*args)) self.assertFalse(func(*args)) def assertNext(self, it, expected, is_last): self.assertTrueTwice(it.has_next) self.assertEqualTwice(expected, it.peek) self.assertTrueTwice(it.has_next) self.assertEqualTwice(expected, it.peek) self.assertTrueTwice(it.has_next) self.assertEqual(expected, next(it)) if is_last: self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next) else: self.assertTrueTwice(it.has_next) def assertRaisesTwice(self, exc, func, *args): self.assertRaises(exc, func, *args) self.assertRaises(exc, func, *args) def assertTrueTwice(self, func, *args): self.assertTrue(func(*args)) self.assertTrue(func(*args)) class PeekIterTest(BaseIteratorsTest): def test_init_with_sentinel(self): a = iter(['1', '2', 'DONE']) sentinel = 'DONE' self.assertRaises(TypeError, peek_iter, a, sentinel) def get_next(): return next(a) it = peek_iter(get_next, sentinel) self.assertEqual(it.sentinel, sentinel) self.assertNext(it, '1', is_last=False) self.assertNext(it, '2', is_last=True) def test_iter(self): a = ['1', '2', '3'] it = peek_iter(a) self.assertTrue(it is it.__iter__()) a = [] b = [i for i in peek_iter(a)] self.assertEqual([], b) a = ['1'] b = [i for i in peek_iter(a)] self.assertEqual(['1'], b) a = ['1', '2'] b = [i for i in peek_iter(a)] self.assertEqual(['1', '2'], b) a = ['1', '2', '3'] b = [i for i in peek_iter(a)] self.assertEqual(['1', '2', '3'], b) def test_next_with_multi(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 2) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 2) self.assertTrueTwice(it.has_next) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqual(['1', '2'], it.next(2)) self.assertFalseTwice(it.has_next) a = ['1', '2', '3'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqual(['1', '2'], it.next(2)) self.assertTrueTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 2) self.assertTrueTwice(it.has_next) a = ['1', '2', '3', '4'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqual(['1', '2'], it.next(2)) self.assertTrueTwice(it.has_next) self.assertEqual(['3', '4'], it.next(2)) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 2) self.assertFalseTwice(it.has_next) def test_next_with_none(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next) self.assertFalseTwice(it.has_next) a = ['1'] it = peek_iter(a) self.assertEqual('1', it.__next__()) a = ['1'] it = peek_iter(a) self.assertNext(it, '1', is_last=True) a = ['1', '2'] it = peek_iter(a) self.assertNext(it, '1', is_last=False) self.assertNext(it, '2', is_last=True) a = ['1', '2', '3'] it = peek_iter(a) self.assertNext(it, '1', is_last=False) self.assertNext(it, '2', is_last=False) self.assertNext(it, '3', is_last=True) def test_next_with_one(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 1) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqual(['1'], it.next(1)) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 1) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqual(['1'], it.next(1)) self.assertTrueTwice(it.has_next) self.assertEqual(['2'], it.next(1)) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 1) def test_next_with_zero(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertRaisesTwice(StopIteration, it.next, 0) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.next, 0) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.next, 0) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.next, 0) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.next, 0) def test_peek_with_multi(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertEqualTwice([it.sentinel, it.sentinel], it.peek, 2) self.assertFalseTwice(it.has_next) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', it.sentinel], it.peek, 2) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', it.sentinel, it.sentinel], it.peek, 3) self.assertTrueTwice(it.has_next) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2'], it.peek, 2) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2', it.sentinel], it.peek, 3) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2', it.sentinel, it.sentinel], it.peek, 4) self.assertTrueTwice(it.has_next) a = ['1', '2', '3'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2'], it.peek, 2) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2', '3'], it.peek, 3) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1', '2', '3', it.sentinel], it.peek, 4) self.assertTrueTwice(it.has_next) self.assertEqual('1', next(it)) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['2', '3'], it.peek, 2) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['2', '3', it.sentinel], it.peek, 3) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['2', '3', it.sentinel, it.sentinel], it.peek, 4) self.assertTrueTwice(it.has_next) def test_peek_with_none(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertEqualTwice(it.sentinel, it.peek) self.assertFalseTwice(it.has_next) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice('1', it.peek) self.assertEqual('1', next(it)) self.assertFalseTwice(it.has_next) self.assertEqualTwice(it.sentinel, it.peek) self.assertFalseTwice(it.has_next) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice('1', it.peek) self.assertEqual('1', next(it)) self.assertTrueTwice(it.has_next) self.assertEqualTwice('2', it.peek) self.assertEqual('2', next(it)) self.assertFalseTwice(it.has_next) self.assertEqualTwice(it.sentinel, it.peek) self.assertFalseTwice(it.has_next) def test_peek_with_one(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertEqualTwice([it.sentinel], it.peek, 1) self.assertFalseTwice(it.has_next) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1'], it.peek, 1) self.assertEqual('1', next(it)) self.assertFalseTwice(it.has_next) self.assertEqualTwice([it.sentinel], it.peek, 1) self.assertFalseTwice(it.has_next) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['1'], it.peek, 1) self.assertEqual('1', next(it)) self.assertTrueTwice(it.has_next) self.assertEqualTwice(['2'], it.peek, 1) self.assertEqual('2', next(it)) self.assertFalseTwice(it.has_next) self.assertEqualTwice([it.sentinel], it.peek, 1) self.assertFalseTwice(it.has_next) def test_peek_with_zero(self): a = [] it = peek_iter(a) self.assertFalseTwice(it.has_next) self.assertEqualTwice([], it.peek, 0) a = ['1'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.peek, 0) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.peek, 0) a = ['1', '2'] it = peek_iter(a) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.peek, 0) self.assertTrueTwice(it.has_next) self.assertEqualTwice([], it.peek, 0) def test_line_counter(self): a = ['1', '2', '3', '4'] it = peek_iter(a) self.assertEqual(it.counter, 0) it.peek(2) self.assertEqual(it.counter, 0) it.next(2) self.assertEqual(it.counter, 2) it.next() self.assertEqual(it.counter, 3) it.next() self.assertEqual(it.counter, 4) self.assertFalseTwice(it.has_next) class ModifyIterTest(BaseIteratorsTest): def test_init_with_sentinel_args(self): a = iter(['1', '2', '3', 'DONE']) sentinel = 'DONE' def get_next(): return next(a) it = modify_iter(get_next, sentinel, int) expected = [1, 2, 3] self.assertEqual(expected, [i for i in it]) def test_init_with_sentinel_kwargs(self): a = iter([1, 2, 3, 4]) sentinel = 4 def get_next(): return next(a) it = modify_iter(get_next, sentinel, modifier=str) expected = ['1', '2', '3'] self.assertEqual(expected, [i for i in it]) def test_modifier_default(self): a = ['', ' ', ' a ', 'b ', ' c', ' ', ''] it = modify_iter(a) expected = ['', ' ', ' a ', 'b ', ' c', ' ', ''] self.assertEqual(expected, [i for i in it]) def test_modifier_not_callable(self): self.assertRaises(TypeError, modify_iter, [1], modifier='not_callable') def test_modifier_rstrip(self): a = ['', ' ', ' a ', 'b ', ' c', ' ', ''] it = modify_iter(a, modifier=lambda s: s.rstrip()) expected = ['', '', ' a', 'b', ' c', '', ''] self.assertEqual(expected, [i for i in it]) def test_modifier_rstrip_unicode(self): a = ['', ' ', ' a ', 'b ', ' c', ' ', ''] it = modify_iter(a, modifier=lambda s: s.rstrip()) expected = ['', '', ' a', 'b', ' c', '', ''] self.assertEqual(expected, [i for i in it]) pydoctor-21.12.1/pydoctor/test/test_node2stan.py000066400000000000000000000051771416703725300216730ustar00rootroot00000000000000""" Tests for the L{node2stan} module. :See: {test.epydoc.test_epytext2html}, {test.epydoc.test_restructuredtext} """ from pydoctor.test.epydoc.test_epytext2html import epytext2node from pydoctor.test.epydoc.test_restructuredtext import rst2node from pydoctor.node2stan import gettext def test_gettext() -> None: doc = ''' This paragraph is not in any section. Section 1 ========= This is a paragraph in section 1. Section 1.1 ----------- This is a paragraph in section 1.1. Section 2 ========= This is a paragraph in section 2. ''' assert gettext(epytext2node(doc)) == [ 'This paragraph is not in any section.', 'Section 1', 'This is a paragraph in section 1.', 'Section 1.1', 'This is a paragraph in section 1.1.', 'Section 2', 'This is a paragraph in section 2.'] doc = ''' I{B{Inline markup} may be nested; and it may span} multiple lines. - I{Italicized text} - B{Bold-faced text} - C{Source code} - Math: M{m*x+b} Without the capital letter, matching braces are not interpreted as markup: C{my_dict={1:2, 3:4}}. ''' assert gettext(epytext2node(doc)) == [ 'Inline markup', ' may be nested; and it may span', ' multiple lines.', 'Italicized text', 'Bold-faced text', 'Source code', 'Math: ', 'm*x+b', 'Without the capital letter, matching braces are not interpreted as markup: ', 'my_dict=', '{', '1:2, 3:4', '}', '.'] doc = ''' - U{www.python.org} - U{http://www.python.org} - U{The epydoc homepage} - U{The B{I{Python}} homepage } - U{Edward Loper} ''' # TODO: Make it retreive the links refuid attribute. assert gettext(epytext2node(doc)) == ['www.python.org', 'http://www.python.org', 'The epydoc homepage', 'The ', 'Python', ' homepage', 'Edward Loper'] doc = ''' This paragraph is not in any section. ``__ .. note:: This is just a note with nested contents .. image:: https://avatars0.githubusercontent.com/u/50667087?s=200&v=4 :target: https://mfesiem.github.io/docs/msiempy/msiempy.html :alt: Nitro :width: 50 :height: 50 ''' assert gettext(rst2node(doc)) == ['This paragraph is not in any section.', 'mailto:postmaster@example.net', 'This is just a note with nested contents'] pydoctor-21.12.1/pydoctor/test/test_packages.py000066400000000000000000000066101416703725300215450ustar00rootroot00000000000000from pathlib import Path from typing import Type from pydoctor import model testpackages = Path(__file__).parent / 'testpackages' def processPackage(packname: str, systemcls: Type[model.System] = model.System) -> model.System: system = systemcls() system.addPackage(testpackages / packname) system.process() return system def test_relative_import() -> None: system = processPackage("relativeimporttest") cls = system.allobjects['relativeimporttest.mod1.C'] assert isinstance(cls, model.Class) assert cls.bases == ['relativeimporttest.mod2.B'] def test_package_docstring() -> None: system = processPackage("relativeimporttest") assert system.allobjects['relativeimporttest'].docstring == "DOCSTRING" def test_modnamedafterbuiltin() -> None: # well, basically the test is that this doesn't explode: system = processPackage("modnamedafterbuiltin") # but let's test _something_ dict_class = system.allobjects['modnamedafterbuiltin.mod.Dict'] assert isinstance(dict_class, model.Class) assert dict_class.baseobjects == [None] def test_nestedconfusion() -> None: system = processPackage("nestedconfusion") A = system.allobjects['nestedconfusion.mod.nestedconfusion.A'] assert isinstance(A, model.Class) C = system.allobjects['nestedconfusion.mod.C'] assert A.baseobjects[0] is C def test_importingfrompackage() -> None: system = processPackage("importingfrompackage") system.getProcessedModule('importingfrompackage.mod') submod = system.allobjects['importingfrompackage.subpack.submod'] assert isinstance(submod, model.Module) assert submod.state is model.ProcessingState.PROCESSED def test_allgames() -> None: """ Test reparenting of documentables. A name which is defined in module 1, but included in __all__ of module 2 that it is imported into, should end up in the documentation of module 2. """ system = processPackage("allgames") mod1 = system.allobjects['allgames.mod1'] assert isinstance(mod1, model.Module) mod2 = system.allobjects['allgames.mod2'] assert isinstance(mod2, model.Module) # InSourceAll is not moved into mod2, but NotInSourceAll is. assert 'InSourceAll' in mod1.contents assert 'NotInSourceAll' in mod2.contents # Source paths must be unaffected by the move, so that error messages # point to the right source code. moved = mod2.contents['NotInSourceAll'] assert isinstance(moved, model.Class) assert moved.source_path is not None assert moved.source_path.parts[-2:] == ('allgames', 'mod1.py') assert moved.parentMod is mod2 assert moved.parentMod.source_path is not None assert moved.parentMod.source_path.parts[-2:] == ('allgames', 'mod2.py') def test_cyclic_imports() -> None: """ Test whether names are resolved correctly when we have import cycles. The test package contains module 'a' that defines class 'A' and module 'b' that defines class 'B'; each module imports the other. Since the test data is symmetrical, we will at some point be importing a module that has not been fully processed yet, no matter which module gets processed first. """ system = processPackage('cyclic_imports') mod_a = system.allobjects['cyclic_imports.a'] assert mod_a.expandName('B') == 'cyclic_imports.b.B' mod_b = system.allobjects['cyclic_imports.b'] assert mod_b.expandName('A') == 'cyclic_imports.a.A' pydoctor-21.12.1/pydoctor/test/test_sphinx.py000066400000000000000000000526011416703725300213010ustar00rootroot00000000000000""" Tests for Sphinx integration. """ import datetime import io import string import zlib from contextlib import contextmanager from pathlib import Path from typing import Callable, Iterator, List, Optional, Tuple, cast import cachecontrol import pytest import requests from urllib3 import HTTPResponse from hypothesis import assume, given, settings from hypothesis import strategies as st from . import CapLog, FixtureRequest, MonkeyPatch, TempPathFactory from pydoctor import model, sphinx class PydoctorLogger: """ Partial implementation of pydoctor.model.System.msg() that records logged messages. """ def __init__(self) -> None: self.messages: List[Tuple[str, str, int]] = [] def __call__(self, section: str, msg: str, thresh: int = 0) -> None: self.messages.append((section, msg, thresh)) class PydoctorNoLogger: """ Partial implementation of pydoctor.model.System.msg() that asserts if any message is logged. """ def __call__(self, section: str, msg: str, thresh: int = 0) -> None: assert False class InvReader(sphinx.SphinxInventory): _logger: PydoctorLogger class InvWriter(sphinx.SphinxInventoryWriter): _logger: PydoctorLogger @pytest.fixture def inv_reader() -> InvReader: return InvReader(logger=PydoctorLogger()) @pytest.fixture def inv_reader_nolog() -> sphinx.SphinxInventory: return sphinx.SphinxInventory(logger=PydoctorNoLogger()) def get_inv_writer_with_logger(name: str = 'project_name', version: str = '1.2') -> Tuple[InvWriter, PydoctorLogger]: """ @return: Tuple of a Sphinx inventory writer connected to the logger. """ logger = PydoctorLogger() writer = InvWriter( logger=logger, project_name=name, project_version=version, ) return writer, logger @pytest.fixture def inv_writer_nolog() -> sphinx.SphinxInventoryWriter: """ @return: A Sphinx inventory writer that is connected to a null logger. """ return sphinx.SphinxInventoryWriter( logger=PydoctorNoLogger(), project_name='project_name', project_version='2.3.0', ) IGNORE_SYSTEM = cast(model.System, 'ignore-system') """Passed as a System when we don't want the system to be accessed.""" def test_generate_empty_functional() -> None: """ Functional test for index generation of empty API. Header is plain text while content is compressed. """ inv_writer, logger = get_inv_writer_with_logger( name='project-name', version='1.2.0rc1', ) output = io.BytesIO() @contextmanager def openFileForWriting(path: str) -> Iterator[io.BytesIO]: yield output inv_writer._openFileForWriting = openFileForWriting # type: ignore[assignment] inv_writer.generate(subjects=[], basepath='base-path') inventory_path = Path('base-path') / 'objects.inv' expected_log = [( 'sphinx', f'Generating objects inventory at {inventory_path}', 0 )] assert expected_log == logger.messages expected_ouput = b"""# Sphinx inventory version 2 # Project: project-name # Version: 1.2.0rc1 # The rest of this file is compressed with zlib. x\x9c\x03\x00\x00\x00\x00\x01""" assert expected_ouput == output.getvalue() def test_generateContent(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Return a string with inventory for all targeted objects, recursive. """ system = model.System() root1 = model.Package(system, 'package1') root2 = model.Package(system, 'package2') child1 = model.Package(system, 'child1', parent=root2) system.addObject(child1) subjects = [root1, root2] result = inv_writer_nolog._generateContent(subjects) expected_result = ( b'package1 py:module -1 package1.html -\n' b'package2 py:module -1 package2.html -\n' b'package2.child1 py:module -1 package2.child1.html -\n' ) assert expected_result == result def test_generateLine_package(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for package. """ result = inv_writer_nolog._generateLine( model.Package(IGNORE_SYSTEM, 'package1')) assert 'package1 py:module -1 package1.html -\n' == result def test_generateLine_module(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for module. """ result = inv_writer_nolog._generateLine( model.Module(IGNORE_SYSTEM, 'module1')) assert 'module1 py:module -1 module1.html -\n' == result def test_generateLine_class(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for class. """ result = inv_writer_nolog._generateLine( model.Class(IGNORE_SYSTEM, 'class1')) assert 'class1 py:class -1 class1.html -\n' == result def test_generateLine_function(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for function. Functions are inside a module. """ parent = model.Module(IGNORE_SYSTEM, 'module1') result = inv_writer_nolog._generateLine( model.Function(IGNORE_SYSTEM, 'func1', parent)) assert 'module1.func1 py:function -1 module1.html#func1 -\n' == result def test_generateLine_method(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for method. Methods are functions inside a class. """ parent = model.Class(IGNORE_SYSTEM, 'class1') result = inv_writer_nolog._generateLine( model.Function(IGNORE_SYSTEM, 'meth1', parent)) assert 'class1.meth1 py:method -1 class1.html#meth1 -\n' == result def test_generateLine_attribute(inv_writer_nolog: sphinx.SphinxInventoryWriter) -> None: """ Check inventory for attributes. """ parent = model.Class(IGNORE_SYSTEM, 'class1') result = inv_writer_nolog._generateLine( model.Attribute(IGNORE_SYSTEM, 'attr1', parent)) assert 'class1.attr1 py:attribute -1 class1.html#attr1 -\n' == result class UnknownType(model.Documentable): """ Documentable type to help with testing. """ def test_generateLine_unknown() -> None: """ When object type is uknown a message is logged and is handled as generic object. """ inv_writer, logger = get_inv_writer_with_logger() result = inv_writer._generateLine( UnknownType(IGNORE_SYSTEM, 'unknown1')) assert 'unknown1 py:obj -1 unknown1.html -\n' == result assert [( 'sphinx', "Unknown type for unknown1.", -1 )] == logger.messages def test_getPayload_empty(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return empty string. """ content = b"""# Sphinx inventory version 2 # Project: some-name # Version: 2.0 # The rest of this file is compressed with zlib. x\x9c\x03\x00\x00\x00\x00\x01""" result = inv_reader_nolog._getPayload('http://base.ignore', content) assert '' == result def test_getPayload_content(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return content as string. """ payload = "first_line\nsecond line\nit's a snake: \U0001F40D" content = b"""# Ignored line # Project: some-name # Version: 2.0 # commented line. """ + zlib.compress(payload.encode('utf-8')) result = inv_reader_nolog._getPayload('http://base.ignore', content) assert payload == result def test_getPayload_invalid_uncompress(inv_reader: InvReader) -> None: """ Return empty string and log an error when failing to uncompress data. """ base_url = 'http://tm.tld' content = b"""# Project: some-name # Version: 2.0 not-valid-zlib-content""" result = inv_reader._getPayload(base_url, content) assert '' == result assert [( 'sphinx', 'Failed to uncompress inventory from http://tm.tld', -1, )] == inv_reader._logger.messages def test_getPayload_invalid_decode(inv_reader: InvReader) -> None: """ Return empty string and log an error when failing to uncompress data. """ payload = b'\x80' base_url = 'http://tm.tld' content = b"""# Project: some-name # Version: 2.0 """ + zlib.compress(payload) result = inv_reader._getPayload(base_url, content) assert '' == result assert [( 'sphinx', 'Failed to decode inventory from http://tm.tld', -1, )] == inv_reader._logger.messages def test_getLink_not_found(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return None if link does not exists. """ assert None is inv_reader_nolog.getLink('no.such.name') def test_getLink_found(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return the link from internal state. """ inv_reader_nolog._links['some.name'] = ('http://base.tld', 'some/url.php') assert 'http://base.tld/some/url.php' == inv_reader_nolog.getLink('some.name') def test_getLink_self_anchor(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return the link with anchor as target name when link end with $. """ inv_reader_nolog._links['some.name'] = ('http://base.tld', 'some/url.php#$') assert 'http://base.tld/some/url.php#some.name' == inv_reader_nolog.getLink('some.name') def test_update_functional(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Functional test for updating from an empty inventory. """ payload = ( b'some.module1 py:module -1 module1.html -\n' b'other.module2 py:module 0 module2.html Other description\n' ) # Patch URL loader to avoid hitting the system. content = b"""# Sphinx inventory version 2 # Project: some-name # Version: 2.0 # The rest of this file is compressed with zlib. """ + zlib.compress(payload) url = 'http://some.url/api/objects.inv' inv_reader_nolog.update({url: content}, url) assert 'http://some.url/api/module1.html' == inv_reader_nolog.getLink('some.module1') assert 'http://some.url/api/module2.html' == inv_reader_nolog.getLink('other.module2') def test_update_bad_url(inv_reader: InvReader) -> None: """ Log an error when failing to get base url from url. """ inv_reader.update({}, 'really.bad.url') assert inv_reader._links == {} expected_log = [( 'sphinx', 'Failed to get remote base url for really.bad.url', -1 )] assert expected_log == inv_reader._logger.messages def test_update_fail(inv_reader: InvReader) -> None: """ Log an error when failing to get content from url. """ inv_reader.update({}, 'http://some.tld/o.inv') assert inv_reader._links == {} expected_log = [( 'sphinx', 'Failed to get object inventory from http://some.tld/o.inv', -1, )] assert expected_log == inv_reader._logger.messages def test_parseInventory_empty(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return empty dict for empty input. """ result = inv_reader_nolog._parseInventory('http://base.tld', '') assert {} == result def test_parseInventory_single_line(inv_reader_nolog: sphinx.SphinxInventory) -> None: """ Return a dict with a single member. """ result = inv_reader_nolog._parseInventory( 'http://base.tld', 'some.attr py:attr -1 some.html De scription') assert {'some.attr': ('http://base.tld', 'some.html')} == result def test_parseInventory_spaces() -> None: """ Sphinx inventory lines always contain 5 values, separated by spaces. However, the first and fifth value can contain internal spaces. The parser must be able to tell apart separators from internal spaces. """ # Space in first (name) column. assert sphinx._parseInventoryLine( 'key function std:term -1 glossary.html#term-key-function -' ) == ( 'key function', 'std:term', -1, 'glossary.html#term-key-function', '-' ) # Space in last (display name) column. assert sphinx._parseInventoryLine( 'doctest-execution-context std:label -1 library/doctest.html#$ What’s the Execution Context?' ) == ( 'doctest-execution-context', 'std:label', -1, 'library/doctest.html#$', 'What’s the Execution Context?' ) # Space in both first and last column. assert sphinx._parseInventoryLine( 'async def std:label -1 reference/compound_stmts.html#async-def Coroutine function definition' ) == ( 'async def', 'std:label', -1, 'reference/compound_stmts.html#async-def', 'Coroutine function definition' ) def test_parseInventory_invalid_lines(inv_reader: InvReader) -> None: """ Skip line and log an error. """ base_url = 'http://tm.tld' content = ( 'good.attr py:attribute -1 some.html -\n' 'missing.display.name py:attribute 1 some.html\n' 'bad.attr bad format\n' 'very.bad\n' '\n' 'good.again py:module 0 again.html -\n' ) result = inv_reader._parseInventory(base_url, content) assert { 'good.attr': (base_url, 'some.html'), 'good.again': (base_url, 'again.html'), } == result assert [ ( 'sphinx', 'Failed to parse line "missing.display.name py:attribute 1 some.html" for http://tm.tld', -1, ), ( 'sphinx', 'Failed to parse line "bad.attr bad format" for http://tm.tld', -1, ), ('sphinx', 'Failed to parse line "very.bad" for http://tm.tld', -1), ('sphinx', 'Failed to parse line "" for http://tm.tld', -1), ] == inv_reader._logger.messages def test_parseInventory_type_filter(inv_reader: InvReader) -> None: """ Ignore entries that don't have a 'py:' type field. """ base_url = 'https://docs.python.org/3' content = ( 'dict std:label -1 reference/expressions.html#$ Dictionary displays\n' 'dict py:class 1 library/stdtypes.html#$ -\n' 'dict std:2to3fixer 1 library/2to3.html#2to3fixer-$ -\n' ) result = inv_reader._parseInventory(base_url, content) assert { 'dict': (base_url, 'library/stdtypes.html#$'), } == result assert [] == inv_reader._logger.messages maxAgeAmounts = st.integers() | st.just("\x00") maxAgeUnits = st.sampled_from(tuple(sphinx._maxAgeUnits)) | st.just("\x00") class TestParseMaxAge: """ Tests for L{sphinx.parseMaxAge} """ @given( amount=maxAgeAmounts, unit=maxAgeUnits, ) def test_toTimedelta(self, amount: int, unit: str) -> None: """ A parsed max age dictionary consists of valid arguments to L{datetime.timedelta}, and the constructed L{datetime.timedelta} matches the specification. """ maxAge = f"{amount}{unit}" try: parsedMaxAge = sphinx.parseMaxAge(maxAge) except sphinx.InvalidMaxAge: pass else: td = datetime.timedelta(**parsedMaxAge) converter = { 's': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60, 'w': 7 * 24 * 60 * 60 } total_seconds = amount * converter[unit] assert pytest.approx(td.total_seconds()) == total_seconds class ClosingBytesIO(io.BytesIO): """ A L{io.BytesIO} instance that closes itself after all its data has been read. This mimics the behavior of L{http.client.HTTPResponse} in the standard library. """ def read(self, size: Optional[int] = None) -> bytes: data = super().read(size) if self.tell() >= len(self.getvalue()): self.close() return data def test_ClosingBytesIO() -> None: """ L{ClosingBytesIO} closes itself when all its data has been read. """ data = b'some data' cbio = ClosingBytesIO(data) buffer = [cbio.read(1)] assert not cbio.closed buffer.append(cbio.read()) assert cbio.closed assert b''.join(buffer) == data # type:ignore[unreachable] class TestIntersphinxCache: """ Tests for L{sphinx.IntersphinxCache} """ @pytest.fixture def send_returns(self, monkeypatch: MonkeyPatch) -> Callable[[HTTPResponse], MonkeyPatch]: """ Return a function that patches L{requests.adapters.HTTPAdapter.send} so that it returns the provided L{requests.Response}. """ def send_returns(urllib3_response: HTTPResponse) -> MonkeyPatch: def send( self: requests.adapters.HTTPAdapter, request: requests.PreparedRequest, **kwargs: object ) -> requests.Response: response: requests.Response response = self.build_response(request, urllib3_response) return response monkeypatch.setattr( requests.adapters.HTTPAdapter, "send", send, ) return monkeypatch return send_returns def test_cache(self, tmp_path: Path, send_returns: Callable[[HTTPResponse], None]) -> None: """ L{IntersphinxCache.get} caches responses to the file system. """ url = "https://cache.example/objects.inv" content = b'content' send_returns( HTTPResponse( body=ClosingBytesIO(content), headers={ 'date': 'Sun, 06 Nov 1994 08:49:37 GMT', }, status=200, preload_content=False, decode_content=False, ), ) loadsCache = sphinx.IntersphinxCache.fromParameters( sessionFactory=requests.Session, cachePath=str(tmp_path), maxAgeDictionary={"weeks": 1} ) assert loadsCache.get(url) == content # Now the response contains different data that will not be # returned when the cache is enabled. send_returns( HTTPResponse( body=ClosingBytesIO(content * 2), headers={ 'date': 'Sun, 06 Nov 1994 08:49:37 GMT', }, status=200, preload_content=False, decode_content=False, ), ) assert loadsCache.get(url) == content readsCacheFromFileSystem = sphinx.IntersphinxCache.fromParameters( sessionFactory=requests.Session, cachePath=str(tmp_path), maxAgeDictionary={"weeks": 1} ) assert readsCacheFromFileSystem.get(url) == content def test_getRaisesException(self, caplog: CapLog) -> None: """ L{IntersphinxCache.get} returns L{None} if an exception is raised while C{GET}ing a URL and logs the exception. """ class _TestException(Exception): pass class _RaisesOnGet: @staticmethod def get(url: str) -> bytes: raise _TestException() session = cast(requests.Session, _RaisesOnGet) cache = sphinx.IntersphinxCache(session=session) assert cache.get("some url") is None assert len(caplog.records) == 1 assert caplog.records[0].levelname == "ERROR" assert caplog.records[0].exc_info is not None assert caplog.records[0].exc_info[0] is _TestException @pytest.fixture(scope='module') def cacheDirectory(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: name = request.module.__name__.split('.')[-1] return tmp_path_factory.mktemp(f'{name}-cache') @given( clearCache=st.booleans(), enableCache=st.booleans(), cacheDirectoryName=st.text( alphabet=sorted(set(string.printable) - set('\\/:*?"<>|\x0c\x0b\t\r\n')), min_size=1, max_size=32, # Avoid upper length on path ), maxAgeAmount=maxAgeAmounts, maxAgeUnit=maxAgeUnits, ) @settings(max_examples=700, deadline=None) def test_prepareCache( cacheDirectory: Path, clearCache: bool, enableCache: bool, cacheDirectoryName: str, maxAgeAmount: int, maxAgeUnit: str, ) -> None: """ The cache directory is deleted when C{clearCache} is L{True}; an L{IntersphinxCache} is created with a session on which is mounted C{cachecontrol.CacheControlAdapter} for C{http} and C{https} URLs. """ # Windows doesn't like paths ending in a space or dot. assume(cacheDirectoryName[-1] not in '. ') # These DOS device names still have special meaning in modern Windows. assume(cacheDirectoryName.upper() not in {'CON', 'PRN', 'AUX', 'NUL'}) assume(not cacheDirectoryName.upper().startswith('COM')) assume(not cacheDirectoryName.upper().startswith('LPT')) cacheDirectory.mkdir(exist_ok=True) for child in cacheDirectory.iterdir(): child.unlink() with open(cacheDirectory / cacheDirectoryName, 'w'): pass try: cache = sphinx.prepareCache( clearCache=clearCache, enableCache=enableCache, cachePath=str(cacheDirectory), maxAge=f"{maxAgeAmount}{maxAgeUnit}" ) except sphinx.InvalidMaxAge: pass else: assert isinstance(cache, sphinx.IntersphinxCache) for scheme in ('https://', 'http://'): hasCacheControl = isinstance( cache._session.adapters[scheme], cachecontrol.CacheControlAdapter, ) if enableCache: assert hasCacheControl else: assert not hasCacheControl if clearCache: assert not cacheDirectory.exists() pydoctor-21.12.1/pydoctor/test/test_templatewriter.py000066400000000000000000000436471416703725300230520ustar00rootroot00000000000000from io import BytesIO from typing import Callable, Union, cast, TYPE_CHECKING import pytest import warnings import sys import tempfile import os from pathlib import Path, PurePath from pydoctor import model, templatewriter, stanutils from pydoctor.templatewriter import (FailedToCreateTemplate, StaticTemplate, pages, writer, TemplateLookup, Template, HtmlTemplate, UnsupportedTemplateVersion, OverrideTemplateNotAllowed) from pydoctor.templatewriter.pages.table import ChildTable from pydoctor.templatewriter.summary import isClassNodePrivate, isPrivate from pydoctor.test.test_astbuilder import fromText from pydoctor.test.test_packages import processPackage if TYPE_CHECKING: from twisted.web.template import Flattenable # Newer APIs from importlib_resources should arrive to stdlib importlib.resources in Python 3.9. if sys.version_info >= (3, 9): from importlib.abc import Traversable else: Traversable = Path else: Traversable = object if sys.version_info < (3, 9): import importlib_resources else: import importlib.resources as importlib_resources template_dir = importlib_resources.files("pydoctor.themes") / "base" def filetext(path: Union[Path, Traversable]) -> str: with path.open('r', encoding='utf-8') as fobj: t = fobj.read() return t def flatten(t: "Flattenable") -> str: io = BytesIO() writer.flattenToFile(io, t) return io.getvalue().decode() def getHTMLOf(ob: model.Documentable) -> str: wr = templatewriter.TemplateWriter(Path(), TemplateLookup(template_dir)) f = BytesIO() wr._writeDocsForOne(ob, f) return f.getvalue().decode() def test_simple() -> None: src = ''' def f(): """This is a docstring.""" ''' mod = fromText(src) v = getHTMLOf(mod.contents['f']) assert 'This is a docstring' in v def test_empty_table() -> None: mod = fromText('') t = ChildTable(pages.DocGetter(), mod, [], ChildTable.lookup_loader(TemplateLookup(template_dir))) flattened = flatten(t) assert 'The renderer named' not in flattened def test_nonempty_table() -> None: mod = fromText('def f(): pass') t = ChildTable(pages.DocGetter(), mod, mod.contents.values(), ChildTable.lookup_loader(TemplateLookup(template_dir))) flattened = flatten(t) assert 'The renderer named' not in flattened def test_rest_support() -> None: system = model.System() system.options.docformat = 'restructuredtext' system.options.verbosity = 4 src = ''' def f(): """This is a docstring for f.""" ''' mod = fromText(src, system=system) html = getHTMLOf(mod.contents['f']) assert "
    " not in html
    
    def test_document_code_in_init_module() -> None:
        system = processPackage("codeininit")
        html = getHTMLOf(system.allobjects['codeininit'])
        assert 'functionInInit' in html
    
    def test_basic_package(tmp_path: Path) -> None:
        system = processPackage("basic")
        w = writer.TemplateWriter(tmp_path, TemplateLookup(template_dir))
        system.options.htmlusesplitlinks = True
        system.options.htmlusesorttable = True
        w.prepOutputDirectory()
        root, = system.rootobjects
        w._writeDocsFor(root)
        w.writeSummaryPages(system)
        for ob in system.allobjects.values():
            url = ob.url
            if '#' in url:
                url = url[:url.find('#')]
            assert (tmp_path / url).is_file()
        with open(tmp_path / 'basic.html', encoding='utf-8') as f:
            assert 'Package docstring' in f.read()
    
    def test_hasdocstring() -> None:
        system = processPackage("basic")
        from pydoctor.templatewriter.summary import hasdocstring
        assert not hasdocstring(system.allobjects['basic._private_mod'])
        assert hasdocstring(system.allobjects['basic.mod.C.f'])
        sub_f = system.allobjects['basic.mod.D.f']
        assert hasdocstring(sub_f) and not sub_f.docstring
    
    def test_missing_variable() -> None:
        mod = fromText('''
        """Module docstring.
    
        @type thisVariableDoesNotExist: Type for non-existent variable.
        """
        ''')
        html = getHTMLOf(mod)
        assert 'thisVariableDoesNotExist' not in html
    
    
    @pytest.mark.parametrize(
        'className',
        ['NewClassThatMultiplyInherits', 'OldClassThatMultiplyInherits'],
    )
    def test_multipleInheritanceNewClass(className: str) -> None:
        """
        A class that has multiple bases has all methods in its MRO
        rendered.
        """
        system = processPackage("multipleinheritance")
    
        cls = next(
            cls
            for cls in system.allobjects.values()
            if cls.name == className
        )
    
        html = getHTMLOf(cls)
    
        assert "methodA" in html
        assert "methodB" in html
    
    def test_html_template_version() -> None:
        lookup = TemplateLookup(template_dir)
        for template in lookup._templates.values():
            if isinstance(template, HtmlTemplate) and not len(template.text.strip()) == 0:
                assert template.version >= 1
    
    def test_template_lookup_get_template() -> None:
    
        lookup = TemplateLookup(template_dir)
    
        here = Path(__file__).parent
    
        index = lookup.get_template('index.html')
        assert isinstance(index, HtmlTemplate)
        assert index.text == filetext(template_dir / 'index.html')
    
        lookup.add_template(HtmlTemplate(name='footer.html', 
                                text=filetext(here / 'testcustomtemplates' / 'faketemplate' / 'footer.html')))
    
        footer = lookup.get_template('footer.html')
        assert isinstance(footer, HtmlTemplate)
        assert footer.text == filetext(here / 'testcustomtemplates' / 'faketemplate' / 'footer.html')
    
        index2 = lookup.get_template('index.html')
        assert isinstance(index2, HtmlTemplate)
        assert index2.text == filetext(template_dir / 'index.html')
    
        lookup = TemplateLookup(template_dir)
    
        footer = lookup.get_template('footer.html')
        assert isinstance(footer, HtmlTemplate)
        assert footer.text == filetext(template_dir / 'footer.html')
    
        subheader = lookup.get_template('subheader.html')
        assert isinstance(subheader, HtmlTemplate)
        assert subheader.version == -1
    
        table = lookup.get_template('table.html')
        assert isinstance(table, HtmlTemplate)
        assert table.version == 1
    
    def test_template_lookup_add_template_warns() -> None:
    
        lookup = TemplateLookup(template_dir)
    
        here = Path(__file__).parent
    
        with pytest.warns(UserWarning) as catch_warnings:
            with (here / 'testcustomtemplates' / 'faketemplate' / 'nav.html').open('r', encoding='utf-8') as fobj:
                lookup.add_template(HtmlTemplate(text=fobj.read(), name='nav.html'))
        assert len(catch_warnings) == 1, [str(w.message) for w in catch_warnings]
        assert "Your custom template 'nav.html' is out of date" in str(catch_warnings.pop().message)
    
        with pytest.warns(UserWarning) as catch_warnings:
            with (here / 'testcustomtemplates' / 'faketemplate' / 'table.html').open('r', encoding='utf-8') as fobj:
                lookup.add_template(HtmlTemplate(text=fobj.read(), name='table.html'))
        assert len(catch_warnings) == 1, [str(w.message) for w in catch_warnings]
        assert "Could not read 'table.html' template version" in str(catch_warnings.pop().message)
    
        with pytest.warns(UserWarning) as catch_warnings:
            with (here / 'testcustomtemplates' / 'faketemplate' / 'summary.html').open('r', encoding='utf-8') as fobj:
                lookup.add_template(HtmlTemplate(text=fobj.read(), name='summary.html'))
        assert len(catch_warnings) == 1, [str(w.message) for w in catch_warnings]
        assert "Could not read 'summary.html' template version" in str(catch_warnings.pop().message)
    
        with pytest.warns(UserWarning) as catch_warnings:
            lookup.add_templatedir(here / 'testcustomtemplates' / 'faketemplate')
        assert len(catch_warnings) == 2, [str(w.message) for w in catch_warnings]
    
    def test_template_lookup_add_template_allok() -> None:
    
        here = Path(__file__).parent
    
        with warnings.catch_warnings(record=True) as catch_warnings:
            warnings.simplefilter("always")
            lookup = TemplateLookup(template_dir)
            lookup.add_templatedir(here / 'testcustomtemplates' / 'allok')
        assert len(catch_warnings) == 0, [str(w.message) for w in catch_warnings]
    
    def test_template_lookup_add_template_raises() -> None:
    
        here = Path(__file__).parent
    
        lookup = TemplateLookup(template_dir)
    
        with pytest.raises(UnsupportedTemplateVersion):
            lookup.add_template(HtmlTemplate(name="nav.html", text="""
            
            """))
    
        with pytest.raises(ValueError):
            lookup.add_template(HtmlTemplate(name="nav.html", text=" Words "))
        
        with pytest.raises(OverrideTemplateNotAllowed):
            lookup.add_template(HtmlTemplate(name="apidocs.css", text=""))
    
        with pytest.raises(OverrideTemplateNotAllowed):
            lookup.add_template(StaticTemplate(name="index.html", data=bytes()))
    
        lookup.add_templatedir(here / 'testcustomtemplates' / 'subfolders')
    
        with pytest.raises(OverrideTemplateNotAllowed):
            lookup.add_template(StaticTemplate('static', data=bytes()))
        with pytest.raises(OverrideTemplateNotAllowed):
            lookup.add_template(HtmlTemplate('static/fonts', text=""))
        with pytest.raises(OverrideTemplateNotAllowed):
            lookup.add_template(HtmlTemplate('Static/Fonts', text=""))
        # Should not fail
        lookup.add_template(StaticTemplate('tatic/fonts', data=bytes()))
    
    
    def test_template_fromdir_fromfile_failure() -> None:
    
        here = Path(__file__).parent
        
        with pytest.raises(FailedToCreateTemplate):
            [t for t in Template.fromdir(here / 'testcustomtemplates' / 'thisfolderdonotexist')]
        
        template = Template.fromfile(here / 'testcustomtemplates' / 'subfolders', PurePath())
        assert not template
    
        template = Template.fromfile(here / 'testcustomtemplates' / 'thisfolderdonotexist', PurePath('whatever'))
        assert not template
    
    def test_template() -> None:
    
        here = Path(__file__).parent
    
        js_template = Template.fromfile(here / 'testcustomtemplates' / 'faketemplate', PurePath('pydoctor.js'))
        html_template = Template.fromfile(here / 'testcustomtemplates' / 'faketemplate', PurePath('nav.html'))
    
        assert isinstance(js_template, StaticTemplate)
        assert isinstance(html_template, HtmlTemplate)
    
    def test_template_subfolders_write(tmp_path: Path) -> None:
        here = Path(__file__).parent
        test_build_dir = tmp_path
    
        lookup = TemplateLookup(here / 'testcustomtemplates' / 'subfolders')
    
         # writes only the static template
    
        for t in lookup.templates:
            if isinstance(t, StaticTemplate):
                t.write(test_build_dir)
    
        assert test_build_dir.joinpath('static').is_dir()
        assert not test_build_dir.joinpath('atemplate.html').exists()
        assert test_build_dir.joinpath('static/info.svg').is_file()
        assert test_build_dir.joinpath('static/lol.svg').is_file()
        assert test_build_dir.joinpath('static/fonts').is_dir()
        assert test_build_dir.joinpath('static/fonts/bar.svg').is_file()
        assert test_build_dir.joinpath('static/fonts/foo.svg').is_file()
    
    def test_template_subfolders_overrides() -> None:
        here = Path(__file__).parent
    
        lookup = TemplateLookup(here / 'testcustomtemplates' / 'subfolders')
    
        atemplate = lookup.get_template('atemplate.html')
        static_info = lookup.get_template('static/info.svg')
        static_lol = lookup.get_template('static/lol.svg')
        static_fonts_bar = lookup.get_template('static/fonts/bar.svg')
        static_fonts_foo = lookup.get_template('static/fonts/foo.svg')
    
        assert isinstance(atemplate, HtmlTemplate)
        assert isinstance(static_info, StaticTemplate)
        assert isinstance(static_lol, StaticTemplate)
        assert isinstance(static_fonts_bar, StaticTemplate)
        assert isinstance(static_fonts_foo, StaticTemplate)
    
        assert len(static_fonts_foo.data) == 0
    
        # Load subfolder contents that will override only one template: static/fonts/foo.svg
        lookup.add_templatedir(here / 'testcustomtemplates' / 'overridesubfolders')
    
        # test nothing changed
        atemplate = lookup.get_template('atemplate.html')
        static_info = lookup.get_template('static/info.svg')
        static_lol = lookup.get_template('static/lol.svg')
        static_fonts_bar = lookup.get_template('static/fonts/bar.svg')
        static_fonts_foo = lookup.get_template('static/fonts/foo.svg')
    
        assert isinstance(atemplate, HtmlTemplate)
        assert isinstance(static_info, StaticTemplate)
        assert isinstance(static_lol, StaticTemplate)
        assert isinstance(static_fonts_bar, StaticTemplate)
        assert isinstance(static_fonts_foo, StaticTemplate)
    
        # Except for the overriden file
        assert len(static_fonts_foo.data) > 0
    
    def test_template_casing() -> None:
        
        here = Path(__file__).parent
    
        html_template1 = Template.fromfile(here / 'testcustomtemplates' / 'casing', PurePath('test1/nav.HTML'))
        html_template2 = Template.fromfile(here / 'testcustomtemplates' / 'casing', PurePath('test2/nav.Html'))
        html_template3 = Template.fromfile(here / 'testcustomtemplates' / 'casing', PurePath('test3/nav.htmL'))
    
        assert isinstance(html_template1, HtmlTemplate)
        assert isinstance(html_template2, HtmlTemplate)
        assert isinstance(html_template3, HtmlTemplate)
    
    def test_templatelookup_casing() -> None:
        here = Path(__file__).parent
    
        lookup = TemplateLookup(here / 'testcustomtemplates' / 'casing' / 'test1')
        lookup.add_templatedir(here / 'testcustomtemplates' / 'casing' / 'test2')
        lookup.add_templatedir(here / 'testcustomtemplates' / 'casing' / 'test3')
    
        assert len(list(lookup.templates)) == 1
    
        lookup = TemplateLookup(here / 'testcustomtemplates' / 'subfolders')
    
        assert lookup.get_template('atemplate.html') == lookup.get_template('ATemplaTe.HTML')
        assert lookup.get_template('static/fonts/bar.svg') == lookup.get_template('StAtic/Fonts/BAr.svg')
    
        static_fonts_bar = lookup.get_template('static/fonts/bar.svg')
        assert static_fonts_bar.name == 'static/fonts/bar.svg'
    
        lookup.add_template(StaticTemplate('Static/Fonts/Bar.svg', bytes()))
    
        static_fonts_bar = lookup.get_template('static/fonts/bar.svg')
        assert static_fonts_bar.name == 'static/fonts/bar.svg' # the Template.name attribute has been changed by add_template()
    
    def is_fs_case_sensitive() -> bool:
        # From https://stackoverflow.com/a/36580834
        with tempfile.NamedTemporaryFile(prefix='TmP') as tmp_file:
            return(not os.path.exists(tmp_file.name.lower()))
    
    @pytest.mark.skipif(not is_fs_case_sensitive(), reason="This test requires a case sensitive file system.")
    def test_template_subfolders_write_casing(tmp_path: Path) -> None:
    
        here = Path(__file__).parent
        test_build_dir = tmp_path
    
        lookup = TemplateLookup(here / 'testcustomtemplates' / 'subfolders')
    
        lookup.add_template(StaticTemplate('static/Info.svg', data=bytes()))
        lookup.add_template(StaticTemplate('Static/Fonts/Bar.svg', data=bytes()))
    
        # writes only the static template
    
        for t in lookup.templates:
            if isinstance(t, StaticTemplate):
                t.write(test_build_dir)
    
        assert test_build_dir.joinpath('static/info.svg').is_file()
        assert not test_build_dir.joinpath('static/Info.svg').is_file()
    
        assert not test_build_dir.joinpath('Static/Fonts').is_dir()
        assert test_build_dir.joinpath('static/fonts/bar.svg').is_file()
    
    
    @pytest.mark.parametrize('func', [isPrivate, isClassNodePrivate])
    def test_isPrivate(func: Callable[[model.Class], bool]) -> None:
        """A documentable object is private if it is private itself or
        lives in a private context.
        """
        mod = fromText('''
        class Public:
            class Inner:
                pass
        class _Private:
            class Inner:
                pass
        ''')
        public = mod.contents['Public']
        assert not func(cast(model.Class, public))
        assert not func(cast(model.Class, public.contents['Inner']))
        private = mod.contents['_Private']
        assert func(cast(model.Class, private))
        assert func(cast(model.Class, private.contents['Inner']))
    
    
    def test_isClassNodePrivate() -> None:
        """A node for a private class with public subclasses is considered public."""
        mod = fromText('''
        class _BaseForPublic:
            pass
        class _BaseForPrivate:
            pass
        class Public(_BaseForPublic):
            pass
        class _Private(_BaseForPrivate):
            pass
        ''')
        assert not isClassNodePrivate(cast(model.Class, mod.contents['Public']))
        assert isClassNodePrivate(cast(model.Class, mod.contents['_Private']))
        assert not isClassNodePrivate(cast(model.Class, mod.contents['_BaseForPublic']))
        assert isClassNodePrivate(cast(model.Class, mod.contents['_BaseForPrivate']))
    
    def test_format_signature() -> None:
        """Test C{pages.format_signature}. 
        
        @note: This test will need to be adapted one we include annotations inside signatures.
        """
        mod = fromText(r'''
        def func(a:Union[bytes, str]=_get_func_default(str), b:Any=re.compile(r'foo|bar'), *args:str, **kwargs:Any) -> Iterator[Union[str, bytes]]:
            ...
        ''')
        assert ("""(a=_get_func_default(str), b=re.compile("""
                """r'foo|"""
                """bar'), *args, **kwargs)""") in flatten(pages.format_signature(cast(model.Function, mod.contents['func'])))
    
    def test_format_decorators() -> None:
        """Test C{pages.format_decorators}"""
        mod = fromText(r'''
        @string_decorator(set('\\/:*?"<>|\f\v\t\r\n'))
        @simple_decorator(max_examples=700, deadline=None, option=range(10))
        def func():
            ...
        ''')
        stan = stanutils.flatten(list(pages.format_decorators(cast(model.Function, mod.contents['func']))))
        assert stan == ("""@string_decorator(set('"""
                        r"""\\/:*?"<>|\f\v\t\r\n"""
                        """'))
    @simple_decorator""" """(max_examples=700, deadline=None, option=range(10))
    """) pydoctor-21.12.1/pydoctor/test/test_type_fields.py000066400000000000000000000322171416703725300223000ustar00rootroot00000000000000from typing import List from textwrap import dedent from pydoctor.epydoc.markup import ParseError, get_parser_by_name from pydoctor.test.epydoc.test_restructuredtext import prettify from pydoctor.test import NotFoundLinker, CapSys from pydoctor.test.epydoc import parse_docstring from pydoctor.test.test_epydoc2stan import docstring2html from pydoctor.test.test_astbuilder import fromText from pydoctor.stanutils import flatten from pydoctor.napoleon.docstring import TokenType from pydoctor.epydoc.markup._types import ParsedTypeDocstring from pydoctor import model from twisted.web.template import Tag def doc2html(doc: str, markup: str, processtypes: bool = False) -> str: return ''.join(prettify(flatten(parse_docstring(doc, markup, processtypes).to_stan(NotFoundLinker()))).splitlines()) def test_types_to_node_no_markup() -> None: cases = [ 'rtype: list of int or float or None', "rtype: {'F', 'C', 'N'}, default 'N'", "rtype: DataFrame, optional", "rtype: List[str] or list(bytes), optional",] for s in cases: assert doc2html(':'+s, 'restructuredtext', False) == doc2html('@'+s, 'epytext') assert doc2html(':'+s, 'restructuredtext', True) == doc2html('@'+s, 'epytext') def test_to_node_markup() -> None: cases = [ ('L{me}', '`me`'), ('B{No!}', '**No!**'), ('I{here}', '*here*'), ('L{complicated string} or L{strIO }', '`complicated string` or `strIO `') ] for epystr, rststr in cases: assert doc2html(rststr, 'restructuredtext') == doc2html(epystr, 'epytext') def test_parsed_type_convert_obj_tokens_to_stan() -> None: convert_obj_tokens_cases = [ ([("list", TokenType.OBJ), ("(", TokenType.DELIMITER), ("int", TokenType.OBJ), (")", TokenType.DELIMITER)], [(Tag('code', children=['list', '(', 'int', ')']), TokenType.OBJ)]), ([("list", TokenType.OBJ), ("(", TokenType.DELIMITER), ("int", TokenType.OBJ), (")", TokenType.DELIMITER), (", ", TokenType.DELIMITER), ("optional", TokenType.CONTROL)], [(Tag('code', children=['list', '(', 'int', ')']), TokenType.OBJ), (", ", TokenType.DELIMITER), ("optional", TokenType.CONTROL)]), ] ann = ParsedTypeDocstring("") for tokens_types, expected_token_types in convert_obj_tokens_cases: assert str(ann._convert_obj_tokens_to_stan(tokens_types, NotFoundLinker()))==str(expected_token_types) def typespec2htmlvianode(s: str, markup: str) -> str: err: List[ParseError] = [] parsed_doc = get_parser_by_name(markup)(s, err, False) assert not err ann = ParsedTypeDocstring(parsed_doc.to_node(), warns_on_unknown_tokens=True) html = flatten(ann.to_stan(NotFoundLinker())) assert not ann.warnings return html def typespec2htmlviastr(s: str) -> str: ann = ParsedTypeDocstring(s, warns_on_unknown_tokens=True) html = flatten(ann.to_stan(NotFoundLinker())) assert not ann.warnings return html def test_parsed_type() -> None: parsed_type_cases = [ ('list of int or float or None', 'list of int or float or None'), ("{'F', 'C', 'N'}, default 'N'", """{'F', 'C', 'N'}, default 'N'"""), ("DataFrame, optional", "DataFrame, optional"), ("List[str] or list(bytes), optional", "List[str] or list(bytes), optional"), (('`complicated string` or `strIO `', 'L{complicated string} or L{strIO }'), 'complicated string or strIO'), ] for string, excepted_html in parsed_type_cases: rst_string = '' epy_string = '' if isinstance(string, tuple): rst_string, epy_string = string elif isinstance(string, str): rst_string = epy_string = string assert typespec2htmlviastr(rst_string) == excepted_html assert typespec2htmlvianode(rst_string, 'restructuredtext') == excepted_html assert typespec2htmlvianode(epy_string, 'epytext') == excepted_html def test_processtypes(capsys: CapSys) -> None: """ Currently, numpy and google type parsing happens both at the string level with L{pydoctor.napoleon.docstring.TypeDocstring} and at the docutils nodes L{ParsedTypeDocstring} for type fields (``type`` and ``rtype``). """ cases = [ ( ( """ @param arg: A param. @type arg: list of int or float or None """, """ :param arg: A param. :type arg: list of int or float or None """, """ Args: arg (list of int or float or None): A param. """, """ Args ---- arg: list of int or float or None A param. """, ), ("list of int or float or None", "list of int or float or None") ), ( ( """ @param arg: A param. @type arg: L{complicated string} or L{strIO }, optional """, """ :param arg: A param. :type arg: `complicated string` or `strIO `, optional """, """ Args: arg (`complicated string` or `strIO `, optional): A param. """, """ Args ---- arg: `complicated string` or `strIO `, optional A param. """, ), ("complicated string or strIO, optional", "complicated string or strIO, optional") ), ] for strings, excepted_html in cases: epy_string, rst_string, goo_string, numpy_string = strings excepted_html_no_process_types, excepted_html_type_processed = excepted_html assert flatten(parse_docstring(epy_string, 'epytext').fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_no_process_types assert flatten(parse_docstring(rst_string, 'restructuredtext').fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_no_process_types assert flatten(parse_docstring(dedent(goo_string), 'google').fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_type_processed assert flatten(parse_docstring(dedent(numpy_string), 'numpy').fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_type_processed assert flatten(parse_docstring(epy_string, 'epytext', processtypes=True).fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_type_processed assert flatten(parse_docstring(rst_string, 'restructuredtext', processtypes=True).fields[-1].body().to_stan(NotFoundLinker())) == excepted_html_type_processed def test_processtypes_more() -> None: # Using numpy style-only because it suffice. cases = [ (""" Yields ------ working: bool Whether it's working. not_working: bool Whether it's not working. """, """
    • working: bool - Whether it's working.
    • not_working: bool - Whether it's not working.
    """), (""" Returns ------- name: str the name description. content: str the content description. """, """
    • name: str - the name description.
    • content: str - the content description.
    """), ] for string, excepted_html in cases: assert flatten(parse_docstring(dedent(string), 'numpy').fields[-1].body().to_stan(NotFoundLinker())).strip() == excepted_html def test_processtypes_with_system(capsys: CapSys) -> None: system = model.System() system.options.processtypes = True mod = fromText(''' a = None """ Variable documented by inline docstring. @type: list of int or float or None """ ''', modname='test', system=system) a = mod.contents['a'] docstring2html(a) assert isinstance(a.parsed_type, ParsedTypeDocstring) fmt = flatten(a.parsed_type.to_stan(NotFoundLinker())) captured = capsys.readouterr().out assert not captured assert "list of int or float or None" == fmt def test_processtypes_corner_cases(capsys: CapSys) -> None: """ The corner cases does not trigger any warnings because they are still valid types. Warnings should be triggered in L{pydoctor.napoleon.docstring.TypeDocstring._trigger_warnings}, we should be careful with triggering warnings because whether the type spec triggers warnings is used to check is a string is a valid type or not. """ def process(typestr: str) -> str: system = model.System() system.options.processtypes = True mod = fromText(f''' a = None """ @type: {typestr} """ ''', modname='test', system=system) a = mod.contents['a'] docstring2html(a) assert isinstance(a.parsed_type, ParsedTypeDocstring) fmt = flatten(a.parsed_type.to_stan(NotFoundLinker())) captured = capsys.readouterr().out assert not captured return fmt assert process('default[str]') == "default[str]" assert process('[str]') == "[str]" assert process('[,]') == "[, ]" assert process('[[]]') == "[[]]" assert process(', [str]') == ", [str]" assert process(' of [str]') == "of [str]" # this is a bit weird assert process(' or [str]') == "or[str]" assert process(': [str]') == ": [str]" assert process("'hello'[str]") == "'hello'[str]" assert process('"hello"[str]') == "\"hello\"[str]" assert process('`hello`[str]') == "hello[str]" assert process('`hello `_[str]') == """hello[str]""" assert process('**hello**[str]') == "hello[str]" assert process('["hello" or str, default: 2]') == """["hello" or str, default: 2]""" assert process('Union[`hello <>`_[str]]') == """Union[`hello <>`_[str]]""" def test_processtypes_warning_unexpected_element(capsys: CapSys) -> None: epy_string = """ @param arg: A param. @type arg: L{complicated string} or L{strIO }, optional >>> print('example') """ rst_string = """ :param arg: A param. :type arg: `complicated string` or `strIO `, optional >>> print('example') """ expected = """complicated string or strIO, optional""" # Test epytext epy_errors: List[ParseError] = [] epy_parsed = get_parser_by_name('epytext')(epy_string, epy_errors, True) assert len(epy_errors)==1 assert "Unexpected element in type specification field: element 'doctest_block'" in epy_errors.pop().descr() assert flatten(epy_parsed.fields[-1].body().to_stan(NotFoundLinker())).replace('\n', '') == expected # Test restructuredtext rst_errors: List[ParseError] = [] rst_parsed = get_parser_by_name('restructuredtext')(rst_string, rst_errors, True) assert len(rst_errors)==1 assert "Unexpected element in type specification field: element 'doctest_block'" in rst_errors.pop().descr() assert flatten(rst_parsed.fields[-1].body().to_stan(NotFoundLinker())).replace('\n', ' ') == expected pydoctor-21.12.1/pydoctor/test/test_utils.py000066400000000000000000000032331416703725300211250ustar00rootroot00000000000000from typing import Dict, Optional import pytest from pydoctor.templatewriter.util import CaseInsensitiveDict class TestCaseInsensitiveDict: @pytest.fixture(autouse=True) def setup(self) -> None: """CaseInsensitiveDict instance with "Accept" header.""" self.case_insensitive_dict: CaseInsensitiveDict[str] = CaseInsensitiveDict() self.case_insensitive_dict['Accept'] = 'application/json' def test_list(self) -> None: assert list(self.case_insensitive_dict) == ['Accept'] possible_keys = pytest.mark.parametrize('key', ('accept', 'ACCEPT', 'aCcEpT', 'Accept')) @possible_keys def test_getitem(self, key: str) -> None: assert self.case_insensitive_dict[key] == 'application/json' @possible_keys def test_delitem(self, key: str) -> None: del self.case_insensitive_dict[key] assert key not in self.case_insensitive_dict def test_lower_items(self) -> None: assert list(self.case_insensitive_dict.lower_items()) == [('accept', 'application/json')] def test_repr(self) -> None: assert repr(self.case_insensitive_dict) == "{'Accept': 'application/json'}" def test_copy(self) -> None: copy = self.case_insensitive_dict.copy() assert copy is not self.case_insensitive_dict assert copy == self.case_insensitive_dict @pytest.mark.parametrize( 'other, result', ( ({'AccePT': 'application/json'}, True), ({}, False), (None, False) ) ) def test_instance_equality(self, other: Optional[Dict[str, str]], result: bool) -> None: assert (self.case_insensitive_dict == other) is result pydoctor-21.12.1/pydoctor/test/test_zopeinterface.py000066400000000000000000000415141416703725300226270ustar00rootroot00000000000000 from typing import cast from pydoctor.test.test_astbuilder import fromText, type2html from pydoctor.test.test_packages import processPackage from pydoctor.zopeinterface import ZopeInterfaceClass, ZopeInterfaceSystem from pydoctor.epydoc.markup import ParsedDocstring from pydoctor import model from pydoctor.stanutils import flatten from . import CapSys, NotFoundLinker # we set up the same situation using both implements and # classImplements and run the same tests. def test_implements() -> None: src = ''' import zope.interface class IFoo(zope.interface.Interface): pass class IBar(zope.interface.Interface): pass class Foo: zope.interface.implements(IFoo) class FooBar(Foo): zope.interface.implements(IBar) class OnlyBar(Foo): zope.interface.implementsOnly(IBar) ''' implements_test(src) def test_classImplements() -> None: src = ''' import zope.interface class IFoo(zope.interface.Interface): pass class IBar(zope.interface.Interface): pass class Foo: pass class FooBar(Foo): pass class OnlyBar(Foo): pass zope.interface.classImplements(Foo, IFoo) zope.interface.classImplements(FooBar, IBar) zope.interface.classImplementsOnly(OnlyBar, IBar) ''' implements_test(src) def test_implementer() -> None: src = ''' import zope.interface class IFoo(zope.interface.Interface): pass class IBar(zope.interface.Interface): pass @zope.interface.implementer(IFoo) class Foo: pass @zope.interface.implementer(IBar) class FooBar(Foo): pass class OnlyBar(Foo): zope.interface.implementsOnly(IBar) ''' implements_test(src) def implements_test(src: str) -> None: mod = fromText(src, modname='zi', systemcls=ZopeInterfaceSystem) ifoo = mod.contents['IFoo'] ibar = mod.contents['IBar'] foo = mod.contents['Foo'] foobar = mod.contents['FooBar'] onlybar = mod.contents['OnlyBar'] assert isinstance(ifoo, ZopeInterfaceClass) assert isinstance(ibar, ZopeInterfaceClass) assert isinstance(foo, ZopeInterfaceClass) assert isinstance(foobar, ZopeInterfaceClass) assert isinstance(onlybar, ZopeInterfaceClass) assert ifoo.isinterface and ibar.isinterface assert not foo.isinterface and not foobar.isinterface and not foobar.isinterface assert not foo.implementsOnly and not foobar.implementsOnly assert onlybar.implementsOnly assert foo.implements_directly == ['zi.IFoo'] assert foo.allImplementedInterfaces == ['zi.IFoo'] assert foobar.implements_directly == ['zi.IBar'] assert foobar.allImplementedInterfaces == ['zi.IBar', 'zi.IFoo'] assert onlybar.implements_directly == ['zi.IBar'] assert onlybar.allImplementedInterfaces == ['zi.IBar'] assert ifoo.implementedby_directly == [foo] assert ibar.implementedby_directly == [foobar, onlybar] def test_subclass_with_same_name() -> None: src = ''' class A: pass class A(A): pass ''' fromText(src, modname='zi', systemcls=ZopeInterfaceSystem) def test_multiply_inheriting_interfaces() -> None: src = ''' from zope.interface import Interface, implements class IOne(Interface): pass class ITwo(Interface): pass class One: implements(IOne) class Two: implements(ITwo) class Both(One, Two): pass ''' mod = fromText(src, modname='zi', systemcls=ZopeInterfaceSystem) B = mod.contents['Both'] assert isinstance(B, ZopeInterfaceClass) assert len(list(B.allImplementedInterfaces)) == 2 def test_attribute(capsys: CapSys) -> None: src = ''' import zope.interface as zi class C(zi.Interface): attr = zi.Attribute("documented attribute") bad_attr = zi.Attribute(0) ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) assert len(mod.contents['C'].contents) == 2 attr = mod.contents['C'].contents['attr'] assert attr.kind is model.DocumentableKind.ATTRIBUTE assert attr.name == 'attr' assert attr.docstring == "documented attribute" bad_attr = mod.contents['C'].contents['bad_attr'] assert bad_attr.kind is model.DocumentableKind.ATTRIBUTE assert bad_attr.name == 'bad_attr' assert bad_attr.docstring is None captured = capsys.readouterr().out assert captured == 'mod:5: definition of attribute "bad_attr" should have docstring as its sole argument\n' def test_interfaceclass() -> None: system = processPackage('interfaceclass', systemcls=ZopeInterfaceSystem) mod = system.allobjects['interfaceclass.mod'] I = mod.contents['MyInterface'] assert isinstance(I, ZopeInterfaceClass) assert I.isinterface assert I.docstring == "This is my interface." J = mod.contents['AnInterface'] assert isinstance(J, ZopeInterfaceClass) assert J.isinterface def test_warnerproofing() -> None: src = ''' from zope import interface Interface = interface.Interface class IMyInterface(Interface): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) I = mod.contents['IMyInterface'] assert isinstance(I, ZopeInterfaceClass) assert I.isinterface def test_zopeschema(capsys: CapSys) -> None: src = ''' from zope import schema, interface class IMyInterface(interface.Interface): text = schema.TextLine(description="fun in a bun") undoc = schema.Bool() bad = schema.ASCII(description=False) ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) text = mod.contents['IMyInterface'].contents['text'] assert text.docstring == 'fun in a bun' assert type2html(text)== "schema.TextLine" assert text.kind is model.DocumentableKind.SCHEMA_FIELD undoc = mod.contents['IMyInterface'].contents['undoc'] assert undoc.docstring is None assert type2html(undoc) == "schema.Bool" assert undoc.kind is model.DocumentableKind.SCHEMA_FIELD bad = mod.contents['IMyInterface'].contents['bad'] assert bad.docstring is None assert type2html(bad) == "schema.ASCII" assert bad.kind is model.DocumentableKind.SCHEMA_FIELD captured = capsys.readouterr().out assert captured == 'mod:6: description of field "bad" is not a string literal\n' def test_aliasing_in_class() -> None: src = ''' from zope import interface class IMyInterface(interface.Interface): Attrib = interface.Attribute attribute = Attrib("fun in a bun") ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) attr = mod.contents['IMyInterface'].contents['attribute'] assert attr.docstring == 'fun in a bun' assert attr.kind is model.DocumentableKind.ATTRIBUTE def test_zopeschema_inheritance() -> None: src = ''' from zope import schema, interface from zope.schema import Int as INTEGERSCHMEMAFIELD class MyTextLine(schema.TextLine): pass class MyOtherTextLine(MyTextLine): pass class IMyInterface(interface.Interface): mytext = MyTextLine(description="fun in a bun") myothertext = MyOtherTextLine(description="fun in another bun") myint = INTEGERSCHMEMAFIELD(description="not as much fun") ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) mytext = mod.contents['IMyInterface'].contents['mytext'] assert mytext.docstring == 'fun in a bun' assert flatten(cast(ParsedDocstring, mytext.parsed_type).to_stan(NotFoundLinker())) == "MyTextLine" assert mytext.kind is model.DocumentableKind.SCHEMA_FIELD myothertext = mod.contents['IMyInterface'].contents['myothertext'] assert myothertext.docstring == 'fun in another bun' assert flatten(cast(ParsedDocstring, myothertext.parsed_type).to_stan(NotFoundLinker())) == "MyOtherTextLine" assert myothertext.kind is model.DocumentableKind.SCHEMA_FIELD myint = mod.contents['IMyInterface'].contents['myint'] assert flatten(cast(ParsedDocstring, myint.parsed_type).to_stan(NotFoundLinker())) == "INTEGERSCHMEMAFIELD" assert myint.kind is model.DocumentableKind.SCHEMA_FIELD def test_docsources_includes_interface() -> None: src = ''' from zope import interface class IInterface(interface.Interface): def method(self): """documentation""" class Implementation: interface.implements(IInterface) def method(self): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) imethod = mod.contents['IInterface'].contents['method'] method = mod.contents['Implementation'].contents['method'] assert imethod in method.docsources(), list(method.docsources()) def test_docsources_includes_baseinterface() -> None: src = ''' from zope import interface class IBase(interface.Interface): def method(self): """documentation""" class IExtended(IBase): pass class Implementation: interface.implements(IExtended) def method(self): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) imethod = mod.contents['IBase'].contents['method'] method = mod.contents['Implementation'].contents['method'] assert imethod in method.docsources(), list(method.docsources()) def test_docsources_interface_attribute() -> None: src = ''' from zope import interface class IInterface(interface.Interface): attr = interface.Attribute("""documentation""") @interface.implementer(IInterface) class Implementation: attr = True ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) iattr = mod.contents['IInterface'].contents['attr'] attr = mod.contents['Implementation'].contents['attr'] assert iattr in list(attr.docsources()) def test_implementer_decoration() -> None: src = ''' from zope.interface import Interface, implementer class IMyInterface(Interface): def method(self): """documentation""" @implementer(IMyInterface) class Implementation: def method(self): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) iface = mod.contents['IMyInterface'] impl = mod.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert impl.implements_directly == [iface.fullName()] def test_docsources_from_moduleprovides() -> None: src = ''' from zope import interface class IBase(interface.Interface): def bar(): """documentation""" interface.moduleProvides(IBase) def bar(): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) imethod = mod.contents['IBase'].contents['bar'] function = mod.contents['bar'] assert imethod in function.docsources(), list(function.docsources()) def test_interfaceallgames() -> None: system = processPackage('interfaceallgames', systemcls=ZopeInterfaceSystem) mod = system.allobjects['interfaceallgames.interface'] iface = mod.contents['IAnInterface'] assert isinstance(iface, ZopeInterfaceClass) assert [o.fullName() for o in iface.implementedby_directly] == [ 'interfaceallgames.implementation.Implementation' ] def test_implementer_with_star() -> None: """ If the implementer call contains a split out empty list, don't fail on attempting to process it. """ src = ''' from zope.interface import Interface, implementer extra_interfaces = () class IMyInterface(Interface): def method(self): """documentation""" @implementer(IMyInterface, *extra_interfaces) class Implementation: def method(self): pass ''' mod = fromText(src, systemcls=ZopeInterfaceSystem) iface = mod.contents['IMyInterface'] impl = mod.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert isinstance(iface, ZopeInterfaceClass) assert impl.implements_directly == [iface.fullName()] def test_implementer_nonname(capsys: CapSys) -> None: """ Non-name arguments passed to @implementer are warned about and then ignored. """ src = ''' from zope.interface import implementer @implementer(123) class Implementation: pass ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) impl = mod.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert impl.implements_directly == [] captured = capsys.readouterr().out assert captured == 'mod:3: Interface argument 1 does not look like a name\n' def test_implementer_nonclass(capsys: CapSys) -> None: """ Non-class arguments passed to @implementer are warned about but are stored as implemented interfaces. """ src = ''' from zope.interface import implementer var = 'not a class' @implementer(var) class Implementation: pass ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) impl = mod.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert impl.implements_directly == ['mod.var'] captured = capsys.readouterr().out assert captured == 'mod:4: Supposed interface "mod.var" not detected as a class\n' def test_implementer_plainclass(capsys: CapSys) -> None: """ A non-interface class passed to @implementer will be warned about but will be stored as an implemented interface. """ src = ''' from zope.interface import implementer class C: pass @implementer(C) class Implementation: pass ''' mod = fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) C = mod.contents['C'] impl = mod.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert isinstance(C, ZopeInterfaceClass) assert not C.isinterface assert C.kind is model.DocumentableKind.CLASS assert impl.implements_directly == ['mod.C'] captured = capsys.readouterr().out assert captured == 'mod:5: Class "mod.C" is not an interface\n' def test_implementer_not_found(capsys: CapSys) -> None: """ An unknown class passed to @implementer is warned about if its full name is part of our system. """ src = ''' from zope.interface import implementer from twisted.logger import ILogObserver @implementer(ILogObserver, mod.INoSuchInterface) class Implementation: pass ''' fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) captured = capsys.readouterr().out assert captured == 'mod:4: Interface "mod.INoSuchInterface" not found\n' def test_implementer_reparented() -> None: """ A class passed to @implementer can be found even when it is moved to a different module. """ system = ZopeInterfaceSystem() mod_iface = fromText(''' from zope.interface import Interface class IMyInterface(Interface): pass ''', modname='_private', system=system) mod_export = fromText('', modname='public', system=system) mod_impl = fromText(''' from zope.interface import implementer from _private import IMyInterface @implementer(IMyInterface) class Implementation: pass ''', modname='app', system=system) iface = mod_iface.contents['IMyInterface'] assert isinstance(iface, ZopeInterfaceClass) iface.reparent(mod_export, 'IMyInterface') assert iface.fullName() == 'public.IMyInterface' assert 'IMyInterface' not in mod_iface.contents impl = mod_impl.contents['Implementation'] assert isinstance(impl, ZopeInterfaceClass) assert impl.implements_directly == ['_private.IMyInterface'] assert iface.implementedby_directly == [] system.postProcess() assert impl.implements_directly == ['public.IMyInterface'] assert iface.implementedby_directly == [impl] def test_implementer_nocall(capsys: CapSys) -> None: """ Report a warning when @implementer is used without calling it. """ src = ''' import zope.interface @zope.interface.implementer class C: pass ''' fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) captured = capsys.readouterr().out assert captured == "mod:3: @implementer requires arguments\n" def test_classimplements_badarg(capsys: CapSys) -> None: """ Report a warning when the arguments to classImplements() don't make sense. """ src = ''' from zope.interface import Interface, classImplements class IBar(Interface): pass def f(): pass classImplements() classImplements(None, IBar) classImplements(f, IBar) classImplements(g, IBar) ''' fromText(src, modname='mod', systemcls=ZopeInterfaceSystem) captured = capsys.readouterr().out assert captured == ( 'mod:7: required argument to classImplements() missing\n' 'mod:8: argument 1 to classImplements() is not a class name\n' 'mod:9: argument "mod.f" to classImplements() is not a class\n' 'mod:10: argument "g" to classImplements() not found\n' ) pydoctor-21.12.1/pydoctor/test/testcustomtemplates/000077500000000000000000000000001416703725300225045ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/allok/000077500000000000000000000000001416703725300236065ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/allok/nav.html000066400000000000000000000010571416703725300252630ustar00rootroot00000000000000 pydoctor-21.12.1/pydoctor/test/testcustomtemplates/allok/pydoctor.js000066400000000000000000000000311416703725300260010ustar00rootroot00000000000000alert( 'Hello, world!' );pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/000077500000000000000000000000001416703725300237505ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test1/000077500000000000000000000000001416703725300250105ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test1/nav.HTML000066400000000000000000000000001416703725300262500ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test2/000077500000000000000000000000001416703725300250115ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test2/nav.Html000066400000000000000000000000001416703725300264110ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test3/000077500000000000000000000000001416703725300250125ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/casing/test3/nav.htmL000066400000000000000000000000001416703725300264120ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/000077500000000000000000000000001416703725300251465ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/footer.html000066400000000000000000000001271416703725300273320ustar00rootroot00000000000000
    Footer2
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/header.html000066400000000000000000000000701416703725300272610ustar00rootroot00000000000000
    Header
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/nav.html000066400000000000000000000011471416703725300266230ustar00rootroot00000000000000 pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/pydoctor.js000066400000000000000000000000321416703725300273420ustar00rootroot00000000000000alert( 'Hello, world!' ); pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/random.html000066400000000000000000000000341416703725300273110ustar00rootroot00000000000000 Random words pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/subheader.html000066400000000000000000000000531416703725300277740ustar00rootroot00000000000000
    Page header
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/summary.html000066400000000000000000000001161416703725300275270ustar00rootroot00000000000000
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/faketemplate/table.html000066400000000000000000000001651416703725300271250ustar00rootroot00000000000000
    Random words
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/overridesubfolders/000077500000000000000000000000001416703725300264145ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/overridesubfolders/static/000077500000000000000000000000001416703725300277035ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/overridesubfolders/static/fonts/000077500000000000000000000000001416703725300310345ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/overridesubfolders/static/fonts/foo.svg000066400000000000000000000000171416703725300323360ustar00rootroot00000000000000I'm not empty! pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/000077500000000000000000000000001416703725300246545ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/atemplate.html000066400000000000000000000000701416703725300275130ustar00rootroot00000000000000
    html template here
    pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/000077500000000000000000000000001416703725300261435ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/fonts/000077500000000000000000000000001416703725300272745ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/fonts/bar.svg000066400000000000000000000000001416703725300305470ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/fonts/foo.svg000066400000000000000000000000001416703725300305660ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/info.svg000066400000000000000000000000001416703725300276050ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testcustomtemplates/subfolders/static/lol.svg000066400000000000000000000000001416703725300274400ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/000077500000000000000000000000001416703725300210315ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/allgames/000077500000000000000000000000001416703725300226165ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/allgames/__init__.py000066400000000000000000000004071416703725300247300ustar00rootroot00000000000000# pre comment -7 # pre comment -6 # pre comment -5 # pre comment -4 # pre comment -3 # pre comment -2 # pre comment -1 """Package docstring.""" # post comment 1 # post comment 2 # post comment 3 # post comment 4 # post comment 5 # post comment 6 # post comment 7 pydoctor-21.12.1/pydoctor/test/testpackages/allgames/mod1.py000066400000000000000000000001301416703725300240220ustar00rootroot00000000000000 __all__ = ['InSourceAll'] class InSourceAll: pass class NotInSourceAll: pass pydoctor-21.12.1/pydoctor/test/testpackages/allgames/mod2.py000066400000000000000000000001431416703725300240270ustar00rootroot00000000000000__all__ = ['InSourceAll', 'NotInSourceAll'] from allgames.mod1 import InSourceAll, NotInSourceAll pydoctor-21.12.1/pydoctor/test/testpackages/basic/000077500000000000000000000000001416703725300221125ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/basic/__init__.py000066400000000000000000000004071416703725300242240ustar00rootroot00000000000000# pre comment -7 # pre comment -6 # pre comment -5 # pre comment -4 # pre comment -3 # pre comment -2 # pre comment -1 """Package docstring.""" # post comment 1 # post comment 2 # post comment 3 # post comment 4 # post comment 5 # post comment 6 # post comment 7 pydoctor-21.12.1/pydoctor/test/testpackages/basic/_private_mod.py000066400000000000000000000000221416703725300251260ustar00rootroot00000000000000def f(): pass pydoctor-21.12.1/pydoctor/test/testpackages/basic/mod.py000066400000000000000000000016341416703725300232470ustar00rootroot00000000000000""" Module docstring. @var CONSTANT: A shiny constant. """ class C: """Class docstring. This docstring has lines, paragraphs and everything! Please see L{CONSTANT}. @ivar notreally: even a field! @since: 2.1 """ class S: pass def f(self): """Method docstring of C.f.""" @some_random_decorator @some_other_decorator def h(self): """Method docstring.""" @some_random_decorator @classmethod def cls_method(cls): pass @staticmethod def static_method(): pass class D(C): """Subclass docstring.""" class T: pass def f(self): # no docstring, should be inherited from superclass pass def g(self): pass @classmethod def cls_method2(cls): pass def static_method2(): pass static_method2 = staticmethod(static_method2) def _private(): pass pydoctor-21.12.1/pydoctor/test/testpackages/codeininit/000077500000000000000000000000001416703725300231565ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/codeininit/__init__.py000066400000000000000000000000751416703725300252710ustar00rootroot00000000000000def functionInInit(self): "why do people put code here?" pydoctor-21.12.1/pydoctor/test/testpackages/cyclic_imports/000077500000000000000000000000001416703725300240545ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/cyclic_imports/__init__.py000066400000000000000000000000001416703725300261530ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/cyclic_imports/a.py000066400000000000000000000000441416703725300246440ustar00rootroot00000000000000from .b import B class A: b: B pydoctor-21.12.1/pydoctor/test/testpackages/cyclic_imports/b.py000066400000000000000000000000441416703725300246450ustar00rootroot00000000000000from .a import A class B: a: A pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/000077500000000000000000000000001416703725300252415ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/__init__.py000066400000000000000000000000001416703725300273400ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/mod.py000066400000000000000000000000601416703725300263660ustar00rootroot00000000000000from importingfrompackage.subpack import submod pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/subpack/000077500000000000000000000000001416703725300266715ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/subpack/__init__.py000066400000000000000000000000001416703725300307700ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/importingfrompackage/subpack/submod.py000066400000000000000000000000021416703725300305240ustar00rootroot00000000000000# pydoctor-21.12.1/pydoctor/test/testpackages/interfaceallgames/000077500000000000000000000000001416703725300244775ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/interfaceallgames/__init__.py000066400000000000000000000000021416703725300266000ustar00rootroot00000000000000# pydoctor-21.12.1/pydoctor/test/testpackages/interfaceallgames/_implementation.py000066400000000000000000000002211416703725300302300ustar00rootroot00000000000000from zope.interface import implements from interfaceallgames.interface import IAnInterface class Implementation: implements(IAnInterface) pydoctor-21.12.1/pydoctor/test/testpackages/interfaceallgames/implementation.py000066400000000000000000000001331416703725300300730ustar00rootroot00000000000000from interfaceallgames._implementation import Implementation __all__ = ["Implementation"] pydoctor-21.12.1/pydoctor/test/testpackages/interfaceallgames/interface.py000066400000000000000000000001171416703725300270100ustar00rootroot00000000000000from zope.interface import Interface class IAnInterface(Interface): pass pydoctor-21.12.1/pydoctor/test/testpackages/interfaceclass/000077500000000000000000000000001416703725300240175ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/interfaceclass/__init__.py000066400000000000000000000000071416703725300261250ustar00rootroot00000000000000#empty pydoctor-21.12.1/pydoctor/test/testpackages/interfaceclass/mod.py000066400000000000000000000004551416703725300251540ustar00rootroot00000000000000import zope.interface as zi import zope.schema as zs class MyInterfaceClass(zi.interface.InterfaceClass): pass MyInterface = MyInterfaceClass("MyInterface") """This is my interface.""" class AnInterface(MyInterface): def foo(): pass a = zi.Attribute("...") f = zs.Choice() pydoctor-21.12.1/pydoctor/test/testpackages/liveobject/000077500000000000000000000000001416703725300231575ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/liveobject/__init__.py000066400000000000000000000000101416703725300252570ustar00rootroot00000000000000# empty pydoctor-21.12.1/pydoctor/test/testpackages/liveobject/mod.py000066400000000000000000000002611416703725300243070ustar00rootroot00000000000000class C: def m(self): "this is a docstring" def __m(self): "this method's name gets mangled" m = C().m class B: pass exec('''class D(B): pass''') pydoctor-21.12.1/pydoctor/test/testpackages/modnamedafterbuiltin/000077500000000000000000000000001416703725300252265ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/modnamedafterbuiltin/__init__.py000066400000000000000000000000101416703725300273260ustar00rootroot00000000000000# empty pydoctor-21.12.1/pydoctor/test/testpackages/modnamedafterbuiltin/dict.py000066400000000000000000000000201416703725300265130ustar00rootroot00000000000000# empty too :-) pydoctor-21.12.1/pydoctor/test/testpackages/modnamedafterbuiltin/mod.py000066400000000000000000000000331416703725300263530ustar00rootroot00000000000000class Dict(dict): pass pydoctor-21.12.1/pydoctor/test/testpackages/multipleinheritance/000077500000000000000000000000001416703725300250765ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/multipleinheritance/__init__.py000066400000000000000000000000001416703725300271750ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/multipleinheritance/mod.py000066400000000000000000000012301416703725300262230ustar00rootroot00000000000000class NewBaseClassA: def methodA(self): """ This is method A. """ class NewBaseClassB: def methodB(self): """ This is method B. """ class NewClassThatMultiplyInherits(NewBaseClassA, NewBaseClassB): def methodC(self): """ This is method C. """ class OldBaseClassA: def methodA(self): """ This is method A. """ class OldBaseClassB: def methodB(self): """ This is method B. """ class OldClassThatMultiplyInherits(OldBaseClassA, OldBaseClassB): def methodC(self): """ This is method C. """ pydoctor-21.12.1/pydoctor/test/testpackages/nestedconfusion/000077500000000000000000000000001416703725300242375ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/nestedconfusion/__init__.py000066400000000000000000000000021416703725300263400ustar00rootroot00000000000000# pydoctor-21.12.1/pydoctor/test/testpackages/nestedconfusion/mod.py000066400000000000000000000001061416703725300253650ustar00rootroot00000000000000class C: pass class nestedconfusion: class A(C): pass pydoctor-21.12.1/pydoctor/test/testpackages/relativeimporttest/000077500000000000000000000000001416703725300247775ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/relativeimporttest/__init__.py000066400000000000000000000000471416703725300271110ustar00rootroot00000000000000# this is for testing! """DOCSTRING""" pydoctor-21.12.1/pydoctor/test/testpackages/relativeimporttest/mod1.py000066400000000000000000000001161416703725300262070ustar00rootroot00000000000000from .mod2 import B class C(B): """This is not a docstring.""" pass pydoctor-21.12.1/pydoctor/test/testpackages/relativeimporttest/mod2.py000066400000000000000000000000221416703725300262040ustar00rootroot00000000000000class B: pass pydoctor-21.12.1/pydoctor/test/testpackages/report_trigger/000077500000000000000000000000001416703725300240675ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/test/testpackages/report_trigger/__init__.py000066400000000000000000000002151416703725300261760ustar00rootroot00000000000000""" This is used to check reporting handling as part of functional tests. """ def top_level() -> None: """ @bad_field: bla """ pydoctor-21.12.1/pydoctor/test/testpackages/report_trigger/report_module.py000066400000000000000000000002351416703725300273210ustar00rootroot00000000000000""" Just a module which will raise some reports. """ def missing_arg_docs(x: int) -> None: """ There is no apidoc for `x`. L{Bad Link} """ pydoctor-21.12.1/pydoctor/themes/000077500000000000000000000000001416703725300166615ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/themes/__init__.py000066400000000000000000000013461416703725300207760ustar00rootroot00000000000000""" Package directory used to store pydoctor templates. Usage example: >>> template_lookup = TemplateLookup(importlib_resources.files('pydoctor.themes') / 'base') """ import sys from typing import Iterator # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. if sys.version_info < (3, 9): import importlib_resources else: import importlib.resources as importlib_resources def get_themes() -> Iterator[str]: """ Get the list of the available themes. """ for path in importlib_resources.files('pydoctor.themes').iterdir(): if not path.name.startswith('_') and not path.is_file(): yield path.name pydoctor-21.12.1/pydoctor/themes/base/000077500000000000000000000000001416703725300175735ustar00rootroot00000000000000pydoctor-21.12.1/pydoctor/themes/base/apidocs.css000066400000000000000000000235211416703725300217320ustar00rootroot00000000000000body { display: flex; flex-direction: column; min-height: 100vh; overflow-y: scroll; } nav.navbar { width:100%; margin-bottom: 0; } nav.navbar > .navbar-header { margin-right: 0; margin-left: 0; height: 100%; display: inline-block; } .page-header { margin-top: 22px; position: sticky; top: 0; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; background-color: #fff; margin-bottom: 3px; border-bottom: 0; box-shadow: 0 0 8px 8px #fff; } .navbar-brand { padding: 0; } .navbar-brand a, .navbar-brand span { color:#777777; padding: 15px; display: inline-block; } .navbar-brand *:first-child { padding-right: 0; } .navbar-brand *:last-child { padding-left: 0; padding-right: 0; } .navbar-brand a:hover { color: #444444; text-decoration: none; } a.projecthome:hover { color: #23527c; } .navlinks { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; } .navlinks > a { padding: 10px 0 10px 15px; } .navlinks > a:hover { background-color: transparent; text-decoration: none; } .page-header h1 { margin: 0; } .categoryHeader { font-size: 24px; color: #777; margin-bottom: 1.8em; } /* Footer */ footer.navbar { margin: auto 0 0 0; padding-top: 15px; padding-bottom: 15px; background-color: #fff; border-width: 1px 0 0 0; border-radius: 0; text-align: center; } a[name] { position: relative; bottom: 80px; font-size: 0; } ul { margin-top: 10px; margin-left: 10px; padding-left: 10px; } li { padding-top: 5px; padding-bottom: 5px; } li a { text-decoration: none; } ul ul { border-left-color: #e1f5fe; border-left-width: 1px; border-left-style: solid; } ul ul ul { border-left-color: #b3e5fc; } ul ul ul ul { border-left-color: #81d4fa; } ul ul ul ul ul { border-left-color: #4fc3f7; } ul ul ul ul ul ul { border-left-color: #29b6f6; } ul ul ul ul ul ul ul { border-left-color: #03a9f4; } ul ul ul ul ul ul ul { border-left-color: #039be5; } .pre { white-space: pre; } .undocumented { font-style: italic; color: #9e9e9e; } .functionBody p { margin-top: 6px; margin-bottom: 6px; } #splitTables > p { margin-bottom: 5px; } #splitTables > table { margin-bottom: 20px; width: 100%; border: 0; } #splitTables > table tr { border-bottom-color: #eee; border-bottom-width: 1px; border-bottom-style: solid; width: 100%; } #splitTables > table tr td { padding: 5px; border-left-color: #eee; border-left-width: 1px; border-left-style: solid; } .fieldTable { width: 100%; display: block; border: 0; } /* Arg name */ .fieldArg { margin-right: 7px; } .fieldArg:before { margin-right: 6px; content: "\2022"; font-size: 14px; } .fieldTable tr:not(.fieldStart) td:first-child, .valueTable tr:not(.fieldStart) td:first-child{ padding: 3px 4px 3px 10px; } .fieldTable tr td { padding: 2px; } /* Argument name + type column table */ .fieldTable tr td.fieldArgContainer { width: 250px; word-break: break-word; } /* parameters names in parameters table */ .fieldTable tr td.fieldArgContainer > .fieldArg { display: inline; } /* parameters types (in parameters table) */ .fieldTable tr td.fieldArgContainer > code { /* we don't want word break for the types because we already add tags inside the type HTML, and that should suffice. */ word-break: normal; display: inline-flex; flex-wrap: wrap; } /* Argument description column or return value desc, etc */ .fieldTable tr td::nth-child(2) { padding-left: 10px; } /* Kind column table */ #splitTables > table tr td:first-child { /* border-left: none; */ width: 150px; } /* Attr name column table */ #splitTables > table tr td:nth-child(2) { width: 200px; word-break: break-word; } /* For smaller displays, i.e. half screen or mobile phone */ @media only screen and (max-width: 1000px) { /* Fix size of summary table columns */ #splitTables > table { table-layout: fixed; } /* Kind column table */ #splitTables > table tr td:first-child { border-left: none; width: 20%; } /* Attr name column table */ #splitTables > table tr td:nth-child(2) { width: 35%; } /* Summary column table */ #splitTables > table tr td:nth-child(3) { width: 45%; } } @media only screen and (max-width: 650px) { /* Argument name + type column table */ .fieldTable tr td.fieldArgContainer { width: 175px; } } @media only screen and (max-width: 400px) { /* Argument name + type column table */ .fieldTable tr td.fieldArgContainer { width: 125px; } } tr.package { background-color: #fff3e0; } tr.module { background-color: #fff8e1; } tr.class, tr.classvariable, tr.baseclassvariable { background-color: #fffde7; } tr.instancevariable, tr.baseinstancevariable, tr.variable, tr.attribute, tr.property { background-color: #f3e5f5; } tr.interface { background-color: #fbe9e7; } tr.method, tr.function, tr.basemethod, tr.baseclassmethod, tr.classmethod { background-color: #f1f8e9; } tr.private { background-color: #f1f1f1; } .fieldName { font-weight: bold; } #childList > div { margin: 10px; padding: 10px; padding-bottom: 5px; } .functionBody { margin-left: 15px; } .functionBody > #part { font-style: italic; } .functionBody > #part > a { text-decoration: none; } .functionBody .interfaceinfo { font-style: italic; margin-bottom: 3px; margin-top: 3px; } .functionBody > .undocumented { margin-top: 6px; margin-bottom: 6px; } /* - Links to class/function/etc names are nested like this: label This applies to inline docstring content marked up as code, for example L{foo} in epytext or `bar` in restructuredtext, but also to links that are present in summary tables. - 'functionHeader' is used for lines like `def func():` and `var =` */ code, .literal, .pre, #childList > div .functionHeader, #splitTables > table tr td:nth-child(2), .fieldArg { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } code, #childList > div .functionHeader, .fieldArg { color: #222222; } code > a, #childList > div .functionHeader a { color:#c7254e; background-color:#f9f2f4; } /* top navagation bar */ .page-header > h1 { margin-top: 0; } .page-header > h1 > code { color: #971c3a; } /* This defines the code style, it's black on light gray. It also overwrite the default values inherited from bootstrap min */ code, .literal { padding:2px 4px; background-color: #f4f4f4; border-radius:4px } a.sourceLink { color: #337ab7!important; font-weight: normal; background-color: transparent!important; } #childList > div { border-left-color: #03a9f4; border-left-width: 1px; border-left-style: solid; background: #fafafa; } .moduleDocstring { margin: 20px; } #partOf { margin-top: -13px; margin-bottom: 19px; } .fromInitPy { font-style: italic; } pre { padding-left: 0; } /* Private stuff */ body.private-hidden #splitTables .private, body.private-hidden #childList .private, body.private-hidden #summaryTree .private { display: none; } /* Show private */ #showPrivate:hover { text-decoration: none; } #showPrivate button { padding: 10px; } #showPrivate button:hover { text-decoration: none; } #current-docs-container { font-style: italic; padding-top: 11px; } /* Deprecation stuff */ .deprecationNotice { margin: 10px; } /* Syntax highlighting for source code */ .py-string { color: #337ab7; } .py-comment { color: #309078; font-style: italic; } .py-keyword { font-weight: bold; } .py-defname { color: #a947b8; font-weight: bold; } .py-builtin { color: #fc7844; font-weight: bold; } /* Doctest */ pre.py-doctest { padding: .5em; } .py-prompt, .py-more { color: #a8a8a8; } .py-output { color: #c7254e; } /* Admonitions */ div.rst-admonition p.rst-admonition-title:after { content: ":"; } div.rst-admonition p.rst-admonition-title { margin: 0; padding: 0.1em 0 0.35em 0em; font-weight: bold; } div.rst-admonition p.rst-admonition-title { color: #333333; } div.rst-admonition { padding: 8px; margin-bottom: 20px; background-color: #EEE; border: 1px solid #CCC; border-radius: 4px; } div.warning, div.attention, div.danger, div.error, div.caution { background-color: #ffcf9cb0; border: 1px solid #ffbbaa; } div.danger p.rst-admonition-title, div.error p.rst-admonition-title, div.caution p.rst-admonition-title { color: #b94a48; } div.tip p.rst-admonition-title, div.hint p.rst-admonition-title, div.important p.rst-admonition-title{ color: #3a87ad; } div.tip, div.hint, div.important { background-color: #d9edf7; border-color: #bce8f1; } /* Version modified style */ .rst-versionmodified { display: block; font-weight: bold; } /* Constant values repr */ pre.constant-value { padding: .5em; } .rst-variable-linewrap { color: #604000; font-weight: bold; } .rst-variable-ellipsis { color: #604000; font-weight: bold; } .rst-variable-quote { color: #604000; font-weight: bold; } /* Those two are currently not used */ .rst-variable-group { color: #000000; } .rst-variable-op { color: #000000; } .rst-variable-string { color: #337ab7; } .rst-variable-unknown { color: #a00000; font-weight: bold; } .rst-re { color: #000000; } .rst-re-char { color: #337ab7; } .rst-re-op { color: #fc7844; } .rst-re-group { color: #309078; } .rst-re-ref { color: #890000; } pydoctor-21.12.1/pydoctor/themes/base/attribute-child.html000066400000000000000000000016711416703725300235520ustar00rootroot00000000000000
    attribute = (source)
    Docstring. Value of the attribute if it's a constant.
    pydoctor-21.12.1/pydoctor/themes/base/common.html000066400000000000000000000031121416703725300217460ustar00rootroot00000000000000 Head
    Nav
    something documentation
    Inheritance info.

    View In Hierarchy

    A docstring.

    Inherited from :