├── .coveragerc ├── .cruft.json ├── .github ├── FUNDING.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── art ├── example.gif ├── logo.png ├── logo.xcf ├── 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.-adding-examples.md │ └── 3.-testing-examples.md ├── example_of_examples.py ├── examples ├── __init__.py ├── api.py ├── example_objects.py └── registry.py ├── pyproject.toml ├── scripts ├── clean.sh ├── done.sh ├── lint.sh └── test.sh ├── setup.cfg ├── tests ├── __init__.py ├── example_module_fail.py ├── example_module_pass.py ├── example_project_separate │ ├── __init__.py │ ├── api.py │ └── api_examples.py ├── no_examples_module.py ├── test_api.py ├── test_project_separate.py └── test_registry.py └── xamples ├── pyproject.toml └── xamples └── __init__.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | omit = 5 | *tests* 6 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/timothycrosley/cookiecutter-python/", 3 | "commit": "9be01014cde7ec06bd52415655d52ca30a9e9bcc", 4 | "context": { 5 | "cookiecutter": { 6 | "full_name": "Timothy Crosley", 7 | "email": "timothy.crosley@gmail.com", 8 | "github_username": "timothycrosley", 9 | "project_name": "examples", 10 | "description": "Tests and Documentation Done by Example.", 11 | "version": "1.0.1", 12 | "_template": "https://github.com/timothycrosley/cookiecutter-python/" 13 | } 14 | }, 15 | "directory": "" 16 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: "timothycrosley" 2 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.8] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: pip cache 16 | uses: actions/cache@v1 17 | with: 18 | path: ~/.cache/pip 19 | key: lint-pip-${{ hashFiles('**/pyproject.toml') }} 20 | restore-keys: | 21 | lint-pip- 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v1 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | python -m pip install --upgrade poetry 32 | poetry install 33 | 34 | - name: Lint 35 | run: ./scripts/lint.sh 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8] 12 | os: [ubuntu-latest, ubuntu-18.04, macos-latest, windows-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Ubuntu cache 17 | uses: actions/cache@v1 18 | if: startsWith(matrix.os, 'ubuntu') 19 | with: 20 | path: ~/.cache/pip 21 | key: 22 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} 23 | restore-keys: | 24 | ${{ matrix.os }}-${{ matrix.python-version }}- 25 | 26 | - name: macOS cache 27 | uses: actions/cache@v1 28 | if: startsWith(matrix.os, 'macOS') 29 | with: 30 | path: ~/Library/Caches/pip 31 | key: 32 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} 33 | restore-keys: | 34 | ${{ matrix.os }}-${{ matrix.python-version }}- 35 | 36 | - name: Windows cache 37 | uses: actions/cache@v1 38 | if: startsWith(matrix.os, 'windows') 39 | with: 40 | path: c:\users\runneradmin\appdata\local\pip\cache 41 | key: 42 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} 43 | restore-keys: | 44 | ${{ matrix.os }}-${{ matrix.python-version }}- 45 | 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v1 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | python -m pip install --upgrade poetry 55 | poetry install 56 | - name: Test 57 | shell: bash 58 | run: | 59 | poetry run pytest tests/ -s --cov=examples/ --cov-report=term-missing ${@-} 60 | poetry run coverage xml 61 | - name: Report Coverage 62 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' 63 | uses: codecov/codecov-action@v1.0.6 64 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Install the latest 2 | =================== 3 | 4 | To install the latest version of eXamples (AKA: xamples) simply run: 5 | 6 | `pip3 install examples` 7 | 8 | OR 9 | 10 | `poetry add examples` 11 | 12 | OR 13 | 14 | `pipenv install examples` 15 | 16 | see the [Installation QuickStart](https://timothycrosley.github.io/examples/docs/quick_start/1.-installation/) for more instructions. 17 | 18 | Changelog 19 | ========= 20 | ## 1.0.1 - 30 December 2019 21 | - Updated pydantic supported versions. 22 | 23 | ## 1.0.1 - 15 September 2019 24 | - Improved type hint checking compatibility. 25 | 26 | ## 1.0.0 - 10 September 2019 27 | - Initial Release. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Timothy Crosley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![eXamples - Python Tests and Documentation Done by Example.](https://raw.github.com/timothycrosley/examples/art/logo_large.png)](https://timothycrosley.github.io/examples/) 2 | _________________ 3 | 4 | [![PyPI version](https://badge.fury.io/py/examples.svg)](http://badge.fury.io/py/examples) 5 | [![Test Status](https://github.com/timothycrosley/examples/workflows/Test/badge.svg?branch=main)](https://github.com/timothycrosley/examples/actions?query=workflow%3ATest) 6 | [![Lint Status](https://github.com/timothycrosley/examples/workflows/Lint/badge.svg?branch=main)](https://github.com/timothycrosley/examples/actions?query=workflow%3ALint) 7 | [![codecov](https://codecov.io/gh/timothycrosley/examples/branch/main/graph/badge.svg)](https://codecov.io/gh/timothycrosley/examples) 8 | [![Join the chat at https://gitter.im/timothycrosley/examples](https://badges.gitter.im/timothycrosley/examples.svg)](https://gitter.im/timothycrosley/examples?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.python.org/pypi/examples/) 10 | [![Downloads](https://pepy.tech/badge/examples)](https://pepy.tech/project/examples) 11 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 12 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) 13 | _________________ 14 | 15 | [Read Latest Documentation](https://timothycrosley.github.io/examples/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/examples/) 16 | _________________ 17 | 18 | **eXamples** (AKA: xamples for SEO) is a Python3 library enabling interactable, self-documenting, and self-verifying examples. These examples are attached directly to Python functions using decorators or via separate `MODULE_examples.py` source files. 19 | 20 | [![Example Usage Gif](https://raw.githubusercontent.com/timothycrosley/examples/main/art/example.gif)](https://raw.githubusercontent.com/timothycrosley/examples/main/art/example.gif) 21 | 22 | Key Features: 23 | 24 | * **Simple and Obvious API**: Add `@examples.example(*args, **kwargs)` decorators for each example you want to add to a function. 25 | * **Auto Documenting**: Examples, by default, get added to your functions docstring viewable both in interactive interpreters and when using [portray](https://timothycrosley.github.io/portray/) or [pdocs](https://timothycrosley.github.io/pdocs/). 26 | * **Signature Validating**: All examples can easily be checked to ensure they match the function signature (and type annotations!) with a single call (`examples.verify_all_signatures()`). 27 | * **Act as Tests**: Examples act as additional test cases, that can easily be verified using a single test case in your favorite test runner: (`examples.test_all_examples()`). 28 | * **Async Compatibility**: Examples can be attached and tested as easily against async functions as non-async ones. 29 | 30 | What's Missing: 31 | 32 | * **Class Support**: Currently examples can only be attached to individual functions. Class and method support is planned for a future release. 33 | 34 | ## Quick Start 35 | 36 | The following guides should get you up and running using eXamples in no time. 37 | 38 | 1. [Installation](https://timothycrosley.github.io/examples/docs/quick_start/1.-installation/) - TL;DR: Run `pip3 install examples` within your projects virtual environment. 39 | 2. [Adding Examples](https://timothycrosley.github.io/examples/docs/quick_start/2.-adding-examples/) - 40 | TL;DR: Add example decorators that represent each of your examples: 41 | 42 | # my_module_with_examples.py 43 | from examples import example 44 | 45 | @example(1, number_2=1, _example_returns=2) 46 | def add(number_1: int, number_2: int) -> int: 47 | return number_1 + number_2 48 | 49 | 3. [Verify and Test Examples](https://timothycrosley.github.io/examples/docs/quick_start/3.-testing-examples/) - 50 | TL;DR: run `examples.verify_and_test_examples` within your projects test cases. 51 | 52 | # test_my_module_with_examples.py 53 | from examples import verify_and_test_examples 54 | 55 | import my_module_with_examples 56 | 57 | 58 | def test_examples_verifying_signature(): 59 | verify_and_test_examples(my_module_with_examples) 60 | 61 | 4. Introspect Examples - 62 | 63 | import examples 64 | 65 | from my_module_with_examples import add 66 | 67 | 68 | examples.get_examples(add)[0].use() == 2 69 | 70 | ## Why Create Examples? 71 | 72 | I've always wanted a way to attach examples to functions in a way that would be re-useable for documentation, testing, and API proposes. 73 | Just like moving Python parameter types from comments into type annotations has made them more broadly useful, I hope examples can do the same for example calls. 74 | 75 | I hope you too find `eXamples` useful! 76 | 77 | ~Timothy Crosley 78 | -------------------------------------------------------------------------------- /art/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/art/example.gif -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/art/logo.png -------------------------------------------------------------------------------- /art/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/art/logo.xcf -------------------------------------------------------------------------------- /art/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/art/logo_large.png -------------------------------------------------------------------------------- /art/logo_large.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/art/logo_large.xcf -------------------------------------------------------------------------------- /docs/contributing/1.-contributing-guide.md: -------------------------------------------------------------------------------- 1 | Contributing to eXamples (AKA: xamples) 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 eXamples set up for local development 9 | The first step when contributing to any project is getting it set up on your local machine. eXamples 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/eXamples/fork). 24 | 2. Clone your fork to your local file system: 25 | `git clone https://github.com/$GITHUB_ACCOUNT/eXamples.git` 26 | 3. `cd eXamples 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/eXamples/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 eXamples project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/examples). 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 eXamples 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 | - 15 | 16 | 17 | -------------------------------------------- 18 | 19 | A sincere thanks to everyone who helps make eXamples (AKA xamples) into a great Python3 project! 20 | 21 | ~Timothy Crosley 22 | -------------------------------------------------------------------------------- /docs/quick_start/1.-installation.md: -------------------------------------------------------------------------------- 1 | Install `examples` into your projects virtual environment: 2 | 3 | `pip3 install examples` 4 | 5 | OR 6 | 7 | `poetry add examples` 8 | 9 | OR 10 | 11 | `pipenv install examples` 12 | 13 | 14 | 15 | !!! info 16 | Optionally, you can also install eXamples using its alias `xamples`. 17 | -------------------------------------------------------------------------------- /docs/quick_start/2.-adding-examples.md: -------------------------------------------------------------------------------- 1 | # Adding Examples 2 | 3 | eXamples makes it easy to add examples to any existing function call. It also provides flexibility in where you define, and you utilize examples. 4 | 5 | ## Decorator Usage 6 | 7 | The most straight forward way to add examples is via the `@example` decorator. A decorator would be added above a function definition for each example 8 | you wish to add. This is the recommended approach for simple APIs and/or APIs with only a few examples. 9 | 10 | ```python3 11 | from examples import example 12 | 13 | @example(1, number_2=1) 14 | @example(1, 2, _example_returns=3) 15 | def add(number_1: int, number_2: int): 16 | return number_1 + number_2 17 | ``` 18 | 19 | Every positional and keyword argument passed into the decorator represents the same passed into the function. 20 | Except, for the following magic parameters (all prefixed with `_example_`): 21 | 22 | - *_example_returns*: The exact result you expect the example to return. 23 | - *_example_raises*: An exception you expect the example to raise (can't be combined with above) 24 | - *_example_doc_string*: If True example is added to the functions docstring. 25 | 26 | See the [API reference documentation](https://timothycrosley.github.io/examples/reference/examples/api/#example) for a complete definition. 27 | 28 | !!! tip 29 | Examples save the bare minimum information when attached to a function, adding very low overhead. 30 | The next approach does provide a no-overhead alternative, but generally overhead shouldn't need to be the primary consideration. You can also combine the two approaches as needed, both for the same or different functions. 31 | 32 | ## Separate Example Registration 33 | 34 | Alternatively, if you have a lot of examples, you can store the examples in a separate module. This can allow you 35 | to harness the power of eXamples while keeping your implementation code clean and uncluttered. 36 | As long as you test your examples, you still won't have to worry about them ever becoming out of sync with your 37 | implementation code. 38 | 39 | To do this, you can utilize the `examples.add_example_to` function, to add examples to a function: 40 | 41 | ```python3 42 | # implementation.py 43 | 44 | 45 | def multiply(number_1: int, number_2: int): 46 | return number_1 * number_2 47 | ``` 48 | 49 | ```python3 50 | # implementation_examples.py 51 | 52 | from examples import add_examples_to 53 | 54 | from .implementation import multiply 55 | 56 | multiply_example = add_example_to(multiply) 57 | multiply_example(2, 2, _example_returns=4) 58 | multiply_example(1, 1) 59 | 60 | # OR 61 | 62 | add_example_to(multiply)(2, 2, _example_returns=4) 63 | add_example_to(multiply)(1, 1) 64 | 65 | # Optionally, you can even name and make these examples importable! 66 | 67 | multiply_2_by_2 = add_example_to(multiply)(2, 2, _example_returns=4) 68 | ``` 69 | 70 | `add_example_to` returns a function that takes the same arguments as the `@example` decorator used above. 71 | See the [API reference documentation](https://timothycrosley.github.io/examples/reference/examples/api/#add_example_to) for a complete definition. 72 | 73 | !!! warning 74 | When using this approach, it's important to remember that examples won't get added if the example module is never imported. 75 | This can be overcome with documentation and/or strategically importing the examples elsewhere in your code, such as `__init__.py`. 76 | On the other hand, this fact can be utilized to incur the overhead of examples only when running in a development environment. 77 | 78 | ## Custom Registry 79 | 80 | By default `eXamples` creates example registries on-demand per a module that contains functions with examples. 81 | If you need more fine-tuned control over the registration or usage of examples, you can create and reuse your own example registry. 82 | 83 | ``` 84 | from examples import Examples 85 | 86 | my_examples = Examples() 87 | 88 | @my_examples.example(argument_1="value") 89 | def my_function(argument_1): 90 | return argument_1 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/quick_start/3.-testing-examples.md: -------------------------------------------------------------------------------- 1 | # Testing and Verifying Examples 2 | 3 | One of the great thing about using programmatically defined examples is that it enables testing, type verification, and interaction. 4 | 5 | ## Discovering and Interacting with Examples 6 | 7 | By default, all examples are added to the docstring of any function that includes them. These examples are grouped under an "Examples:" section at the bottom of the `__doc__` string. 8 | If you want to use or interact with one of the examples, you can easily do so via the examples libraries `get_examples` function: 9 | 10 | 11 | ``` 12 | from examples import get_examples 13 | 14 | import module_with_examples 15 | 16 | 17 | get_examples(module_with_examples.function_with_examples)[0].use() 18 | ``` 19 | 20 | The function returns a list of all examples for a passed-in function or module. Any of these examples can be introspected, interacted with, and directly used. 21 | For a full definition of the actions you can perform against a single example, see the [API reference documentation for the CallableExample class](https://timothycrosley.github.io/examples/reference/examples/example_objects/#callableexample). 22 | 23 | 24 | ## Verifying Examples 25 | 26 | The most basic mechanism eXamples provides for ensuring examples don't fall out of sync with the function they call is `signature_verification.` 27 | Signature verification ensures the parameters presented in the example match up with the parameters of the associated function. 28 | By default, it then takes the additional step of verifying that the types provided and returned by the example match those specified by the functions 29 | type annotations. 30 | 31 | Signature verification can be performed over a single example, a function, a module, or all examples defined. 32 | In general, for most projects, a module is the right level of specificity, to ensure that signatures are verified across your project. 33 | 34 | ``` 35 | from examples import verify_signatures 36 | 37 | import module_with_examples 38 | 39 | 40 | def test_example_signatures(): 41 | verify_signatures(module_with_examples) 42 | ``` 43 | 44 | ## Testing Examples 45 | 46 | Beyond signature verification, examples can also operate as complete test cases for your project. 47 | When using examples as test cases, success happens when: 48 | 49 | - Your example doesn't specify a return value AND calling the function with your example's provided parameters doesn't raise an exception. 50 | - Your example does specify a return value AND calling the function with your example's provided parameters returns that exact return value. 51 | - Your example doesn't specify a return value, but it does specify an _example_raises exception type or instance, AND a matching exception is raised when calling the function. 52 | 53 | To test examples, you can use eXample's `test_examples` function. This function follows the same general guidelines as signature verification, and can also run over a function or module. 54 | 55 | ``` 56 | from examples import test_examples 57 | 58 | import module_with_examples 59 | 60 | 61 | def test_examples(): 62 | test_examples(module_with_examples) 63 | ``` 64 | 65 | ## Testing and Verifying Examples in One Step 66 | 67 | For many projects, it makes sense to both verify the signature of and test all examples. eXamples provides a `verify_and_test_examples` convenience function to do them both in one step: 68 | 69 | ``` 70 | from examples import verify_and_test_examples 71 | 72 | import module_with_examples 73 | 74 | 75 | def test_examples_including_their_signature(): 76 | verify_and_test_examples(module_with_examples) 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /example_of_examples.py: -------------------------------------------------------------------------------- 1 | from examples import example 2 | 3 | 4 | @example(1, 1, _example_returns=2) 5 | def add(number_1: int, number_2: int) -> int: 6 | return number_1 + number_2 7 | 8 | 9 | @example(2, 2, _example_returns=4) 10 | @example(1, 1) 11 | def multiply(number_1: int, number_2: int) -> int: 12 | """Multiply two numbers_together""" 13 | return number_1 * number_2 14 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | from examples.api import ( 2 | add_example_to, 3 | example, 4 | get_examples, 5 | test_all_examples, 6 | test_examples, 7 | verify_all_signatures, 8 | verify_and_test_all_examples, 9 | verify_and_test_examples, 10 | verify_signatures, 11 | ) 12 | from examples.registry import Examples 13 | 14 | __version__ = "1.0.2" 15 | __all__ = [ 16 | "__version__", 17 | "add_example_to", 18 | "example", 19 | "example_returns", 20 | "get_examples", 21 | "verify_signatures", 22 | "test_examples", 23 | "verify_and_test_examples", 24 | "verify_all_signatures", 25 | "test_all_examples", 26 | "verify_and_test_all_examples", 27 | "Examples", 28 | ] 29 | -------------------------------------------------------------------------------- /examples/api.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | from types import FunctionType, ModuleType 3 | from typing import Any, Callable, List 4 | 5 | from examples import registry 6 | from examples.example_objects import CallableExample, NotDefined 7 | 8 | 9 | def example( 10 | *args, 11 | _example_returns: Any = NotDefined, 12 | _example_raises: Any = None, 13 | _example_doc_string: bool = True, 14 | **kwargs, 15 | ) -> Callable: 16 | """A decorator that adds an example to the decorated function. 17 | 18 | Everything passed in to the example will be passed into the wrapped function in the 19 | unchanged when testing or using the example. 20 | 21 | Except, for the following magic parameters (all prefixed with `_example_`): 22 | 23 | - *_example_returns*: The exact result you expect the example to return. 24 | - *_example_raises*: An exception you expect the example to raise (can't be combined with above) 25 | - *_example_doc_string*: If True example is added to the functions doc string. 26 | """ 27 | 28 | def wrap_example(function: Callable) -> Callable: 29 | attached_module_name = function.__module__ 30 | if attached_module_name not in registry.module_registry: 31 | registry.module_registry[attached_module_name] = registry.Examples() 32 | module_registry = registry.module_registry[attached_module_name] 33 | return module_registry.example( 34 | *args, _example_returns=_example_returns, _example_raises=_example_raises, **kwargs 35 | )(function) 36 | 37 | return wrap_example 38 | 39 | 40 | def add_example_to(function: Callable) -> Callable: 41 | """Returns a function that when called will add an example to the provide function. 42 | 43 | This can be re-used multiple times to add multiple examples: 44 | 45 | add_example = add_example_to(my_sum_function) 46 | add_example(1, 2) 47 | add_example(2, 3, _example_returns=5) 48 | 49 | Or, can be used once for a single example: 50 | 51 | add_example_to(my_sum_function)(1, 1, _example_returns=5) 52 | 53 | The returned function follows the same signature as the @example decorator except 54 | it returns the produced examples, allowing you to expose it: 55 | 56 | add_example = add_example_to(my_sum_function)(1, 1) 57 | """ 58 | 59 | def example_factory( 60 | *args, 61 | _example_returns: Any = NotDefined, 62 | _example_raises: Any = None, 63 | _example_doc_string: bool = True, 64 | **kwargs, 65 | ) -> CallableExample: 66 | example( 67 | *args, 68 | _example_returns=_example_returns, 69 | _example_raises=_example_raises, 70 | _example_doc_string=_example_doc_string, 71 | **kwargs, 72 | )(function) 73 | return get_examples(function)[-1] 74 | 75 | return example_factory 76 | 77 | 78 | @singledispatch 79 | def get_examples(item: Any) -> List[CallableExample]: 80 | """Returns all examples associated with the provided item. 81 | Provided item should be of type function, module, or module name. 82 | """ 83 | raise NotImplementedError(f"Currently examples can not be attached to {type(item)}.") 84 | 85 | 86 | @get_examples.register(FunctionType) 87 | def _get_examples_callable(item: FunctionType) -> List[CallableExample]: 88 | """Returns all examples registered for a function""" 89 | module_examples = registry.module_registry.get(item.__module__, None) 90 | if not module_examples: 91 | return [] 92 | 93 | return module_examples.get(item) 94 | 95 | 96 | @get_examples.register(str) 97 | def _get_examples_module_name(item: str) -> List[CallableExample]: 98 | """Returns all examples registered to a module name""" 99 | module_examples = registry.module_registry.get(item, None) 100 | return module_examples.examples if module_examples else [] 101 | 102 | 103 | @get_examples.register(ModuleType) 104 | def _get_examples_module(item: ModuleType) -> List[CallableExample]: 105 | """Returns all examples registered to a module""" 106 | return _get_examples_module_name(item.__name__) 107 | 108 | 109 | @singledispatch 110 | def verify_signatures(item: Any, verify_types: bool = True) -> None: 111 | """Verifies the signature of all examples associated with the provided item. 112 | Provided item should be of type function, module, or module name. 113 | 114 | - *verify_types*: If `True` all examples will have have their types checked against 115 | their associated functions type annotations. 116 | """ 117 | raise NotImplementedError(f"Currently examples can not be attached to {type(item)}.") 118 | 119 | 120 | @verify_signatures.register(str) 121 | def _verify_module_name_signatures(item: str, verify_types: bool = True) -> None: 122 | """Verify signatures associated with the provided module name.""" 123 | module_examples = registry.module_registry.get(item, None) 124 | if not module_examples: 125 | raise ValueError( 126 | f"Tried verifying example signatures for {item} module but " 127 | "no examples are defined for that module." 128 | ) 129 | module_examples.verify_signatures(verify_types=verify_types) 130 | 131 | 132 | @verify_signatures.register(ModuleType) 133 | def _verify_module_signatures(item: ModuleType, verify_types: bool = True) -> None: 134 | """Verify signatures associated with the provided module.""" 135 | _verify_module_name_signatures(item.__name__, verify_types=verify_types) 136 | 137 | 138 | @verify_signatures.register(FunctionType) 139 | def _verify_function_signature(item: FunctionType, verify_types: bool = True) -> None: 140 | """Verify signatures associated with the provided module.""" 141 | examples = get_examples(item) 142 | if not examples: 143 | raise ValueError( 144 | f"Tried verifying example signatures for {item} function but " 145 | "no examples are defined for that function." 146 | ) 147 | 148 | for function_example in examples: 149 | function_example.verify_signature(verify_types=verify_types) 150 | 151 | 152 | @singledispatch 153 | def test_examples(item: Any, verify_return_type: bool = True) -> None: 154 | """Run all examples verifying they work as defined against the associated function. 155 | Provided item should be of type function, module, or module name. 156 | 157 | - *verify_return_type*: If `True` all examples will have have their return value types 158 | checked against their associated functions type annotations. 159 | """ 160 | raise NotImplementedError(f"Currently examples can not be attached to {type(item)}.") 161 | 162 | 163 | @test_examples.register(str) 164 | def _test_module_name_examples(item: str, verify_return_type: bool = True) -> None: 165 | """Tests all examples associated with the provided module name.""" 166 | module_examples = registry.module_registry.get(item, None) 167 | if not module_examples: 168 | raise ValueError( 169 | f"Tried testing example for {item} module but " 170 | "no examples are defined for that module." 171 | ) 172 | module_examples.test_examples(verify_return_type=verify_return_type) 173 | 174 | 175 | @test_examples.register(ModuleType) 176 | def _test_module_examples(item: ModuleType, verify_return_type: bool = True) -> None: 177 | """Tests all examples associated with the provided module.""" 178 | _test_module_name_examples(item.__name__, verify_return_type=verify_return_type) 179 | 180 | 181 | @test_examples.register(FunctionType) 182 | def _test_function_examples(item: FunctionType, verify_return_type: bool = True) -> None: 183 | """Tests all examples associated with the provided function.""" 184 | examples = get_examples(item) 185 | if not examples: 186 | raise ValueError( 187 | f"Tried testing example for {item} function but " 188 | "no examples are defined for that function." 189 | ) 190 | 191 | for function_example in examples: 192 | function_example.test(verify_return_type=verify_return_type) 193 | 194 | 195 | @singledispatch 196 | def verify_and_test_examples(item: Any, verify_return_type: bool = True) -> None: 197 | """Verifies the signature of all examples associated with the provided item then 198 | runs all examples verifying they work as defined. 199 | Provided item should be of type function, module, or module name. 200 | 201 | - *verify_types*: If `True` all examples will have have their types checked against 202 | their associated functions type annotations. 203 | """ 204 | raise NotImplementedError(f"Currently examples can not be attached to {type(item)}.") 205 | 206 | 207 | @verify_and_test_examples.register(str) 208 | def _verify_and_test_module_name_examples(item: str, verify_types: bool = True) -> None: 209 | """Verify signatures associated with the provided module name.""" 210 | module_examples = registry.module_registry.get(item, None) 211 | if not module_examples: 212 | raise ValueError( 213 | f"Tried verifying example signatures and running tests for {item} module " 214 | "but no examples are defined for that module." 215 | ) 216 | module_examples.verify_and_test_examples(verify_types=verify_types) 217 | 218 | 219 | @verify_and_test_examples.register(ModuleType) 220 | def _verify_and_test_module_examples(item: ModuleType, verify_types: bool = True) -> None: 221 | """Verify signatures associated with the provided module.""" 222 | _verify_and_test_module_name_examples(item.__name__, verify_types=verify_types) 223 | 224 | 225 | @verify_and_test_examples.register(FunctionType) 226 | def _verify_and_test_function_examples(item: FunctionType, verify_types: bool = True) -> None: 227 | """Verify signatures associated with the provided module.""" 228 | examples = get_examples(item) 229 | if not examples: 230 | raise ValueError( 231 | f"Tried verifying example signatures and running tests for {item} function" 232 | " but no examples are defined for that function." 233 | ) 234 | 235 | for function_example in examples: 236 | function_example.verify_and_test(verify_types=verify_types) 237 | 238 | 239 | def verify_all_signatures(verify_types: bool = False) -> None: 240 | """Verify all examples against their associated functions signatures.""" 241 | for module_name in registry.module_registry: 242 | verify_signatures(module_name, verify_types=verify_types) 243 | 244 | 245 | def test_all_examples(verify_return_type: bool = False) -> None: 246 | """Tests all examples against their associated functions.""" 247 | for module_name in registry.module_registry: 248 | test_examples(module_name, verify_return_type=verify_return_type) 249 | 250 | 251 | def verify_and_test_all_examples(verify_types: bool = False) -> None: 252 | """Tests all examples while verifying them against their associated functions signatures.""" 253 | for module_name in registry.module_registry: 254 | verify_and_test_examples(module_name, verify_types=verify_types) 255 | -------------------------------------------------------------------------------- /examples/example_objects.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | from pprint import pformat 4 | from typing import Any, Callable, get_type_hints 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class NotDefined: 10 | """This exists to allow distinctly checking for a parameter not passed in 11 | vs. one that is passed in as None. 12 | """ 13 | 14 | pass 15 | 16 | 17 | class CallableExample: 18 | """Defines a single Example call against a callable.""" 19 | 20 | __slots__ = ("args", "kwargs", "callable_object", "returns", "raises") 21 | 22 | def __init__( 23 | self, callable_object: Callable, args, kwargs, returns: Any = NotDefined, raises: Any = None 24 | ): 25 | self.args = args 26 | self.kwargs = kwargs 27 | self.callable_object = callable_object 28 | if raises and returns is not NotDefined: 29 | raise ValueError("Cannot specify both raises and returns on a single example.") 30 | self.returns = returns 31 | self.raises = raises 32 | 33 | def verify_signature(self, verify_types: bool = True): 34 | """Verifies that the example makes sense against the functions signature.""" 35 | bound = inspect.signature(self.callable_object).bind(*self.args, **self.kwargs) 36 | 37 | annotations = get_type_hints(self.callable_object) 38 | if verify_types and annotations: 39 | test_type_hints = {} 40 | typed_example_values = {} 41 | for parameter_name, parameter_value in bound.arguments.items(): 42 | type_hint = annotations.get(parameter_name, None) 43 | test_type_hints[parameter_name] = type_hint 44 | typed_example_values[parameter_name] = parameter_value 45 | 46 | if self.returns is not NotDefined and "return" in annotations: 47 | test_type_hints["returns"] = annotations["return"] 48 | typed_example_values["returns"] = self.returns 49 | 50 | class ExamplesModel(BaseModel): 51 | __annotations__ = test_type_hints 52 | 53 | ExamplesModel(**typed_example_values) 54 | 55 | def use(self) -> Any: 56 | """Runs the given example, giving back the result returned from running the example call.""" 57 | if inspect.iscoroutinefunction(self.callable_object): 58 | loop = asyncio.get_event_loop() 59 | call = self.callable_object(*self.args, **self.kwargs) 60 | if loop.is_running(): 61 | return call # pragma: no cover 62 | 63 | function = asyncio.ensure_future(call, loop=loop) 64 | loop.run_until_complete(function) 65 | return function.result() 66 | return self.callable_object(*self.args, **self.kwargs) 67 | 68 | def test(self, verify_return_type: bool = True): 69 | """Tests the given example, ensuring the return value matches that specified.""" 70 | try: 71 | result = self.use() 72 | except BaseException as exception: 73 | if not self.raises: 74 | raise 75 | 76 | if (type(self.raises) == type and not isinstance(exception, self.raises)) or ( 77 | type(self.raises) != type 78 | and ( 79 | not isinstance(exception, type(self.raises)) 80 | or self.raises.args != exception.args 81 | ) 82 | ): 83 | raise AssertionError( 84 | f"Example expected {repr(self.raises)} to be raised but " 85 | f"instead {repr(exception)} was raised" 86 | ) 87 | return 88 | 89 | if self.raises: 90 | raise AssertionError( 91 | f"Example expected {repr(self.raises)} to be raised " 92 | f"but instead {repr(result)} was returned" 93 | ) 94 | elif self.returns is not NotDefined: 95 | if result != self.returns: 96 | raise AssertionError( 97 | f"Example's expected return value of '{self.returns}' " 98 | f"does not not match actual return value of `{result}`" 99 | ) 100 | 101 | if verify_return_type: 102 | type_hints = get_type_hints(self.callable_object) 103 | if type_hints and "return" in type_hints: 104 | 105 | class ExampleReturnsModel(BaseModel): 106 | __annotations__ = {"returns": type_hints["return"]} 107 | 108 | ExampleReturnsModel(returns=result) 109 | 110 | def verify_and_test(self, verify_types: bool = True) -> None: 111 | self.verify_signature(verify_types=verify_types) 112 | self.test(verify_return_type=verify_types) 113 | 114 | def __str__(self): 115 | arg_str = ",\n ".join(repr(arg) for arg in self.args) 116 | if self.kwargs: 117 | arg_str += ",\n " if arg_str else "" 118 | arg_str += ",\n ".join( 119 | f"{name}={repr(value)}" for name, value in self.kwargs.items() 120 | ) 121 | 122 | call_str = f"{self.callable_object.__name__}(\n {arg_str}\n)" 123 | if self.returns is not NotDefined: 124 | call_str += f"\n == \n{pformat(self.returns)}" 125 | elif self.raises: 126 | call_str += f"\nraises {pformat(self.raises)}" 127 | return call_str 128 | 129 | def __repr__(self): 130 | return f"Example:\n{str(self)}" 131 | -------------------------------------------------------------------------------- /examples/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, List, Optional 2 | 3 | from examples.example_objects import CallableExample, NotDefined 4 | 5 | 6 | class Examples: 7 | """An object that holds a set of examples as they are registered.""" 8 | 9 | __slots__ = ("examples", "add_to_doc_strings", "_callable_mapping") 10 | 11 | def __init__(self, add_to_doc_strings: bool = True): 12 | self.examples: List[CallableExample] = [] 13 | self.add_to_doc_strings: bool = add_to_doc_strings 14 | self._callable_mapping: Dict[Callable, list] = {} 15 | 16 | def _add_to_doc_string(self, function: Callable, example: CallableExample) -> None: 17 | if function.__doc__ is None: 18 | function.__doc__ = "" 19 | 20 | indent: int = 4 21 | for line in reversed(function.__doc__.split("\n")): 22 | if line.strip(): 23 | indent = len(line) - len(line.lstrip(" ")) 24 | indent_spaces: str = " " * indent 25 | 26 | if "Examples:" not in function.__doc__: 27 | function.__doc__ += f"\n\n{indent_spaces}Examples:\n\n" 28 | 29 | indented_example = str(example).replace("\n", f"\n{indent_spaces} ") 30 | function.__doc__ += f"\n\n{indent_spaces} {indented_example}" 31 | function.__doc__ += "\n-------" 32 | 33 | def example( 34 | self, 35 | *args, 36 | _example_returns: Any = NotDefined, 37 | _example_raises: Any = None, 38 | _example_doc_string: Optional[bool] = None, 39 | **kwargs, 40 | ) -> Callable: 41 | def example_wrapper(function): 42 | new_example = CallableExample( 43 | function, returns=_example_returns, raises=_example_raises, args=args, kwargs=kwargs 44 | ) 45 | self._callable_mapping.setdefault(function, []).append(new_example) 46 | if _example_doc_string or (_example_doc_string is None and self.add_to_doc_strings): 47 | self._add_to_doc_string(function, new_example) 48 | self.examples.append(new_example) 49 | return function 50 | 51 | return example_wrapper 52 | 53 | def verify_signatures(self, verify_types: bool = True) -> None: 54 | for example in self.examples: 55 | example.verify_signature(verify_types=verify_types) 56 | 57 | def test_examples(self, verify_return_type: bool = True) -> None: 58 | for example in self.examples: 59 | example.test(verify_return_type=verify_return_type) 60 | 61 | def verify_and_test_examples(self, verify_types: bool = True) -> None: 62 | for example in self.examples: 63 | example.verify_and_test(verify_types=verify_types) 64 | 65 | def get(self, function: Callable) -> List[CallableExample]: 66 | """Returns back any examples registered for a specific function""" 67 | return self._callable_mapping.get(function, []) 68 | 69 | 70 | module_registry: Dict[str, Examples] = {} 71 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "examples" 3 | version = "1.0.2" 4 | description = "Tests and Documentation Done by Example." 5 | authors = ["Timothy Crosley "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.6" 11 | pydantic = ">=0.32.2<2.0.0" 12 | 13 | [tool.poetry.dev-dependencies] 14 | vulture = "^1.0" 15 | bandit = "^1.6" 16 | pytest = "^5.1" 17 | safety = "^1.8" 18 | isort = "^5.7.0" 19 | flake8-bugbear = "^19.8" 20 | black = {version = "^18.3-alpha.0", allow-prereleases = true} 21 | mypy = "^0.730.0" 22 | ipython = "^7.7" 23 | pytest-cov = "^2.7" 24 | pytest-mock = "^1.10" 25 | pep8-naming = "^0.8.2" 26 | portray = "^1.3.0" 27 | cruft = "^1.1" 28 | numpy = "^1.18.0" 29 | 30 | [tool.portray.mkdocs.theme] 31 | favicon = "art/logo.png" 32 | logo = "art/logo.png" 33 | name = "material" 34 | palette = {primary = "orange", accent = "blue"} 35 | 36 | [build-system] 37 | requires = ["poetry>=0.12"] 38 | build-backend = "poetry.masonry.api" 39 | 40 | [tool.black] 41 | line-length = 100 42 | 43 | [tool.isort] 44 | profile = "hug" 45 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | poetry run isort examples/ tests/ 5 | poetry run black examples tests/ 6 | -------------------------------------------------------------------------------- /scripts/done.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | ./scripts/clean.sh 5 | ./scripts/test.sh 6 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | poetry run cruft check 5 | poetry run mypy --ignore-missing-imports examples/ 6 | poetry run isort --check --diff examples/ tests/ 7 | poetry run black --check examples/ tests/ 8 | poetry run flake8 examples/ tests/ 9 | poetry run safety check -i 39462 10 | poetry run bandit -r examples 11 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | ./scripts/lint.sh 5 | poetry run pytest -s --cov=examples/ --cov=tests --cov-report=term-missing ${@-} --cov-report html 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | extend-ignore = 4 | E203 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#slices 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/tests/__init__.py -------------------------------------------------------------------------------- /tests/example_module_fail.py: -------------------------------------------------------------------------------- 1 | from examples import example 2 | 3 | 4 | @example(1, 2) 5 | @example(number_1=1, number_2=1, _example_returns=2) 6 | @example() 7 | def add(number_1: int, number_2: int = 1) -> int: 8 | return number_1 + number_2 9 | 10 | 11 | @example(3, 2, _example_returns="apple") 12 | @example(2, 2) 13 | def multiply(number_1: int, number_2: int) -> int: 14 | return number_1 * number_2 15 | 16 | 17 | @example(1, 1, _example_raises=NotImplementedError) 18 | @example(1, 2, _example_raises=NotImplementedError("No division support! This is just an POC.")) 19 | def divide(number_1: int, number_2: int): 20 | return number_1 / number_2 21 | -------------------------------------------------------------------------------- /tests/example_module_pass.py: -------------------------------------------------------------------------------- 1 | from examples import example 2 | 3 | 4 | @example(1, 2) 5 | @example(1) 6 | @example(number_1=1, number_2=1, _example_returns=2) 7 | def add(number_1: int, number_2: int = 1) -> int: 8 | return number_1 + number_2 9 | 10 | 11 | @example(2, 2) 12 | @example(3, 2, _example_returns=6) 13 | def multiply(number_1: int, number_2: int) -> int: 14 | return number_1 * number_2 15 | 16 | 17 | @example(1, 1, _example_raises=NotImplementedError) 18 | @example(1, 2, _example_raises=NotImplementedError("No division support! This is just an POC.")) 19 | def divide(number_1: int, number_2: int): 20 | raise NotImplementedError("No division support! This is just an POC.") 21 | -------------------------------------------------------------------------------- /tests/example_project_separate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/examples/2667ad2793c008d4f4c1c62851c99bf7d0d36290/tests/example_project_separate/__init__.py -------------------------------------------------------------------------------- /tests/example_project_separate/api.py: -------------------------------------------------------------------------------- 1 | """This is the API part of an example that demonstrates separating examples from implementation""" 2 | 3 | 4 | def add(number_1: int, number_2: int) -> int: 5 | """Adds two numbers together.""" 6 | return number_1 + number_2 7 | 8 | 9 | def multiply(number_1: int, number_2: int) -> int: 10 | """Multiplies two numbers together.""" 11 | return number_1 * number_2 12 | -------------------------------------------------------------------------------- /tests/example_project_separate/api_examples.py: -------------------------------------------------------------------------------- 1 | """This is the example portion of an example that demonstrates examples and implementation separate. 2 | 3 | One potential example about this pattern is the examples add **no** overhead unless imported. 4 | """ 5 | from examples import add_example_to 6 | 7 | from .api import add, multiply 8 | 9 | add_example = add_example_to(add) 10 | add_example(1, 1) 11 | add_example(2, 2, _example_returns=4) 12 | 13 | multiply_example = add_example_to(multiply) 14 | multiply_example(2, 2) 15 | multiply_example(1, 1, _example_returns=1) 16 | -------------------------------------------------------------------------------- /tests/no_examples_module.py: -------------------------------------------------------------------------------- 1 | def function(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from examples import api 4 | from examples.example_objects import CallableExample 5 | 6 | from . import example_module_fail, example_module_pass, no_examples_module 7 | 8 | 9 | def test_get_examples(): 10 | with pytest.raises(NotImplementedError): 11 | api.get_examples(42) # examples can't be associated to arbitrary types 12 | 13 | module_examples = api.get_examples(example_module_pass) 14 | assert module_examples == api.get_examples(example_module_pass.__name__) 15 | assert module_examples 16 | assert type(module_examples) == list 17 | for example in module_examples: 18 | assert type(example) == CallableExample 19 | 20 | function_examples = api.get_examples(example_module_pass.add) 21 | assert function_examples 22 | assert type(function_examples) == list 23 | for example in function_examples: 24 | assert type(example) == CallableExample 25 | 26 | assert api.get_examples(no_examples_module) == [] 27 | assert api.get_examples(no_examples_module.function) == [] 28 | 29 | 30 | def test_verify_signatures(): 31 | with pytest.raises(NotImplementedError): 32 | api.verify_signatures(42) # examples can't be associated to arbitrary types 33 | 34 | with pytest.raises(ValueError): 35 | api.verify_signatures(no_examples_module.function) # Examples must be defined 36 | 37 | with pytest.raises(ValueError): 38 | api.verify_signatures(no_examples_module) # Examples must be defined 39 | 40 | api.verify_signatures(example_module_pass) 41 | api.verify_signatures(example_module_pass, verify_types=False) 42 | api.verify_signatures(example_module_pass.__name__) 43 | api.verify_signatures(example_module_pass.__name__, verify_types=False) 44 | api.verify_signatures(example_module_pass.add) 45 | api.verify_signatures(example_module_pass.add, verify_types=False) 46 | 47 | with pytest.raises(Exception): 48 | api.verify_signatures(example_module_fail) 49 | with pytest.raises(Exception): 50 | api.verify_signatures(example_module_fail, verify_types=False) 51 | with pytest.raises(Exception): 52 | api.verify_signatures(example_module_fail.__name__) 53 | with pytest.raises(Exception): 54 | api.verify_signatures(example_module_fail.__name__, verify_types=False) 55 | with pytest.raises(Exception): 56 | api.verify_signatures(example_module_fail.add) 57 | with pytest.raises(Exception): 58 | api.verify_signatures(example_module_fail.add, verify_types=False) 59 | 60 | 61 | def test_test_examples(): 62 | with pytest.raises(NotImplementedError): 63 | api.test_examples(42) # examples can't be associated to arbitrary types 64 | 65 | with pytest.raises(ValueError): 66 | api.test_examples(no_examples_module.function) # Examples must be defined 67 | 68 | with pytest.raises(ValueError): 69 | api.test_examples(no_examples_module) # Examples must be defined 70 | 71 | api.test_examples(example_module_pass) 72 | api.test_examples(example_module_pass, verify_return_type=False) 73 | api.test_examples(example_module_pass.__name__) 74 | api.test_examples(example_module_pass.__name__, verify_return_type=False) 75 | api.test_examples(example_module_pass.add) 76 | api.test_examples(example_module_pass.add, verify_return_type=False) 77 | 78 | with pytest.raises(Exception): 79 | api.test_examples(example_module_fail) 80 | with pytest.raises(Exception): 81 | api.test_examples(example_module_fail, verify_return_type=False) 82 | with pytest.raises(Exception): 83 | api.test_examples(example_module_fail.__name__) 84 | with pytest.raises(Exception): 85 | api.test_examples(example_module_fail.__name__, verify_return_type=False) 86 | with pytest.raises(Exception): 87 | api.test_examples(example_module_fail.add) 88 | with pytest.raises(Exception): 89 | api.test_examples(example_module_fail.add, verify_return_type=False) 90 | with pytest.raises(Exception): 91 | api.test_examples(example_module_fail.multiply) 92 | 93 | 94 | def test_verify_and_test_examples(): 95 | with pytest.raises(NotImplementedError): 96 | api.verify_and_test_examples(42) # examples can't be associated to arbitrary types 97 | 98 | with pytest.raises(ValueError): 99 | api.verify_and_test_examples(no_examples_module.function) # Examples must be defined 100 | 101 | with pytest.raises(ValueError): 102 | api.verify_and_test_examples(no_examples_module) # Examples must be defined 103 | 104 | api.verify_and_test_examples(example_module_pass) 105 | api.verify_and_test_examples(example_module_pass, verify_types=False) 106 | api.verify_and_test_examples(example_module_pass.__name__) 107 | api.verify_and_test_examples(example_module_pass.__name__, verify_types=False) 108 | api.verify_and_test_examples(example_module_pass.add) 109 | api.verify_and_test_examples(example_module_pass.add, verify_types=False) 110 | 111 | with pytest.raises(Exception): 112 | api.verify_and_test_examples(example_module_fail) 113 | with pytest.raises(Exception): 114 | api.verify_and_test_examples(example_module_fail, verify_types=False) 115 | with pytest.raises(Exception): 116 | api.verify_and_test_examples(example_module_fail.__name__) 117 | with pytest.raises(Exception): 118 | api.verify_and_test_examples(example_module_fail.__name__, verify_types=False) 119 | with pytest.raises(Exception): 120 | api.verify_and_test_examples(example_module_fail.add) 121 | with pytest.raises(Exception): 122 | api.verify_and_test_examples(example_module_fail.add, verify_types=False) 123 | 124 | 125 | def test_verify_all_signatures(): 126 | with pytest.raises(Exception): 127 | api.verify_all_signatures() 128 | with pytest.raises(Exception): 129 | api.verify_all_signatures(verify_types=False) 130 | 131 | 132 | def test_test_all_examples(): 133 | with pytest.raises(Exception): 134 | api.test_all_examples() 135 | with pytest.raises(Exception): 136 | api.test_all_examples(verify_return_type=False) 137 | 138 | 139 | def test_verify_and_test_all_examples(): 140 | with pytest.raises(Exception): 141 | api.verify_and_test_all_examples() 142 | with pytest.raises(Exception): 143 | api.verify_and_test_all_examples(verify_types=False) 144 | 145 | 146 | def test_cant_specify_both_returns_and_raises(): 147 | with pytest.raises(ValueError): 148 | 149 | @api.example(_example_raises=ValueError, _example_returns=True) 150 | def my_function(): 151 | pass 152 | 153 | 154 | def test_wrong_exception_type(): 155 | with pytest.raises(AssertionError): 156 | 157 | @api.example(_example_raises=ValueError) 158 | def my_example(): 159 | raise NotImplementedError("This isn't implemented") 160 | 161 | api.verify_and_test_examples(my_example) 162 | 163 | 164 | def test_wrong_exception_args(): 165 | with pytest.raises(AssertionError): 166 | 167 | @api.example(_example_raises=ValueError("Unexpected Value")) 168 | def my_new_example(): 169 | raise ValueError("Expected Value") 170 | 171 | api.verify_and_test_examples(my_new_example) 172 | 173 | 174 | def test_return_instead_of_exception(): 175 | with pytest.raises(AssertionError): 176 | 177 | @api.example(_example_raises=ValueError("Unexpected Value")) 178 | def my_third_example(): 179 | return True 180 | 181 | api.verify_and_test_examples(my_third_example) 182 | 183 | 184 | def test_async_example(): 185 | @api.example(1, _example_returns=1) 186 | async def my_function(number_1: int): 187 | return number_1 188 | 189 | api.verify_and_test_examples(my_function) 190 | -------------------------------------------------------------------------------- /tests/test_project_separate.py: -------------------------------------------------------------------------------- 1 | import examples 2 | 3 | from .example_project_separate import api, api_examples 4 | 5 | 6 | def test_separate_project_examples(): 7 | assert api_examples 8 | assert api 9 | 10 | examples.verify_and_test_examples(api) 11 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | from examples.registry import Examples 2 | 3 | 4 | def test_doc_strings(): 5 | my_examples = Examples() 6 | 7 | @my_examples.example(1, 2) 8 | def add(number_1: int, number_2: int) -> int: 9 | return number_1 + number_2 10 | 11 | assert add.__doc__ and "Examples" in add.__doc__ and "add" in add.__doc__ and "1" in add.__doc__ 12 | assert "Example:" in repr(my_examples.get(add)[0]) 13 | 14 | my_docless_examples = Examples(add_to_doc_strings=False) 15 | 16 | @my_docless_examples.example(1, 2) 17 | def add_docless(number_1: int, number_2: int) -> int: 18 | return number_1 + number_2 19 | 20 | assert not add_docless.__doc__ 21 | -------------------------------------------------------------------------------- /xamples/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "xamples" 3 | version = "1.0.0" 4 | description = "Tests and Documentation Done by Example. An alias for the project `examples` with better SEO opportunities" 5 | authors = ["Timothy Crosley "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.6" 10 | examples = "^0.0.2" 11 | 12 | [build-system] 13 | requires = ["poetry>=0.12"] 14 | build-backend = "poetry.masonry.api" 15 | -------------------------------------------------------------------------------- /xamples/xamples/__init__.py: -------------------------------------------------------------------------------- 1 | from examples import * 2 | --------------------------------------------------------------------------------