├── .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 | [![pdocs - Documentation Powered by Your Python Code.](https://raw.github.com/timothycrosley/pdocs/master/art/logo_large.png)](https://timothycrosley.github.io/pdocs/) 2 | _________________ 3 | 4 | [![PyPI version](https://badge.fury.io/py/pdocs.svg)](http://badge.fury.io/py/pdocs) 5 | [![Build Status](https://travis-ci.org/timothycrosley/pdocs.svg?branch=master)](https://travis-ci.org/timothycrosley/pdocs) 6 | [![codecov](https://codecov.io/gh/timothycrosley/pdocs/branch/master/graph/badge.svg)](https://codecov.io/gh/timothycrosley/pdocs) 7 | [![Join the chat at https://gitter.im/pdocs/community](https://badges.gitter.im/pdocs/community.svg)](https://gitter.im/pdocs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.python.org/pypi/pdocs/) 9 | [![Downloads](https://pepy.tech/badge/pdocs)](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 | [![asciicast](https://asciinema.org/a/265744.svg)](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 | 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 | 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 | 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 |
43 | ${next.body()} 44 |
45 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /pdocs/templates/html_index.mako: -------------------------------------------------------------------------------- 1 | ## -*- coding: utf-8 -*- 2 | <%! 3 | import pygments 4 | import pdocs.doc 5 | import pdocs.html_helpers as hh 6 | %> 7 | 8 | <%inherit file="html_frame.mako"/> 9 | 10 | <%def name="show_module_list(roots)"> 11 |

Python module list

12 | 13 | % for root in roots: 14 | 15 | 16 | 21 | 22 | % endfor 23 |
${root.name} 17 | % if len(root.docstring.strip()) > 0: 18 |
${root.docstring | hh.mark}
19 | % endif 20 |
24 | 25 | 26 | <%block name="title"> 27 | Python module index 28 | 29 | 30 | 31 |
${show_module_list(roots)}
32 | -------------------------------------------------------------------------------- /pdocs/templates/html_module.mako: -------------------------------------------------------------------------------- 1 | ## -*- coding: utf-8 -*- 2 | <%! 3 | import pygments 4 | import pdocs.doc 5 | import pdocs.html_helpers as hh 6 | %> 7 | 8 | <%inherit file="html_frame.mako"/> 9 | 10 | <%def name="show_source(d)"> 11 | % if show_source_code and d.source is not None and len(d.source) > 0: 12 | 13 |
14 | ${hh.decode(hh.clean_source_lines(d.source))} 15 |
16 | % endif 17 | 18 | 19 | <%def name="show_desc(d, limit=None)"> 20 | <% 21 | inherits = (hasattr(d, 'inherits') 22 | and (len(d.docstring) == 0 23 | or d.docstring == d.inherits.docstring)) 24 | docstring = (d.inherits.docstring if inherits else d.docstring).strip() 25 | if limit is not None: 26 | docstring = hh.glimpse(docstring, limit) 27 | %> 28 | % if len(docstring) > 0: 29 | % if inherits: 30 |
${docstring | hh.mark}
31 | % else: 32 |
${docstring | hh.mark}
33 | % endif 34 | % endif 35 | % if not isinstance(d, pdocs.doc.Module): 36 |
${show_source(d)}
37 | % endif 38 | 39 | 40 | <%def name="show_inheritance(d)"> 41 | % if hasattr(d, 'inherits'): 42 |

43 | Inheritance: 44 | % if hasattr(d.inherits, 'cls'): 45 | ${hh.link(module, d.inherits.cls.refname, link_prefix)}.${hh.link(module, d.inherits.refname, link_prefix)} 46 | % else: 47 | ${hh.link(module, d.inherits.refname), link_prefix} 48 | % endif 49 |

50 | % endif 51 | 52 | 53 | <%def name="show_column_list(items, numcols=3)"> 54 |
    55 | % for item in items: 56 |
  • ${item}
  • 57 | % endfor 58 |
59 | 60 | 61 | <%def name="show_module(module)"> 62 | <% 63 | variables = module.variables() 64 | classes = module.classes() 65 | functions = module.functions() 66 | submodules = module.submodules 67 | %> 68 | 69 | <%def name="show_func(f)"> 70 |
71 |
72 |

${f.funcdef()} ${hh.ident(f.name)}(

${f.spec() | h})

73 |
74 | ${show_inheritance(f)} 75 | ${show_desc(f)} 76 |
77 | 78 | 79 | % if 'http_server' in context.keys() and http_server: 80 | 88 | % endif 89 | 90 |
91 |

${module.name} module

92 | ${module.docstring | hh.mark} 93 | ${show_source(module)} 94 |
95 | 96 |
97 | % if len(variables) > 0: 98 |

Module variables

99 | % for v in variables: 100 |
101 |

var ${hh.ident(v.name)}

102 | ${show_desc(v)} 103 |
104 | % endfor 105 | % endif 106 | 107 | % if len(functions) > 0: 108 |

Functions

109 | % for f in functions: 110 | ${show_func(f)} 111 | % endfor 112 | % endif 113 | 114 | % if len(classes) > 0: 115 |

Classes

116 | % for c in classes: 117 | <% 118 | class_vars = c.class_variables() 119 | smethods = c.functions() 120 | inst_vars = c.instance_variables() 121 | methods = c.methods() 122 | mro = c.module.mro(c) 123 | %> 124 |
125 |

class ${hh.ident(c.name)}

126 | ${show_desc(c)} 127 | 128 |
129 | % if len(mro) > 0: 130 |

Ancestors (in MRO)

131 |
    132 | % for cls in mro: 133 |
  • ${hh.link(module, cls.refname, link_prefix)}
  • 134 | % endfor 135 |
136 | % endif 137 | % if len(class_vars) > 0: 138 |

Class variables

139 | % for v in class_vars: 140 |
141 |

var ${hh.ident(v.name)}

142 | ${show_inheritance(v)} 143 | ${show_desc(v)} 144 |
145 | % endfor 146 | % endif 147 | % if len(smethods) > 0: 148 |

Static methods

149 | % for f in smethods: 150 | ${show_func(f)} 151 | % endfor 152 | % endif 153 | % if len(inst_vars) > 0: 154 |

Instance variables

155 | % for v in inst_vars: 156 |
157 |

var ${hh.ident(v.name)}

158 | ${show_inheritance(v)} 159 | ${show_desc(v)} 160 |
161 | % endfor 162 | % endif 163 | % if len(methods) > 0: 164 |

Methods

165 | % for f in methods: 166 | ${show_func(f)} 167 | % endfor 168 | % endif 169 |
170 |
171 | % endfor 172 | % endif 173 | 174 | % if len(submodules) > 0: 175 |

Sub-modules

176 | % for m in submodules: 177 |
178 |

${hh.link(module, m.refname, link_prefix)}

179 | ${show_desc(m, limit=300)} 180 |
181 | % endfor 182 | % endif 183 |
184 | 185 | 186 | <%def name="module_index(module)"> 187 | <% 188 | variables = module.variables() 189 | classes = module.classes() 190 | functions = module.functions() 191 | submodules = module.submodules 192 | parent = module.parent 193 | %> 194 | 245 | 246 | 247 | <%block name="title"> 248 | ${module.name} API documentation 249 | 250 | 251 | 252 | ${module_index(module)} 253 |
${show_module(module)}
-------------------------------------------------------------------------------- /pdocs/templates/text.mako: -------------------------------------------------------------------------------- 1 | ## Define mini-templates for each portion of the doco. 2 | 3 | <%def name="h1(s)"># ${s} 4 | 5 | <%def name="h2(s)">## ${s} 6 | 7 | <%def name="h3(s)">### ${s} 8 | 9 | <%def name="h4(s)">#### ${s} 10 | 11 | <%def name="par(s)"> 12 | % if s: 13 | ${s} 14 | 15 | % endif 16 | 17 | 18 | <%def name="function(func, class_level=False)" buffered="True"> 19 | <% 20 | returns = func.return_annotation() 21 | if returns: 22 | returns = ' -> ' + returns 23 | parsed_ds = func.parsed_docstring 24 | %> 25 | % if class_level: 26 | ${h4(func.name)} 27 | % else: 28 | ${h3(func.name)} 29 | % endif 30 | 31 | ```python3 32 | def ${func.name}( 33 | ${",\n ".join(func.params())} 34 | )${returns} 35 | ``` 36 | 37 | % if parsed_ds: 38 | <% 39 | short_desc = parsed_ds.short_description 40 | long_desc = parsed_ds.long_description 41 | params = parsed_ds.params 42 | ret = parsed_ds.returns 43 | raises = parsed_ds.raises 44 | %> 45 | ${par(short_desc)} 46 | ${par(long_desc)} 47 | 48 | % if params: 49 | **Parameters:** 50 | 51 | | Name | Type | Description | Default | 52 | |---|---|---|---| 53 | % for p in params: 54 | | ${p.arg_name} | ${p.type_name} | ${p.description.replace('\n', '
')} | ${p.default} | 55 | % endfor 56 | % endif 57 | 58 | % if ret: 59 | 60 | **${"Yields:" if ret.is_generator else "Returns:"}** 61 | 62 | | Type | Description | 63 | |---|---| 64 | ## TODO: handle multiline descriptions 65 | | ${ret.type_name} | ${ret.description.replace('\n', '
')} | 66 | % endif 67 | % if raises: 68 | 69 | **Raises:** 70 | 71 | | Type | Description | 72 | |---|---| 73 | % for r in raises: 74 | ## TODO: handle multiline descriptions 75 | | ${r.type_name} | ${r.description.replace('\n', '
')} | 76 | % endfor 77 | % endif 78 | % else: 79 | ${func.docstring} 80 | % endif 81 | 82 | % if show_source_code and func.source: 83 | 84 | ??? example "View Source" 85 | ${"\n ".join(func.source)} 86 | 87 | % endif 88 | 89 | 90 | <%def name="variable(var)" buffered="True"> 91 | ```python3 92 | ${var.name} 93 | ``` 94 | <% 95 | var_pd = var.parsed_docstring 96 | if var_pd: 97 | short_desc = var_pd.short_description 98 | long_desc = var_pd.long_description 99 | %> 100 | % if var_pd: 101 | ${par(short_desc)} 102 | ${par(long_desc)} 103 | % else: 104 | ${var.docstring} 105 | % endif 106 | 107 | 108 | 109 | <%def name="class_(cls)" buffered="True"> 110 | ${h3(cls.name)} 111 | 112 | ```python3 113 | class ${cls.name}( 114 | ${",\n ".join(cls.params())} 115 | ) 116 | ``` 117 | <% 118 | cls_pd = cls.parsed_docstring 119 | if cls_pd: 120 | short_desc = cls_pd.short_description 121 | long_desc = cls_pd.long_description 122 | params = cls_pd.params 123 | %> 124 | 125 | % if cls_pd: 126 | % if short_desc: 127 | ${short_desc} 128 | 129 | % endif 130 | %if long_desc: 131 | ${long_desc} 132 | % endif 133 | % if params: 134 | ${h4("Attributes")} 135 | 136 | | Name | Type | Description | Default | 137 | |---|---|---|---| 138 | % for p in params: 139 | | ${p.arg_name} | ${p.type_name} | ${p.description.replace('\n', '
')} | ${p.default} | 140 | % endfor 141 | % endif 142 | % else: 143 | ${cls.docstring} 144 | % endif 145 | 146 | % if show_source_code and cls.source: 147 | 148 | ??? example "View Source" 149 | ${"\n ".join(cls.source)} 150 | 151 | ------ 152 | 153 | % endif 154 | 155 | <% 156 | class_vars = cls.class_variables() 157 | static_methods = cls.functions() 158 | inst_vars = cls.instance_variables() 159 | methods = cls.methods() 160 | mro = cls.mro() 161 | subclasses = cls.subclasses() 162 | %> 163 | % if mro: 164 | ${h4('Ancestors (in MRO)')} 165 | % for c in mro: 166 | * ${c.refname} 167 | % endfor 168 | % endif 169 | 170 | % if subclasses: 171 | ${h4('Descendants')} 172 | % for c in subclasses: 173 | * ${c.refname} 174 | % endfor 175 | % endif 176 | 177 | % if class_vars: 178 | ${h4('Class variables')} 179 | % for v in class_vars: 180 | ${variable(v)} 181 | 182 | % endfor 183 | % endif 184 | 185 | % if static_methods: 186 | ${h4('Static methods')} 187 | % for f in static_methods: 188 | ${function(f, True)} 189 | 190 | % endfor 191 | % endif 192 | 193 | % if inst_vars: 194 | ${h4('Instance variables')} 195 | % for v in inst_vars: 196 | ${variable(v)} 197 | 198 | % endfor 199 | % endif 200 | % if methods: 201 | ${h4('Methods')} 202 | % for m in methods: 203 | ${function(m, True)} 204 | 205 | % endfor 206 | % endif 207 | 208 | 209 | 210 | ## Start the output logic for an entire module. 211 | 212 | <% 213 | variables = module.variables() 214 | classes = module.classes() 215 | functions = module.functions() 216 | submodules = module.submodules 217 | heading = 'Namespace' if module.is_namespace else 'Module' 218 | parsed_ds = module.parsed_docstring 219 | %> 220 | 221 | ${h1(heading + " " + module.name)} 222 | % if parsed_ds: 223 | ${par(parsed_ds.short_description)} 224 | ${par(parsed_ds.long_description)} 225 | ## TODO: add meta (example and notes) 226 | % else: 227 | ${module.docstring} 228 | % endif 229 | 230 | % if show_source_code and module.source: 231 | 232 | ??? example "View Source" 233 | ${"\n ".join(module.source)} 234 | 235 | % endif 236 | 237 | % if submodules: 238 | ${h2("Sub-modules")} 239 | % for m in submodules: 240 | * [${m.name}](${m.name.split(".")[-1]}/) 241 | % endfor 242 | % endif 243 | 244 | % if variables: 245 | ${h2("Variables")} 246 | % for v in variables: 247 | ${variable(v)} 248 | 249 | % endfor 250 | % endif 251 | 252 | % if functions: 253 | ${h2("Functions")} 254 | % for f in functions: 255 | ${function(f)} 256 | 257 | % endfor 258 | % endif 259 | 260 | % if classes: 261 | ${h2("Classes")} 262 | % for c in classes: 263 | ${class_(c)} 264 | 265 | % endfor 266 | % endif 267 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pdocs" 3 | version = "1.2.0" 4 | description = "A simple program and library to auto generate API documentation for Python modules." 5 | authors = ["Timothy Crosley "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.7" 11 | Markdown = ">=3.0.0" 12 | Mako = ">1.2.2" 13 | hug = ">=2.6" 14 | docstring_parser = ">=0.7.2" 15 | 16 | [tool.poetry.dev-dependencies] 17 | mypy = ">=0.720.0" 18 | isort = ">=4.3" 19 | pytest = ">=5.1" 20 | pytest-cov = ">=2.7" 21 | flake8-bugbear = ">=19.8" 22 | bandit = ">=1.6" 23 | vulture = ">=1.0" 24 | safety = ">=1.8" 25 | black = {version = ">=18.3-alpha.0", allow-prereleases = true} 26 | pytest-mock = ">=1.10" 27 | ipython = ">=7.7" 28 | flake8 = ">=3.7" 29 | pep8-naming = ">=0.8.2" 30 | types-Markdown = ">0.0" 31 | 32 | [tool.poetry.scripts] 33 | pdocs = "pdocs.cli:__hug__.cli" 34 | 35 | [tool.portray.mkdocs.theme] 36 | favicon = "art/logo.png" 37 | logo = "art/logo.png" 38 | name = "material" 39 | palette = {primary = "deep purple", accent = "pink"} 40 | [build-system] 41 | requires = ["poetry>=0.12"] 42 | build-backend = "poetry.masonry.api" 43 | 44 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | poetry run isort --recursive pdocs tests/ 4 | poetry run black pdocs/ tests/ -l 100 --exclude malformed 5 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | poetry run mypy --ignore-missing-imports pdocs/ 4 | poetry run isort --check --diff --recursive pdocs/ tests/ 5 | poetry run black --check -l 100 pdocs/ tests/ --exclude malformed 6 | poetry run flake8 pdocs/ tests/ --max-line 100 --ignore F403,F401,W503,E203 --exclude mitmproxy/contrib/*,test/mitmproxy/data/*,release/build/*,*malformed* 7 | poetry run safety check -i 51457 8 | poetry run bandit -r pdocs 9 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | # ./scripts/lint.sh 4 | poetry run pytest --cov=pdocs --cov=tests --cov-report=term-missing ${@} --cov-report html tests --capture=no --color=yes 5 | -------------------------------------------------------------------------------- /tests/docstring_parser/example_google.py: -------------------------------------------------------------------------------- 1 | """Module level docstring.""" 2 | from dataclasses import dataclass, field 3 | from typing import Dict, Iterator, List, Tuple 4 | 5 | #: The first module level attribute. Comment *before* attribute. 6 | first_attribute: int = 1 7 | second_attribute = "abc" #: str: The second module level attribute. *Inline* style. 8 | third_attribute: List[int] = [1, 2, 3] 9 | """The third module level attribute. Docstring *after* attribute. 10 | 11 | Multiple paragraphs are supported. 12 | """ 13 | not_attribute = 123 # Not attribute description because ':' is missing. 14 | 15 | 16 | def add(x: int, y: int = 1) -> int: 17 | """Return $x + y$. 18 | 19 | Args: 20 | x: The first parameter. 21 | y: The second parameter. Default={default}. 22 | 23 | Returns: 24 | int: Added value. 25 | 26 | Examples: 27 | Examples should be written in doctest format. 28 | 29 | >>> add(1, 2) 30 | 3 31 | 32 | !!! note 33 | You can use the [Admonition extension of 34 | MkDocs](https://squidfunk.github.io/mkdocs-material/extensions/admonition/). 35 | 36 | Note: 37 | `Note` section is converted into the Admonition. 38 | """ 39 | return x + y 40 | 41 | 42 | def gen(n) -> Iterator[str]: 43 | """Yield a numbered string. 44 | 45 | Args: 46 | n (int): The length of iteration. 47 | 48 | Yields: 49 | str: A numbered string. 50 | """ 51 | for x in range(n): 52 | yield f"a{x}" 53 | 54 | 55 | class ExampleClass: 56 | """A normal class. 57 | 58 | Args: 59 | x: The first parameter. 60 | y: The second parameter. 61 | 62 | Raises: 63 | ValueError: If the length of `x` is equal to 0. 64 | """ 65 | 66 | def __init__(self, x: List[int], y: Tuple[str, int]): 67 | if len(x) == 0: 68 | raise ValueError() 69 | self.a: str = "abc" #: The first attribute. Comment *inline* with attribute. 70 | #: The second attribute. Comment *before* attribute. 71 | self.b: Dict[str, int] = {"a": 1} 72 | self.c = None 73 | """int, optional: The third attribute. Docstring *after* attribute. 74 | 75 | Multiple paragraphs are supported.""" 76 | self.d = 100 # Not attribute description because ':' is missing. 77 | 78 | def message(self, n: int) -> List[str]: 79 | """Return a message list. 80 | 81 | Args: 82 | n: Repetition. 83 | """ 84 | return [self.a] * n 85 | 86 | @property 87 | def readonly_property(self): 88 | """str: Read-only property documentation.""" 89 | return "readonly property" 90 | 91 | @property 92 | def readwrite_property(self) -> List[int]: 93 | """Read-write property documentation.""" 94 | return [1, 2, 3] 95 | 96 | @readwrite_property.setter 97 | def readwrite_property(self, value): 98 | """Docstring in setter is ignored.""" 99 | 100 | 101 | @dataclass 102 | class ExampleDataClass: 103 | """A dataclass. 104 | 105 | Args: 106 | x: The first parameter. 107 | 108 | Attributes: 109 | x: The first attribute. 110 | y: The second attribute. 111 | """ 112 | 113 | x: int = 0 114 | y: int = field(default=1, init=False) 115 | -------------------------------------------------------------------------------- /tests/docstring_parser/example_numpy.py: -------------------------------------------------------------------------------- 1 | """Module level docstring.""" 2 | from dataclasses import dataclass, field 3 | from typing import Dict, Iterator, List, Tuple 4 | 5 | 6 | def add(x: int, y: int = 1) -> int: 7 | """Return $x + y$. 8 | 9 | Parameters 10 | ---------- 11 | x 12 | The first parameter. 13 | y 14 | The second parameter. Default={default}. 15 | 16 | Returns 17 | ------- 18 | int 19 | Added value. 20 | 21 | !!! note 22 | The return type must be duplicated in the docstring to comply with the NumPy 23 | docstring style. 24 | 25 | Examples 26 | -------- 27 | Examples should be written in doctest format. 28 | 29 | >>> add(1, 2) 30 | 3 31 | 32 | Note 33 | ---- 34 | MkApi doesn't check an underline that follows a section heading. 35 | Just skip one line. 36 | """ 37 | return x + y 38 | 39 | 40 | def gen(n) -> Iterator[str]: 41 | """Yield a numbered string. 42 | 43 | Parameters 44 | ---------- 45 | n : int 46 | The length of iteration. 47 | 48 | Yields 49 | ------ 50 | str 51 | A numbered string. 52 | """ 53 | for x in range(n): 54 | yield f"a{x}" 55 | 56 | 57 | class ExampleClass: 58 | """A normal class. 59 | 60 | Parameters 61 | ---------- 62 | x 63 | The first parameter. 64 | y 65 | The second parameter. 66 | 67 | Raises 68 | ------ 69 | ValueError 70 | If the length of `x` is equal to 0. 71 | """ 72 | 73 | def __init__(self, x: List[int], y: Tuple[str, int]): 74 | if len(x) == 0: 75 | raise ValueError() 76 | self.a: str = "abc" #: The first attribute. Comment *inline* with attribute. 77 | #: The second attribute. Comment *before* attribute. 78 | self.b: Dict[str, int] = {"a": 1} 79 | self.c = None 80 | """int, optional: The third attribute. Docstring *after* attribute. 81 | 82 | Multiple paragraphs are supported.""" 83 | self.d = 100 # Not attribute description because ':' is missing. 84 | 85 | def message(self, n: int) -> List[str]: 86 | """Return a message list. 87 | 88 | Parameters 89 | ---------- 90 | n 91 | Repetition. 92 | """ 93 | return [self.a] * n 94 | 95 | @property 96 | def readonly_property(self): 97 | """str: Read-only property documentation.""" 98 | return "readonly property" 99 | 100 | @property 101 | def readwrite_property(self) -> List[int]: 102 | """Read-write property documentation.""" 103 | return [1, 2, 3] 104 | 105 | @readwrite_property.setter 106 | def readwrite_property(self, value): 107 | """Docstring in setter is ignored.""" 108 | 109 | 110 | @dataclass 111 | class ExampleDataClass: 112 | """A dataclass. 113 | 114 | Parameters 115 | ---------- 116 | x 117 | The first parameter. 118 | 119 | Attributes 120 | ---------- 121 | x 122 | The first attribute. 123 | y 124 | The second attribute. 125 | """ 126 | 127 | x: int = 0 128 | y: int = field(default=1, init=False) 129 | -------------------------------------------------------------------------------- /tests/modules/README: -------------------------------------------------------------------------------- 1 | Test modules that are not on the Python path 2 | -------------------------------------------------------------------------------- /tests/modules/dirmod/__init__.py: -------------------------------------------------------------------------------- 1 | def simple(): 2 | """ 3 | A docstring. 4 | """ 5 | pass 6 | -------------------------------------------------------------------------------- /tests/modules/index/__init__.py: -------------------------------------------------------------------------------- 1 | ROOT = 1 2 | -------------------------------------------------------------------------------- /tests/modules/index/index.py: -------------------------------------------------------------------------------- 1 | INDEX = 1 2 | -------------------------------------------------------------------------------- /tests/modules/index/two/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/tests/modules/index/two/__init__.py -------------------------------------------------------------------------------- /tests/modules/malformed/syntax.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | # Syntax error 3 | class 4 | # fmt: on 5 | -------------------------------------------------------------------------------- /tests/modules/one.py: -------------------------------------------------------------------------------- 1 | def simple(): 2 | """ 3 | A docstring. 4 | """ 5 | pass 6 | -------------------------------------------------------------------------------- /tests/modules/submods/__init__.py: -------------------------------------------------------------------------------- 1 | def simple(): 2 | """ 3 | A docstring. 4 | """ 5 | pass 6 | -------------------------------------------------------------------------------- /tests/modules/submods/three/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/tests/modules/submods/three/__init__.py -------------------------------------------------------------------------------- /tests/modules/submods/two.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/tests/modules/submods/two.py -------------------------------------------------------------------------------- /tests/onpath/README: -------------------------------------------------------------------------------- 1 | Test modules that are on the Python path from the test perspective. 2 | -------------------------------------------------------------------------------- /tests/onpath/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/pdocs/3d0fceee528db8fb4d107c401f5b42e3ec9d4bbf/tests/onpath/__init__.py -------------------------------------------------------------------------------- /tests/onpath/malformed_syntax.py: -------------------------------------------------------------------------------- 1 | 2 | # Syntax error 3 | class 4 | -------------------------------------------------------------------------------- /tests/onpath/simple.py: -------------------------------------------------------------------------------- 1 | def simple(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/test_doc.py: -------------------------------------------------------------------------------- 1 | import tutils 2 | 3 | import pdocs.doc 4 | import pdocs.extract 5 | 6 | 7 | def test_simple(): 8 | with tutils.tdir(): 9 | m = pdocs.extract.extract_module("./modules/one.py") 10 | assert m 11 | 12 | 13 | class Dummy: 14 | def method(self): 15 | pass 16 | 17 | @classmethod 18 | def class_method(cls): 19 | pass 20 | 21 | @staticmethod 22 | def static_method(): 23 | pass 24 | 25 | 26 | class DummyChild(Dummy): 27 | def class_method(self): 28 | pass 29 | 30 | 31 | def test_is_static(): 32 | assert pdocs.doc._is_method(Dummy, "method") 33 | assert not pdocs.doc._is_method(Dummy, "class_method") 34 | assert not pdocs.doc._is_method(Dummy, "static_method") 35 | 36 | assert pdocs.doc._is_method(DummyChild, "method") 37 | assert pdocs.doc._is_method(DummyChild, "class_method") 38 | assert not pdocs.doc._is_method(Dummy, "static_method") 39 | -------------------------------------------------------------------------------- /tests/test_extract.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tutils 3 | 4 | import pdocs.extract 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "input,expected", 9 | [ 10 | ("foo", ("", "foo")), 11 | ("foo.bar", ("", "foo.bar")), 12 | ("foo/bar.py", ("foo", "bar")), 13 | ("./bar.py", (".", "bar")), 14 | ("./bar.foo", None), 15 | ("", None), 16 | ], 17 | ) 18 | def test_split_module_spec(input, expected): 19 | if expected is None: 20 | with pytest.raises(pdocs.extract.ExtractError): 21 | pdocs.extract.split_module_spec(input) 22 | else: 23 | assert pdocs.extract.split_module_spec(input) == expected 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "path,mod,expected,match", 28 | [ 29 | ("./modules", "one", False, None), 30 | ("./modules", "dirmod", True, None), 31 | ("", "email", True, None), 32 | ("", "csv", False, None), 33 | ("", "html.parser", False, None), 34 | ("", "onpath.simple", False, None), 35 | ("./modules", "nonexistent", False, "not found"), 36 | ("./modules/nonexistent", "foo", False, "not found"), 37 | ("", "nonexistent.module", False, "not import"), 38 | ("./modules/malformed", "syntax", False, "Error importing"), 39 | ("", "onpath.malformed_syntax", False, "Error importing"), 40 | ], 41 | ) 42 | def test_load_module(path, mod, expected, match): 43 | with tutils.tdir(): 44 | if match: 45 | with pytest.raises(pdocs.extract.ExtractError, match=match): 46 | pdocs.extract.load_module(path, mod) 47 | else: 48 | _, ispkg = pdocs.extract.load_module(path, mod) 49 | assert ispkg == expected 50 | 51 | 52 | @pytest.mark.parametrize( 53 | "path,expected,match", 54 | [ 55 | ("./modules/nonexistent.py", None, "not found"), 56 | ("./modules/nonexistent/foo", None, "not found"), 57 | ("nonexistent", None, "not import"), 58 | ("nonexistent.module", None, "not import"), 59 | ("./modules/one.two", None, "Invalid module name"), 60 | ("./modules/malformed/syntax.py", None, "Error importing"), 61 | ("onpath.malformed_syntax", None, "Error importing"), 62 | ("./modules/one.py", ["one"], None), 63 | ("./modules/one", ["one"], None), 64 | ("./modules/dirmod", ["dirmod"], None), 65 | ("./modules/submods", ["submods", "submods.three", "submods.two"], None), 66 | ("csv", ["csv"], None), 67 | ("html.parser", ["html.parser"], None), 68 | ("onpath.simple", ["onpath.simple"], None), 69 | ], 70 | ) 71 | def test_extract_module(path, expected, match): 72 | with tutils.tdir(): 73 | if match: 74 | with pytest.raises(pdocs.extract.ExtractError, match=match): 75 | pdocs.extract.extract_module(path) 76 | else: 77 | ret = pdocs.extract.extract_module(path) 78 | assert sorted([i.name for i in ret.allmodules()]) == expected 79 | 80 | 81 | @pytest.mark.parametrize( 82 | "path,modname,expected", 83 | [ 84 | ("./modules", "one", []), 85 | ("", "modules.one", []), 86 | ("./modules", "dirmod", []), 87 | ("", "modules.dirmod", []), 88 | ("./modules", "submods", ["submods.three", "submods.two"]), 89 | ("", "modules.submods", ["modules.submods.three", "modules.submods.two"]), 90 | ("./modules", "malformed", ["malformed.syntax"]), 91 | ("", "modules.malformed", ["modules.malformed.syntax"]), 92 | ], 93 | ) 94 | def test_submodules(path, modname, expected): 95 | with tutils.tdir(): 96 | ret = pdocs.extract.submodules(path, modname) 97 | assert ret == expected 98 | -------------------------------------------------------------------------------- /tests/test_parse_docstring.py: -------------------------------------------------------------------------------- 1 | import pdocs 2 | from pdocs import doc 3 | from tests import tutils 4 | 5 | 6 | def simple_function(arg1, arg2=1): 7 | """ 8 | Just a simple function. 9 | 10 | Args: 11 | arg1 (str): first argument 12 | arg2 (int): second argument 13 | 14 | Returns: 15 | List[str]: first argument repeated second argument types. 16 | """ 17 | return [arg1] * arg2 18 | 19 | 20 | def test_parse_docstring(): 21 | fun_doc = doc.Function("simple_function", __name__, simple_function) 22 | assert fun_doc.parsed_docstring is not None 23 | parsed = fun_doc.parsed_docstring 24 | assert len(parsed.params) == 2 25 | first_param, second_param = parsed.params 26 | assert first_param.arg_name == "arg1" 27 | assert first_param.type_name == "str" 28 | assert first_param.description == "first argument" 29 | assert first_param.is_optional is False 30 | assert second_param.arg_name == "arg2" 31 | assert second_param.type_name == "int" 32 | assert second_param.description == "second argument" 33 | 34 | 35 | def token_to_alias(raw_text, vocab): 36 | """ 37 | Replaces known tokens with their "tag" form. 38 | 39 | i.e. the alias' in some known vocabulary list. 40 | 41 | Parameters 42 | ---------- 43 | raw_text: pd.Series 44 | contains text with known jargon, slang, etc 45 | vocab: pd.DataFrame 46 | contains alias' keyed on known slang, jargon, etc. 47 | 48 | Returns 49 | ------- 50 | pd.Series 51 | new text, with all slang/jargon replaced with unified representations 52 | """ 53 | pass 54 | 55 | 56 | def test_numpydocs(): 57 | fun_doc = doc.Function("token_to_alias", __name__, token_to_alias) 58 | assert fun_doc.parsed_docstring is not None 59 | parsed = fun_doc.parsed_docstring 60 | assert parsed.short_description == 'Replaces known tokens with their "tag" form.' 61 | assert len(parsed.params) == 2 62 | assert len(parsed.raises) == 0 63 | first_param, second_param = parsed.params 64 | assert first_param.arg_name == "raw_text" 65 | assert first_param.type_name == "pd.Series" 66 | assert first_param.description == "contains text with known jargon, slang, etc" 67 | assert first_param.is_optional is False 68 | assert second_param.arg_name == "vocab" 69 | assert second_param.type_name == "pd.DataFrame" 70 | assert second_param.description == "contains alias' keyed on known slang, jargon, etc." 71 | assert second_param.is_optional is False 72 | returns = parsed.returns 73 | assert ( 74 | returns.description 75 | == "new text, with all slang/jargon replaced with unified representations" 76 | ) 77 | assert returns.return_name is None 78 | assert returns.type_name == "pd.Series" 79 | assert not returns.is_generator 80 | 81 | 82 | def test_parse_numpy_example(): 83 | with tutils.tdir(): 84 | m = pdocs.extract.extract_module("./docstring_parser/example_numpy.py") 85 | 86 | assert m.parsed_docstring 87 | pd = m.parsed_docstring 88 | assert pd.short_description == "Module level docstring." 89 | assert not pd.long_description 90 | 91 | assert "add" in m.doc 92 | add_pd = m.doc["add"].parsed_docstring 93 | assert add_pd.short_description == "Return $x + y$." 94 | assert len(add_pd.params) == 2 95 | x, y = add_pd.params 96 | assert x.description == "The first parameter." 97 | assert y.description == "The second parameter. Default={default}." 98 | assert add_pd.returns.type_name == "int" 99 | assert ( 100 | add_pd.returns.description 101 | == """Added value. 102 | 103 | !!! note 104 | The return type must be duplicated in the docstring to comply with the NumPy 105 | docstring style.""" 106 | ) 107 | 108 | assert "gen" in m.doc 109 | gen_pd = m.doc["gen"].parsed_docstring 110 | assert gen_pd.short_description == "Yield a numbered string." 111 | (n,) = gen_pd.params 112 | assert n.description == "The length of iteration." 113 | assert n.type_name == "int" 114 | assert n.arg_name == "n" 115 | assert gen_pd.returns.type_name == "str" 116 | assert gen_pd.returns.is_generator 117 | assert gen_pd.returns.description == "A numbered string." 118 | 119 | assert "ExampleClass" in m.doc 120 | ex_class = m.doc["ExampleClass"] 121 | ec_pd = ex_class.parsed_docstring 122 | assert ec_pd.short_description == "A normal class." 123 | x, y = ec_pd.params 124 | assert x.description == "The first parameter." 125 | assert y.description == "The second parameter." 126 | assert len(ec_pd.raises) == 1 127 | raises = ec_pd.raises[0] 128 | assert raises.type_name == "ValueError" 129 | assert raises.description == "If the length of `x` is equal to 0." 130 | msg_pd = ex_class.doc["message"].parsed_docstring 131 | assert msg_pd.short_description == "Return a message list." 132 | n = msg_pd.params[0] 133 | assert n.arg_name == "n" 134 | assert n.description == "Repetition." 135 | assert "readonly_property" in ex_class.doc_init 136 | ro_prop_pd = ex_class.doc_init["readonly_property"].parsed_docstring 137 | assert ro_prop_pd.short_description == "str: Read-only property documentation." 138 | assert "readwrite_property" in ex_class.doc_init 139 | rw_prop_pd = ex_class.doc_init["readwrite_property"].parsed_docstring 140 | assert rw_prop_pd.short_description == "Read-write property documentation." 141 | 142 | 143 | def test_parse_google_example(): 144 | with tutils.tdir(): 145 | m = pdocs.extract.extract_module("./docstring_parser/example_google.py") 146 | 147 | assert m.parsed_docstring 148 | pd = m.parsed_docstring 149 | assert pd.short_description == "Module level docstring." 150 | assert not pd.long_description 151 | 152 | assert "add" in m.doc 153 | add_pd = m.doc["add"].parsed_docstring 154 | assert add_pd.short_description == "Return $x + y$." 155 | assert len(add_pd.params) == 2 156 | x, y = add_pd.params 157 | assert x.description == "The first parameter." 158 | assert y.description == "The second parameter. Default={default}." 159 | assert add_pd.returns.type_name == "int" 160 | assert add_pd.returns.description == "Added value." 161 | 162 | assert "gen" in m.doc 163 | gen_pd = m.doc["gen"].parsed_docstring 164 | assert gen_pd.short_description == "Yield a numbered string." 165 | (n,) = gen_pd.params 166 | assert n.description == "The length of iteration." 167 | assert n.type_name == "int" 168 | assert n.arg_name == "n" 169 | assert gen_pd.returns.type_name == "str" 170 | assert gen_pd.returns.is_generator 171 | assert gen_pd.returns.description == "A numbered string." 172 | 173 | assert "ExampleClass" in m.doc 174 | ex_class = m.doc["ExampleClass"] 175 | ec_pd = ex_class.parsed_docstring 176 | assert ec_pd.short_description == "A normal class." 177 | x, y = ec_pd.params 178 | assert x.description == "The first parameter." 179 | assert y.description == "The second parameter." 180 | assert len(ec_pd.raises) == 1 181 | raises = ec_pd.raises[0] 182 | assert raises.type_name == "ValueError" 183 | assert raises.description == "If the length of `x` is equal to 0." 184 | msg_pd = ex_class.doc["message"].parsed_docstring 185 | assert msg_pd.short_description == "Return a message list." 186 | n = msg_pd.params[0] 187 | assert n.arg_name == "n" 188 | assert n.description == "Repetition." 189 | assert "readonly_property" in ex_class.doc_init 190 | ro_prop_pd = ex_class.doc_init["readonly_property"].parsed_docstring 191 | assert ro_prop_pd.short_description == "str: Read-only property documentation." 192 | assert "readwrite_property" in ex_class.doc_init 193 | rw_prop_pd = ex_class.doc_init["readwrite_property"].parsed_docstring 194 | assert rw_prop_pd.short_description == "Read-write property documentation." 195 | -------------------------------------------------------------------------------- /tests/test_pdoc.py: -------------------------------------------------------------------------------- 1 | import pdocs 2 | 3 | 4 | def test_pdocs(): 5 | assert pdocs 6 | -------------------------------------------------------------------------------- /tests/test_render.py: -------------------------------------------------------------------------------- 1 | import tutils 2 | 3 | import pdocs.doc 4 | import pdocs.extract 5 | import pdocs.render 6 | 7 | 8 | def test_html_module(): 9 | with tutils.tdir(): 10 | m = pdocs.extract.extract_module("./modules/one") 11 | assert pdocs.render.html_module(m) 12 | 13 | 14 | def test_html_module_index(): 15 | with tutils.tdir(): 16 | roots = [ 17 | pdocs.extract.extract_module("./modules/one"), 18 | pdocs.extract.extract_module("./modules/submods"), 19 | ] 20 | assert pdocs.render.html_index(roots) 21 | -------------------------------------------------------------------------------- /tests/test_static.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | import tutils 5 | 6 | import pdocs.extract 7 | import pdocs.static 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "modspec,ident,path", 12 | [ 13 | ("./modules/one", "one", "one.html"), 14 | ("./modules/dirmod", "dirmod", "dirmod.html"), 15 | ("./modules/submods", "submods", "submods/index.html"), 16 | ("./modules/submods", "submods.two", "submods/two.html"), 17 | ("./modules/submods", "submods.three", "submods/three.html"), 18 | ("./modules/index", "index", "index/index.html"), 19 | ], 20 | ) 21 | def test_module_path(modspec, ident, path): 22 | with tutils.tdir(): 23 | root = pdocs.extract.extract_module(modspec) 24 | submod = root.find_ident(ident) 25 | 26 | mp = pdocs.static.module_to_path(submod) 27 | assert mp == pathlib.Path(path) 28 | 29 | retmod = pdocs.static.path_to_module([root], mp) 30 | assert retmod.name == submod.name 31 | 32 | retmod = pdocs.static.path_to_module([root], mp.with_suffix("")) 33 | assert retmod.name == submod.name 34 | 35 | 36 | def test_path_to_module(): 37 | with tutils.tdir(): 38 | root = pdocs.extract.extract_module("./modules/submods") 39 | with pytest.raises(pdocs.static.StaticError): 40 | pdocs.static.path_to_module([root], pathlib.Path("nonexistent")) 41 | 42 | 43 | def test_static(tmpdir): 44 | dst = pathlib.Path(str(tmpdir)) 45 | with tutils.tdir(): 46 | one = pdocs.extract.extract_module("./modules/one") 47 | two = pdocs.extract.extract_module("./modules/submods") 48 | assert not pdocs.static.would_overwrite(dst, [one]) 49 | assert not pdocs.static.would_overwrite(dst, [one, two]) 50 | -------------------------------------------------------------------------------- /tests/tutils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | 4 | 5 | @contextlib.contextmanager 6 | def tdir(): 7 | """ 8 | A small helper to place us within the test directory. 9 | """ 10 | old_dir = os.getcwd() 11 | os.chdir(os.path.dirname(__file__)) 12 | yield 13 | os.chdir(old_dir) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37, lint 3 | skipsdist = True 4 | toxworkdir={env:TOX_WORK_DIR:.tox} 5 | 6 | [testenv] 7 | deps = 8 | {env:CI_DEPS:} 9 | -rrequirements.txt 10 | passenv = CODECOV_TOKEN CI CI_* TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* SNAPSHOT_* OPENSSL RTOOL_* 11 | setenv = HOME = {envtmpdir} 12 | commands = 13 | pdoc --version 14 | pytest --timeout 60 --cov-report='' --cov=pdoc {posargs} 15 | {env:CI_COMMANDS:python -c ""} 16 | 17 | [testenv:lint] 18 | commands = 19 | pdoc --version 20 | flake8 pdoc test 21 | mypy --ignore-missing-imports ./pdoc 22 | --------------------------------------------------------------------------------