├── .codecov.yml
├── .coveragerc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── art
├── logo.png
├── logo_large.png
└── logo_large.xcf
├── docs
├── contributing
│ ├── 1.-contributing-guide.md
│ ├── 2.-coding-standard.md
│ ├── 3.-code-of-conduct.md
│ └── 4.-acknowledgements.md
└── quick_start
│ ├── 1.-installation.md
│ ├── 2.-cli.md
│ └── 3.-api.md
├── pdocs
├── __init__.py
├── _version.py
├── api.py
├── cli.py
├── defaults.py
├── doc.py
├── extract.py
├── html_helpers.py
├── logo.py
├── render.py
├── static.py
└── templates
│ ├── LICENSE
│ ├── README.md
│ ├── css.mako
│ ├── html_frame.mako
│ ├── html_index.mako
│ ├── html_module.mako
│ └── text.mako
├── pyproject.toml
├── scripts
├── clean.sh
├── lint.sh
└── test.sh
├── tests
├── docstring_parser
│ ├── example_google.py
│ └── example_numpy.py
├── modules
│ ├── README
│ ├── dirmod
│ │ └── __init__.py
│ ├── index
│ │ ├── __init__.py
│ │ ├── index.py
│ │ └── two
│ │ │ └── __init__.py
│ ├── malformed
│ │ └── syntax.py
│ ├── one.py
│ └── submods
│ │ ├── __init__.py
│ │ ├── three
│ │ └── __init__.py
│ │ └── two.py
├── onpath
│ ├── README
│ ├── __init__.py
│ ├── malformed_syntax.py
│ └── simple.py
├── test_doc.py
├── test_extract.py
├── test_parse_docstring.py
├── test_pdoc.py
├── test_render.py
├── test_static.py
└── tutils.py
└── tox.ini
/.codecov.yml:
--------------------------------------------------------------------------------
1 | comment: off
2 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | raise NotImplementedError()
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | .DS_Store
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | build
10 | eggs
11 | .eggs
12 | parts
13 | var
14 | sdist
15 | develop-eggs
16 | .installed.cfg
17 | lib
18 | lib64
19 | MANIFEST
20 |
21 | # Installer logs
22 | pip-log.txt
23 | npm-debug.log
24 | pip-selfcheck.json
25 |
26 | # Unit test / coverage reports
27 | .coverage
28 | .tox
29 | nosetests.xml
30 | htmlcov
31 | .cache
32 | .pytest_cache
33 | .mypy_cache
34 |
35 | # Translations
36 | *.mo
37 |
38 | # Mr Developer
39 | .mr.developer.cfg
40 | .project
41 | .pydevproject
42 |
43 | # SQLite
44 | test_exp_framework
45 |
46 | # npm
47 | node_modules/
48 |
49 | # dolphin
50 | .directory
51 | libpeerconnection.log
52 |
53 | # setuptools
54 | dist
55 |
56 | # IDE Files
57 | atlassian-ide-plugin.xml
58 | .idea/
59 | *.swp
60 | *.kate-swp
61 | .ropeproject/
62 |
63 | # Python3 Venv Files
64 | .venv/
65 | bin/
66 | include/
67 | lib/
68 | lib64
69 | pyvenv.cfg
70 | share/
71 | venv/
72 | .python-version
73 |
74 | # Cython
75 | *.c
76 |
77 | # Emacs backup
78 | *~
79 |
80 | # VSCode
81 | /.vscode
82 |
83 | # Automatically generated files
84 | docs/preconvert
85 | site/
86 | out
87 | poetry.lock
88 |
89 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: python
3 | cache: pip
4 | install:
5 | - pip3 install poetry
6 | - poetry install
7 | matrix:
8 | include:
9 | - os: linux
10 | sudo: required
11 | python: 3.6
12 | - os: linux
13 | sudo: required
14 | python: 3.7
15 | env: DEPLOY=yes
16 | - os: osx
17 | language: generic
18 | script:
19 | - bash scripts/test.sh
20 | after_script:
21 | - bash <(curl -s https://codecov.io/bash)
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Install the latest
2 | ===================
3 |
4 | To install the latest version of pdocs simply run:
5 |
6 | `pip3 install pdocs`
7 |
8 | OR
9 |
10 | `poetry add pdocs`
11 |
12 | OR
13 |
14 | `pipenv install pdocs`
15 |
16 | see the [Installation QuickStart](https://timothycrosley.github.io/portray/docs/quick_start/1.-installation/) for more instructions.
17 |
18 | Changelog
19 | =========
20 | ## 1.2.0 - Dec 18th 2022
21 | - Dropped Python 3.6 support
22 | - Bumped dependencies
23 | - General fixes
24 |
25 | ## 1.1.1 - January 29th 2021
26 | - Fixed #18: Incorrectly identified "google docstring" format should not break building.
27 |
28 | ## 1.1.0 - January 15th 2021
29 | - Added numpydoc and googledoc support.
30 |
31 | ## 1.0.2 - June 7th 2020
32 | - Improved support for a variety of package hierchies.
33 |
34 | ## 1.0.1 - September 3rd 2019
35 | - Fixed a bug determining class methods
36 |
37 | ## 1.0.0 - September 2nd 2019
38 | Initial API stable release of pdocs
39 |
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Code written on this repository post August 30th 2019 is done so under the MIT License:
2 |
3 | MIT License
4 |
5 | Copyright (c) 2019 Timothy Crosley
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | All code written before this date, which is contained in the following commit: https://github.com/timothycrosley/pdocs/commit/7cf925101e4ffc5690f2952ac9ad0b7b0410b4f8
26 |
27 | Followed the compatible "Unlicense" License:
28 |
29 | This is free and unencumbered software released into the public domain.
30 |
31 | Anyone is free to copy, modify, publish, use, compile, sell, or
32 | distribute this software, either in source code form or as a compiled
33 | binary, for any purpose, commercial or non-commercial, and by any
34 | means.
35 |
36 | In jurisdictions that recognize copyright laws, the author or authors
37 | of this software dedicate any and all copyright interest in the
38 | software to the public domain. We make this dedication for the benefit
39 | of the public at large and to the detriment of our heirs and
40 | successors. We intend this dedication to be an overt act of
41 | relinquishment in perpetuity of all present and future rights to this
42 | software under copyright law.
43 |
44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
45 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
46 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
47 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
48 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
49 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
50 | OTHER DEALINGS IN THE SOFTWARE.
51 |
52 | For more information, please refer to
53 |
54 | In the most important areas, the licenses have the same intent and general guidelines. And, they both are compatible with each-other.
55 | The change was undertaking simply because the MIT license is more universally known.
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://timothycrosley.github.io/pdocs/)
2 | _________________
3 |
4 | [](http://badge.fury.io/py/pdocs)
5 | [](https://travis-ci.org/timothycrosley/pdocs)
6 | [](https://codecov.io/gh/timothycrosley/pdocs)
7 | [](https://gitter.im/pdocs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8 | [](https://pypi.python.org/pypi/pdocs/)
9 | [](https://pepy.tech/project/pdocs)
10 | _________________
11 |
12 | [Read Latest Documentation](https://timothycrosley.github.io/pdocs/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/pdocs/)
13 | _________________
14 |
15 |
16 | `pdocs` is a library and a command line program to discover the public
17 | interface of a Python module or package. The `pdocs` script can be used to
18 | generate Markdown or HTML of a module's public interface, or it can be used
19 | to run an HTTP server that serves generated HTML for installed modules.
20 |
21 | `pdocs` is an MIT Licensed fork of [pdoc](https://github.com/mitmproxy/pdoc)'s original implementation by Andrew Gallant (@BurntSushi).
22 | with the goal of staying true to the original vision layed out by the project's creator.
23 |
24 | NOTE: For most projects, the best way to use `pdocs` is using [portray](https://timothycrosley.github.io/portray/).
25 |
26 | [](https://asciinema.org/a/265744)
27 |
28 | Features
29 | --------
30 |
31 | * Support for documenting data representation by traversing the abstract syntax
32 | to find docstrings for module, class and instance variables.
33 | * For cases where docstrings aren't appropriate (like a
34 | [namedtuple](http://docs.python.org/2.7/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields)),
35 | the special variable `__pdocs__` can be used in your module to
36 | document any identifier in your public interface.
37 | * Usage is simple. Just write your documentation as Markdown. There are no
38 | added special syntax rules.
39 | * `pdocs` respects your `__all__` variable when present.
40 | * `pdocs` will automatically link identifiers in your docstrings to its
41 | corresponding documentation.
42 | * When `pdocs` is run as an HTTP server, external linking is supported between
43 | packages.
44 | * The `pdocs` HTTP server will cache generated documentation and automatically
45 | regenerate it if the source code has been updated.
46 | * When available, source code for modules, functions and classes can be viewed
47 | in the HTML documentation.
48 | * Inheritance is used when possible to infer docstrings for class members.
49 |
50 | The above features are explained in more detail in pdocs's documentation.
51 |
52 | `pdocs` is compatible with Python 3.6 and newer.
53 |
54 | ## Quick Start
55 |
56 | The following guides should get you up using pdocs in no time:
57 |
58 | 1. [Installation](https://timothycrosley.github.io/pdocs/docs/quick_start/1.-installation/) - TL;DR: Run `pip3 install pdocs` within your projects virtual environment.
59 | 2. [Command Line Usage](https://timothycrosley.github.io/pdocs/docs/quick_start/2.-cli/) - TL;DR: Run `pdocs server YOUR_MODULES` to test and `pdocs as_html YOUR_MODULES` to generate HTML.
60 | 3. [API Usage](https://timothycrosley.github.io/pdocs/docs/quick_start/3.-api/) - TL;DR: Everything available via the CLI is also easily available programmatically from within Python.
61 |
62 | ## Differences Between pdocs and pdoc
63 |
64 | Below is a running list of intentional differences between [pdoc](https://github.com/mitmproxy/pdoc) and [pdocs](https://github.com/timothycrosley/pdocs):
65 |
66 | - pdocs has built-in support for Markdown documentation generation (as needed by [portray](https://timothycrosley.github.io/portray/)).
67 | - pdocs has built-in support for the inclusion of Type Annotation information in reference documentation.
68 | - pdocs requires Python 3.6+; pdoc maintains Python2 compatibility as of the latest public release.
69 | - pdocs uses the most recent development tools to ensure long-term maintainability (mypy, black, isort, flake8, bandit, ...)
70 | - pdocs generates project documentation to a temporary folder when serving locally, instead of including a live server. An intentional trade-off between simplicity and performance.
71 | - pdocs provides a simplified Python API in addition to CLI API.
72 | - pdocs is actively maintained.
73 | - pdocs uses [hug CLI and sub-commands](https://github.com/timothycrosley/pdocs/blob/master/pdocs/cli.py#L1), pdoc uses [argparse and a single command](https://github.com/mitmproxy/pdoc/blob/master/pdoc/cli.py#L1).
74 | - pdoc provides textual documentation from the command-line, pdocs removed this feature for API simplicity.
75 |
76 | ## Notes on Licensing and Fork
77 |
78 | The original pdoc followed the [Unlicense license](https://unlicense.org/), and as such so does the initial commit to this fork [here](https://github.com/timothycrosley/pdocs/commit/7cf925101e4ffc5690f2952ac9ad0b7b0410b4f8).
79 | Unlicense is fully compatible with MIT, and the reason for the switch going forward is because MIT is a more standard and well-known license.
80 |
81 | As seen by that commit, I chose to fork with fresh history, as the project is very old (2013) and I felt many of the commits that happened in the past might, instead of helping to debug issues, lead to red herrings due to the many changes that have happened
82 | in the Python eco-system since that time. If you desire to see the complete history for any reason, it remains available on the original [pdoc repository](https://github.com/mitmproxy/pdoc).
83 |
84 | ## Why Create `pdocs`?
85 |
86 | I created `pdocs` to help power [portray](https://timothycrosley.github.io/portray/) while staying true to the original vision of `pdoc` and maintain
87 | MIT license compatibility. In the end I created it to help power better documentation websites for Python projects.
88 |
89 | I hope you, too, find `pdocs` useful!
90 |
91 | ~Timothy Crosley
92 |
--------------------------------------------------------------------------------
/art/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/art/logo.png
--------------------------------------------------------------------------------
/art/logo_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/art/logo_large.png
--------------------------------------------------------------------------------
/art/logo_large.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/art/logo_large.xcf
--------------------------------------------------------------------------------
/docs/contributing/1.-contributing-guide.md:
--------------------------------------------------------------------------------
1 | Contributing to pdocs
2 | ========
3 |
4 | Looking for a useful open source project to contribute to?
5 | Want your contributions to be warmly welcomed and acknowledged?
6 | Welcome! You have found the right place.
7 |
8 | ## Getting pdocs set up for local development
9 | The first step when contributing to any project is getting it set up on your local machine. pdocs aims to make this as simple as possible.
10 |
11 | Account Requirements:
12 |
13 | - [A valid GitHub account](https://github.com/join)
14 |
15 | Base System Requirements:
16 |
17 | - Python3.6+
18 | - poetry
19 | - bash or a bash compatible shell (should be auto-installed on Linux / Mac)
20 |
21 | Once you have verified that you system matches the base requirements you can start to get the project working by following these steps:
22 |
23 | 1. [Fork the project on GitHub](https://github.com/timothycrosley/pdocs/fork).
24 | 2. Clone your fork to your local file system:
25 | `git clone https://github.com/$GITHUB_ACCOUNT/pdocs.git`
26 | 3. `cd pdocs
27 | 4. `poetry install`
28 |
29 | ## Making a contribution
30 | Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request:
31 |
32 | 1. Check the [issues page](https://github.com/timothycrosley/pdocs/issues) on GitHub to see if the task you want to complete is listed there.
33 | - If it's listed there, write a comment letting others know you are working on it.
34 | - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control.
35 | - If you're not sure if it's something that is good for the main pdocs project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/pdocs).
36 | 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`.
37 | 3. Do your magic here.
38 | 4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project.
39 | 5. Submit a pull request to the main project repository via GitHub.
40 |
41 | Thanks for the contribution! It will quickly get reviewed, and, once accepted, will result in your name being added to the acknowledgments list :).
42 |
43 | ## Thank you!
44 | I can not tell you how thankful I am for the hard work done by pdocs contributors like *you*.
45 |
46 | Thank you!
47 |
48 | ~Timothy Crosley
49 |
50 |
--------------------------------------------------------------------------------
/docs/contributing/2.-coding-standard.md:
--------------------------------------------------------------------------------
1 | # HOPE 8 -- Style Guide for Hug Code
2 |
3 | | | |
4 | | ------------| ------------------------------------------- |
5 | | HOPE: | 8 |
6 | | Title: | Style Guide for Hug Code |
7 | | Author(s): | Timothy Crosley |
8 | | Status: | Active |
9 | | Type: | Process |
10 | | Created: | 19-May-2019 |
11 | | Updated: | 17-August-2019 |
12 |
13 | ## Introduction
14 |
15 | This document gives coding conventions for the Hug code comprising the Hug core as well as all official interfaces, extensions, and plugins for the framework.
16 | Optionally, projects that use Hug are encouraged to follow this HOPE and link to it as a reference.
17 |
18 | ## PEP 8 Foundation
19 |
20 | All guidelines in this document are in addition to those defined in Python's [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) guidelines.
21 |
22 | ## Line Length
23 |
24 | Too short of lines discourage descriptive variable names where they otherwise make sense.
25 | Too long of lines reduce overall readability and make it hard to compare 2 files side by side.
26 | There is no perfect number: but for Hug, we've decided to cap the lines at 100 characters.
27 |
28 | ## Descriptive Variable names
29 |
30 | Naming things is hard. Hug has a few strict guidelines on the usage of variable names, which hopefully will reduce some of the guesswork:
31 | - No one character variable names.
32 | - Except for x, y, and z as coordinates.
33 | - It's not okay to override built-in functions.
34 | - Except for `id`. Guido himself thought that shouldn't have been moved to the system module. It's too commonly used, and alternatives feel very artificial.
35 | - Avoid Acronyms, Abbreviations, or any other short forms - unless they are almost universally understand.
36 |
37 | ## Adding new modules
38 |
39 | New modules added to the a project that follows the HOPE-8 standard should all live directly within the base `PROJECT_NAME/` directory without nesting. If the modules are meant only for internal use within the project, they should be prefixed with a leading underscore. For example, def _internal_function. Modules should contain a docstring at the top that gives a general explanation of the purpose and then restates the project's use of the MIT license.
40 | There should be a `tests/test_$MODULE_NAME.py` file created to correspond to every new module that contains test coverage for the module. Ideally, tests should be 1:1 (one test object per code object, one test method per code method) to the extent cleanly possible.
41 |
42 | ## Automated Code Cleaners
43 |
44 | All code submitted to Hug should be formatted using Black and isort.
45 | Black should be run with the line length set to 100, and isort with Black compatible settings in place.
46 |
47 | ## Automated Code Linting
48 |
49 | All code submitted to hug should run through the following tools:
50 |
51 | - Black and isort verification.
52 | - Flake8
53 | - flake8-bugbear
54 | - Bandit
55 | - pep8-naming
56 | - vulture
57 | - safety
58 |
--------------------------------------------------------------------------------
/docs/contributing/3.-code-of-conduct.md:
--------------------------------------------------------------------------------
1 | # HOPE 11 -- Code of Conduct
2 |
3 | | | |
4 | | ------------| ------------------------------------------- |
5 | | HOPE: | 11 |
6 | | Title: | Code of Conduct |
7 | | Author(s): | Timothy Crosley |
8 | | Status: | Active |
9 | | Type: | Process |
10 | | Created: | 17-August-2019 |
11 | | Updated: | 17-August-2019 |
12 |
13 | ## Abstract
14 |
15 | Defines the Code of Conduct for Hug and all related projects.
16 |
17 | ## Our Pledge
18 |
19 | In the interest of fostering an open and welcoming environment, we as
20 | contributors and maintainers pledge to making participation in our project and
21 | our community a harassment-free experience for everyone, regardless of age, body
22 | size, disability, ethnicity, sex characteristics, gender identity and expression,
23 | level of experience, education, socio-economic status, nationality, personal
24 | appearance, race, religion, or sexual identity and orientation.
25 |
26 | ## Our Standards
27 |
28 | Examples of behavior that contributes to creating a positive environment
29 | include:
30 |
31 | * Using welcoming and inclusive language
32 | * Being respectful of differing viewpoints and experiences
33 | * Gracefully accepting constructive criticism
34 | * Focusing on what is best for the community
35 | * Showing empathy towards other community members
36 |
37 | Examples of unacceptable behavior by participants include:
38 |
39 | * The use of sexualized language or imagery and unwelcome sexual attention or
40 | advances
41 | * Trolling, insulting/derogatory comments, and personal or political attacks
42 | * Public or private harassment
43 | * Publishing others' private information, such as a physical or electronic
44 | address, without explicit permission
45 | * Other conduct which could reasonably be considered inappropriate in a
46 | professional setting
47 |
48 | ## Our Responsibilities
49 |
50 | Project maintainers are responsible for clarifying the standards of acceptable
51 | behavior and are expected to take appropriate and fair corrective action in
52 | response to any instances of unacceptable behavior.
53 |
54 | Project maintainers have the right and responsibility to remove, edit, or
55 | reject comments, commits, code, wiki edits, issues, and other contributions
56 | that are not aligned to this Code of Conduct, or to ban temporarily or
57 | permanently any contributor for other behaviors that they deem inappropriate,
58 | threatening, offensive, or harmful.
59 |
60 | ## Scope
61 |
62 | This Code of Conduct applies both within project spaces and in public spaces
63 | when an individual is representing the project or its community. Examples of
64 | representing a project or community include using an official project e-mail
65 | address, posting via an official social media account, or acting as an appointed
66 | representative at an online or offline event. Representation of a project may be
67 | further defined and clarified by project maintainers.
68 |
69 | ## Enforcement
70 |
71 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
72 | reported by contacting [timothy.crosley@gmail.com](mailto:timothy.crosley@gmail.com). All
73 | complaints will be reviewed and investigated and will result in a response that
74 | is deemed necessary and appropriate to the circumstances. Confidentiality will be maintained
75 | with regard to the reporter of an incident.
76 | Further details of specific enforcement policies may be posted separately.
77 |
78 | Project maintainers who do not follow or enforce the Code of Conduct in good
79 | faith may face temporary or permanent repercussions as determined by other
80 | members of the project's leadership.
81 |
82 | ## Attribution
83 |
84 | This Code of Conduct is adapted from the [Contributor Covenant][https://www.contributor-covenant.org], version 1.4,
85 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
86 |
87 | For answers to common questions about this code of conduct, see
88 | https://www.contributor-covenant.org/faq
89 |
--------------------------------------------------------------------------------
/docs/contributing/4.-acknowledgements.md:
--------------------------------------------------------------------------------
1 | Contributors
2 | ===================
3 |
4 | ## Core Developers
5 | - Timothy Edmund Crosley (@timothycrosley)
6 |
7 | ## Notable Bug Reporters
8 | -
9 |
10 | ## Code Contributors
11 | -
12 |
13 | ## Documenters
14 | - Zach Valenta (@zachvalenta)
15 |
16 |
17 | --------------------------------------------
18 |
19 | A sincere thanks to everyone who helps make pdocs into a great Python3 project!
20 |
21 | ~Timothy Crosley
22 |
--------------------------------------------------------------------------------
/docs/quick_start/1.-installation.md:
--------------------------------------------------------------------------------
1 | Install `pdocs` into your projects virtual environment:
2 |
3 | `pip3 install pdocs`
4 |
5 | OR
6 |
7 | `poetry add pdocs`
8 |
9 | OR
10 |
11 | `pipenv install pdocs`
12 |
13 |
14 |
15 | !!! info
16 | It's important that `pdocs` be installed in your project's environment as it needs to introspect your package to generate reference documentation. You must also have your project installed in the environment for this to work.
17 |
--------------------------------------------------------------------------------
/docs/quick_start/2.-cli.md:
--------------------------------------------------------------------------------
1 | # Command Line Usage
2 |
3 | Once installed, `pdocs` exposes a simple command line utility for generating documentation websites.
4 |
5 | To verify the tool is installed correctly, run `pdocs` from the command line and you should be given the available commands and the version of pdocs installed.
6 | To get help for any individual subcommand run `pdocs SUBCOMMAND --help`:
7 |
8 |
9 |
10 | ## Serving Documentation Locally
11 |
12 | Before you push documentation out publicly, it's a good idea to test it locally and ensure it looks as desired.
13 |
14 | Running `pdocs server YOUR_MODULES` will generate a new static website for your project in a temporary directory and start a local server to allow you to browse it (at localhost:8080 by default).
15 |
16 |
17 |
18 | This command takes an optional `--port` and `--host` argument if you wish to override the defaults.
19 |
20 | !!! tip
21 | Every module you pass in to pdocs must be installed or otherwise available on your `PYTHON_PATH`
22 |
23 | ## Outputting HTML Locally
24 |
25 | You can also output `pdocs`'s generated documentation to a local directory.
26 | To do so run `pdocs as_html YOUR_MODULES`:
27 |
28 |
29 |
30 | By default the generated documentation is outputted into a `site` subdirectory.
31 | If this directory exists for any reason, including previous documentation generation,
32 | the command will fail. Passing in `--overwrite` will delete any existing directory
33 | before output to ensure the command passes. You can change the output directory using `-o DIRECTORY`.
34 |
35 | ## Outputting Markdown Locally
36 |
37 | It is also straight-forward to output Markdown documentation for your project generated from your code via `pdocs`.
38 | To do so run `pdocs as_markdown YOUR_MODULES`:
39 |
40 | By default the generated documentation is outputted into a `docs` subdirectory.
41 | If this directory exists for any reason, including previous documentation generation,
42 | the command will fail. Passing in `--overwrite` will delete any existing directory
43 | before output to ensure the command passes. You can change the output directory using `-o DIRECTORY`.
44 |
45 |
46 |
--------------------------------------------------------------------------------
/docs/quick_start/3.-api.md:
--------------------------------------------------------------------------------
1 | # Programmatic Python API Usage
2 |
3 | Every command available from the command line utility is also available directly as function calls within Python.
4 | To use the Python API, `import pdocs` and then call the desired function call:
5 |
6 |
7 |
8 | Every function is type hinted and takes and returns only builtin Python objects.
9 |
10 | For a full definition of the API see the [API reference documentation](https://timothycrosley.github.io/pdocs/reference/pdocs/api/).
11 |
--------------------------------------------------------------------------------
/pdocs/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Module pdoc provides types and functions for accessing the public
3 | documentation of a Python module. This includes modules (and
4 | sub-modules), functions, classes and module, class and instance
5 | variables. Docstrings are taken from modules, functions and classes
6 | using the special `__doc__` attribute. Docstrings for variables are
7 | extracted by examining the module's abstract syntax tree.
8 |
9 | The public interface of a module is determined through one of two
10 | ways. If `__all__` is defined in the module, then all identifiers in
11 | that list will be considered public. No other identifiers will be
12 | considered as public. Conversely, if `__all__` is not defined, then
13 | `pdoc` will heuristically determine the public interface. There are
14 | three rules that are applied to each identifier in the module:
15 |
16 | 1. If the name starts with an underscore, it is **not** public.
17 |
18 | 2. If the name is defined in a different module, it is **not** public.
19 |
20 | 3. If the name refers to an immediate sub-module, then it is public.
21 |
22 | Once documentation for a module is created with `pdoc.Module`, it
23 | can be output as either HTML or plain text using the covenience
24 | functions `pdoc.html` and `pdoc.text`, or the corresponding methods
25 | `pdoc.Module.html` and `pdoc.Module.text`.
26 |
27 | Alternatively, you may run an HTTP server with the `pdoc` script
28 | included with this module.
29 |
30 |
31 | Compatibility
32 | -------------
33 | `pdoc` requires Python 3.6 or later.
34 |
35 |
36 | Contributing
37 | ------------
38 | `pdoc` [is on GitHub](https://github.com/mitmproxy/pdoc). Pull
39 | requests and bug reports are welcome.
40 |
41 |
42 | Linking to other identifiers
43 | ----------------------------
44 | In your documentation, you may link to other identifiers in
45 | your module or submodules. Linking is automatically done for
46 | you whenever you surround an identifier with a back quote
47 | (grave). The identifier name must be fully qualified. For
48 | example, `pdoc.Doc.docstring` is correct while
49 | `Doc.docstring` is incorrect.
50 |
51 | If the `pdoc` script is used to run an HTTP server, then external
52 | linking to other packages installed is possible. No extra work is
53 | necessary; simply use the fully qualified path. For example,
54 | `nflvid.slice` will create a link to the `nflvid.slice`
55 | function, which is **not** a part of `pdoc` at all.
56 |
57 |
58 | Where does pdoc get documentation from?
59 | ---------------------------------------
60 | Broadly speaking, `pdoc` gets everything you see from introspecting the
61 | module. This includes words describing a particular module, class,
62 | function or variable. While `pdoc` does some analysis on the source
63 | code of a module, importing the module itself is necessary to use
64 | Python's introspection features.
65 |
66 | In Python, objects like modules, functions, classes and methods have
67 | a special attribute named `__doc__` which contains that object's
68 | *docstring*. The docstring comes from a special placement of a string
69 | in your source code. For example, the following code shows how to
70 | define a function with a docstring and access the contents of that
71 | docstring:
72 |
73 | #!python
74 | >>> def test():
75 | ... '''This is a docstring.'''
76 | ... pass
77 | ...
78 | >>> test.__doc__
79 | 'This is a docstring.'
80 |
81 | Something similar can be done for classes and modules too. For classes,
82 | the docstring should come on the line immediately following `class
83 | ...`. For modules, the docstring should start on the first line of
84 | the file. These docstrings are what you see for each module, class,
85 | function and method listed in the documentation produced by `pdoc`.
86 |
87 | The above just about covers *standard* uses of docstrings in Python.
88 | `pdoc` extends the above in a few important ways.
89 |
90 |
91 | ### Special docstring conventions used by `pdoc`
92 |
93 | **Firstly**, docstrings can be inherited. Consider the following code
94 | sample:
95 |
96 | #!python
97 | >>> class A (object):
98 | ... def test():
99 | ... '''Docstring for A.'''
100 | ...
101 | >>> class B (A):
102 | ... def test():
103 | ... pass
104 | ...
105 | >>> print(A.test.__doc__)
106 | Docstring for A.
107 | >>> print(B.test.__doc__)
108 | None
109 |
110 | In Python, the docstring for `B.test` is empty, even though one was
111 | defined in `A.test`. If `pdoc` generates documentation for the above
112 | code, then it will automatically attach the docstring for `A.test` to
113 | `B.test` only if `B.test` does not have a docstring. In the default
114 | HTML output, an inherited docstring is grey.
115 |
116 | **Secondly**, docstrings can be attached to variables, which includes
117 | module (or global) variables, class variables and instance variables.
118 | Python by itself [does not allow docstrings to be attached to
119 | variables](http://www.python.org/dev/peps/pep-0224). For example:
120 |
121 | #!python
122 | variable = "SomeValue"
123 | '''Docstring for variable.'''
124 |
125 | The resulting `variable` will have no `__doc__` attribute. To
126 | compensate, `pdoc` will read the source code when it's available to
127 | infer a connection between a variable and a docstring. The connection
128 | is only made when an assignment statement is followed by a docstring.
129 |
130 | Something similar is done for instance variables as well. By
131 | convention, instance variables are initialized in a class's `__init__`
132 | method. Therefore, `pdoc` adheres to that convention and looks for
133 | docstrings of variables like so:
134 |
135 | #!python
136 | def __init__(self):
137 | self.variable = "SomeValue"
138 | '''Docstring for instance variable.'''
139 |
140 | Note that `pdoc` only considers attributes defined on `self` as
141 | instance variables.
142 |
143 | Class and instance variables can also have inherited docstrings.
144 |
145 | **Thirdly and finally**, docstrings can be overridden with a special
146 | `__pdoc__` dictionary that `pdoc` inspects if it exists. The keys of
147 | `__pdoc__` should be identifiers within the scope of the module. (In
148 | the case of an instance variable `self.variable` for class `A`, its
149 | module identifier would be `A.variable`.) The values of `__pdoc__`
150 | should be docstrings.
151 |
152 | This particular feature is useful when there's no feasible way of
153 | attaching a docstring to something. A good example of this is a
154 | [namedtuple](http://goo.gl/akfXJ9):
155 |
156 | #!python
157 | __pdoc__ = {}
158 |
159 | Table = namedtuple('Table', ['types', 'names', 'rows'])
160 | __pdoc__['Table.types'] = 'Types for each column in the table.'
161 | __pdoc__['Table.names'] = 'The names of each column in the table.'
162 | __pdoc__['Table.rows'] = 'Lists corresponding to each row in the table.'
163 |
164 | `pdoc` will then show `Table` as a class with documentation for the
165 | `types`, `names` and `rows` members.
166 |
167 | Note that assignments to `__pdoc__` need to placed where they'll be
168 | executed when the module is imported. For example, at the top level
169 | of a module or in the definition of a class.
170 |
171 | If `__pdoc__[key] = None`, then `key` will not be included in the
172 | public interface of the module.
173 | """
174 |
175 | from pdocs._version import __version__
176 | from pdocs.api import as_html, as_markdown, server
177 |
--------------------------------------------------------------------------------
/pdocs/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.2.0"
2 |
--------------------------------------------------------------------------------
/pdocs/api.py:
--------------------------------------------------------------------------------
1 | """This module defines the programmatic API that can be used to interact with `pdocs`
2 | to generate and view documentation from Python source code.
3 |
4 | If you want to extend `pdocs` or use it directly from within Python - this is the place
5 | to start.
6 | """
7 | import os
8 | import pathlib
9 | import sys
10 | import tempfile
11 | import webbrowser
12 |
13 | import hug
14 |
15 | import pdocs.extract
16 | import pdocs.logo
17 | import pdocs.render
18 | import pdocs.static
19 | from pdocs import defaults
20 |
21 |
22 | def as_html(
23 | modules: list,
24 | output_dir: str = defaults.HTML_OUTPUT_DIRECTORY,
25 | overwrite: bool = False,
26 | external_links: bool = False,
27 | exclude_source: bool = False,
28 | link_prefix: str = "",
29 | template_dir: str = "",
30 | ) -> str:
31 | """Produces HTML formatted output into the specified output_dir.
32 |
33 | - *modules*: One or more python module names. These may be import paths resolvable in the
34 | current environment, or file paths to a Python module or package.
35 | - *output_dir*: The directory to output HTML files to.
36 | - *overwrite*: If set, will overwrites any existing files in the output location.
37 | - *external_links*: When set, identifiers to external modules are turned into links.
38 | - *exclude_source*: When set, source code will not be viewable in the generated HTML.
39 | - *link_prefix*: A prefix to use for every link in the generated documentation otherwise
40 | relative links will be used.
41 | - *template_dir*: Specify a directory containing override Mako templates.
42 |
43 | Returns the `output_dir` on success.
44 | """
45 | if template_dir:
46 | pdocs.render.tpl_lookup.directories.insert(0, template_dir)
47 |
48 | roots = _get_root_modules(modules)
49 | destination = _destination(output_dir, roots, overwrite)
50 | pdocs.static.html_out(
51 | destination,
52 | roots,
53 | external_links=external_links,
54 | source=not exclude_source,
55 | link_prefix=link_prefix,
56 | )
57 | return output_dir
58 |
59 |
60 | def as_markdown(
61 | modules: list,
62 | output_dir: str = defaults.MARKDOWN_OUTPUT_DIRECTORY,
63 | overwrite: bool = False,
64 | exclude_source: bool = False,
65 | template_dir: str = "",
66 | ) -> str:
67 | """Produces Markdown formatted output into the specified output_dir.
68 |
69 | - *modules*: One or more python module names. These may be import paths resolvable in the
70 | current environment, or file paths to a Python module or package.
71 | - *output_dir*: The directory to output HTML files to.
72 | - *overwrite*: If set, will overwrites any existing files in the output location.
73 | - *exclude_source*: When set, source code will not be viewable in the generated Markdown.
74 | - *template_dir*: Specify a directory containing override Mako templates.
75 |
76 | Returns the `output_dir` on success.
77 | """
78 | if template_dir:
79 | pdocs.render.tpl_lookup.directories.insert(0, template_dir)
80 |
81 | roots = _get_root_modules(modules)
82 | destination = _destination(output_dir, roots, overwrite)
83 | pdocs.static.md_out(destination, roots, source=not exclude_source)
84 | return output_dir
85 |
86 |
87 | def server(
88 | modules: list,
89 | external_links: bool = False,
90 | exclude_source: bool = False,
91 | link_prefix: str = "",
92 | template_dir: str = "",
93 | open_browser: bool = False,
94 | port: int = defaults.SERVER_PORT,
95 | host: str = defaults.SERVER_HOST,
96 | ) -> None:
97 | """Runs a development webserver enabling you to browse documentation locally.
98 |
99 | - *modules*: One or more python module names. These may be import paths resolvable in the
100 | current environment, or file paths to a Python module or package.
101 | - *external_links*: When set, identifiers to external modules are turned into links.
102 | - *exclude_source*: When set, source code will not be viewable in the generated HTML.
103 | - *link_prefix*: A prefix to use for every link in the generated documentation otherwise
104 | relative links will be used.
105 | - *template_dir*: Specify a directory containing override Mako templates.
106 | - *open_browser*: If true a browser will be opened pointing at the documentation server
107 | - *port*: The port to expose your documentation on (defaults to: `8000`)
108 | - *host*: The host to expose your documentation on (defaults to `"127.0.0.1"`)
109 | """
110 | with tempfile.TemporaryDirectory() as output_dir:
111 | as_html(
112 | modules,
113 | overwrite=True,
114 | output_dir=output_dir,
115 | external_links=external_links,
116 | template_dir=template_dir,
117 | )
118 |
119 | if len(modules) == 1:
120 | output_dir = os.path.join(output_dir, modules[0])
121 |
122 | api = hug.API("Doc Server")
123 |
124 | @hug.static("/", api=api)
125 | def my_static_dirs(): # pragma: no cover
126 | return (output_dir,)
127 |
128 | @hug.startup(api=api)
129 | def custom_startup(*args, **kwargs): # pragma: no cover
130 | print(pdocs.logo.ascii_art)
131 | if open_browser:
132 | webbrowser.open_new(f"http://{host}:{port}")
133 |
134 | api.http.serve(host=host, port=port, no_documentation=True, display_intro=False)
135 |
136 |
137 | def _get_root_modules(module_names):
138 | if not module_names:
139 | sys.exit("Please provide one or more modules")
140 | try:
141 | return [pdocs.extract.extract_module(module_name) for module_name in module_names]
142 | except pdocs.extract.ExtractError as error:
143 | sys.exit(str(error))
144 |
145 |
146 | def _destination(directory, root_modules, overwrite):
147 | destination = pathlib.Path(directory)
148 | if not overwrite and pdocs.static.would_overwrite(destination, root_modules):
149 | sys.exit("Rendering would overwrite files, but --overwrite is not set")
150 | return destination
151 |
--------------------------------------------------------------------------------
/pdocs/cli.py:
--------------------------------------------------------------------------------
1 | import hug
2 |
3 | from pdocs import api, logo
4 |
5 | cli = hug.cli(api=hug.API(__name__, doc=logo.ascii_art))
6 | cli(api.as_html)
7 | cli(api.as_markdown)
8 | cli(api.server)
9 |
--------------------------------------------------------------------------------
/pdocs/defaults.py:
--------------------------------------------------------------------------------
1 | SERVER_PORT = 8080
2 | SERVER_HOST = "127.0.0.1"
3 |
4 | HTML_OUTPUT_DIRECTORY = "site"
5 | MARKDOWN_OUTPUT_DIRECTORY = "docs"
6 |
7 | MARKDOWN_EXTENSIONS = [
8 | "markdown.extensions.abbr",
9 | "markdown.extensions.admonition",
10 | "markdown.extensions.attr_list",
11 | "markdown.extensions.def_list",
12 | "markdown.extensions.fenced_code",
13 | "markdown.extensions.footnotes",
14 | "markdown.extensions.tables",
15 | "markdown.extensions.smarty",
16 | "markdown.extensions.toc",
17 | ]
18 | MARKDOWN_EXTENSION_CONFIGS = {
19 | "markdown.extensions.smarty": {
20 | "smart_angled_quotes": False,
21 | "smart_dashes": True,
22 | "smart_quotes": False,
23 | "smart_ellipses": True,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pdocs/doc.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import inspect
3 | import typing
4 |
5 | import docstring_parser
6 |
7 | try: # python >= 3.9
8 | from types import GenericAlias
9 | except ImportError: # python >= 3.7
10 | from typing import _GenericAlias as GenericAlias # type: ignore
11 |
12 | __pdoc__ = {}
13 |
14 |
15 | def _source(obj):
16 | """
17 | Returns the source code of the Python object `obj` as a list of
18 | lines. This tries to extract the source from the special
19 | `__wrapped__` attribute if it exists. Otherwise, it falls back
20 | to `inspect.getsourcelines`.
21 |
22 | If neither works, then the empty list is returned.
23 | """
24 | try:
25 | return inspect.getsourcelines(obj.__wrapped__)[0]
26 | except BaseException:
27 | pass
28 | try:
29 | return inspect.getsourcelines(obj)[0]
30 | except Exception:
31 | return []
32 |
33 |
34 | def _var_docstrings(tree, module, cls=None, init=False):
35 | """
36 | Extracts variable docstrings given `tree` as the abstract syntax,
37 | `module` as a `pdoc.Module` containing `tree` and an option `cls`
38 | as a `pdoc.Class` corresponding to the tree. In particular, `cls`
39 | should be specified when extracting docstrings from a class or an
40 | `__init__` method. Finally, `init` should be `True` when searching
41 | the AST of an `__init__` method so that `_var_docstrings` will only
42 | accept variables starting with `self.` as instance variables.
43 |
44 | A dictionary mapping variable name to a `pdoc.Variable` object is
45 | returned.
46 | """
47 | vs = {}
48 | children = list(ast.iter_child_nodes(tree))
49 | for i, child in enumerate(children):
50 | if isinstance(child, ast.Assign) and len(child.targets) == 1:
51 | if not init and isinstance(child.targets[0], ast.Name):
52 | name = child.targets[0].id
53 | elif (
54 | isinstance(child.targets[0], ast.Attribute)
55 | and isinstance(child.targets[0].value, ast.Name)
56 | and child.targets[0].value.id == "self"
57 | ):
58 | name = child.targets[0].attr
59 | else:
60 | continue
61 | if not _is_exported(name) and name not in getattr(module, "__all__", []):
62 | continue
63 |
64 | docstring = ""
65 | if (
66 | i + 1 < len(children)
67 | and isinstance(children[i + 1], ast.Expr)
68 | and isinstance(children[i + 1].value, ast.Str)
69 | ):
70 | docstring = children[i + 1].value.s
71 |
72 | vs[name] = Variable(name, module, docstring, cls=cls)
73 | return vs
74 |
75 |
76 | def _is_exported(ident_name):
77 | """
78 | Returns `True` if `ident_name` matches the export criteria for an
79 | identifier name.
80 |
81 | This should not be used by clients. Instead, use
82 | `pdoc.Module.is_public`.
83 | """
84 | return not ident_name.startswith("_")
85 |
86 |
87 | def _is_method(cls: typing.Type, method_name: str) -> bool:
88 | """
89 | Returns `True` if the given method is a regular method,
90 | i.e. it's neither annotated with @classmethod nor @staticmethod.
91 | """
92 | func = getattr(cls, method_name, None)
93 | if inspect.ismethod(func):
94 | # If the function is already bound, it's a classmethod.
95 | # Regular methods are not bound before initialization.
96 | return False
97 | for c in inspect.getmro(cls):
98 | if method_name in c.__dict__:
99 | return not isinstance(c.__dict__[method_name], staticmethod)
100 | else:
101 | raise ValueError(
102 | "{method_name} not found in {cls}.".format(method_name=method_name, cls=cls)
103 | )
104 |
105 |
106 | def _filter(items, kind, attributes_set=(), attributes_not_set=(), sort=True):
107 | items = (item for item in items if isinstance(item, kind))
108 | for attribute_set in attributes_set:
109 | items = (item for item in items if getattr(item, attribute_set, False))
110 | for attribute_not_set in attributes_not_set:
111 | items = (item for item in items if not getattr(item, attribute_not_set, False))
112 | if sort:
113 | return sorted(items)
114 | else:
115 | return tuple(items)
116 |
117 |
118 | class Doc(object):
119 | """
120 | A base class for all documentation objects.
121 |
122 | A documentation object corresponds to *something* in a Python module
123 | that has a docstring associated with it. Typically, this only includes
124 | modules, classes, functions and methods. However, `pdoc` adds support
125 | for extracting docstrings from the abstract syntax tree, which means
126 | that variables (module, class or instance) are supported too.
127 |
128 | A special type of documentation object `pdoc.External` is used to
129 | represent identifiers that are not part of the public interface of
130 | a module. (The name "External" is a bit of a misnomer, since it can
131 | also correspond to unexported members of the module, particularly in
132 | a class's ancestor list.)
133 | """
134 |
135 | def __init__(self, name, module, docstring):
136 | """
137 | Initializes a documentation object, where `name` is the public
138 | identifier name, `module` is a `pdoc.Module` object, and
139 | `docstring` is a string containing the docstring for `name`.
140 | """
141 | self.module = module
142 | """
143 | The module documentation object that this object was defined
144 | in.
145 | """
146 |
147 | self.name = name
148 | """
149 | The identifier name for this object.
150 | """
151 |
152 | self.docstring = inspect.cleandoc(docstring or "")
153 | """
154 | The docstring for this object. It has already been cleaned
155 | by `inspect.cleandoc`.
156 | """
157 |
158 | try:
159 | self.parsed_docstring = docstring_parser.parse(self.docstring)
160 | except (docstring_parser.ParseError, ValueError):
161 | self.parsed_docstring = None
162 | """
163 | The parsed docstring for this object.
164 | """
165 |
166 | @property
167 | def source(self):
168 | """
169 | Returns the source code of the Python object `obj` as a list of
170 | lines. This tries to extract the source from the special
171 | `__wrapped__` attribute if it exists. Otherwise, it falls back
172 | to `inspect.getsourcelines`.
173 |
174 | If neither works, then the empty list is returned.
175 | """
176 | raise NotImplementedError("source() method should be implemented by sub casses")
177 |
178 | @property
179 | def refname(self):
180 | """
181 | Returns an appropriate reference name for this documentation
182 | object. Usually this is its fully qualified path. Every
183 | documentation object must provide this property.
184 |
185 | e.g., The refname for this property is
186 | pdoc.Doc.refname.
187 | """
188 | raise NotImplementedError("refname() method should be implemented by sub casses")
189 |
190 | def __lt__(self, other):
191 | return self.name < other.name
192 |
193 | def is_empty(self):
194 | """
195 | Returns true if the docstring for this object is empty.
196 | """
197 | return len(self.docstring.strip()) == 0
198 |
199 |
200 | class Module(Doc):
201 | """
202 | Representation of a module's documentation.
203 | """
204 |
205 | __pdoc__["Module.module"] = "The Python module object."
206 | __pdoc__[
207 | "Module.name"
208 | ] = """
209 | The name of this module with respect to the context in which
210 | it was imported. It is always an absolute import path.
211 | """
212 |
213 | def __init__(self, name, module, parent):
214 | """
215 | Creates a `Module` documentation object given the actual
216 | module Python object.
217 | """
218 | super().__init__(name, module, inspect.getdoc(module))
219 | self.parent = parent
220 |
221 | self.doc = {}
222 | """A mapping from identifier name to a documentation object."""
223 |
224 | self.refdoc = {}
225 | """
226 | The same as `pdoc.Module.doc`, but maps fully qualified
227 | identifier names to documentation objects.
228 | """
229 |
230 | self.submodules = []
231 |
232 | vardocs = {}
233 | try:
234 | tree = ast.parse(inspect.getsource(self.module))
235 | vardocs = _var_docstrings(tree, self, cls=None)
236 | except BaseException:
237 | pass
238 | self._declared_variables = vardocs.keys()
239 |
240 | public = self.__public_objs()
241 | for name, obj in public.items():
242 | # Skip any identifiers that already have doco.
243 | if name in self.doc and not self.doc[name].is_empty():
244 | continue
245 |
246 | # Functions and some weird builtins?, plus methods, classes,
247 | # modules and module level variables.
248 | if inspect.isroutine(obj):
249 | self.doc[name] = Function(name, self, obj)
250 | elif inspect.isclass(obj):
251 | self.doc[name] = Class(name, self, obj)
252 | elif name in vardocs:
253 | self.doc[name] = vardocs[name]
254 | else:
255 | # Catch all for variables.
256 | self.doc[name] = Variable(name, self, "", cls=None)
257 |
258 | # Now see if we can grab inheritance relationships between classes.
259 | for docobj in self.doc.values():
260 | if isinstance(docobj, Class):
261 | docobj._fill_inheritance()
262 |
263 | # Build the reference name dictionary.
264 | for _basename, docobj in self.doc.items():
265 | self.refdoc[docobj.refname] = docobj
266 | if isinstance(docobj, Class):
267 | for v in docobj.class_variables():
268 | self.refdoc[v.refname] = v
269 | for v in docobj.instance_variables():
270 | self.refdoc[v.refname] = v
271 | for f in docobj.methods():
272 | self.refdoc[f.refname] = f
273 | for f in docobj.functions():
274 | self.refdoc[f.refname] = f
275 |
276 | # Finally look for more docstrings in the __pdoc__ override.
277 | for name, docstring in getattr(self.module, "__pdoc__", {}).items():
278 | refname = "%s.%s" % (self.refname, name)
279 | if docstring is None:
280 | self.doc.pop(name, None)
281 | self.refdoc.pop(refname, None)
282 | continue
283 |
284 | dobj = self.find_ident(refname)
285 | if isinstance(dobj, External):
286 | continue
287 | dobj.docstring = inspect.cleandoc(docstring)
288 | try:
289 | dobj.parsed_docstring = docstring_parser.parse(dobj.docstring)
290 | except docstring_parser.ParseError:
291 | dobj.parsed_docstring = None
292 |
293 | @property
294 | def source(self):
295 | return _source(self.module)
296 |
297 | @property
298 | def refname(self):
299 | return self.name
300 |
301 | @property
302 | def is_namespace(self):
303 | """Returns `True` if this module represents a
304 | [namespace package](https://packaging.python.org/guides/packaging-namespace-packages/).
305 | """
306 | return self.module.__spec__.origin in (None, "namespace")
307 |
308 | def mro(self, cls):
309 | """
310 | Returns a method resolution list of ancestor documentation objects
311 | for `cls`, which must be a documentation object.
312 |
313 | The list will contain objects belonging to `pdoc.Class` or
314 | `pdoc.External`. Objects belonging to the former are exported
315 | classes either in this module or in one of its sub-modules.
316 | """
317 | return [self.find_class(c) for c in inspect.getmro(cls.cls) if c not in (cls.cls, object)]
318 |
319 | def descendents(self, cls):
320 | """
321 | Returns a descendent list of documentation objects for `cls`,
322 | which must be a documentation object.
323 |
324 | The list will contain objects belonging to `pdoc.Class` or
325 | `pdoc.External`. Objects belonging to the former are exported
326 | classes either in this module or in one of its sub-modules.
327 | """
328 | if cls.cls == type or not hasattr(cls.cls, "__subclasses__"):
329 | # Is this right?
330 | return []
331 |
332 | downs = cls.cls.__subclasses__()
333 | return list(map(lambda c: self.find_class(c), downs))
334 |
335 | def is_public(self, name):
336 | """
337 | Returns `True` if and only if an identifier with name `name` is
338 | part of the public interface of this module. While the names
339 | of sub-modules are included, identifiers only exported by
340 | sub-modules are not checked.
341 |
342 | `name` should be a fully qualified name, e.g.,
343 | pdoc.Module.is_public.
344 | """
345 | return name in self.refdoc
346 |
347 | def find_class(self, cls):
348 | """
349 | Given a Python `cls` object, try to find it in this module
350 | or in any of the exported identifiers of the submodules.
351 | """
352 | for doc_cls in self.classes():
353 | if cls is doc_cls.cls:
354 | return doc_cls
355 | for module in self.submodules:
356 | doc_cls = module.find_class(cls)
357 | if not isinstance(doc_cls, External):
358 | return doc_cls
359 | return External("%s.%s" % (cls.__module__, cls.__name__))
360 |
361 | def find_ident(self, name, _seen=None):
362 | """
363 | Searches this module and **all** of its sub/super-modules for an
364 | identifier with name `name` in its list of exported
365 | identifiers according to `pdoc`. Note that unexported
366 | sub-modules are searched.
367 |
368 | A bare identifier (without `.` separators) will only be checked
369 | for in this module.
370 |
371 | The documentation object corresponding to the identifier is
372 | returned. If one cannot be found, then an instance of
373 | `External` is returned populated with the given identifier.
374 | """
375 | _seen = _seen or set()
376 | if self in _seen:
377 | return None
378 | _seen.add(self)
379 |
380 | if name == self.refname:
381 | return self
382 | if name in self.refdoc:
383 | return self.refdoc[name]
384 | for module in self.submodules:
385 | o = module.find_ident(name, _seen=_seen)
386 | if not isinstance(o, (External, type(None))):
387 | return o
388 | # Traverse also up-level super-modules
389 | module = self.parent
390 | while module is not None:
391 | o = module.find_ident(name, _seen=_seen)
392 | if not isinstance(o, (External, type(None))):
393 | return o
394 | module = module.parent
395 | return External(name)
396 |
397 | def variables(self):
398 | """
399 | Returns all documented module level variables in the module
400 | sorted alphabetically as a list of `pdoc.Variable`.
401 | """
402 | return _filter(self.doc.values(), Variable)
403 |
404 | def classes(self):
405 | """
406 | Returns all documented module level classes in the module
407 | sorted alphabetically as a list of `pdoc.Class`.
408 | """
409 | return _filter(self.doc.values(), Class)
410 |
411 | def functions(self):
412 | """
413 | Returns all documented module level functions in the module
414 | sorted alphabetically as a list of `pdoc.Function`.
415 | """
416 | return _filter(self.doc.values(), Function)
417 |
418 | def __is_exported(self, name, module):
419 | """
420 | Returns `True` if and only if `pdoc` considers `name` to be
421 | a public identifier for this module where `name` was defined
422 | in the Python module `module`.
423 |
424 | If this module has an `__all__` attribute, then `name` is
425 | considered to be exported if and only if it is a member of
426 | this module's `__all__` list.
427 |
428 | If `__all__` is not set, then whether `name` is exported or
429 | not is heuristically determined. Firstly, if `name` starts
430 | with an underscore, it will not be considered exported.
431 | Secondly, if `name` was defined in a module other than this
432 | one, it will not be considered exported. In all other cases,
433 | `name` will be considered exported.
434 | """
435 | if hasattr(self.module, "__all__"):
436 | return name in self.module.__all__
437 | if not _is_exported(name):
438 | return False
439 | if module is not None and self.module.__name__ != module.__name__:
440 | return name in self._declared_variables
441 | return True
442 |
443 | def __public_objs(self):
444 | """
445 | Returns a dictionary mapping a public identifier name to a
446 | Python object.
447 | """
448 | members = dict(inspect.getmembers(self.module))
449 | return dict(
450 | [
451 | (name, obj)
452 | for name, obj in members.items()
453 | if self.__is_exported(name, inspect.getmodule(obj))
454 | ]
455 | )
456 |
457 | def allmodules(self):
458 | yield self
459 | for i in self.submodules:
460 | yield from i.allmodules()
461 |
462 | def toroot(self):
463 | n = self
464 | while n:
465 | yield n
466 | n = n.parent
467 |
468 |
469 | class Class(Doc):
470 | """
471 | Representation of a class's documentation.
472 | """
473 |
474 | def __init__(self, name, module, class_obj):
475 | """
476 | Same as `pdocs.Doc.__init__`, except `class_obj` must be a
477 | Python class object. The docstring is gathered automatically.
478 | """
479 | super().__init__(name, module, inspect.getdoc(class_obj))
480 |
481 | self.cls = class_obj
482 | """The class Python object."""
483 |
484 | self.doc = {}
485 | """A mapping from identifier name to a `pdoc.Doc` objects."""
486 |
487 | self.doc_init = {}
488 | """
489 | A special version of `pdoc.Class.doc` that contains
490 | documentation for instance variables found in the `__init__`
491 | method.
492 | """
493 |
494 | public = self.__public_objs()
495 | try:
496 | # First try and find docstrings for class variables.
497 | # Then move on to finding docstrings for instance variables.
498 | # This must be optional, since not all modules have source
499 | # code available.
500 | cls_ast = ast.parse(inspect.getsource(self.cls)).body[0]
501 | self.doc = _var_docstrings(cls_ast, self.module, cls=self)
502 |
503 | for n in cls_ast.body if "__init__" in public else []:
504 | if isinstance(n, ast.FunctionDef) and n.name == "__init__":
505 | self.doc_init = _var_docstrings(n, self.module, cls=self, init=True)
506 | break
507 | except BaseException:
508 | pass
509 |
510 | # Convert the public Python objects to documentation objects.
511 | for name, obj in public.items():
512 | # Skip any identifiers that already have doco.
513 | if name in self.doc and not self.doc[name].is_empty():
514 | continue
515 | if name in self.doc_init:
516 | # Let instance members override class members.
517 | continue
518 |
519 | if inspect.isroutine(obj):
520 | self.doc[name] = Function(
521 | name, self.module, obj, cls=self, method=_is_method(self.cls, name)
522 | )
523 | elif isinstance(obj, property):
524 | docstring = getattr(obj, "__doc__", "")
525 | self.doc_init[name] = Variable(name, self.module, docstring, cls=self)
526 | elif not inspect.isbuiltin(obj) and not inspect.isroutine(obj):
527 | if name in getattr(self.cls, "__slots__", []):
528 | self.doc_init[name] = Variable(name, self.module, "", cls=self)
529 | else:
530 | self.doc[name] = Variable(name, self.module, "", cls=self)
531 |
532 | @property
533 | def source(self):
534 | return _source(self.cls)
535 |
536 | @property
537 | def refname(self):
538 | return "%s.%s" % (self.module.refname, self.cls.__name__)
539 |
540 | def class_variables(self):
541 | """
542 | Returns all documented class variables in the class, sorted
543 | alphabetically as a list of `pdoc.Variable`.
544 | """
545 | return _filter(self.doc.values(), Variable)
546 |
547 | def instance_variables(self):
548 | """
549 | Returns all instance variables in the class, sorted
550 | alphabetically as a list of `pdoc.Variable`. Instance variables
551 | are attributes of `self` defined in a class's `__init__`
552 | method.
553 | """
554 | return _filter(self.doc_init.values(), Variable)
555 |
556 | def methods(self):
557 | """
558 | Returns all documented methods as `pdoc.Function` objects in
559 | the class, sorted alphabetically.
560 |
561 | Unfortunately, this also includes class methods.
562 | """
563 | return _filter(self.doc.values(), Function, attributes_set=("method",))
564 |
565 | def functions(self):
566 | """
567 | Returns all documented static functions as `pdoc.Function`
568 | objects in the class, sorted alphabetically.
569 | """
570 | return _filter(self.doc.values(), Function, attributes_not_set=("method",))
571 |
572 | def params(self):
573 | """Returns back the parameters for the classes __init__ method"""
574 | params = Function._params(self.cls.__init__)
575 | return params[1:] if params[0] == "self" else params
576 |
577 | def _fill_inheritance(self):
578 | """
579 | Traverses this class's ancestor list and attempts to fill in
580 | missing documentation from its ancestor's documentation.
581 |
582 | The first pass connects variables, methods and functions with
583 | their inherited couterparts. (The templates will decide how to
584 | display docstrings.) The second pass attempts to add instance
585 | variables to this class that were only explicitly declared in
586 | a parent class. This second pass is necessary since instance
587 | variables are only discoverable by traversing the abstract
588 | syntax tree.
589 | """
590 | mro = [c for c in self.module.mro(self) if c != self and isinstance(c, Class)]
591 |
592 | def search(d, fdoc):
593 | for c in mro:
594 | doc = fdoc(c)
595 | if d.name in doc and isinstance(d, type(doc[d.name])):
596 | return doc[d.name]
597 | return None
598 |
599 | for fdoc in (lambda c: c.doc_init, lambda c: c.doc):
600 | for d in fdoc(self).values():
601 | dinherit = search(d, fdoc)
602 | if dinherit is not None:
603 | d.inherits = dinherit
604 |
605 | # Since instance variables aren't part of a class's members,
606 | # we need to manually deduce inheritance. Oh lawdy.
607 | for c in mro:
608 | for name in filter(lambda n: n not in self.doc_init, c.doc_init):
609 | d = c.doc_init[name]
610 | self.doc_init[name] = Variable(d.name, d.module, "", cls=self)
611 | self.doc_init[name].inherits = d
612 |
613 | def mro(self):
614 | """Returns back the Method Resolution Order (MRO) for this class"""
615 | return [
616 | self.module.find_class(cls)
617 | for cls in inspect.getmro(self.cls)
618 | if cls not in (self.cls, object, self)
619 | ]
620 |
621 | def subclasses(self):
622 | """Returns back all subclasses of this class"""
623 | if isinstance(self.cls, GenericAlias):
624 | return []
625 | return [self.module.find_class(cls) for cls in type.__subclasses__(self.cls)]
626 |
627 | def __public_objs(self):
628 | """
629 | Returns a dictionary mapping a public identifier name to a
630 | Python object. This counts the `__init__` method as being
631 | public.
632 | """
633 | _pdoc = getattr(self.module.module, "__pdoc__", {})
634 |
635 | def forced_out(name):
636 | return _pdoc.get("%s.%s" % (self.name, name), False) is None
637 |
638 | def exported(name):
639 | if _is_exported(name) and not forced_out(name):
640 | return name
641 |
642 | idents = dict(inspect.getmembers(self.cls))
643 | return dict([(n, o) for n, o in idents.items() if exported(n)])
644 |
645 |
646 | class Function(Doc):
647 | """
648 | Representation of documentation for a Python function or method.
649 | """
650 |
651 | def __init__(self, name, module, func_obj, cls=None, method=False):
652 | """
653 | Same as `pdoc.Doc.__init__`, except `func_obj` must be a
654 | Python function object. The docstring is gathered automatically.
655 |
656 | `cls` should be set when this is a method or a static function
657 | beloing to a class. `cls` should be a `pdoc.Class` object.
658 |
659 | `method` should be `True` when the function is a method. In
660 | all other cases, it should be `False`.
661 | """
662 | super().__init__(name, module, inspect.getdoc(func_obj))
663 |
664 | self.func = func_obj
665 | """The Python function object."""
666 |
667 | self.cls = cls
668 | """
669 | The `pdoc.Class` documentation object if this is a method. If
670 | not, this is None.
671 | """
672 |
673 | self.method = method
674 | """
675 | Whether this function is a method or not.
676 |
677 | In particular, static class methods have this set to False.
678 | """
679 |
680 | @property
681 | def source(self):
682 | return _source(self.func)
683 |
684 | @property
685 | def refname(self):
686 | if self.cls is None:
687 | return "%s.%s" % (self.module.refname, self.name)
688 | else:
689 | return "%s.%s" % (self.cls.refname, self.name)
690 |
691 | def funcdef(self):
692 | """
693 | Generates the string of keywords used to define the function, for
694 | example `def` or `async def`.
695 | """
696 | keywords = []
697 |
698 | if self._is_async():
699 | keywords.append("async")
700 |
701 | keywords.append("def")
702 |
703 | return " ".join(keywords)
704 |
705 | def _is_async(self):
706 | """
707 | Returns whether is function is asynchronous, either as a coroutine or an
708 | async generator.
709 | """
710 | try:
711 | # Both of these are required because coroutines aren't classified as
712 | # async generators and vice versa.
713 | return inspect.iscoroutinefunction(self.func) or inspect.isasyncgenfunction(self.func)
714 | except AttributeError:
715 | return False
716 |
717 | def spec(self):
718 | """
719 | Returns a nicely formatted spec of the function's parameter
720 | list as a string. This includes argument lists, keyword
721 | arguments and default values.
722 | """
723 | return ", ".join(self.params())
724 |
725 | @staticmethod
726 | def _signature(function):
727 | try:
728 | return inspect.signature(function)
729 | except (TypeError, ValueError): # We can't get a Python signature (likely C function)
730 | return False
731 |
732 | def return_annotation(self):
733 | """Returns back return type annotation if a valid one is found"""
734 | signature = self._signature(self.func)
735 | if not signature or signature.return_annotation == inspect._empty:
736 | return ""
737 |
738 | return inspect.formatannotation(signature.return_annotation)
739 |
740 | def params(self):
741 | """
742 | Returns a list where each element is a nicely formatted
743 | parameter of this function. This includes argument lists,
744 | keyword arguments and default values.
745 | """
746 | return self._params(self.func)
747 |
748 | @classmethod
749 | def _params(cls, function):
750 | """
751 | Returns a list where each element is a nicely formatted
752 | parameter of this function. This includes argument lists,
753 | keyword arguments and default values.
754 | """
755 | signature = cls._signature(function)
756 | if not signature:
757 | return ["..."]
758 |
759 | # The following is taken almost verbatim from the Python stdlib
760 | # https://github.com/python/cpython/blob/3.6/Lib/inspect.py#L3017
761 | #
762 | # This is done simply because it is hard to unstringify the result since commas could
763 | # be present beyond just between parameters.
764 | params = []
765 | render_pos_only_separator = False
766 | render_kw_only_separator = True
767 | for param in signature.parameters.values():
768 | kind = param.kind
769 | if kind == inspect._POSITIONAL_ONLY:
770 | render_pos_only_separator = True
771 | elif render_pos_only_separator:
772 | params.append("/")
773 | render_pos_only_separator = False
774 |
775 | if kind == inspect._VAR_POSITIONAL:
776 | render_kw_only_separator = False
777 | elif kind == inspect._KEYWORD_ONLY and render_kw_only_separator:
778 | params.append("*")
779 | render_kw_only_separator = False
780 |
781 | params.append(str(param))
782 |
783 | if render_pos_only_separator:
784 | params.append("/")
785 |
786 | return params
787 |
788 | def __lt__(self, other):
789 | # Push __init__ to the top.
790 | if "__init__" in (self.name, other.name):
791 | return self.name != other.name and self.name == "__init__"
792 | else:
793 | return self.name < other.name
794 |
795 |
796 | class Variable(Doc):
797 | """
798 | Representation of a variable's documentation. This includes
799 | module, class and instance variables.
800 | """
801 |
802 | def __init__(self, name, module, docstring, cls=None):
803 | """
804 | Same as `pdoc.Doc.__init__`, except `cls` should be provided
805 | as a `pdoc.Class` object when this is a class or instance
806 | variable.
807 | """
808 | super().__init__(name, module, docstring)
809 |
810 | self.cls = cls
811 | """
812 | The `podc.Class` object if this is a class or instance
813 | variable. If not, this is None.
814 | """
815 |
816 | @property
817 | def source(self):
818 | return []
819 |
820 | @property
821 | def refname(self):
822 | if self.cls is None:
823 | return "%s.%s" % (self.module.refname, self.name)
824 | else:
825 | return "%s.%s" % (self.cls.refname, self.name)
826 |
827 |
828 | class External(Doc):
829 | """
830 | A representation of an external identifier. The textual
831 | representation is the same as an internal identifier, but without
832 | any context. (Usually this makes linking more difficult.)
833 |
834 | External identifiers are also used to represent something that is
835 | not exported but appears somewhere in the public interface (like
836 | the ancestor list of a class).
837 | """
838 |
839 | __pdoc__[
840 | "External.docstring"
841 | ] = """
842 | An empty string. External identifiers do not have
843 | docstrings.
844 | """
845 | __pdoc__[
846 | "External.module"
847 | ] = """
848 | Always `None`. External identifiers have no associated
849 | `pdoc.Module`.
850 | """
851 | __pdoc__[
852 | "External.name"
853 | ] = """
854 | Always equivalent to `pdoc.External.refname` since external
855 | identifiers are always expressed in their fully qualified
856 | form.
857 | """
858 |
859 | def __init__(self, name):
860 | """
861 | Initializes an external identifier with `name`, where `name`
862 | should be a fully qualified name.
863 | """
864 | super().__init__(name, None, "")
865 |
866 | @property
867 | def source(self):
868 | return []
869 |
870 | @property
871 | def refname(self):
872 | return self.name
873 |
--------------------------------------------------------------------------------
/pdocs/extract.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 | import os
3 | import pkgutil
4 | import typing
5 |
6 | import pdocs.doc
7 |
8 |
9 | class ExtractError(Exception):
10 | pass
11 |
12 |
13 | def split_module_spec(spec: str) -> typing.Tuple[str, str]:
14 | """
15 | Splits a module specification into a base path (which may be empty), and a module name.
16 |
17 | Raises ExtactError if the spec is invalid.
18 | """
19 | if not spec:
20 | raise ExtractError("Empty module spec.")
21 | if (os.sep in spec) or (os.altsep and os.altsep in spec):
22 | dirname, fname = os.path.split(spec)
23 | if fname.endswith(".py"):
24 | mname, _ = os.path.splitext(fname)
25 | return dirname, mname
26 | else:
27 | if "." in fname:
28 | raise ExtractError(
29 | "Invalid module name {fname}. Mixing path and module specifications "
30 | "is not supported.".format(fname=fname)
31 | )
32 | return dirname, fname
33 | else:
34 | return "", spec
35 |
36 |
37 | def load_module(basedir: str, module: str) -> typing.Tuple[typing.Any, bool]:
38 | """
39 | Returns a module object, and whether the module is a package or not.
40 | """
41 | ispackage = False
42 | if basedir:
43 | mods = module.split(".")
44 | dirname = os.path.join(basedir, *mods[:-1])
45 | modname = mods[-1]
46 |
47 | pkgloc = os.path.join(dirname, modname, "__init__.py")
48 | fileloc = os.path.join(dirname, modname + ".py")
49 |
50 | if os.path.exists(pkgloc):
51 | location, ispackage = pkgloc, True
52 | elif os.path.exists(fileloc):
53 | location, ispackage = fileloc, False
54 | else:
55 | raise ExtractError(
56 | "Module {module} not found in {basedir}".format(module=module, basedir=basedir)
57 | )
58 |
59 | ispec = importlib.util.spec_from_file_location(modname, location)
60 | assert ispec
61 | mobj = importlib.util.module_from_spec(ispec)
62 | try:
63 | # This can literally raise anything
64 | ispec.loader.exec_module(mobj) # type: ignore
65 | except Exception as e:
66 | raise ExtractError("Error importing {location}: {e}".format(location=location, e=e))
67 | return mobj, ispackage
68 | else:
69 | try:
70 | # This can literally raise anything
71 | m = importlib.import_module(module)
72 | except ImportError as e:
73 | raise ExtractError("Could not import module {module}: {e}".format(module=module, e=e))
74 | except Exception as e:
75 | raise ExtractError("Error importing {module}: {e}".format(module=module, e=e))
76 | # This is the only case where we actually have to test whether we're a package
77 | if getattr(m, "__package__", False) and getattr(m, "__path__", False):
78 | ispackage = True
79 | return m, ispackage
80 |
81 |
82 | def submodules(dname: typing.Optional[str], mname: str) -> typing.Sequence[str]:
83 | """
84 | Return a list of submodule names using a file path or import based name
85 |
86 | If dname is None or empty string, mname will be used as import name.
87 | Otherwise, the relative directory path at dname will be joined to
88 | package name mname, and used as the base path for searching.
89 | """
90 | if dname:
91 | return _submodules_from_pathing(dname, mname)
92 | else:
93 | return _submodules_from_import_name(mname)
94 |
95 |
96 | def _submodules_from_import_name(mname: str) -> typing.Sequence[str]:
97 | """
98 | Return a list of fully qualified submodules within a package
99 |
100 | mname is an import based module name
101 | """
102 | spec = importlib.util.find_spec(mname)
103 | if not spec:
104 | return []
105 |
106 | loc = spec.submodule_search_locations
107 | if loc is None:
108 | # Case of mname corresponding to a terminal module, and not a package
109 | # iter_modules returns everything it can find anywhere if loc is None,
110 | # which is not what we want
111 | return []
112 | as_imported = importlib.import_module(mname)
113 | if getattr(as_imported, "__path__", None):
114 | [loc.append(path) for path in as_imported.__path__ if path not in loc] # type: ignore
115 | ret = []
116 | for mi in pkgutil.iter_modules(loc, prefix=mname + "."):
117 | if isinstance(mi, tuple):
118 | # Python 3.5 compat
119 | ret.append(mi[1])
120 | else:
121 | ret.append(mi.name)
122 | ret.sort()
123 | return ret
124 |
125 |
126 | def _submodules_from_pathing(dname: str, mname: str) -> typing.Sequence[str]:
127 | """
128 | Return a list of fully qualified submodules within a package, given a
129 | base directory and a fully qualified module name.
130 |
131 | dname is a directory file path, under which mname is stored,
132 | and mname is module to search for submodules from
133 | """
134 | loc = os.path.join(dname, *mname.split("."))
135 | ret = []
136 | for mi in pkgutil.iter_modules([loc], prefix=mname + "."):
137 | if isinstance(mi, tuple):
138 | # Python 3.5 compat
139 | ret.append(mi[1])
140 | else:
141 | ret.append(mi.name)
142 | ret.sort()
143 | return ret
144 |
145 |
146 | def _extract_module(dname: str, mname: str, parent=None) -> typing.Any:
147 | m, pkg = load_module(dname, mname)
148 | mod = pdocs.doc.Module(mname, m, parent)
149 | if pkg:
150 | for submodule_full_name in submodules(dname, mname):
151 | if submodule_full_name.split(".")[-1].startswith("_"):
152 | continue
153 |
154 | mod.submodules.append(_extract_module(dname, submodule_full_name, parent=mod))
155 | return mod
156 |
157 |
158 | def extract_module(spec: str):
159 | """
160 | Extracts and returns a module object. The spec argument can have the
161 | following forms:
162 |
163 | Simple module: "foo.bar"
164 | Module path: "./path/to/module"
165 | File path: "./path/to/file.py"
166 |
167 | This function always invalidates caches to enable hot load and reload.
168 |
169 | May raise ExtactError.
170 | """
171 | importlib.invalidate_caches()
172 | dname, mname = split_module_spec(spec)
173 | return _extract_module(dname, mname)
174 |
--------------------------------------------------------------------------------
/pdocs/html_helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import sys
4 |
5 | import markdown
6 | import pygments
7 | import pygments.formatters
8 | import pygments.lexers
9 |
10 | import pdocs.doc
11 | import pdocs.render
12 | from pdocs import defaults
13 |
14 | # From language reference, but adds '.' to allow fully qualified names.
15 | pyident = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_.]+$")
16 | indent = re.compile(r"^\s*")
17 |
18 | _markdown = markdown.Markdown(
19 | output_format="html",
20 | extensions=defaults.MARKDOWN_EXTENSIONS,
21 | extension_configs=defaults.MARKDOWN_EXTENSION_CONFIGS,
22 | )
23 |
24 |
25 | def _markdown_render(text):
26 | return _markdown.reset().convert(text)
27 |
28 |
29 | def decode(s):
30 | if sys.version_info[0] < 3 and isinstance(s, str):
31 | return s.decode("utf-8", "ignore")
32 | return s
33 |
34 |
35 | def ident(s):
36 | return '%s' % s
37 |
38 |
39 | def sourceid(dobj):
40 | return "source-%s" % dobj.refname
41 |
42 |
43 | def clean_source_lines(lines):
44 | """
45 | Cleans the source code so that pygments can render it well.
46 |
47 | Returns one string with all of the source code.
48 | """
49 | base_indent = len(indent.match(lines[0]).group(0))
50 | base_indent = 0
51 | for line in lines:
52 | if len(line.strip()) > 0:
53 | base_indent = len(indent.match(lines[0]).group(0))
54 | break
55 | lines = [line[base_indent:] for line in lines]
56 |
57 | if sys.version_info[0] < 3:
58 | pylex = pygments.lexers.PythonLexer()
59 | else:
60 | pylex = pygments.lexers.Python3Lexer()
61 |
62 | htmlform = pygments.formatters.HtmlFormatter(cssclass="codehilite")
63 | return pygments.highlight("".join(lines), pylex, htmlform)
64 |
65 |
66 | def linkify(parent, match, link_prefix):
67 | matched = match.group(0)
68 | ident = matched[1:-1]
69 | name, url = lookup(parent, ident, link_prefix)
70 | if name is None:
71 | return matched
72 | return "[`%s`](%s)" % (name, url)
73 |
74 |
75 | def mark(text, module_list=None, linky=True):
76 | if linky:
77 | text, _ = re.subn("\b\n\b", " ", text)
78 | return _markdown_render(text.strip())
79 |
80 |
81 | def glimpse(s, length=100):
82 | if len(s) < length:
83 | return s
84 | return s[0:length] + "..."
85 |
86 |
87 | def module_url(parent, m, link_prefix):
88 | """
89 | Returns a URL for `m`, which must be an instance of `Module`.
90 | Also, `m` must be a submodule of the module being documented.
91 |
92 | Namely, '.' import separators are replaced with '/' URL
93 | separators. Also, packages are translated as directories
94 | containing `index.html` corresponding to the `__init__` module,
95 | while modules are translated as regular HTML files with an
96 | `.m.html` suffix. (Given default values of
97 | `pdoc.html_module_suffix` and `pdoc.html_package_name`.)
98 | """
99 | if parent.name == m.name:
100 | return ""
101 |
102 | base = m.name.replace(".", "/")
103 | if len(link_prefix) == 0:
104 | base = os.path.relpath(base, parent.name.replace(".", "/"))
105 | url = base[len("../") :] if base.startswith("../") else "" if base == ".." else base
106 | if m.submodules:
107 | index = pdocs.render.html_package_name
108 | url = url + "/" + index if url else index
109 | else:
110 | url += pdocs.render.html_module_suffix
111 | return link_prefix + url
112 |
113 |
114 | def external_url(refname):
115 | """
116 | Attempts to guess an absolute URL for the external identifier
117 | given.
118 |
119 | Note that this just returns the refname with an ".ext" suffix.
120 | It will be up to whatever is interpreting the URLs to map it
121 | to an appropriate documentation page.
122 | """
123 | return "/%s.ext" % refname
124 |
125 |
126 | def is_external_linkable(name):
127 | return pyident.match(name) and "." in name
128 |
129 |
130 | def lookup(module, refname, link_prefix):
131 | """
132 | Given a fully qualified identifier name, return its refname
133 | with respect to the current module and a value for a `href`
134 | attribute. If `refname` is not in the public interface of
135 | this module or its submodules, then `None` is returned for
136 | both return values. (Unless this module has enabled external
137 | linking.)
138 |
139 | In particular, this takes into account sub-modules and external
140 | identifiers. If `refname` is in the public API of the current
141 | module, then a local anchor link is given. If `refname` is in the
142 | public API of a sub-module, then a link to a different page with
143 | the appropriate anchor is given. Otherwise, `refname` is
144 | considered external and no link is used.
145 | """
146 | d = module.find_ident(refname)
147 | if isinstance(d, pdocs.doc.External):
148 | if is_external_linkable(refname):
149 | return d.refname, external_url(d.refname)
150 | else:
151 | return None, None
152 | if isinstance(d, pdocs.doc.Module):
153 | return d.refname, module_url(module, d, link_prefix)
154 | if module.is_public(d.refname):
155 | return d.name, "#%s" % d.refname
156 | return d.refname, "%s#%s" % (module_url(module, d.module, link_prefix), d.refname)
157 |
158 |
159 | def link(parent, refname, link_prefix):
160 | """
161 | A convenience wrapper around `href` to produce the full
162 | `a` tag if `refname` is found. Otherwise, plain text of
163 | `refname` is returned.
164 | """
165 | name, url = lookup(parent, refname, link_prefix)
166 | if name is None:
167 | return refname
168 | return '%s' % (url, name)
169 |
--------------------------------------------------------------------------------
/pdocs/logo.py:
--------------------------------------------------------------------------------
1 | from pdocs._version import __version__
2 |
3 | ascii_art = rf"""
4 |
5 | 88
6 | 88
7 | 88
8 | 8b,dPPYba, ,adPPYb,88 ,adPPYba, ,adPPYba, ,adPPYba,
9 | 88P' "8a a8" `Y88 a8" "8a a8" "" I8[ ""
10 | 88 d8 8b 88 8b d8 8b `"Y8ba,
11 | 88b, ,a8" "8a, ,d88 "8a, ,a8" "8a, ,aa aa ]8I
12 | 88`YbbdP"' `"8bbdP"Y8 `"YbbdP"' `"Ybbd8"' `"YbbdP"'
13 | 88
14 | 88 - Documentation Powered by Your Python Code -
15 |
16 | Version: {__version__}
17 | Copyright Timothy Edmund Crosley 2019 MIT License
18 | """
19 |
20 | __doc__ = f"""
21 | ```python
22 | {ascii_art}
23 | ```
24 | """
25 |
--------------------------------------------------------------------------------
/pdocs/render.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import re
3 | import typing
4 |
5 | from mako.exceptions import TopLevelLookupException
6 | from mako.lookup import TemplateLookup
7 |
8 | import pdocs.doc
9 |
10 | html_module_suffix = ".html"
11 | html_package_name = "index.html"
12 | """
13 | The file name to use for a package's `__init__.py` module.
14 | """
15 |
16 | _template_path = [os.path.join(os.path.dirname(__file__), "templates")]
17 | """
18 | A list of paths to search for Mako templates used to produce the
19 | plain text and HTML output. Each path is tried until a template is
20 | found.
21 | """
22 |
23 | tpl_lookup = TemplateLookup(
24 | directories=_template_path, cache_args={"cached": True, "cache_type": "memory"}
25 | )
26 | """
27 | A `mako.lookup.TemplateLookup` object that knows how to load templates
28 | from the file system. You may add additional paths by modifying the
29 | object's `directories` attribute.
30 | """
31 |
32 |
33 | def _get_tpl(name):
34 | """
35 | Returns the Mako template with the given name. If the template cannot be
36 | found, a nicer error message is displayed.
37 | """
38 | try:
39 | t = tpl_lookup.get_template(name)
40 | except TopLevelLookupException:
41 | locs = [os.path.join(p, name.lstrip("/")) for p in _template_path]
42 | raise IOError(2, "No template at any of: %s" % ", ".join(locs))
43 | return t
44 |
45 |
46 | def html_index(roots: typing.Sequence[pdocs.doc.Module], link_prefix: str = "/") -> str:
47 | """
48 | Render an HTML module index.
49 | """
50 | t = _get_tpl("/html_index.mako")
51 | t = t.render(roots=roots, link_prefix=link_prefix)
52 | return t.strip()
53 |
54 |
55 | def html_module(
56 | mod: pdocs.doc.Module, external_links: bool = False, link_prefix: str = "/", source: bool = True
57 | ) -> str:
58 | """
59 | Returns the documentation for the module `module_name` in HTML
60 | format. The module must be importable.
61 |
62 | If `external_links` is `True`, then identifiers to external modules
63 | are always turned into links.
64 |
65 | If `link_prefix` is `True`, then all links will have that prefix.
66 | Otherwise, links are always relative.
67 |
68 | If `source` is `True`, then source code will be retrieved for
69 | every Python object whenever possible. This can dramatically
70 | decrease performance when documenting large modules.
71 | """
72 | t = _get_tpl("/html_module.mako")
73 | t = t.render(
74 | module=mod, external_links=external_links, link_prefix=link_prefix, show_source_code=source
75 | )
76 | return t.strip()
77 |
78 |
79 | def text(mod: pdocs.doc.Module, source: bool = True) -> str:
80 | """Returns the documentation for the module `module_name` in plain
81 | text format. The module must be importable.
82 |
83 | *source* - If set to True (the default) source will be included in the produced output.
84 | """
85 | raw_text = _get_tpl("/text.mako").render(module=mod, show_source_code=source)
86 | text, _ = re.subn("\n *\n *\n+", "\n\n", raw_text.strip().replace("\r\n", "\n"))
87 | return text
88 |
--------------------------------------------------------------------------------
/pdocs/static.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import typing
3 |
4 | import pdocs.doc
5 | import pdocs.render
6 |
7 |
8 | class StaticError(Exception):
9 | pass
10 |
11 |
12 | def module_to_path(m: pdocs.doc.Module, extension="html") -> pathlib.Path:
13 | """
14 | Calculates the filesystem path for the static output of a given module.
15 | """
16 | p = pathlib.Path(*m.name.split("."))
17 | if m.submodules:
18 | p /= f"index.{extension}"
19 | else:
20 | p = p.with_suffix(f".{extension}")
21 | return p
22 |
23 |
24 | def path_to_module(
25 | roots: typing.Sequence[pdocs.doc.Module], path: pathlib.Path
26 | ) -> pdocs.doc.Module:
27 | """
28 | Retrieves the matching module for a given path from a module tree.
29 | """
30 | if path.suffix == ".html":
31 | path = path.with_suffix("")
32 | parts = list(path.parts)
33 | if parts[-1] == "index":
34 | parts = parts[:-1]
35 | elif parts[-1] == "index.m":
36 | parts[-1] = "index"
37 | for root in roots:
38 | mod = root.find_ident(".".join(parts))
39 | if isinstance(mod, pdocs.doc.Module):
40 | return mod
41 | raise StaticError("No matching module for {path}".format(path=path))
42 |
43 |
44 | def would_overwrite(destination: pathlib.Path, roots: typing.Sequence[pdocs.doc.Module]) -> bool:
45 | """Would rendering root to dst overwrite any file?"""
46 | if len(roots) > 1:
47 | path = destination / "index.html"
48 | if path.exists():
49 | return True
50 | for root in roots:
51 | if destination.joinpath(root.name).exists():
52 | return True
53 | return False
54 |
55 |
56 | def html_out(
57 | dst: pathlib.Path,
58 | roots: typing.Sequence[pdocs.doc.Module],
59 | external_links: bool = True,
60 | link_prefix: str = "",
61 | source: bool = False,
62 | ):
63 | if len(roots) > 1:
64 | dst.mkdir(parents=True, exist_ok=True)
65 | p = dst / "index.html"
66 | idx = pdocs.render.html_index(roots, link_prefix=link_prefix)
67 | p.write_text(idx, encoding="utf-8")
68 | for root in roots:
69 | for m in root.allmodules():
70 | p = dst.joinpath(module_to_path(m))
71 | p.parent.mkdir(parents=True, exist_ok=True)
72 | out = pdocs.render.html_module(
73 | m, external_links=external_links, link_prefix=link_prefix, source=source
74 | )
75 | p.write_text(out, encoding="utf-8")
76 |
77 |
78 | def md_out(
79 | dst: pathlib.Path,
80 | roots: typing.Sequence[pdocs.doc.Module],
81 | externel_links: bool = True,
82 | source: bool = False,
83 | ):
84 | for root in roots:
85 | for m in root.allmodules():
86 | p = dst.joinpath(module_to_path(m, extension="md"))
87 | p.parent.mkdir(parents=True, exist_ok=True)
88 | out = pdocs.render.text(m, source=source)
89 | p.write_text(out, encoding="utf-8")
90 |
--------------------------------------------------------------------------------
/pdocs/templates/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) HTML5 Boilerplate
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/pdocs/templates/README.md:
--------------------------------------------------------------------------------
1 | The license included in this directory is for
2 | [HTML5 Boiler Plate](http://html5boilerplate.com). Some of the HTML and CSS
3 | used here is derived from that project.
4 |
--------------------------------------------------------------------------------
/pdocs/templates/css.mako:
--------------------------------------------------------------------------------
1 | <%def name="pdocs()">
2 | html, body {
3 | margin: 0;
4 | padding: 0;
5 | min-height: 100%;
6 | }
7 | body {
8 | background: #fff;
9 | font-family: "Source Sans Pro", "Helvetica Neueue", Helvetica, sans;
10 | font-weight: 300;
11 | font-size: 16px;
12 | line-height: 1.6em;
13 | }
14 | #content {
15 | width: 70%;
16 | max-width: 850px;
17 | float: left;
18 | padding: 30px 60px;
19 | border-left: 1px solid #ddd;
20 | }
21 | #sidebar {
22 | width: 25%;
23 | float: left;
24 | padding: 30px;
25 | overflow: hidden;
26 | }
27 | #nav {
28 | font-size: 130%;
29 | margin: 0 0 15px 0;
30 | }
31 |
32 | #top {
33 | display: block;
34 | position: fixed;
35 | bottom: 5px;
36 | left: 5px;
37 | font-size: .85em;
38 | text-transform: uppercase;
39 | }
40 |
41 | #footer {
42 | font-size: .75em;
43 | padding: 5px 30px;
44 | border-top: 1px solid #ddd;
45 | text-align: right;
46 | }
47 | #footer p {
48 | margin: 0 0 0 30px;
49 | display: inline-block;
50 | }
51 |
52 | h1, h2, h3, h4, h5 {
53 | font-weight: 300;
54 | }
55 | h1 {
56 | font-size: 2.5em;
57 | line-height: 1.1em;
58 | margin: 0 0 .50em 0;
59 | }
60 |
61 | h2 {
62 | font-size: 1.75em;
63 | margin: 1em 0 .50em 0;
64 | }
65 |
66 | h3 {
67 | margin: 25px 0 10px 0;
68 | }
69 |
70 | h4 {
71 | margin: 0;
72 | font-size: 105%;
73 | }
74 |
75 | a {
76 | color: #058;
77 | text-decoration: none;
78 | transition: color .3s ease-in-out;
79 | }
80 |
81 | a:hover {
82 | color: #e08524;
83 | transition: color .3s ease-in-out;
84 | }
85 |
86 | pre, code, .mono, .name {
87 | font-family: "Ubuntu Mono", "Cousine", "DejaVu Sans Mono", monospace;
88 | }
89 |
90 | .title .name {
91 | font-weight: bold;
92 | }
93 | .section-title {
94 | margin-top: 2em;
95 | }
96 | .ident {
97 | color: #900;
98 | }
99 |
100 | code {
101 | background: #f9f9f9;
102 | }
103 |
104 | pre {
105 | background: #fefefe;
106 | border: 1px solid #ddd;
107 | box-shadow: 2px 2px 0 #f3f3f3;
108 | margin: 0 30px;
109 | padding: 15px 30px;
110 | }
111 |
112 | .codehilite {
113 | margin: 0 30px 10px 30px;
114 | }
115 |
116 | .codehilite pre {
117 | margin: 0;
118 | }
119 | .codehilite .err { background: #ff3300; color: #fff !important; }
120 |
121 | table#module-list {
122 | font-size: 110%;
123 | }
124 |
125 | table#module-list tr td:first-child {
126 | padding-right: 10px;
127 | white-space: nowrap;
128 | }
129 |
130 | table#module-list td {
131 | vertical-align: top;
132 | padding-bottom: 8px;
133 | }
134 |
135 | table#module-list td p {
136 | margin: 0 0 7px 0;
137 | }
138 |
139 | .def {
140 | display: table;
141 | }
142 |
143 | .def p {
144 | display: table-cell;
145 | vertical-align: top;
146 | text-align: left;
147 | }
148 |
149 | .def p:first-child {
150 | white-space: nowrap;
151 | }
152 |
153 | .def p:last-child {
154 | width: 100%;
155 | }
156 |
157 |
158 | #index {
159 | list-style-type: none;
160 | margin: 0;
161 | padding: 0;
162 | }
163 | ul#index .class_name {
164 | /* font-size: 110%; */
165 | font-weight: bold;
166 | }
167 | #index ul {
168 | margin: 0;
169 | }
170 |
171 | .item {
172 | margin: 0 0 15px 0;
173 | }
174 |
175 | .item .class {
176 | margin: 0 0 25px 30px;
177 | }
178 |
179 | .item .class ul.class_list {
180 | margin: 0 0 20px 0;
181 | }
182 |
183 | .item .name {
184 | background: #fafafa;
185 | margin: 0;
186 | font-weight: bold;
187 | padding: 5px 10px;
188 | border-radius: 3px;
189 | display: inline-block;
190 | min-width: 40%;
191 | }
192 | .item .name:hover {
193 | background: #f6f6f6;
194 | }
195 |
196 | .item .empty_desc {
197 | margin: 0 0 5px 0;
198 | padding: 0;
199 | }
200 |
201 | .item .inheritance {
202 | margin: 3px 0 0 30px;
203 | }
204 |
205 | .item .inherited {
206 | color: #666;
207 | }
208 |
209 | .item .desc {
210 | padding: 0 8px;
211 | margin: 0;
212 | }
213 |
214 | .item .desc p {
215 | margin: 0 0 10px 0;
216 | }
217 |
218 | .source_cont {
219 | margin: 0;
220 | padding: 0;
221 | }
222 |
223 | .source_link a {
224 | background: #ffc300;
225 | font-weight: 400;
226 | font-size: .75em;
227 | text-transform: uppercase;
228 | color: #fff;
229 | text-shadow: 1px 1px 0 #f4b700;
230 |
231 | padding: 3px 8px;
232 | border-radius: 2px;
233 | transition: background .3s ease-in-out;
234 | }
235 | .source_link a:hover {
236 | background: #FF7200;
237 | text-shadow: none;
238 | transition: background .3s ease-in-out;
239 | }
240 |
241 | .source {
242 | display: none;
243 | max-height: 600px;
244 | overflow-y: scroll;
245 | margin-bottom: 15px;
246 | }
247 |
248 | .source .codehilite {
249 | margin: 0;
250 | }
251 |
252 | .desc h1, .desc h2, .desc h3 {
253 | font-size: 100% !important;
254 | }
255 | .clear {
256 | clear: both;
257 | }
258 |
259 | @media all and (max-width: 950px) {
260 | #sidebar {
261 | width: 35%;
262 | }
263 | #content {
264 | width: 65%;
265 | }
266 | }
267 | @media all and (max-width: 650px) {
268 | #top {
269 | display: none;
270 | }
271 | #sidebar {
272 | float: none;
273 | width: auto;
274 | }
275 | #content {
276 | float: none;
277 | width: auto;
278 | padding: 30px;
279 | }
280 |
281 | #index ul {
282 | padding: 0;
283 | margin-bottom: 15px;
284 | }
285 | #index ul li {
286 | display: inline-block;
287 | margin-right: 30px;
288 | }
289 | #footer {
290 | text-align: left;
291 | }
292 | #footer p {
293 | display: block;
294 | margin: inherit;
295 | }
296 | }
297 |
298 | /*****************************/
299 | %def>
300 | <%def name="pre()">
301 | * {
302 | box-sizing: border-box;
303 | }
304 | /*! normalize.css v1.1.1 | MIT License | git.io/normalize */
305 |
306 | /* ==========================================================================
307 | HTML5 display definitions
308 | ========================================================================== */
309 |
310 | /**
311 | * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3.
312 | */
313 |
314 | article,
315 | aside,
316 | details,
317 | figcaption,
318 | figure,
319 | footer,
320 | header,
321 | hgroup,
322 | main,
323 | nav,
324 | section,
325 | summary {
326 | display: block;
327 | }
328 |
329 | /**
330 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
331 | */
332 |
333 | audio,
334 | canvas,
335 | video {
336 | display: inline-block;
337 | *display: inline;
338 | *zoom: 1;
339 | }
340 |
341 | /**
342 | * Prevent modern browsers from displaying `audio` without controls.
343 | * Remove excess height in iOS 5 devices.
344 | */
345 |
346 | audio:not([controls]) {
347 | display: none;
348 | height: 0;
349 | }
350 |
351 | /**
352 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
353 | * Known issue: no IE 6 support.
354 | */
355 |
356 | [hidden] {
357 | display: none;
358 | }
359 |
360 | /* ==========================================================================
361 | Base
362 | ========================================================================== */
363 |
364 | /**
365 | * 1. Prevent system color scheme's background color being used in Firefox, IE,
366 | * and Opera.
367 | * 2. Prevent system color scheme's text color being used in Firefox, IE, and
368 | * Opera.
369 | * 3. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
370 | * `em` units.
371 | * 4. Prevent iOS text size adjust after orientation change, without disabling
372 | * user zoom.
373 | */
374 |
375 | html {
376 | background: #fff; /* 1 */
377 | color: #000; /* 2 */
378 | font-size: 100%; /* 3 */
379 | -webkit-text-size-adjust: 100%; /* 4 */
380 | -ms-text-size-adjust: 100%; /* 4 */
381 | }
382 |
383 | /**
384 | * Address `font-family` inconsistency between `textarea` and other form
385 | * elements.
386 | */
387 |
388 | html,
389 | button,
390 | input,
391 | select,
392 | textarea {
393 | font-family: sans-serif;
394 | }
395 |
396 | /**
397 | * Address margins handled incorrectly in IE 6/7.
398 | */
399 |
400 | body {
401 | margin: 0;
402 | }
403 |
404 | /* ==========================================================================
405 | Links
406 | ========================================================================== */
407 |
408 | /**
409 | * Address `outline` inconsistency between Chrome and other browsers.
410 | */
411 |
412 | a:focus {
413 | outline: thin dotted;
414 | }
415 |
416 | /**
417 | * Improve readability when focused and also mouse hovered in all browsers.
418 | */
419 |
420 | a:active,
421 | a:hover {
422 | outline: 0;
423 | }
424 |
425 | /* ==========================================================================
426 | Typography
427 | ========================================================================== */
428 |
429 | /**
430 | * Address font sizes and margins set differently in IE 6/7.
431 | * Address font sizes within `section` and `article` in Firefox 4+, Safari 5,
432 | * and Chrome.
433 | */
434 |
435 | h1 {
436 | font-size: 2em;
437 | margin: 0.67em 0;
438 | }
439 |
440 | h2 {
441 | font-size: 1.5em;
442 | margin: 0.83em 0;
443 | }
444 |
445 | h3 {
446 | font-size: 1.17em;
447 | margin: 1em 0;
448 | }
449 |
450 | h4 {
451 | font-size: 1em;
452 | margin: 1.33em 0;
453 | }
454 |
455 | h5 {
456 | font-size: 0.83em;
457 | margin: 1.67em 0;
458 | }
459 |
460 | h6 {
461 | font-size: 0.67em;
462 | margin: 2.33em 0;
463 | }
464 |
465 | /**
466 | * Address styling not present in IE 7/8/9, Safari 5, and Chrome.
467 | */
468 |
469 | abbr[title] {
470 | border-bottom: 1px dotted;
471 | }
472 |
473 | /**
474 | * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome.
475 | */
476 |
477 | b,
478 | strong {
479 | font-weight: bold;
480 | }
481 |
482 | blockquote {
483 | margin: 1em 40px;
484 | }
485 |
486 | /**
487 | * Address styling not present in Safari 5 and Chrome.
488 | */
489 |
490 | dfn {
491 | font-style: italic;
492 | }
493 |
494 | /**
495 | * Address differences between Firefox and other browsers.
496 | * Known issue: no IE 6/7 normalization.
497 | */
498 |
499 | hr {
500 | -moz-box-sizing: content-box;
501 | box-sizing: content-box;
502 | height: 0;
503 | }
504 |
505 | /**
506 | * Address styling not present in IE 6/7/8/9.
507 | */
508 |
509 | mark {
510 | background: #ff0;
511 | color: #000;
512 | }
513 |
514 | /**
515 | * Address margins set differently in IE 6/7.
516 | */
517 |
518 | p,
519 | pre {
520 | margin: 1em 0;
521 | }
522 |
523 | /**
524 | * Correct font family set oddly in IE 6, Safari 4/5, and Chrome.
525 | */
526 |
527 | code,
528 | kbd,
529 | pre,
530 | samp {
531 | font-family: monospace, serif;
532 | _font-family: 'courier new', monospace;
533 | font-size: 1em;
534 | }
535 |
536 | /**
537 | * Improve readability of pre-formatted text in all browsers.
538 | */
539 |
540 | pre {
541 | white-space: pre;
542 | white-space: pre-wrap;
543 | word-wrap: break-word;
544 | }
545 |
546 | /**
547 | * Address CSS quotes not supported in IE 6/7.
548 | */
549 |
550 | q {
551 | quotes: none;
552 | }
553 |
554 | /**
555 | * Address `quotes` property not supported in Safari 4.
556 | */
557 |
558 | q:before,
559 | q:after {
560 | content: '';
561 | content: none;
562 | }
563 |
564 | /**
565 | * Address inconsistent and variable font size in all browsers.
566 | */
567 |
568 | small {
569 | font-size: 80%;
570 | }
571 |
572 | /**
573 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
574 | */
575 |
576 | sub,
577 | sup {
578 | font-size: 75%;
579 | line-height: 0;
580 | position: relative;
581 | vertical-align: baseline;
582 | }
583 |
584 | sup {
585 | top: -0.5em;
586 | }
587 |
588 | sub {
589 | bottom: -0.25em;
590 | }
591 |
592 | /* ==========================================================================
593 | Lists
594 | ========================================================================== */
595 |
596 | /**
597 | * Address margins set differently in IE 6/7.
598 | */
599 |
600 | dl,
601 | menu,
602 | ol,
603 | ul {
604 | margin: 1em 0;
605 | }
606 |
607 | dd {
608 | margin: 0 0 0 40px;
609 | }
610 |
611 | /**
612 | * Address paddings set differently in IE 6/7.
613 | */
614 |
615 | menu,
616 | ol,
617 | ul {
618 | padding: 0 0 0 40px;
619 | }
620 |
621 | /**
622 | * Correct list images handled incorrectly in IE 7.
623 | */
624 |
625 | nav ul,
626 | nav ol {
627 | list-style: none;
628 | list-style-image: none;
629 | }
630 |
631 | /* ==========================================================================
632 | Embedded content
633 | ========================================================================== */
634 |
635 | /**
636 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
637 | * 2. Improve image quality when scaled in IE 7.
638 | */
639 |
640 | img {
641 | border: 0; /* 1 */
642 | -ms-interpolation-mode: bicubic; /* 2 */
643 | }
644 |
645 | /**
646 | * Correct overflow displayed oddly in IE 9.
647 | */
648 |
649 | svg:not(:root) {
650 | overflow: hidden;
651 | }
652 |
653 | /* ==========================================================================
654 | Figures
655 | ========================================================================== */
656 |
657 | /**
658 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
659 | */
660 |
661 | figure {
662 | margin: 0;
663 | }
664 |
665 | /* ==========================================================================
666 | Forms
667 | ========================================================================== */
668 |
669 | /**
670 | * Correct margin displayed oddly in IE 6/7.
671 | */
672 |
673 | form {
674 | margin: 0;
675 | }
676 |
677 | /**
678 | * Define consistent border, margin, and padding.
679 | */
680 |
681 | fieldset {
682 | border: 1px solid #c0c0c0;
683 | margin: 0 2px;
684 | padding: 0.35em 0.625em 0.75em;
685 | }
686 |
687 | /**
688 | * 1. Correct color not being inherited in IE 6/7/8/9.
689 | * 2. Correct text not wrapping in Firefox 3.
690 | * 3. Correct alignment displayed oddly in IE 6/7.
691 | */
692 |
693 | legend {
694 | border: 0; /* 1 */
695 | padding: 0;
696 | white-space: normal; /* 2 */
697 | *margin-left: -7px; /* 3 */
698 | }
699 |
700 | /**
701 | * 1. Correct font size not being inherited in all browsers.
702 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
703 | * and Chrome.
704 | * 3. Improve appearance and consistency in all browsers.
705 | */
706 |
707 | button,
708 | input,
709 | select,
710 | textarea {
711 | font-size: 100%; /* 1 */
712 | margin: 0; /* 2 */
713 | vertical-align: baseline; /* 3 */
714 | *vertical-align: middle; /* 3 */
715 | }
716 |
717 | /**
718 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in
719 | * the UA stylesheet.
720 | */
721 |
722 | button,
723 | input {
724 | line-height: normal;
725 | }
726 |
727 | /**
728 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
729 | * All other form control elements do not inherit `text-transform` values.
730 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
731 | * Correct `select` style inheritance in Firefox 4+ and Opera.
732 | */
733 |
734 | button,
735 | select {
736 | text-transform: none;
737 | }
738 |
739 | /**
740 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
741 | * and `video` controls.
742 | * 2. Correct inability to style clickable `input` types in iOS.
743 | * 3. Improve usability and consistency of cursor style between image-type
744 | * `input` and others.
745 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
746 | * Known issue: inner spacing remains in IE 6.
747 | */
748 |
749 | button,
750 | html input[type="button"], /* 1 */
751 | input[type="reset"],
752 | input[type="submit"] {
753 | -webkit-appearance: button; /* 2 */
754 | cursor: pointer; /* 3 */
755 | *overflow: visible; /* 4 */
756 | }
757 |
758 | /**
759 | * Re-set default cursor for disabled elements.
760 | */
761 |
762 | button[disabled],
763 | html input[disabled] {
764 | cursor: default;
765 | }
766 |
767 | /**
768 | * 1. Address box sizing set to content-box in IE 8/9.
769 | * 2. Remove excess padding in IE 8/9.
770 | * 3. Remove excess padding in IE 7.
771 | * Known issue: excess padding remains in IE 6.
772 | */
773 |
774 | input[type="checkbox"],
775 | input[type="radio"] {
776 | box-sizing: border-box; /* 1 */
777 | padding: 0; /* 2 */
778 | *height: 13px; /* 3 */
779 | *width: 13px; /* 3 */
780 | }
781 |
782 | /**
783 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
784 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
785 | * (include `-moz` to future-proof).
786 | */
787 |
788 | input[type="search"] {
789 | -webkit-appearance: textfield; /* 1 */
790 | -moz-box-sizing: content-box;
791 | -webkit-box-sizing: content-box; /* 2 */
792 | box-sizing: content-box;
793 | }
794 |
795 | /**
796 | * Remove inner padding and search cancel button in Safari 5 and Chrome
797 | * on OS X.
798 | */
799 |
800 | input[type="search"]::-webkit-search-cancel-button,
801 | input[type="search"]::-webkit-search-decoration {
802 | -webkit-appearance: none;
803 | }
804 |
805 | /**
806 | * Remove inner padding and border in Firefox 3+.
807 | */
808 |
809 | button::-moz-focus-inner,
810 | input::-moz-focus-inner {
811 | border: 0;
812 | padding: 0;
813 | }
814 |
815 | /**
816 | * 1. Remove default vertical scrollbar in IE 6/7/8/9.
817 | * 2. Improve readability and alignment in all browsers.
818 | */
819 |
820 | textarea {
821 | overflow: auto; /* 1 */
822 | vertical-align: top; /* 2 */
823 | }
824 |
825 | /* ==========================================================================
826 | Tables
827 | ========================================================================== */
828 |
829 | /**
830 | * Remove most spacing between table cells.
831 | */
832 |
833 | table {
834 | border-collapse: collapse;
835 | border-spacing: 0;
836 | }
837 | %def>
838 |
839 | <%def name="post()">
840 | /* ==========================================================================
841 | EXAMPLE Media Queries for Responsive Design.
842 | These examples override the primary ('mobile first') styles.
843 | Modify as content requires.
844 | ========================================================================== */
845 |
846 | @media only screen and (min-width: 35em) {
847 | /* Style adjustments for viewports that meet the condition */
848 | }
849 |
850 | @media print,
851 | (-o-min-device-pixel-ratio: 5/4),
852 | (-webkit-min-device-pixel-ratio: 1.25),
853 | (min-resolution: 120dpi) {
854 | /* Style adjustments for high resolution devices */
855 | }
856 |
857 | /* ==========================================================================
858 | Print styles.
859 | Inlined to avoid required HTTP connection: h5bp.com/r
860 | ========================================================================== */
861 |
862 | @media print {
863 | * {
864 | background: transparent !important;
865 | color: #000 !important; /* Black prints faster: h5bp.com/s */
866 | box-shadow: none !important;
867 | text-shadow: none !important;
868 | }
869 |
870 | a,
871 | a:visited {
872 | text-decoration: underline;
873 | }
874 |
875 | a[href]:after {
876 | content: " (" attr(href) ")";
877 | }
878 |
879 | abbr[title]:after {
880 | content: " (" attr(title) ")";
881 | }
882 |
883 | /*
884 | * Don't show links for images, or javascript/internal links
885 | */
886 |
887 | .ir a:after,
888 | a[href^="javascript:"]:after,
889 | a[href^="#"]:after {
890 | content: "";
891 | }
892 |
893 | pre,
894 | blockquote {
895 | border: 1px solid #999;
896 | page-break-inside: avoid;
897 | }
898 |
899 | thead {
900 | display: table-header-group; /* h5bp.com/t */
901 | }
902 |
903 | tr,
904 | img {
905 | page-break-inside: avoid;
906 | }
907 |
908 | img {
909 | max-width: 100% !important;
910 | }
911 |
912 | @page {
913 | margin: 0.5cm;
914 | }
915 |
916 | p,
917 | h2,
918 | h3 {
919 | orphans: 3;
920 | widows: 3;
921 | }
922 |
923 | h2,
924 | h3 {
925 | page-break-after: avoid;
926 | }
927 | }
928 | %def>
929 |
--------------------------------------------------------------------------------
/pdocs/templates/html_frame.mako:
--------------------------------------------------------------------------------
1 | ## -*- coding: utf-8 -*-
2 | <%!
3 | import pygments
4 | import pdocs
5 | import pdocs.html_helpers as hh
6 | %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <%block name="title"/>
15 |
16 |
17 | <%namespace name="css" file="css.mako" />
18 |
19 |
20 |
23 |
24 |
25 |
39 |
40 |
41 | Top
42 |