├── tests
├── __init__.py
├── example_project_separate
│ ├── __init__.py
│ ├── api.py
│ └── api_examples.py
├── no_examples_module.py
├── test_project_separate.py
├── example_module_fail.py
├── example_module_pass.py
├── test_registry.py
└── test_api.py
├── .github
├── FUNDING.yml
└── workflows
│ ├── lint.yml
│ └── test.yml
├── xamples
├── xamples
│ └── __init__.py
└── pyproject.toml
├── art
├── logo.png
├── logo.xcf
├── example.gif
├── logo_large.png
└── logo_large.xcf
├── .coveragerc
├── scripts
├── done.sh
├── clean.sh
├── test.sh
└── lint.sh
├── setup.cfg
├── example_of_examples.py
├── docs
├── quick_start
│ ├── 1.-installation.md
│ ├── 3.-testing-examples.md
│ └── 2.-adding-examples.md
└── contributing
│ ├── 4.-acknowledgements.md
│ ├── 1.-contributing-guide.md
│ ├── 2.-coding-standard.md
│ └── 3.-code-of-conduct.md
├── .cruft.json
├── CHANGELOG.md
├── examples
├── __init__.py
├── registry.py
├── example_objects.py
└── api.py
├── pyproject.toml
├── LICENSE
├── .gitignore
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/example_project_separate/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: "timothycrosley"
2 |
--------------------------------------------------------------------------------
/xamples/xamples/__init__.py:
--------------------------------------------------------------------------------
1 | from examples import *
2 |
--------------------------------------------------------------------------------
/tests/no_examples_module.py:
--------------------------------------------------------------------------------
1 | def function():
2 | pass
3 |
--------------------------------------------------------------------------------
/art/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/examples/HEAD/art/logo.png
--------------------------------------------------------------------------------
/art/logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/examples/HEAD/art/logo.xcf
--------------------------------------------------------------------------------
/art/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/examples/HEAD/art/example.gif
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | omit =
5 | *tests*
6 |
--------------------------------------------------------------------------------
/art/logo_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/examples/HEAD/art/logo_large.png
--------------------------------------------------------------------------------
/art/logo_large.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timothycrosley/examples/HEAD/art/logo_large.xcf
--------------------------------------------------------------------------------
/scripts/done.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euxo pipefail
3 |
4 | ./scripts/clean.sh
5 | ./scripts/test.sh
6 |
--------------------------------------------------------------------------------
/scripts/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euxo pipefail
3 |
4 | poetry run isort examples/ tests/
5 | poetry run black examples tests/
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://timothycrosley.github.io/examples/)
2 | _________________
3 |
4 | [](http://badge.fury.io/py/examples)
5 | [](https://github.com/timothycrosley/examples/actions?query=workflow%3ATest)
6 | [](https://github.com/timothycrosley/examples/actions?query=workflow%3ALint)
7 | [](https://codecov.io/gh/timothycrosley/examples)
8 | [](https://gitter.im/timothycrosley/examples?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 | [](https://pypi.python.org/pypi/examples/)
10 | [](https://pepy.tech/project/examples)
11 | [](https://github.com/psf/black)
12 | [](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 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------