├── requirements-test-py26.txt
├── docs
├── _static
│ ├── logo.png
│ └── style.css
├── guide
│ ├── toc.rst.inc
│ ├── installing.rst
│ ├── usage.rst
│ └── custom-matchers.rst
├── reference
│ ├── toc.rst.inc
│ ├── strings.rst
│ ├── numbers.rst
│ ├── operators.rst
│ ├── collections.rst
│ └── general.rst
├── _templates
│ ├── sidebar
│ │ └── index.html
│ └── layout.html
├── index.rst
├── Makefile
└── conf.py
├── requirements-test-py32.txt
├── .gitignore
├── MANIFEST.in
├── setup.cfg
├── requirements-test.txt
├── requirements-dev.txt
├── CHANGELOG.md
├── tox.ini
├── LICENSE
├── .travis.yml
├── callee
├── functions.py
├── __init__.py
├── types.py
├── numbers.py
├── _compat.py
├── attributes.py
├── strings.py
├── objects.py
├── general.py
├── operators.py
├── collections.py
└── base.py
├── tests
├── test_all.py
├── test_types.py
├── __init__.py
├── test_attributes.py
├── test_objects.py
├── test_numbers.py
├── test_functions.py
├── test_strings.py
├── test_general.py
├── test_base.py
└── test_operators.py
├── README.rst
├── setup.py
└── tasks.py
/requirements-test-py26.txt:
--------------------------------------------------------------------------------
1 | # Backports for testing on Python 2.6
2 |
3 | unittest2
4 |
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xion/callee/HEAD/docs/_static/logo.png
--------------------------------------------------------------------------------
/requirements-test-py32.txt:
--------------------------------------------------------------------------------
1 | # Backports for testing on Python 3.2 and below
2 |
3 | mock
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | *.egg-info
3 | dist
4 |
5 | .tox
6 | .cache
7 | .coverage
8 |
9 | docs/_build
10 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include requirements-test.txt
3 | include requirements-test-*.txt
4 | recursive-exclude * *.pyc
5 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | ; Configuration file for auxiliary Python-related tools
2 |
3 | [flake8]
4 | ignore = W503, E402, E731
5 | show-source = 1
6 |
--------------------------------------------------------------------------------
/docs/guide/toc.rst.inc:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :maxdepth: 1
3 |
4 | /guide/installing
5 | /guide/usage
6 | /guide/custom-matchers
7 |
--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | # Test-only dependencies
2 |
3 | pytest
4 |
5 | # TODO: settle on Taipan from PyPI rather than pulling it from GitHub
6 | git+https://github.com/Xion/taipan.git#egg=taipan
7 |
--------------------------------------------------------------------------------
/docs/_static/style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom CSS styles, used by files from _templates/
3 | */
4 |
5 | .logo {
6 | position: relative;
7 | left: -8px;
8 |
9 | margin-bottom: 15px;
10 | }
11 |
--------------------------------------------------------------------------------
/docs/reference/toc.rst.inc:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :maxdepth: 2
3 |
4 | /reference/general
5 | /reference/strings
6 | /reference/numbers
7 | /reference/collections
8 | /reference/operators
9 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # Dependencies useful for development
2 | # (but not required by the library code or its tests)
3 |
4 | tox>=1.8
5 | invoke>=0.13
6 | flake8
7 |
8 | # docs
9 | Sphinx>=1.1
10 | alabaster
11 |
--------------------------------------------------------------------------------
/docs/_templates/sidebar/index.html:
--------------------------------------------------------------------------------
1 |
Links
2 |
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.3.1
4 |
5 | * Python 3.6 & 3.7 support
6 |
7 | ## 0.3
8 |
9 | * `strict=` param in `SubclassOf`.
10 | * `exact=` param in `InstanceOf`.
11 |
12 | ### New matchers
13 |
14 | * `Either` (alias: `OneOf`, `Xor`)
15 | * `FileLike`
16 | * `OrderedDict`
17 | * `Coroutine` and `CoroutineFunction`
18 |
19 | ## 0.2.2
20 |
21 | * Fix a bug in `__repr__` of `And` and `Or` matchers
22 |
23 | ## 0.2.1
24 |
25 | * `desc=` argument to the `Matching` / `ArgThat` matcher
26 |
27 | ### New matchers
28 |
29 | * `Captor` (an argument captor)
30 |
31 | ## 0.1.2
32 |
33 | * Better `repr()` of the `Matching` / `ArgThat` matcher's predicate
34 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | minversion=1.8
3 | envlist=py26, py27, pypy, py33, py34, py35, py36, py37, pypy3, flake8
4 | skip_missing_interpreters=true
5 |
6 | [testenv]
7 | deps=-rrequirements-test.txt
8 | commands=py.test
9 |
10 | [testenv:py26]
11 | deps=
12 | {[testenv:py27]deps}
13 | -rrequirements-test-py26.txt
14 |
15 | [testenv:py27]
16 | deps=
17 | {[testenv]deps}
18 | -rrequirements-test-py32.txt
19 |
20 | [testenv:pypy]
21 | deps={[testenv:py27]deps}
22 |
23 | # pypy3 is currently Python 3.2 so it needs the mock backport
24 | [testenv:pypy3]
25 | deps=
26 | {[testenv]deps}
27 | -rrequirements-test-py32.txt
28 |
29 | [testenv:flake8]
30 | basepython=python2.7
31 | deps=
32 | {[testenv]deps}
33 | flake8
34 | commands=flake8 callee tests
35 |
--------------------------------------------------------------------------------
/docs/reference/strings.rst:
--------------------------------------------------------------------------------
1 | String matchers
2 | ===============
3 |
4 | .. currentmodule:: callee.strings
5 |
6 | The :class:`String` matcher is the one you'd be using most of the time to match string arguments.
7 |
8 | More specialized matchers can distinguish between native Python 2/3 types for strings and binary data.
9 |
10 | .. autoclass:: String
11 |
12 | .. autoclass:: Unicode
13 |
14 | .. autoclass:: Bytes
15 |
16 |
17 | Patterns
18 | ********
19 |
20 | These matchers check whether the string is of certain form.
21 |
22 | Matching may be done based on prefix, suffix, or one of the various ways of specifying strings patterns,
23 | such as regular expressions.
24 |
25 | .. autoclass:: StartsWith
26 |
27 | .. autoclass:: EndsWith
28 |
29 | .. autoclass:: Glob
30 |
31 | .. autoclass:: Regex
32 |
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {#
2 | Extension of default Sphinx layout template
3 | #}
4 | {% extends '!layout.html' %}
5 |
6 |
7 | {# Include our own CSS file with the theme's stylesheets #}
8 | {% set css_files = css_files + ['_static/style.css'] %}
9 |
10 |
11 | {% block footer %}
12 | {{ super() }}
13 |
14 | {# Alabaster theme doesn't provide a proper hook to add additional copyright text to the footer,
15 | so we cheat a little to make it look okay-ish #}
16 | {# TODO: file an issue (or submit a PR) with Alabaster and reference it here #}
17 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/docs/reference/numbers.rst:
--------------------------------------------------------------------------------
1 | Numeric matchers
2 | ================
3 |
4 | .. currentmodule:: callee.numbers
5 |
6 | These matchers allow you to assert on specific numeric types, such as :class:`int`\ s or :class:`float`\ s
7 | They are often combined with :doc:`operator matchers ` to formulate constaints on numeric arguments of mocks:
8 |
9 | .. code-block:: python
10 |
11 | from callee import Integer, GreaterThan
12 | mock_foo.assert_called_with(Integer() & GreaterThan(42))
13 |
14 |
15 | Integers
16 | ********
17 |
18 | .. autoclass:: Integer
19 |
20 | .. autoclass:: Long
21 |
22 | .. autoclass:: Integral
23 |
24 |
25 | Rational numbers
26 | ****************
27 |
28 | .. autoclass:: Fraction
29 |
30 | .. autoclass:: Rational
31 |
32 |
33 | Floating point numbers
34 | **********************
35 |
36 | .. autoclass:: Float
37 |
38 | .. autoclass:: Real
39 |
40 |
41 | Complex numbers
42 | ***************
43 |
44 | .. autoclass:: Complex
45 |
46 |
47 | All numbers
48 | ***********
49 |
50 | .. autoclass:: Number
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Karol Kuczmarski
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 | * Neither the name of callee nor the names of its contributors
14 | may be used to endorse or promote products derived from this software
15 | without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Travis CI configuration file
3 | #
4 |
5 | language: python
6 | python:
7 | - '2.7'
8 | - pypy
9 | - '3.4'
10 | - '3.5'
11 | - '3.6'
12 | - '3.7'
13 | - pypy3
14 |
15 | install: pip install tox-travis
16 | sudo: false
17 |
18 | script: tox
19 |
20 |
21 | #
22 | # Deploy
23 | #
24 |
25 | deploy:
26 | provider: pypi
27 | user: "Xion"
28 | password:
29 | secure: E9p9pqSLBQNnxXUVzwqj8FKzBq9+PTIFDUYFBlh9hJ7ccXWUUtP1TNxLe6TBdw11CQjoW1G//7zkWBiTtE7Fq3p2lAA/A+OUUObOYXt15UX77rX/BWZODotQVwUdVHFRSUtCuq78P3Gg1yapy47jkN5c1/gfqq0JSEEbfJnoEquq7XXXN6EFHF7913Y9vB5DZHc3mZR7prsSCDKvKEChFyQEzyS9UyWR9BJ3UBayYt4l4N30pnOfbf21Mgsb3Nuz+dE+UVZGYFIZQ4sMZqwqgfafJ2f5TQoPZHwpyI2okRxk6v8K77MeEUXAU0v5xiCzM111uT/G6vPJaYo9rRewdP13Pk4WPBZFlmCT3xH2NduL3TC0W5P8pVy+An8kY5RRK4lHzuFd+3NR/KCqsmXZKtaKpPialCZl8D9g/XWP3P+vGrEPe+TPKFTeVwGR42Bb+l8A/LarKTXfzXE9t9mc9VIYk9D/J6QjFpBtCnMN1tWcRS131dhMO+PZJ7LHI3EhWn61UkCXzxzbcpB+lYkeCNqUAaSEjokub5o6M+jKaBc2Zbw0znmt8KWBYn4CQw38wWWQ+ZMeiTG1we701qiw2xy//+QZ/Pla4Ilxi89W4HuIFeTmQoJrq7dIIBWj6Y04OYpabOVet2zznq6NgPkamOcVdC9sv8M4BNg+7F+fGWU=
30 | skip_existing: true
31 | on:
32 | tags: true
33 |
34 |
35 | #
36 | # Meta
37 | #
38 |
39 | branches:
40 | only:
41 | # Run CI on pushes and PRs to master
42 | - master
43 | # Also on tags, so that deploys are triggered.
44 | # (This regex matches semantic versions like v1.2.3-rc4+2016.02.22)
45 | - /^\d+\.\d+(\.\d+)?.*$/
46 |
47 | git:
48 | # Don't set this to 1
49 | # (see note at https://docs.travis-ci.com/user/customizing-the-build#Git-Clone-Depth)
50 | depth: 5
51 |
52 | cache:
53 | - pip
54 |
55 |
--------------------------------------------------------------------------------
/docs/reference/operators.rst:
--------------------------------------------------------------------------------
1 | .. _operators
2 |
3 | Operator matchers
4 | =================
5 |
6 | .. currentmodule:: callee.operators
7 |
8 |
9 | Comparisons
10 | ***********
11 |
12 | These matchers use Python's relational operators: `<`, `>=`, etc.
13 |
14 | .. autoclass:: Less
15 | .. autoclass:: LessThan
16 | .. autoclass:: Lt
17 |
18 | .. autoclass:: LessOrEqual
19 | .. autoclass:: LessOrEqualTo
20 | .. autoclass:: Le
21 |
22 | .. autoclass:: Greater
23 | .. autoclass:: GreaterThan
24 | .. autoclass:: Gt
25 |
26 | .. autoclass:: GreaterOrEqual
27 | .. autoclass:: GreaterOrEqualTo
28 | .. autoclass:: Ge
29 |
30 |
31 | By length
32 | ---------
33 |
34 | In addition to simple comparison matchers described, *callee* offers a set of dedicated matchers for asserting
35 | on object's `len`\ gth. You can use them in conjunction with any Python :class:`Sequence`: a :class:`str`\ ing,
36 | :class:`list`, :class:`collections.deque`, and so on.
37 |
38 | .. autoclass:: Shorter
39 | .. autoclass:: ShorterThan
40 |
41 | .. autoclass:: ShorterOrEqual
42 | .. autoclass:: ShorterOrEqualTo
43 |
44 | .. autoclass:: Longer
45 | .. autoclass:: LongerThan
46 |
47 | .. autoclass:: LongerOrEqual
48 | .. autoclass:: LongerOrEqualTo
49 |
50 |
51 | Memberships
52 | ***********
53 |
54 | .. autoclass:: Contains(value)
55 |
56 | .. autoclass:: In(container)
57 |
58 |
59 | Identity
60 | ********
61 |
62 | .. autoclass:: Is
63 | .. autoclass:: IsNot
64 |
65 |
66 | Equality
67 | ********
68 |
69 | .. note:: You will most likely never use the following matcher, but it's included for completeness.
70 | .. autoclass:: Eq
71 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. callee documentation master file, created by
2 | sphinx-quickstart on Wed Oct 14 11:24:04 2015.
3 |
4 | callee
5 | ======
6 |
7 | *callee* provides a wide collection of **argument matchers** to use with the standard ``unittest.mock`` library.
8 |
9 | It allows you to write simple, readable, and powerful assertions
10 | on how the tested code should interact with your mocks:
11 |
12 | .. code-block:: python
13 |
14 | from callee import Dict, StartsWith, String
15 |
16 | mock_requests.get.assert_called_with(
17 | String() & StartsWith('https://'),
18 | params=Dict(String(), String()))
19 |
20 | With *callee*, you can avoid both the overly lenient |mock.ANY|_, as well as the tedious, error-prone code
21 | that manually checks |Mock.call_args|_ and |Mock.call_args_list|_.
22 |
23 | .. |mock.ANY| replace:: ``mock.ANY``
24 | .. _mock.ANY: https://docs.python.org/3/library/unittest.mock.html#any
25 | .. |Mock.call_args| replace:: ``Mock.call_args``
26 | .. _Mock.call_args: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args
27 | .. |Mock.call_args_list| replace:: ``call_args_list``
28 | .. _Mock.call_args_list: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args_list
29 |
30 |
31 | User's Guide
32 | ------------
33 |
34 | Start here for the installation instructions and a quick, complete overview of *callee* and its usage.
35 |
36 | .. include:: guide/toc.rst.inc
37 |
38 |
39 | API Reference
40 | -------------
41 |
42 | If you are looking for detailed information about all the matchers offered by *callee*,
43 | this is the place to go.
44 |
45 | .. include:: reference/toc.rst.inc
46 |
--------------------------------------------------------------------------------
/docs/reference/collections.rst:
--------------------------------------------------------------------------------
1 | Collection matchers
2 | ===================
3 |
4 | .. currentmodule:: callee.collections
5 |
6 | Besides allowing you to assert about various collection types (lists, sets, etc.),
7 | these matchers can also verify the *elements* inside those collections.
8 |
9 | This way, you can express even complex conditions in a concise and readable manner.
10 | Here's a couple of examples:
11 |
12 | .. code-block:: python
13 |
14 | # list of ints
15 | List(Integer())
16 | List(of=Integer())
17 | List(int) # types are also accepted as item matchers
18 |
19 | # list of strings starting with 'http://'
20 | List(of=String() & StartsWith('http://'))
21 |
22 | # dictionary mapping strings to strings
23 | Dict(String(), String())
24 |
25 | # dict with string keys (no restriction on values)
26 | Dict(keys=String())
27 |
28 | # list of dicts mapping strings to some custom type
29 | List(Dict(String(), Foo))
30 |
31 |
32 | Abstract collection types
33 | *************************
34 |
35 | These mostly correspond to the `abstract base classes`_ defined in the standard |collections module|_.
36 |
37 | .. _abstract base classes: https://docs.python.org/library/collections.html#collections-abstract-base-classes
38 |
39 | .. |collections module| replace:: :mod:`collections` module
40 | .. _collections module: https://docs.python.org/library/collections.html
41 |
42 | .. autoclass:: Iterable
43 |
44 | .. autoclass:: Generator
45 |
46 | .. autoclass:: Sequence
47 |
48 | .. autoclass:: Mapping
49 |
50 |
51 | Concrete collections
52 | ********************
53 |
54 | These match the particular Python built-in collections types, like :class:`list` or :class:`dict`.
55 |
56 | .. autoclass:: List
57 |
58 | .. autoclass:: Set
59 |
60 | .. autoclass:: Dict
61 |
62 | .. autoclass:: OrderedDict
63 |
--------------------------------------------------------------------------------
/docs/reference/general.rst:
--------------------------------------------------------------------------------
1 | General matchers
2 | ================
3 |
4 | .. currentmodule:: callee.general
5 |
6 | These matchers are the most general breed that is not specific to any
7 | particular kind of objects. They allow you to match mock parameters
8 | based on their Python types, object attributes, and even arbitrary
9 | boolean predicates.
10 |
11 | .. autoclass:: Any
12 |
13 | .. autoclass:: Matching
14 | .. autoclass:: ArgThat
15 |
16 | .. autoclass:: Captor
17 |
18 |
19 | Type matchers
20 | *************
21 |
22 | .. TODO: consider extracting these to a separate document to mirror the module structure
23 | .. currentmodule:: callee.types
24 |
25 | Use these matchers to assert on the type of objects passed to your mocks.
26 |
27 | .. autoclass:: InstanceOf
28 | .. autoclass:: IsA
29 |
30 | .. autoclass:: SubclassOf
31 | .. autoclass:: Inherits
32 |
33 | .. autoclass:: Type
34 |
35 | .. autoclass:: Class
36 |
37 |
38 | Attribute matchers
39 | ******************
40 |
41 | .. TODO: consider extracting these to a separate document to mirror the module structure
42 | .. currentmodule:: callee.attributes
43 |
44 | These match objects based on their Python attributes.
45 |
46 | .. autoclass:: Attrs
47 |
48 | .. autoclass:: HasAttrs
49 |
50 |
51 | Function matchers
52 | *****************
53 |
54 | .. TODO: consider extracting these to a separate document to mirror the module structure
55 | .. currentmodule:: callee.functions
56 |
57 | .. autoclass:: Callable
58 |
59 | .. autoclass:: Function
60 |
61 | .. autoclass:: GeneratorFunction
62 |
63 | .. autoclass:: CoroutineFunction
64 |
65 |
66 | Object matchers
67 | ***************
68 |
69 | .. TODO: consider extracting these to a separate document to mirror the module structure
70 | .. currentmodule:: callee.objects
71 |
72 | .. autoclass:: Bytes
73 |
74 | .. autoclass:: Coroutine
75 |
76 | .. autoclass:: FileLike
77 |
--------------------------------------------------------------------------------
/callee/functions.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers related to functions and other similar callables.
3 | """
4 | import inspect
5 |
6 | from callee._compat import asyncio
7 | from callee.base import BaseMatcher
8 |
9 |
10 | __all__ = [
11 | 'Callable', 'Function', 'GeneratorFunction', 'CoroutineFunction',
12 | ]
13 |
14 |
15 | class FunctionMatcher(BaseMatcher):
16 | """Matches values of callable types.
17 | This class shouldn't be used directly.
18 | """
19 | def __repr__(self):
20 | return "<%s>" % (self.__class__.__name__,)
21 |
22 |
23 | class Callable(FunctionMatcher):
24 | """Matches any callable object (as per the :func:`callable` function)."""
25 |
26 | def match(self, value):
27 | return callable(value)
28 |
29 |
30 | class Function(FunctionMatcher):
31 | """Matches any Python function."""
32 |
33 | def match(self, value):
34 | return inspect.isfunction(value)
35 |
36 |
37 | class GeneratorFunction(FunctionMatcher):
38 | """Matches a generator function, i.e. one that uses ``yield`` in its body.
39 |
40 | .. note::
41 |
42 | This is distinct from matching a *generator*,
43 | i.e. an iterable result of calling the generator function,
44 | or a generator comprehension (``(... for x in ...)``).
45 | The :class:`~callee.collections.Generator` matcher
46 | should be used for those objects instead.
47 | """
48 | def match(self, value):
49 | return inspect.isgeneratorfunction(value)
50 |
51 |
52 | class CoroutineFunction(FunctionMatcher):
53 | """Matches a coroutine function.
54 |
55 | A coroutine function is an asynchronous function defined using the
56 | ``@asyncio.coroutine`` or the ``async def`` syntax.
57 |
58 | These are only available in Python 3.4 and above.
59 | On previous versions of Python, no object will match this matcher.
60 | """
61 | def match(self, value):
62 | return asyncio and asyncio.iscoroutinefunction(value)
63 |
--------------------------------------------------------------------------------
/callee/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | callee
3 | """
4 | __version__ = "0.3.1"
5 | __description__ = "Argument matchers for unittest.mock"
6 | __author__ = "Karol Kuczmarski"
7 | __license__ = "BSD"
8 |
9 |
10 | from callee.attributes import Attrs, Attr, HasAttrs, HasAttr
11 | from callee.base import \
12 | And, Either, Eq, Is, IsNot, OneOf, Or, Matcher, Not, Xor
13 | from callee.collections import \
14 | Dict, Generator, Iterable, List, Mapping, OrderedDict, Sequence, Set
15 | from callee.general import Any, ArgThat, Captor, Matching
16 | from callee.functions import \
17 | Callable, CoroutineFunction, Function, GeneratorFunction
18 | from callee.numbers import (Complex, Float, Fraction, Int, Integer, Integral,
19 | Long, Number, Rational, Real)
20 | from callee.objects import Bytes, Coroutine, FileLike
21 | from callee.operators import (
22 | Contains,
23 | Ge, Greater, GreaterOrEqual, GreaterOrEqualTo, GreaterThan, Gt,
24 | In,
25 | Le, Less, LessOrEqual, LessOrEqualTo, LessThan,
26 | Longer, LongerOrEqual, LongerOrEqualTo, LongerThan, Lt,
27 | Shorter, ShorterOrEqual, ShorterOrEqualTo, ShorterThan)
28 | from callee.strings import \
29 | EndsWith, Glob, Regex, StartsWith, String, Unicode
30 | from callee.types import InstanceOf, IsA, SubclassOf, Inherits, Type, Class
31 |
32 |
33 | __all__ = [
34 | 'Matcher',
35 | 'Eq', 'Is', 'IsNot',
36 | 'Not', 'And', 'Or', 'Either', 'OneOf', 'Xor',
37 |
38 | 'Attrs', 'Attr', 'HasAttrs', 'HasAttr',
39 |
40 | 'Iterable', 'Generator',
41 | 'Sequence', 'List', 'Set',
42 | 'Mapping', 'Dict', 'OrderedDict',
43 |
44 | 'Any', 'Matching', 'ArgThat', 'Captor',
45 |
46 | 'Callable', 'Function', 'GeneratorFunction',
47 | 'CoroutineFunction',
48 |
49 | 'Number',
50 | 'Complex', 'Real', 'Float', 'Rational', 'Fraction',
51 | 'Integral', 'Integer', 'Int', 'Long',
52 |
53 | 'Bytes',
54 | 'Coroutine',
55 | 'FileLike',
56 |
57 | 'Less', 'LessThan', 'Lt',
58 | 'LessOrEqual', 'LessOrEqualTo', 'Le',
59 | 'Greater', 'GreaterThan', 'Gt',
60 | 'GreaterOrEqual', 'GreaterOrEqualTo', 'Ge',
61 | 'Shorter', 'ShorterThan', 'ShorterOrEqual', 'ShorterOrEqualTo',
62 | 'Longer', 'LongerThan', 'LongerOrEqual', 'LongerOrEqualTo',
63 | 'Contains', 'In',
64 |
65 | 'String', 'Unicode',
66 | 'StartsWith', 'EndsWith', 'Glob', 'Regex',
67 |
68 | 'InstanceOf', 'IsA', 'SubclassOf', 'Inherits', 'Type', 'Class',
69 | ]
70 |
--------------------------------------------------------------------------------
/tests/test_all.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests that touch more than a single module.
3 | """
4 | import inspect
5 | from operator import countOf
6 |
7 | import callee
8 | from tests import TestCase
9 |
10 |
11 | class Init(TestCase):
12 | """Tests for the __init__ module."""
13 |
14 | def test_exports__unique(self):
15 | """Test that __all__ contains unique names."""
16 | exports = callee.__all__
17 | export_set = set(exports)
18 | repeats = [name for name in export_set if countOf(exports, name) > 1]
19 | self.assertEmpty(
20 | repeats,
21 | msg="callee.__all__ contains repeated entries: %s" % repeats)
22 |
23 | def test_exports__only_available(self):
24 | """Test that __all__ has only names that are available
25 | (i.e. have been imported from submodules into the __init__ module).
26 | """
27 | missing = set(n for n in callee.__all__
28 | if getattr(callee, n, None) is None)
29 | self.assertEmpty(
30 | missing,
31 | msg="callee.__all__ contains unresolved names: %s" % missing)
32 |
33 | def test_exports__only_exported_by_submodules(self):
34 | """Test that __all__ contains only names that are actually exported
35 | by the submodules.
36 | """
37 | exported_by_submodules = set()
38 | for mod in self.get_submodules():
39 | exported_by_submodules.update(mod.__all__)
40 | exported_by_root = set(callee.__all__)
41 |
42 | private_exports = exported_by_root - exported_by_submodules
43 | self.assertEmpty(
44 | private_exports,
45 | msg="callee.__all__ has private symbols: %s" % private_exports)
46 |
47 | def test_exports__all_of_submodule_exports(self):
48 | """Test that __all__ contains all the publically exported names
49 | from the (public) submodules.
50 | """
51 | exported_by_submodules = set()
52 | for mod in self.get_submodules():
53 | exported_by_submodules.update(mod.__all__)
54 | exported_by_root = set(callee.__all__)
55 |
56 | missing_exports = exported_by_submodules - exported_by_root
57 | self.assertEmpty(missing_exports,
58 | msg="some public symbols missing "
59 | "from callee.__all__: %s" % missing_exports)
60 |
61 | # Utility functions
62 |
63 | def get_submodules(self):
64 | """Get an iterable of submodules in the library, w/o private ones."""
65 | for name, obj in vars(callee).items():
66 | if not name.startswith('_') and inspect.ismodule(obj):
67 | yield obj
68 |
--------------------------------------------------------------------------------
/callee/types.py:
--------------------------------------------------------------------------------
1 | """
2 | Type-related matchers.
3 | """
4 | import inspect
5 |
6 | from callee.base import BaseMatcher
7 |
8 |
9 | __all__ = [
10 | 'InstanceOf', 'IsA', 'SubclassOf', 'Inherits', 'Type', 'Class',
11 | ]
12 |
13 |
14 | class TypeMatcher(BaseMatcher):
15 | """Matches an object to a type.
16 | This class shouldn't be used directly.
17 | """
18 | def __init__(self, type_):
19 | """:param type\ _: Type to match against"""
20 | if not isinstance(type_, type):
21 | raise TypeError("%s requires a type, got %r" % (
22 | self.__class__.__name__, type_))
23 | self.type_ = type_
24 |
25 | def __repr__(self):
26 | return "<%s %r>" % (self.__class__.__name__, self.type_)
27 |
28 |
29 | # TODO: reverse of this matcher (TypeOf / ClassOf)
30 | class InstanceOf(TypeMatcher):
31 | """Matches an object that's an instance of given type
32 | (as per `isinstance`).
33 | """
34 | def __init__(self, type_, exact=False):
35 | """
36 | :param type\ _: Type to match against
37 | :param exact:
38 |
39 | If True, the match will only succeed if the value type matches
40 | given ``type_`` exactly.
41 | Otherwise (the default), a subtype of ``type_`` will also match.
42 | """
43 | super(InstanceOf, self).__init__(type_)
44 | self.exact = exact
45 |
46 | def match(self, value):
47 | if self.exact:
48 | return type(value) is self.type_
49 | else:
50 | return isinstance(value, self.type_)
51 |
52 | IsA = InstanceOf
53 |
54 |
55 | # TODO: reverse of this matcher (SuperclassOf)
56 | class SubclassOf(TypeMatcher):
57 | """Matches a class that's a subclass of given type
58 | (as per `issubclass`).
59 | """
60 | def __init__(self, type_, strict=False):
61 | """
62 | :param type\ _: Type to match against
63 | :param strict:
64 |
65 | If True, the match if only succeed if the value is a _strict_
66 | subclass of ``type_`` -- that is, it's not ``type_`` itself.
67 | Otherwise (the default), any subclass of ``type_`` matches.
68 | """
69 | super(SubclassOf, self).__init__(type_)
70 | self.strict = strict
71 |
72 | def match(self, value):
73 | if not isinstance(value, type):
74 | return False
75 | if not issubclass(value, self.type_):
76 | return False
77 | if value is self.type_ and self.strict:
78 | return False
79 | return True
80 |
81 | Inherits = SubclassOf
82 |
83 |
84 | class Type(BaseMatcher):
85 | """Matches any Python type object."""
86 |
87 | def match(self, value):
88 | return isinstance(value, type)
89 |
90 | def __repr__(self):
91 | return ""
92 |
93 |
94 | class Class(BaseMatcher):
95 | """Matches a class (but not any other type object)."""
96 |
97 | def match(self, value):
98 | return inspect.isclass(value)
99 |
100 | def __repr__(self):
101 | return ""
102 |
103 |
104 | # TODO: Module() matcher
105 |
--------------------------------------------------------------------------------
/callee/numbers.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers for numbers.
3 | """
4 | from __future__ import absolute_import
5 |
6 | import fractions
7 | import numbers
8 |
9 | from callee._compat import IS_PY3
10 | from callee.base import BaseMatcher
11 |
12 |
13 | __all__ = [
14 | 'Number',
15 | 'Complex', 'Real', 'Float', 'Rational', 'Fraction',
16 | 'Integral', 'Integer', 'Int', 'Long',
17 | ]
18 |
19 |
20 | class NumericMatcher(BaseMatcher):
21 | """Matches some number type.
22 | This class shouldn't be used directly.
23 | """
24 | #: Number class to match.
25 | #: Must be overridden in subclasses.
26 | CLASS = None
27 |
28 | def __init__(self):
29 | assert self.CLASS, "must specify number type to match"
30 |
31 | def match(self, value):
32 | return isinstance(value, self.CLASS)
33 |
34 | def __repr__(self):
35 | return "<%s>" % (self.__class__.__name__,)
36 |
37 |
38 | class Number(NumericMatcher):
39 | """Matches any number
40 | (integer, float, complex, custom number types, etc.).
41 | """
42 | CLASS = numbers.Number
43 |
44 |
45 | class Complex(NumericMatcher):
46 | """Matches any complex number.
47 |
48 | This *includes* all real, rational, and integer numbers as well,
49 | which in Python translates to `float`\ s, fractions, and `int`\ egers.
50 | """
51 | CLASS = numbers.Complex
52 |
53 |
54 | # TODO: consider adding a dedicated matcher for the ``complex`` type;
55 | # right now, though, ``IsA(complex)`` and ``Complex() & ~Real()`` are probably
56 | # acceptable workarounds
57 |
58 |
59 | class Real(NumericMatcher):
60 | """Matches any real number.
61 |
62 | This includes all rational and integer numbers as well, which in Python
63 | translates to fractions, and `int`\ egers.
64 | """
65 | CLASS = numbers.Real
66 |
67 |
68 | class Float(NumericMatcher):
69 | """Matches a floating point number."""
70 |
71 | CLASS = float
72 |
73 |
74 | class Rational(NumericMatcher):
75 | """Matches a rational number.
76 | This includes all `int`\ eger numbers as well.
77 | """
78 | CLASS = numbers.Rational
79 |
80 |
81 | class Fraction(NumericMatcher):
82 | """Matches a fraction object."""
83 |
84 | CLASS = fractions.Fraction
85 |
86 |
87 | class Integral(NumericMatcher):
88 | """Matches any integer.
89 | This ignores the length of integer's internal representation on Python 2.
90 | """
91 | CLASS = int if IS_PY3 else (int, long)
92 |
93 |
94 | class Integer(NumericMatcher):
95 | """Matches a regular integer.
96 |
97 | On Python 3, there is no distinction between regular and long integer,
98 | making this matcher and :class:`Long` equivalent.
99 |
100 | On Python 2, this matches the :class:`int` integers exclusively.
101 | """
102 | CLASS = int
103 |
104 | #: Alias for :class:`Integer`.
105 | Int = Integer
106 |
107 |
108 | class Long(NumericMatcher):
109 | """Matches a long integer.
110 |
111 | On Python 3, this is the same as regular integer, making this matcher
112 | and :class:`Integer` equivalent.
113 |
114 | On Python 2, this matches the :class:`long` integers exclusively.
115 | """
116 | CLASS = int if IS_PY3 else long
117 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | callee
2 | ======
3 |
4 | Argument matchers for *unittest.mock*
5 |
6 | |Version| |Development Status| |Python Versions| |License| |Build Status|
7 |
8 | .. |Version| image:: https://img.shields.io/pypi/v/callee.svg?style=flat
9 | :target: https://pypi.python.org/pypi/callee
10 | :alt: Version
11 | .. |Development Status| image:: https://img.shields.io/pypi/status/callee.svg?style=flat
12 | :target: https://pypi.python.org/pypi/callee/
13 | :alt: Development Status
14 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/callee.svg?style=flat
15 | :target: https://pypi.python.org/pypi/callee
16 | :alt: Python versions
17 | .. |License| image:: https://img.shields.io/pypi/l/callee.svg?style=flat
18 | :target: https://github.com/Xion/callee/blob/master/LICENSE
19 | :alt: License
20 | .. |Build Status| image:: https://img.shields.io/travis/Xion/callee.svg?style=flat
21 | :target: https://travis-ci.org/Xion/callee
22 | :alt: Build Status
23 |
24 |
25 | More robust tests
26 | ~~~~~~~~~~~~~~~~~
27 |
28 | Python's `mocking library`_ (or its `backport`_ for Python <3.3) is simple, reliable, and easy to use.
29 | But it is also a little lacking when it comes to asserting what calls a mock has received.
30 |
31 | You can be either very specific::
32 |
33 | my_mock.assert_called_once_with(42, some_foo_object, 'certain string')
34 |
35 | or extremely general::
36 |
37 | my_mock.assert_called_with(ANY, ANY, ANY)
38 | # passes as long as argument count is the same
39 |
40 | | The former can make your tests over-specified, and thus fragile.
41 | | The latter could make them too broad, missing some erroneous cases and possibly letting your code fail in production.
42 |
43 | ----
44 |
45 | *callee* provides **argument matchers** that allow you to be exactly as precise as you want::
46 |
47 | my_mock.assert_called_with(GreaterThan(0), InstanceOf(Foo), String())
48 |
49 | without tedious, handcrafted, and poorly readable code that checks ``call_args`` or ``call_args_list``::
50 |
51 | self.assertGreater(mock.call_args[0][0], 0)
52 | self.assertIsInstance(mock.call_args[0][1], Foo)
53 | self.assertIsInstance(mock.call_args[0][2], str)
54 |
55 | It has plenty of matcher types to fit all common and uncommon needs, and you can easily write your own if necessary.
56 |
57 | .. _mocking library: https://docs.python.org/3/library/unittest.mock.html
58 | .. _backport: https://pypi.python.org/pypi/mock
59 |
60 |
61 | Installation
62 | ~~~~~~~~~~~~
63 |
64 | Installing *callee* is easy with pip::
65 |
66 | $ pip install callee
67 |
68 | | *callee* support goes all the way back to Python 2.6.
69 | | It also works both with the ``unittest.mock`` module from Python 3.3+ or its backport.
70 |
71 |
72 | API reference
73 | ~~~~~~~~~~~~~
74 |
75 | See the `documentation`_ for complete reference on the library usage and all available matchers.
76 |
77 | .. _documentation: http://callee.readthedocs.org
78 |
79 |
80 | Contributing
81 | ~~~~~~~~~~~~
82 |
83 | Contributions are welcome!
84 | If you need ideas, head to the issue tracker or search for the various ``TODO``\ s scattered around the codebase.
85 | Or just think what matchers you'd like to add :)
86 |
87 | After cloning the repository, this should get you up and running::
88 |
89 | # ... create virtualenv as necessary ...
90 | pip install -r requirements-dev.txt
91 | tox
92 |
93 | To regenerate documentation and display it in the browser, simply run::
94 |
95 | inv docs
96 |
97 | Happy hacking!
98 |
--------------------------------------------------------------------------------
/callee/_compat.py:
--------------------------------------------------------------------------------
1 | """
2 | Compatibility shims for different Python versions.
3 | """
4 | from __future__ import absolute_import
5 |
6 | try:
7 | import asyncio
8 | except ImportError:
9 | asyncio = None
10 | try:
11 | from collections import OrderedDict # Python 2.7+
12 | except ImportError:
13 | try:
14 | from ordereddict import OrderedDict # Python 2.6 with the shim library
15 | except ImportError:
16 | OrderedDict = None
17 | import inspect
18 | import sys
19 |
20 |
21 | __all__ = [
22 | 'asyncio',
23 | 'OrderedDict',
24 | 'IS_PY3',
25 | 'STRING_TYPES', 'casefold',
26 | 'metaclass',
27 | 'getargspec',
28 | ]
29 |
30 |
31 | IS_PY3 = sys.version_info[0] == 3
32 |
33 | STRING_TYPES = (str,) if IS_PY3 else (basestring,)
34 | casefold = getattr(str, 'casefold', None) or (lambda s: s.lower())
35 |
36 |
37 | class MetaclassDecorator(object):
38 | """Decorator for creating a class through a metaclass.
39 |
40 | Unlike ``__metaclass__`` attribute from Python 2, or ``metaclass=`` keyword
41 | argument from Python 3, the ``@metaclass`` decorator works with both
42 | versions of the language.
43 |
44 | Example::
45 |
46 | @metaclass(MyMetaclass)
47 | class MyClass(object):
48 | pass
49 | """
50 | def __init__(self, meta):
51 | if not issubclass(meta, type):
52 | raise TypeError(
53 | "expected a metaclass, got %s instead" % type(meta).__name__)
54 | self.metaclass = meta
55 |
56 | def __call__(self, cls):
57 | """Apply the decorator to given class.
58 |
59 | This recreates the class using the previously supplied metaclass.
60 | """
61 | # Copyright (c) Django Software Foundation and individual contributors.
62 | # All rights reserved.
63 |
64 | original_dict = cls.__dict__.copy()
65 | original_dict.pop('__dict__', None)
66 | original_dict.pop('__weakref__', None)
67 |
68 | slots = original_dict.get('__slots__')
69 | if slots is not None:
70 | if isinstance(slots, str):
71 | slots = [slots]
72 | for slot in slots:
73 | original_dict.pop(slot)
74 |
75 | return self.metaclass(cls.__name__, cls.__bases__, original_dict)
76 |
77 | metaclass = MetaclassDecorator
78 | del MetaclassDecorator
79 |
80 |
81 | def getargspec(obj):
82 | """Portable version of inspect.getargspec().
83 |
84 | Necessary because the original is no longer available
85 | starting from Python 3.6.
86 |
87 | :return: 4-tuple of (argnames, varargname, kwargname, defaults)
88 |
89 | Note that distinction between positional-or-keyword and keyword-only
90 | parameters will be lost, as the original getargspec() doesn't honor it.
91 | """
92 | try:
93 | return inspect.getargspec(obj)
94 | except AttributeError:
95 | pass # we let a TypeError through
96 |
97 | # translate the signature object back into the 4-tuple
98 | argnames = []
99 | varargname, kwargname = None, None
100 | defaults = []
101 | for name, param in inspect.signature(obj):
102 | if param.kind == inspect.Parameter.VAR_POSITIONAL:
103 | varargname = name
104 | elif param.kind == inspect.Parameter.VAR_KEYWORD:
105 | kwargname = name
106 | else:
107 | argnames.append(name)
108 | if param.default is not inspect.Parameter.empty:
109 | defaults.append(param.default)
110 | defaults = defaults or None
111 |
112 | return argnames, varargname, kwargname, defaults
113 |
--------------------------------------------------------------------------------
/tests/test_types.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for type-related matchers.
3 | """
4 | import callee.types as __unit__
5 | from tests import MatcherTestCase
6 |
7 |
8 | class InstanceOf(MatcherTestCase):
9 |
10 | def test_invalid_type(self):
11 | with self.assertRaises(TypeError):
12 | __unit__.InstanceOf(object())
13 |
14 | def test_none(self):
15 | self.assert_match(None, object)
16 | self.assert_no_match(None, self.Class)
17 |
18 | def test_zero(self):
19 | self.assert_match(0, int)
20 | self.assert_no_match(0, self.Class)
21 |
22 | def test_string(self):
23 | s = "Alice has a cat"
24 | self.assert_match(s, str)
25 | self.assert_no_match(s, self.Class)
26 |
27 | def test_class__inexact(self):
28 | self.assert_match(self.Class(), self.Class)
29 | self.assert_no_match(self.Class(), int)
30 |
31 | def test_class__exact(self):
32 | self.assert_match(self.Class(), self.Class, exact=True)
33 | self.assert_no_match(self.Class(), object, exact=True)
34 |
35 | def test_meta(self):
36 | self.assert_match(self.Class, type)
37 | self.assert_match(self.Class, type, exact=True)
38 | self.assert_match(type, type)
39 | self.assert_match(type, type, exact=True)
40 |
41 | test_repr = lambda self: self.assert_repr(__unit__.InstanceOf(object))
42 |
43 | # Utility code
44 |
45 | class Class(object):
46 | pass
47 |
48 | def assert_match(self, value, type_, exact=False):
49 | return super(InstanceOf, self) \
50 | .assert_match(__unit__.InstanceOf(type_, exact), value)
51 |
52 | def assert_no_match(self, value, type_, exact=False):
53 | return super(InstanceOf, self) \
54 | .assert_no_match(__unit__.InstanceOf(type_, exact), value)
55 |
56 |
57 | class SubclassOf(MatcherTestCase):
58 |
59 | def test_invalid_type(self):
60 | with self.assertRaises(TypeError):
61 | __unit__.SubclassOf(object())
62 |
63 | def test_non_types(self):
64 | self.assert_no_match(None, object)
65 | self.assert_no_match(0, object)
66 | self.assert_no_match("Alice has a cat", object)
67 | self.assert_no_match((), object)
68 | self.assert_no_match([], object)
69 |
70 | def test_non_types__strict(self):
71 | self.assert_no_match(None, object, strict=True)
72 | self.assert_no_match(0, object, strict=True)
73 | self.assert_no_match("Alice has a cat", object, strict=True)
74 | self.assert_no_match((), object, strict=True)
75 | self.assert_no_match([], object, strict=True)
76 |
77 | def test_types(self):
78 | self.assert_match(self.Class, object)
79 | self.assert_match(self.Class, self.Class)
80 | self.assert_no_match(object, self.Class)
81 | self.assert_match(object, object)
82 |
83 | def test_types__strict(self):
84 | self.assert_match(self.Class, object, strict=True)
85 | self.assert_no_match(self.Class, self.Class, strict=True)
86 | self.assert_no_match(object, self.Class, strict=True)
87 | self.assert_no_match(object, object, strict=True)
88 |
89 | test_repr = lambda self: self.assert_repr(__unit__.SubclassOf(object))
90 |
91 | # Utility code
92 |
93 | class Class(object):
94 | pass
95 |
96 | def assert_match(self, value, type_, strict=False):
97 | return super(SubclassOf, self) \
98 | .assert_match(__unit__.SubclassOf(type_, strict), value)
99 |
100 | def assert_no_match(self, value, type_, strict=False):
101 | return super(SubclassOf, self) \
102 | .assert_no_match(__unit__.SubclassOf(type_, strict), value)
103 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | callee
4 | ======
5 |
6 | {description}
7 | """
8 | import ast
9 | import os
10 | from setuptools import find_packages, setup
11 | import sys
12 |
13 |
14 | # Utility functions
15 |
16 | def read_tags(filename):
17 | """Reads values of "magic tags" defined in the given Python file.
18 |
19 | :param filename: Python filename to read the tags from
20 | :return: Dictionary of tags
21 | """
22 | with open(filename) as f:
23 | ast_tree = ast.parse(f.read(), filename)
24 |
25 | res = {}
26 | for node in ast.walk(ast_tree):
27 | if type(node) is not ast.Assign:
28 | continue
29 |
30 | target = node.targets[0]
31 | if type(target) is not ast.Name:
32 | continue
33 | if not (target.id.startswith('__') and target.id.endswith('__')):
34 | continue
35 |
36 | name = target.id[2:-2]
37 | res[name] = ast.literal_eval(node.value)
38 |
39 | return res
40 |
41 |
42 | def read_requirements(filename='requirements.txt'):
43 | """Reads the list of requirements from given file.
44 |
45 | :param filename: Filename to read the requirements from.
46 | Uses ``'requirements.txt'`` by default.
47 |
48 | :return: Requirements as list of strings
49 | """
50 | # allow for some leeway with the argument
51 | if not filename.startswith('requirements'):
52 | filename = 'requirements-' + filename
53 | if not os.path.splitext(filename)[1]:
54 | filename += '.txt' # no extension, add default
55 |
56 | def valid_line(line):
57 | line = line.strip()
58 | return line and not any(line.startswith(p) for p in ('#', '-'))
59 |
60 | def extract_requirement(line):
61 | egg_eq = '#egg='
62 | if egg_eq in line:
63 | _, requirement = line.split(egg_eq, 1)
64 | return requirement
65 | return line
66 |
67 | with open(filename) as f:
68 | lines = f.readlines()
69 | return list(map(extract_requirement, filter(valid_line, lines)))
70 |
71 |
72 | # setup() call
73 |
74 | tags = read_tags(os.path.join('callee', '__init__.py'))
75 | __doc__ = __doc__.format(**tags)
76 |
77 | tests_require = read_requirements('test')
78 | if sys.version_info < (2, 7):
79 | tests_require.extend(read_requirements('test-py26'))
80 | if sys.version_info < (3, 3):
81 | tests_require.extend(read_requirements('test-py32'))
82 |
83 | setup(
84 | name="callee",
85 | version=tags['version'],
86 | description=tags['description'],
87 | long_description=__doc__,
88 | author=tags['author'],
89 | url="https://github.com/Xion/callee",
90 | license=tags['license'],
91 |
92 | classifiers=[
93 | "Development Status :: 3 - Alpha",
94 | "Intended Audience :: Developers",
95 | "License :: OSI Approved :: BSD License",
96 | "Operating System :: OS Independent",
97 | "Programming Language :: Python",
98 | "Programming Language :: Python :: 2.6",
99 | "Programming Language :: Python :: 2.7",
100 | "Programming Language :: Python :: 3.3",
101 | "Programming Language :: Python :: 3.4",
102 | "Programming Language :: Python :: 3.5",
103 | "Programming Language :: Python :: 3.6",
104 | "Programming Language :: Python :: 3.7",
105 | "Programming Language :: Python :: Implementation :: CPython",
106 | "Programming Language :: Python :: Implementation :: PyPy",
107 | "Topic :: Software Development :: Testing",
108 | ],
109 |
110 | platforms='any',
111 | packages=find_packages(exclude=['tests']),
112 |
113 | tests_require=tests_require,
114 | )
115 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Test package.
3 | """
4 | import os
5 | import sys
6 |
7 | try:
8 | import unittest.mock as mock
9 | except ImportError:
10 | import mock
11 |
12 | from taipan.testing import TestCase as _TestCase
13 |
14 | from callee._compat import asyncio
15 |
16 |
17 | __all__ = [
18 | 'IS_PY34', 'IS_PY35',
19 | 'MatcherTestCase',
20 | 'python_code'
21 | ]
22 |
23 |
24 | IS_PY34 = sys.version_info >= (3, 4)
25 | IS_PY35 = sys.version_info >= (3, 5)
26 |
27 |
28 | class TestCase(_TestCase):
29 | """Base class for all test cases."""
30 |
31 | def setUp(self):
32 | super(TestCase, self).setUpClass()
33 |
34 | # display full diffs when equality assertions fail under py.test
35 | self.maxDiff = None
36 |
37 |
38 | class MatcherTestCase(TestCase):
39 | """Base class for matcher test cases."""
40 |
41 | def assert_match(self, matcher, value):
42 | m = mock.Mock()
43 | m(value)
44 | m.assert_called_with(matcher)
45 | return True
46 |
47 | def assert_no_match(self, matcher, value):
48 | m = mock.Mock()
49 | m(value)
50 | with self.assertRaises(AssertionError):
51 | m.assert_called_with(matcher)
52 | return True
53 |
54 | def assert_repr(self, matcher, *args):
55 | """Assert on the representation of a matcher.
56 |
57 | :param matcher: Matcher object
58 |
59 | Positional arguments are expected to have their repr's be contained
60 | within the matcher's repr.
61 | """
62 | repr_ = "%r" % matcher
63 | for arg in args:
64 | arg_repr = "%r" % arg
65 | self.assertIn(
66 | arg_repr, repr_,
67 | msg="%s (repr of %s) didn't contain expected value %s" % (
68 | repr_, matcher.__class__.__name__, arg_repr))
69 |
70 | def await_(self, coroutine):
71 | """Run given asynchronous coroutine to completion.
72 | This prevents a warning from being emitted when it goes out of scope.
73 |
74 | :param coroutine: A coroutine or a coroutine function
75 | """
76 | self.assertIsNotNone(
77 | asyncio,
78 | msg="Tried to use asyncio on unsupported Python version")
79 |
80 | loop = asyncio.new_event_loop()
81 | if asyncio.iscoroutinefunction(coroutine):
82 | coroutine = coroutine(loop)
83 | loop.run_until_complete(coroutine)
84 | loop.close()
85 |
86 | return coroutine
87 |
88 |
89 | # Utility functions
90 |
91 | def python_code(source):
92 | """Format Python source code, given as a string, for use with ``exec``.
93 |
94 | Use it like so::
95 |
96 | exec(python_code('''
97 | async def foo():
98 | pass
99 | '''))
100 |
101 | This allows to use newer syntactic constructs that'd cause SyntaxError
102 | on older Python versions.
103 | """
104 | # (Whitespace shenanigans adapted from sphinx.utils.prepare_docstring)
105 |
106 | # remove excess whitespace from source code lines which will be there
107 | # if the code was given as indented, multiline string
108 | lines = source.expandtabs().splitlines()
109 | margin = sys.maxsize
110 | for line in lines:
111 | code_len = len(line.strip())
112 | if code_len > 0:
113 | indent = len(line) - code_len
114 | margin = min(margin, indent)
115 | if margin < sys.maxsize:
116 | for i in range(len(lines)):
117 | lines[i] = lines[i][margin:]
118 |
119 | # ensure there is an empty line at the end
120 | if lines and lines[-1]:
121 | lines.append('')
122 |
123 | return os.linesep.join(lines)
124 |
--------------------------------------------------------------------------------
/callee/attributes.py:
--------------------------------------------------------------------------------
1 | """
2 | Attribute-based matchers.
3 | """
4 | from itertools import starmap
5 | from operator import itemgetter
6 |
7 | from callee.base import BaseMatcher, Eq
8 |
9 |
10 | __all__ = [
11 | 'Attrs', 'Attr', 'HasAttrs', 'HasAttr',
12 | ]
13 |
14 |
15 | class Attrs(BaseMatcher):
16 | """Matches objects based on their attributes.
17 |
18 | To match successfully, the object needs to:
19 |
20 | * have all the attributes whose names were passed
21 | as positional arguments (regardless of their values)
22 | * have the attribute names/values that correspond exactly
23 | to keyword arguments' names and values
24 |
25 | Examples::
26 |
27 | Attrs('foo') # `foo` attribute with any value
28 | Attrs('foo', 'bar') # `foo` and `bar` attributes with any values
29 | Attrs(foo=42) # `foo` attribute with value of 42
30 | Attrs(bar=Integer()) # `bar` attribute whose value is an integer
31 | Attrs('foo', bar='x') # `foo` with any value, `bar` with value of 'x'
32 | """
33 | def __init__(self, *args, **kwargs):
34 | if not (args or kwargs):
35 | raise TypeError("%s() requires at least one argument" % (
36 | self.__class__.__name__,))
37 |
38 | self.attr_names = list(args)
39 | self.attr_dict = dict((k, v if isinstance(v, BaseMatcher) else Eq(v))
40 | for k, v in kwargs.items())
41 |
42 | def match(self, value):
43 | for name in self.attr_names:
44 | # Can't use hasattr() here because it swallows *all* exceptions
45 | # from attribute access in Python 2.x, not just AttributeError.
46 | # More details: https://hynek.me/articles/hasattr/
47 | try:
48 | getattr(value, name)
49 | except AttributeError:
50 | return False
51 |
52 | for name, matcher in self.attr_dict.items():
53 | # Separately handle retrieving of the attribute value,
54 | # so that any stray AttributeErrors from the matcher itself
55 | # are correctly propagated.
56 | try:
57 | attrvalue = getattr(value, name)
58 | except AttributeError:
59 | return False
60 | if not matcher.match(attrvalue):
61 | return False
62 |
63 | return True
64 |
65 | def __repr__(self):
66 | """Return a representation of the matcher."""
67 | # get both the names-only and valued attributes and sort them by name
68 | sentinel = object()
69 | attrs = [(name, sentinel)
70 | for name in self.attr_names] + list(self.attr_dict.items())
71 | attrs.sort(key=itemgetter(0))
72 |
73 | def attr_repr(name, value):
74 | # include the value with attribute name whenever necessary
75 | if value is sentinel:
76 | return name
77 | value = value.value if isinstance(value, Eq) else value
78 | return "%s=%r" % (name, value)
79 |
80 | return "<%s %s>" % (self.__class__.__name__,
81 | " ".join(starmap(attr_repr, attrs)))
82 |
83 |
84 | class Attr(Attrs):
85 | """Matches objects that have an attribute with given name and value,
86 | as given by a keyword argument.
87 | """
88 | def __init__(self, **kwargs):
89 | if not len(kwargs) == 1:
90 | raise TypeError("Attr() requires exactly one keyword argument")
91 | super(Attr, self).__init__(**kwargs)
92 |
93 |
94 | class HasAttrs(Attrs):
95 | """Matches objects that have all of the specified attribute names,
96 | regardless of their values.
97 | """
98 | def __init__(self, *args):
99 | super(HasAttrs, self).__init__(*args)
100 |
101 |
102 | class HasAttr(HasAttrs):
103 | """Matches object that have an attribute with given name,
104 | regardless of its value.
105 | """
106 | def __init__(self, name):
107 | super(HasAttr, self).__init__(name)
108 |
--------------------------------------------------------------------------------
/docs/guide/installing.rst:
--------------------------------------------------------------------------------
1 | Installing
2 | ==========
3 |
4 | **TL;DR**: On Python 2.6, 2.7, and 3.3+, simply use *pip* (preferably inside virtualenv):
5 |
6 | .. code-block:: shell
7 |
8 | $ pip install callee
9 |
10 | More detailed instructions and additional notes can be found below.
11 |
12 |
13 | Compatibility
14 | *************
15 |
16 | *callee* itself has no external depedencies: it only needs Python. Both Python 2 and Python 3 is supported,
17 | with some caveats:
18 |
19 | * if you're using Python 2, you need version 2.6 or 2.7
20 | * if you use Python 3, you need at least version 3.3
21 |
22 | The library is tested against both CPython (the standard Python implementation) and `PyPy`_.
23 |
24 | .. _PyPy: http://pypy.org/
25 |
26 |
27 | About the mock library
28 | **********************
29 |
30 | Although it's not a hard dependency, by design *callee* is meant to be used with the ``unittest.mock`` module,
31 | which implements *mock objects* for testing.
32 |
33 | In Python 3.3 and later, this module is a part of `the standard library`_, and it's already available on any Python distribution.
34 |
35 | In earlier versions of Python -- including 2.7 and even 2.6 -- you should be using the `backport`_ called ``mock``.
36 | It has the exact same interface as ``unittest.mock``, and can be used to write forward-compatible test code.
37 | You can install it from PyPI with *pip*:
38 |
39 | .. code-block:: shell
40 |
41 | $ pip install mock
42 |
43 | If you plan to run your tests against both Python 2.x and 3.x, the recommended way of importing the mock library
44 | is the following:
45 |
46 | .. code-block:: python
47 |
48 | try:
49 | import unittest.mock as mock
50 | except ImportError:
51 | import mock
52 |
53 | You can then use the mock classes in your tests by referring to them as |mock.Mock|_ or |mock.MagicMock|_.
54 | Additionally, you'll also have a convenient access to the rest of the mocking functionality, like the |@mock.patch|_
55 | decorator.
56 |
57 | .. _the standard library: https://docs.python.org/3/library/unittest.mock.html
58 | .. _backport: https://pypi.python.org/pypi/mock
59 |
60 | .. |mock.Mock| replace:: ``mock.Mock``
61 | .. _mock.Mock: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock
62 | .. |mock.MagicMock| replace:: ``mock.MagicMock``
63 | .. _mock.MagicMock: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock
64 | .. |@mock.patch| replace:: ``@mock.patch``
65 | .. _@mock.patch: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
66 |
67 |
68 | Instructions
69 | ************
70 |
71 | The preferred way to install *callee* is through *pip*:
72 |
73 | .. code-block:: shell
74 |
75 | $ pip install callee
76 |
77 | This will get you the most recent version available on `PyPI`_.
78 |
79 | .. _PyPI: https://pypi.python.org/pypi/callee/
80 |
81 | Bleeding edge
82 | -------------
83 |
84 | If you want to work with the development version instead, you may either manually clone it using Git, or have *pip*
85 | install it directly from the Git repository.
86 |
87 | The first option is especially useful when you need to make some modifications to the library itself
88 | (which you'll hopefully contribute back via a pull request!). If that's the case, clone the library
89 | and install it in development mode:
90 |
91 | .. code-block:: shell
92 |
93 | $ git clone https://github.com/Xion/callee.git
94 | Initialized empty Git repository in ~/dev/callee/.git/
95 | $ cd callee
96 | # activate/create your virtualenv if necessary
97 | $ python setup.py develop
98 | ...
99 | Finished processing dependencies for callee
100 |
101 | The second approach is adequate if you want to use some feature of the library that hasn't made it to a PyPI release yet
102 | but don't need to make your own modifications. You can tell *pip* to pull the library directly from its Git repository:
103 |
104 | .. code-block:: shell
105 |
106 | # activate/create your virtualenv if necessary
107 | $ pip install git+https://github.com/Xion/callee.git#egg=callee
108 |
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | """
2 | Automation tasks, aided by the Invoke package.
3 | """
4 | import logging
5 | import os
6 | import webbrowser
7 | import sys
8 |
9 | from invoke import task
10 | from invoke.exceptions import Exit
11 | from invoke.runners import Result
12 |
13 | try:
14 | input = raw_input
15 | except NameError:
16 | pass # Python 3, input() already works like raw_input() in 2.x.
17 |
18 |
19 | DOCS_DIR = 'docs'
20 | DOCS_OUTPUT_DIR = os.path.join(DOCS_DIR, '_build')
21 |
22 |
23 | @task(default=True, help={
24 | 'all': "Whether to run the tests on all environments (using tox)",
25 | 'verbose': "Whether to enable verbose output",
26 | })
27 | def test(ctx, all=False, verbose=False):
28 | """Run the tests."""
29 | cmd = 'tox' if all else 'py.test'
30 | if verbose:
31 | cmd += ' -v'
32 | return ctx.run(cmd, pty=True).return_code
33 |
34 |
35 | @task
36 | def lint(ctx):
37 | """Run the linter."""
38 | return ctx.run('flake8 callee tests', pty=True).return_code
39 |
40 |
41 | @task(help={
42 | 'output': "Documentation output format to produce",
43 | 'rebuild': "Whether to rebuild the documentation from scratch",
44 | 'show': "Whether to show the docs in the browser (default: yes)",
45 | 'verbose': "Whether to enable verbose output",
46 | })
47 | def docs(ctx, output='html', rebuild=False, show=True, verbose=True):
48 | """Build the docs and show them in default web browser."""
49 | sphinx_build = ctx.run(
50 | 'sphinx-build -b {output} {all} {verbose} docs docs/_build'.format(
51 | output=output,
52 | all='-a -E' if rebuild else '',
53 | verbose='-v' if verbose else ''))
54 | if not sphinx_build.ok:
55 | fatal("Failed to build the docs", cause=sphinx_build)
56 |
57 | if show:
58 | path = os.path.join(DOCS_OUTPUT_DIR, 'index.html')
59 | if sys.platform == 'darwin':
60 | path = 'file://%s' % os.path.abspath(path)
61 | webbrowser.open_new_tab(path)
62 |
63 |
64 | # TODO: remove this when PyPI upload from Travis CI is verified to work
65 | @task(help={
66 | 'yes': "Whether to actually perform the upload. "
67 | "By default, a confirmation is necessary.",
68 | })
69 | def upload(ctx, yes=False):
70 | """Upload the package to PyPI.
71 |
72 | This is done by adding a Git tag for the current non-dev version
73 | and pushing to GitHub, which triggers the Travis build & deploy pipeline.
74 | """
75 | import callee
76 | version = callee.__version__
77 |
78 | # check the packages version
79 | # TODO: add a 'release' to automatically bless a version as release one
80 | if version.endswith('-dev'):
81 | fatal("Can't upload a development version (%s) to PyPI!", version)
82 |
83 | # run the upload if it has been confirmed by the user
84 | if not yes:
85 | answer = input("Do you really want to upload to PyPI [y/N]? ")
86 | yes = answer.strip().lower() == 'y'
87 | if not yes:
88 | logging.warning("Aborted -- not uploading to PyPI.")
89 | return -2
90 |
91 | # add a Git tag and push
92 | logging.debug("Tagging current HEAD as %s..." % version)
93 | git_tag = ctx.run('git tag %s' % version)
94 | if not git_tag.ok:
95 | fatal("Failed to add a Git tag for uploaded version %s", version,
96 | cause=git_tag)
97 | git_push = ctx.run('git push && git push --tags')
98 | if not git_push.ok:
99 | fatal("Failed to push the release upstream.", cause=git_push)
100 | logging.info("New version (%s) successfully pushed." % version)
101 |
102 |
103 | # Utility functions
104 |
105 | def fatal(*args, **kwargs):
106 | """Log an error message and exit.
107 |
108 | Following arguments are keyword-only.
109 |
110 | :param exitcode: Optional exit code to use
111 | :param cause: Optional Invoke's Result object, i.e.
112 | result of a subprocess invocation
113 | """
114 | # determine the exitcode to return to the operating system
115 | exitcode = None
116 | if 'exitcode' in kwargs:
117 | exitcode = kwargs.pop('exitcode')
118 | if 'cause' in kwargs:
119 | cause = kwargs.pop('cause')
120 | if not isinstance(cause, Result):
121 | raise TypeError(
122 | "invalid cause of fatal error: expected %r, got %r" % (
123 | Result, type(cause)))
124 | exitcode = exitcode or cause.return_code
125 |
126 | logging.error(*args, **kwargs)
127 | raise Exit(exitcode or -1)
128 |
--------------------------------------------------------------------------------
/tests/test_attributes.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for attribute-based matchers.
3 | """
4 | import callee.attributes as __unit__
5 | from tests import MatcherTestCase
6 |
7 |
8 | class Attrs(MatcherTestCase):
9 | VALUE = 42
10 |
11 | def test_match__invalid(self):
12 | with self.assertRaises(TypeError):
13 | self.assert_match('unused')
14 |
15 | def test_match__names_only__one(self):
16 | foo = self.Object(foo=self.VALUE)
17 | self.assert_match(foo, 'foo')
18 | self.assert_no_match(foo, 'bar')
19 |
20 | def test_match__names_only__several(self):
21 | foo_bar = self.Object(foo=self.VALUE, bar=self.VALUE * 2)
22 |
23 | self.assert_match(foo_bar, 'foo')
24 | self.assert_match(foo_bar, 'bar')
25 | self.assert_match(foo_bar, 'foo', 'bar')
26 |
27 | self.assert_no_match(foo_bar, 'baz')
28 | self.assert_no_match(foo_bar, 'foo', 'baz')
29 |
30 | def test_match__values_only__one(self):
31 | foo = self.Object(foo=self.VALUE)
32 |
33 | self.assert_match(foo, foo=self.VALUE)
34 |
35 | self.assert_no_match(foo, foo=self.VALUE + 1)
36 | self.assert_no_match(foo, bar=self.VALUE)
37 |
38 | def test_match__values_only__several(self):
39 | foo_bar = self.Object(foo=self.VALUE, bar=self.VALUE * 2)
40 |
41 | self.assert_match(foo_bar, foo=self.VALUE)
42 | self.assert_match(foo_bar, bar=self.VALUE * 2)
43 | self.assert_match(foo_bar, foo=self.VALUE, bar=self.VALUE * 2)
44 |
45 | self.assert_no_match(foo_bar, foo=self.VALUE + 1, bar=self.VALUE)
46 | self.assert_no_match(foo_bar, foo=self.VALUE, baz=self.VALUE * 2)
47 |
48 | def test_match__both__one_each(self):
49 | foo_bar = self.Object(foo=self.VALUE, bar=self.VALUE * 2)
50 |
51 | self.assert_match(foo_bar, 'foo', bar=self.VALUE * 2)
52 | self.assert_match(foo_bar, 'bar', foo=self.VALUE)
53 |
54 | self.assert_no_match(foo_bar, 'foo', baz=self.VALUE * 2)
55 | self.assert_no_match(foo_bar, 'baz', foo=self.VALUE)
56 |
57 | def test_match__both__mix(self):
58 | attrs = dict(foo=self.VALUE,
59 | bar=self.VALUE * 2,
60 | baz=self.VALUE * 2 + 1)
61 | foo_bar_baz = self.Object(**attrs)
62 |
63 | self.assert_match(foo_bar_baz, 'bar', 'baz', foo=self.VALUE)
64 | self.assert_match(foo_bar_baz, 'baz',
65 | foo=self.VALUE, bar=self.VALUE * 2)
66 | self.assert_match(foo_bar_baz, **attrs)
67 |
68 | self.assert_no_match(foo_bar_baz, 'qux', **attrs)
69 | self.assert_no_match(foo_bar_baz, 'foo', 'bar', baz='wrong value')
70 |
71 | def test_repr__names_only__one(self):
72 | attrs = __unit__.Attrs('foo')
73 |
74 | r = "%r" % attrs
75 | self.assertIn("foo", r)
76 | self.assertNotIn("=", r) # because no value was given
77 |
78 | def test_repr__names_only__several(self):
79 | attrs = __unit__.Attrs('foo', 'bar')
80 |
81 | r = "%r" % attrs
82 | self.assertIn("foo", r)
83 | self.assertIn("bar", r)
84 | self.assertNotIn("=", r)
85 |
86 | def test_repr__values_only__one(self):
87 | attrs = __unit__.Attrs(foo=self.VALUE)
88 |
89 | r = "%r" % attrs
90 | self.assertIn("foo=", r)
91 | self.assertIn("%r" % self.VALUE, r)
92 |
93 | def test_repr__values_only__several(self):
94 | attrs = __unit__.Attrs(foo=self.VALUE, bar=self.VALUE * 2)
95 |
96 | r = "%r" % attrs
97 | self.assertIn("foo=", r)
98 | self.assertIn("bar=", r)
99 | self.assertIn("%r" % self.VALUE, r)
100 |
101 | def test_repr__both__one_each(self):
102 | attrs = __unit__.Attrs('bar', foo=self.VALUE)
103 |
104 | r = "%r" % attrs
105 | self.assertIn("bar", r)
106 | self.assertNotIn("bar=", r) # because this one doesn't have a value
107 | self.assertIn("foo=", r)
108 | self.assertIn("%r" % self.VALUE, r)
109 |
110 | def test_repr__both__mix(self):
111 | attrs = __unit__.Attrs('bar', 'baz', foo=self.VALUE)
112 |
113 | r = "%r" % attrs
114 | self.assertIn("bar", r)
115 | self.assertNotIn("bar=", r)
116 | self.assertIn("baz", r)
117 | self.assertNotIn("baz=", r)
118 | self.assertIn("%r" % self.VALUE, r)
119 |
120 | # Utility code
121 |
122 | class Object(object):
123 | def __init__(self, **attrs):
124 | for name, value in attrs.items():
125 | setattr(self, name, value)
126 |
127 | def assert_match(self, value, *args, **kwargs):
128 | return super(Attrs, self) \
129 | .assert_match(__unit__.Attrs(*args, **kwargs), value)
130 |
131 | def assert_no_match(self, value, *args, **kwargs):
132 | return super(Attrs, self) \
133 | .assert_no_match(__unit__.Attrs(*args, **kwargs), value)
134 |
--------------------------------------------------------------------------------
/callee/strings.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers for strings.
3 | """
4 | import fnmatch
5 | import re
6 |
7 | from callee._compat import IS_PY3, casefold
8 | from callee.base import BaseMatcher
9 | from callee.objects import Bytes
10 |
11 |
12 | __all__ = [
13 | 'Bytes', # backwards compatibility
14 | 'String', 'Unicode',
15 | 'StartsWith', 'EndsWith',
16 | 'Glob', 'Regex',
17 | ]
18 |
19 |
20 | # String type matchers
21 |
22 | class StringTypeMatcher(BaseMatcher):
23 | """Matches some string type.
24 | This class shouldn't be used directly.
25 | """
26 | #: String class to match.
27 | #: Must be overridden in subclasses.
28 | CLASS = None
29 |
30 | # TODO: support of= param, so we can assert what characters
31 | # the string consists of (e.g. letters, digits as iterables of chars;
32 | # boolean predicate; or matcher)
33 | def __init__(self):
34 | assert self.CLASS, "must specify string type to match"
35 |
36 | def match(self, value):
37 | return isinstance(value, self.CLASS)
38 |
39 | def __repr__(self):
40 | return "<%s>" % (self.__class__.__name__,)
41 |
42 |
43 | class String(StringTypeMatcher):
44 | """Matches any string.
45 |
46 | | On Python 2, this means either :class:`str` or :class:`unicode` objects.
47 | | On Python 3, this means :class:`str` objects exclusively.
48 | """
49 | CLASS = str if IS_PY3 else basestring
50 |
51 |
52 | class Unicode(StringTypeMatcher):
53 | """Matches a Unicode string.
54 |
55 | | On Python 2, this means :class:`unicode` objects exclusively.
56 | | On Python 3, this means :class:`str` objects exclusively.
57 | """
58 | CLASS = str if IS_PY3 else unicode
59 |
60 |
61 | # Infix matchers
62 |
63 | # TODO: generalize for all sequence/collection types
64 |
65 | class StartsWith(BaseMatcher):
66 | """Matches a string starting with given prefix."""
67 |
68 | def __init__(self, prefix):
69 | self.prefix = prefix
70 |
71 | def match(self, value):
72 | return value.startswith(self.prefix)
73 |
74 | def __repr__(self):
75 | return "" % (self.prefix,)
76 |
77 |
78 | class EndsWith(BaseMatcher):
79 | """Matches a string ending with given suffix."""
80 |
81 | def __init__(self, suffix):
82 | self.suffix = suffix
83 |
84 | def match(self, value):
85 | return value.endswith(self.suffix)
86 |
87 | def __repr__(self):
88 | return "" % (self.suffix,)
89 |
90 |
91 | # Pattern matchers
92 |
93 | class Glob(BaseMatcher):
94 | """Matches a string against a Unix shell wildcard pattern.
95 |
96 | See the :mod:`fnmatch` module for more details about those patterns.
97 | """
98 | DEFAULT_CASE = 'system'
99 |
100 | #: fnmatch functions that the matchers uses based on case= argument.
101 | FNMATCH_FUNCTIONS = {
102 | DEFAULT_CASE: fnmatch.fnmatch,
103 | True: fnmatch.fnmatchcase,
104 | False: lambda f, p: fnmatch.fnmatchcase(casefold(f), casefold(p)),
105 | }
106 |
107 | def __init__(self, pattern, case=None):
108 | """
109 | :param pattern: Pattern to match against
110 | :param case:
111 |
112 | Case sensitivity setting. Possible options:
113 |
114 | * ``'system'`` or ``None``: case sensitvity is system-dependent
115 | (this is the default)
116 | * ``True``: matching is case-sensitive
117 | * ``False``: matching is case-insensitive
118 | """
119 | self.pattern = pattern
120 | try:
121 | if case is None:
122 | case = self.DEFAULT_CASE
123 | self.fnmatch = self.FNMATCH_FUNCTIONS[case]
124 | except KeyError:
125 | raise ValueError("invalid case= argument: %r" % (case,))
126 |
127 | def match(self, value):
128 | return self.fnmatch(value, self.pattern)
129 |
130 | def __repr__(self):
131 | return "" % (self.pattern,)
132 |
133 |
134 | class Regex(BaseMatcher):
135 | """Matches a string against a regular expression."""
136 |
137 | REGEX_TYPE = type(re.compile(''))
138 |
139 | def __init__(self, pattern, flags=0):
140 | """
141 | :param pattern: Regular expression to match against.
142 | It can be given as string,
143 | or as a compiled regular expression object
144 | :param flags: Flags to use with a regular expression passed as string
145 | """
146 | if self._is_regex_object(pattern):
147 | if flags and flags != pattern.flags:
148 | raise ValueError("conflicting regex flags: %s vs. %s" % (
149 | bin(flags), bin(pattern.flags)))
150 | else:
151 | pattern = re.compile(pattern, flags)
152 |
153 | self.pattern = pattern
154 |
155 | def _is_regex_object(self, obj):
156 | return isinstance(obj, self.REGEX_TYPE)
157 |
158 | def match(self, value):
159 | return self.pattern.match(value)
160 |
161 | def __repr__(self):
162 | return "" % (self.pattern.pattern,)
163 |
164 |
165 | # TODO: matchers for common string formats: Url, Email, IPv4, IPv6
166 |
--------------------------------------------------------------------------------
/docs/guide/usage.rst:
--------------------------------------------------------------------------------
1 | Using matchers with ``mock``
2 | ============================
3 |
4 | *Mocks* -- or more generally, *test doubles* -- are used to provide the necessary dependencies
5 | (objects, functions, data, etc.) to the code under test. We often configure mocks to expose an interface that the code can rely on. We also expect it to make use of this interface in a well-defined, predictable way.
6 |
7 | In Python, the configuration part mostly taken care of by the ``mock`` library. But when it comes to asserting
8 | that the expected mocks interactions had happened, *callee* can help quite a bit.
9 |
10 |
11 | Example
12 | *******
13 |
14 | Suppose you are testing the controller of a landing page for users that are signed in to your web application.
15 | The page should display a portion of the most recent items of interest -- posts, notifications, or anything else
16 | specific to the service.
17 |
18 | The test seems straightforward enough:
19 |
20 | .. code-block:: python
21 |
22 | @mock.patch.object(database, 'fetch_recent_items')
23 | def test_landing_page(self, mock_fetch_recent_items):
24 | login_user(self.user)
25 | self.http_client.get('/')
26 | mock_fetch_recent_items.assert_called_with(count=8)
27 |
28 | Unfortunately, the assert it contains turns out to be quite brittle. If you think about it, the number of items
29 | to display is very much a UX decision, and it likely changes pretty often as the UI is iterated upon.
30 | But with a test like that, you have to go back and modify the assertion whenever the value is adjusted
31 | in the production code.
32 |
33 | Not good! The test shouldn't really care what the exact count is. As long as it's a positive integer,
34 | maybe except 1 or 2, the test should pass just fine.
35 |
36 | Using **argument matchers** provided by *callee*, you can express this intent clearly and concisely:
37 |
38 | .. code-block:: python
39 |
40 | from callee import GreaterThan, Integer
41 |
42 | # ...
43 | mock_fetch_recent_items.assert_called_with(
44 | count=Integer() & GreaterThan(1))
45 |
46 | Much better! Now you can tweak the layout of the page without further issue.
47 |
48 |
49 | Matching basics
50 | ***************
51 |
52 | You can use all *callee* matchers any time you are asserting on calls received by ``Mock``, ``MagicMock``,
53 | or other ``mock`` objects. They are applicable as arguments to any of the following methods:
54 |
55 | * ``assert_called_with``
56 | * ``assert_called_once_with``
57 | * ``assert_any_call``
58 | * ``assert_not_called``
59 |
60 | Moreover, the |mock.call|_ helper can also accept matchers in place of call arguments. This enables you to also use
61 | the |assert_has_calls|_ method if you like:
62 |
63 | .. code-block:: python
64 |
65 | some_mock.assert_has_calls([
66 | call(0, String()),
67 | call(1, String()),
68 | call(2, String()),
69 | ])
70 |
71 | Finally, you can still leverage matchers even when you're working directly with the ``call_args_list``, ``method_calls``,
72 | or ``mock_calls`` attributes. The only reason you'd still want that, though, is verifying the **exact** calls a mock receives, in order:
73 |
74 | .. code-block:: python
75 |
76 | assert some_mock.call_args_list == [
77 | mock.call(String(), Integer()),
78 | mock.call(String(), 42),
79 | ]
80 |
81 | But most tests don't need to be this rigid, so remember to use this technique sparingly.
82 |
83 | .. |mock.call| replace:: ``mock.call``
84 | .. _mock.call: https://docs.python.org/3/library/unittest.mock.html#calls-as-tuples
85 | .. |assert_has_calls| replace:: ``assert_has_calls``
86 | .. _assert_has_calls: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls
87 |
88 |
89 | Combining matchers
90 | ******************
91 |
92 | .. _logical-expressions:
93 |
94 | Individual matchers, such as :class:`String ` or :class:`Float `,
95 | can be combined to build more complex expressions. This is accomplished with Python's "logical" operators:
96 | ``|``, ``&``, and ``~``.
97 |
98 | Specifically, given matchers ``A`` and ``B``:
99 |
100 | * ``A | B`` matches objects that match ``A`` **or** ``B``
101 | * ``A & B`` matches objects that match both ``A`` **and** ``B``
102 | * ``~A`` matches objects do **not** match ``A``
103 |
104 | Here's a few examples:
105 |
106 | .. code-block:: python
107 |
108 | some_mock.assert_called_with(Number() | InstanceOf(Foo))
109 | some_mock.assert_called_with(String() & ShorterThan(9))
110 | some_mock.assert_called_with(String() & ~Contains('forbidden'))
111 |
112 | All matchers can be combined this way, including any :doc:`custom ones ` that you write yourself.
113 |
114 | .. TODO: mention the existence of And, Or, Not classes
115 |
116 |
117 | Next steps
118 | **********
119 |
120 | Now that you know how to use matchers and how to combine them into more complex expressions, you probably want to
121 | have a look at the wide array of existing matchers offerred by *callee*:
122 |
123 | .. include:: ../reference/toc.rst.inc
124 |
125 | If your needs can't be met by it, there is always a possibility of :doc:`defining your own matchers `
126 | as well.
127 |
--------------------------------------------------------------------------------
/callee/objects.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers for various common kinds of objects.
3 | """
4 | import inspect
5 | import sys
6 |
7 | from callee._compat import asyncio, getargspec
8 | from callee.base import BaseMatcher
9 |
10 |
11 | __all__ = ['Bytes', 'Coroutine', 'FileLike']
12 |
13 |
14 | class ObjectMatcher(BaseMatcher):
15 | """Base class for object matchers.
16 | This class shouldn't be used directly.
17 | """
18 | def __repr__(self):
19 | return "<%s>" % (self.__class__.__name__,)
20 |
21 |
22 | # TODO: Date, DateTime, and Time matchers (with before=/after= params)
23 | # TODO: TimeDelta matcher
24 |
25 |
26 | class Bytes(ObjectMatcher):
27 | """Matches a byte array, i.e. the :class:`bytes` type.
28 |
29 | | On Python 2, :class:`bytes` class is identical to :class:`str` class.
30 | | On Python 3, byte strings are separate class, distinct from :class:`str`.
31 | """
32 | def match(self, value):
33 | return isinstance(value, bytes)
34 |
35 |
36 | class Coroutine(ObjectMatcher):
37 | """Matches an asynchronous coroutine.
38 |
39 | A coroutine is a result of an asynchronous function call, where the async
40 | function has been defined using ``@asyncio.coroutine`` or the ``async def``
41 | syntax.
42 |
43 | These are only available in Python 3.4 and above.
44 | On previous versions of Python, no object will match this matcher.
45 | """
46 | def match(self, value):
47 | return asyncio and asyncio.iscoroutine(value)
48 |
49 |
50 | class FileLike(ObjectMatcher):
51 | """Matches a file-like object.
52 |
53 | In general, a `file-like object` is an object you can ``read`` data from,
54 | or ``write`` data to.
55 | """
56 | def __init__(self, read=True, write=None):
57 | """
58 | :param read:
59 |
60 | Whether only to match objects that do support (``True``)
61 | or don't support (``False``) reading from them.
62 | If ``None`` is passed, reading capability is not matched against.
63 |
64 | :param write:
65 |
66 | Whether only to match objects that do support (``True``)
67 | or don't support (``False``) writing to them.
68 | If ``None`` is passed, writing capability is not matched against.
69 | """
70 | if read is None and write is None:
71 | raise ValueError("cannot match file-like objects "
72 | "that are neither readable nor writable")
73 | self.read = read if read is None else bool(read)
74 | self.write = write if write is None else bool(write)
75 |
76 | def match(self, value):
77 | if self.read is not None:
78 | if self.read != self._is_readable(value):
79 | return False
80 | if self.write is not None:
81 | if self.write != self._is_writable(value):
82 | return False
83 | return True
84 |
85 | def _is_readable(self, obj):
86 | """Check if the argument is a readable file-like object."""
87 | try:
88 | read = getattr(obj, 'read')
89 | except AttributeError:
90 | return False
91 | else:
92 | return is_method(read, max_arity=1)
93 |
94 | def _is_writable(self, obj):
95 | """Check if the argument is a writable file-like object."""
96 | try:
97 | write = getattr(obj, 'write')
98 | except AttributeError:
99 | return False
100 | else:
101 | return is_method(write, min_arity=1, max_arity=1)
102 |
103 | def __repr__(self):
104 | """Return a representation of this matcher."""
105 | requirements = []
106 | if self.read is not None:
107 | requirements.append("read" if self.read else "noread")
108 | if self.write is not None:
109 | requirements.append("write" if self.write else "nowrite")
110 | return "" % "(%s)" % ",".join(requirements)
111 |
112 |
113 | # Utility functions
114 |
115 | def is_method(arg, min_arity=None, max_arity=None):
116 | """Check if argument is a method.
117 |
118 | Optionally, we can also check if minimum or maximum arities
119 | (number of accepted arguments) match given minimum and/or maximum.
120 | """
121 | if not callable(arg):
122 | return False
123 |
124 | if not any(is_(arg) for is_ in (inspect.ismethod,
125 | inspect.ismethoddescriptor,
126 | inspect.isbuiltin)):
127 | return False
128 |
129 | try:
130 | argnames, varargs, kwargs, defaults = getargspec(arg)
131 | except TypeError:
132 | # On CPython 2.x, built-in methods of file aren't inspectable,
133 | # so if it's file.read() or file.write(), we can't tell it for sure.
134 | # Given how this check is being used, assuming the best is probably
135 | # all we can do here.
136 | return True
137 | else:
138 | if argnames and argnames[0] == 'self':
139 | argnames = argnames[1:]
140 |
141 | if min_arity is not None:
142 | actual_min_arity = len(argnames) - len(defaults or ())
143 | assert actual_min_arity >= 0, (
144 | "Minimum arity of %r found to be negative (got %s)!" % (
145 | arg, actual_min_arity))
146 | if int(min_arity) != actual_min_arity:
147 | return False
148 |
149 | if max_arity is not None:
150 | actual_max_arity = sys.maxsize if varargs or kwargs else len(argnames)
151 | if int(max_arity) != actual_max_arity:
152 | return False
153 |
154 | return True
155 |
--------------------------------------------------------------------------------
/docs/guide/custom-matchers.rst:
--------------------------------------------------------------------------------
1 | Creating custom matchers
2 | ========================
3 |
4 | The wide assortment of predefined matchers should be sufficient for a vast majority of your use cases.
5 |
6 | But when they're not, don't worry. *callee* enables you to create your own, custom matchers quickly and succinctly.
7 | Those new matchers will be as capable as the standard ones, too, meaning you can use them in
8 | :ref:`logical expressions `, or with collection matchers such as :class:`~callee.collections.List`.
9 |
10 | Here you can learn about all the possible ways of creating matchers with custom logic.
11 |
12 |
13 | Predicates
14 | **********
15 |
16 | The simplest technique is based on (re)using a *predicate* -- that is, a function that returns a boolean result
17 | (``True`` or ``False``). This is handy when you already have a piece of code that recognizes objects you want to match.
18 |
19 | Suppose you have this function:
20 |
21 | .. code-block:: python
22 |
23 | def is_even(x):
24 | return x % 2 == 0
25 |
26 | In order to turn it into an ad-hoc matcher, all need to do is wrap it in a :class:`Matching` object:
27 |
28 | .. code-block:: python
29 |
30 | mock_compute_half.assert_called_with(Matching(is_even))
31 |
32 | :class:`Matching` (also aliased as :class:`ArgThat`) accepts any callable that takes a single argument -- the object to
33 | match -- and interprets its result as a boolean value.
34 |
35 | As you may expect, returning ``True`` (or any Python "truthy" object) means that given argument matches the criteria.
36 | Otherwise, the match is considered unsuccessful.
37 | (If the function raises an exception, this is also interpreted as a failed match).
38 |
39 | Since it's valid to pass any Python callable to :class:`Matching`/:class:`ArgThat`, you can do basically anything there:
40 |
41 | .. code-block:: python
42 |
43 | Matching(lambda x: x % 2 == 0) # like above
44 | ArgThat(is_prime) # defined elsewhere
45 | Matching(bool) # matches any "truthy" value
46 |
47 | For clearer code, however, you should strive to keep the predicates short and simple. Rather than writing a complicated
48 | ``lambda`` expression, for example, try to break it down and combine :class:`Matching`/:class:`ArgThat` with the built-in
49 | matchers.
50 |
51 | If that proves difficult, it's probably time to consider a custom matcher **class** instead.
52 |
53 |
54 | Matcher classes
55 | ***************
56 |
57 | Ad-hoc matchers created with :class:`Matching`/:class:`ArgThat` are handy for some quick checks, but they have
58 | certain limitations:
59 |
60 | * They cannot accept parameters that modify their behavior (unless you parametrize the callable itself,
61 | which is clever but somewhat tricky and therefore not recommended).
62 | * The error messages they produce are not very informative, which makes it harder to debug and fix tests
63 | that use them.
64 |
65 | These constraints are outgrown quickly when you use the same ad-hoc matcher more than once or twice.
66 |
67 | Subclassing ``Matcher``
68 | -----------------------
69 |
70 | The canonical way of creating a custom matcher type is to inherit from the :class:`~callee.base.Matcher` base class.
71 |
72 | The only method you need to override there is ``match``. It shall take a single argument -- the ``value`` to test --
73 | and return a boolean result:
74 |
75 | .. code-block:: python
76 |
77 | class Even(Matcher):
78 | def match(self, value):
79 | return value % 2 == 0
80 |
81 | The new matcher is immediately usable in assertions:
82 |
83 | .. code-block:: python
84 |
85 | mock_compute_half.assert_called_with(Even())
86 |
87 | or in any other context you'd normally use a matcher in.
88 |
89 | Parametrized matchers
90 | ---------------------
91 |
92 | Because matchers deriving from the :class:`Matcher` class are normal Python objects, their construction
93 | can be parametrized to provide additional flexibility.
94 |
95 | The easiest and most common way is simply to save the arguments of ``__init__`` as attributes on the object,
96 | so that the ``match`` method can access them as needed:
97 |
98 | .. code-block:: python
99 |
100 | class Divisible(Matcher):
101 | """Matches a value that has given divisor."""
102 |
103 | def __init__(self, by):
104 | self.divisor = by
105 |
106 | def match(self, value):
107 | return value % self.divisor == 0
108 |
109 | Usage of such a matcher is rather straightforward:
110 |
111 | .. code-block:: python
112 |
113 | mock_compute_half.assert_called_with(Divisible(by=2))
114 |
115 | Overriding ``__repr__``
116 | -----------------------
117 |
118 | Custom matchers written as classes have one more advantage over ad-hoc ones. It is possible to redefine their
119 | ``__repr__`` method, allowing for more informative error messages on failed assertions.
120 |
121 | As an example, it would be good if ``Divisible`` matcher the from previous section told us what number it expected
122 | for the argument to be divisible by. This is easy enough to add:
123 |
124 | .. code-block:: python
125 |
126 | def __repr__(self):
127 | return "" % (self.divisor,)
128 |
129 | and makes relevant ``AssertionError``\ s more readable:
130 |
131 | .. code-block:: python
132 |
133 | >>> mock_compute_half(3)
134 | >>> mock_compute_half.assert_called_with(Divisible(by=2))
135 | ...
136 | AssertionError: Expected call: mock()
137 | Actual call: mock(3)
138 |
139 | In general, all parametrized matchers should probably override ``__repr__`` to show, at a glance, what parameters
140 | they were instantiated with.
141 |
142 | .. note::
143 |
144 | The convention to surround matcher representations in angle brackets (``<...>``) is followed by
145 | all built-in matchers in *callee*, because it makes it easier to tell them apart from literal values.
146 | Adopting it for your own matches is therefore recommended.
147 |
148 |
149 | Best practices
150 | **************
151 |
152 | Ad-hoc matchers (those created with :class:`Matching`/:class:`ArgThat`) are best used judiciously. Ideally,
153 | you would want to involve them only if:
154 |
155 | * you already have a predicate you can use, or you can define one easily as a ``lambda``
156 | * your test is very short, so that it's easy to debug when it breaks
157 |
158 | As a rule of thumb, whenever you define a function solely to use it with :class:`Matching`/:class:`ArgThat`,
159 | you should strongly consider creating a :class:`Matcher` subclass instead.
160 | There is almost no additional boilerplate involved, and the resulting matcher will be more reusable and easier to extend.
161 |
162 | Plus, if the new matcher turns up to be useful in multiple tests or projects, it can be added to *callee* itself!
163 |
--------------------------------------------------------------------------------
/callee/general.py:
--------------------------------------------------------------------------------
1 | """
2 | General matchers.
3 |
4 | These don't belong to any broader category, and include matchers for common
5 | Python objects, like functions or classes.
6 | """
7 | import inspect
8 |
9 | from callee._compat import IS_PY3, STRING_TYPES
10 | from callee.base import BaseMatcher
11 |
12 |
13 | __all__ = [
14 | 'Any', 'Matching', 'ArgThat', 'Captor',
15 | ]
16 |
17 |
18 | # TODO: introduce custom exception types rather than using built-ins
19 |
20 | # TODO: matchers for positional & keyword arguments,
21 | # e.g. *Args(Integer(), min=1, max=10), **Kwargs(foo=String(), bar=Float())
22 |
23 |
24 | class Any(BaseMatcher):
25 | """Matches any object."""
26 |
27 | def match(self, value):
28 | return True
29 |
30 | def __repr__(self):
31 | return ""
32 |
33 |
34 | class Matching(BaseMatcher):
35 | """Matches an object that satisfies given predicate."""
36 |
37 | MAX_DESC_LENGTH = 32
38 |
39 | def __init__(self, predicate, desc=None):
40 | """
41 | :param predicate: Callable taking a single argument
42 | and returning True or False
43 | :param desc: Optional description of the predicate.
44 | This will be displayed as a part of the error message
45 | on failed assertion.
46 | """
47 | if not callable(predicate):
48 | raise TypeError(
49 | "Matching requires a predicate, got %r" % (predicate,))
50 |
51 | self.predicate = predicate
52 | self.desc = self._validate_desc(desc)
53 |
54 | def _validate_desc(self, desc):
55 | """Validate the predicate description."""
56 | if desc is None:
57 | return desc
58 |
59 | if not isinstance(desc, STRING_TYPES):
60 | raise TypeError(
61 | "predicate description for Matching must be a string, "
62 | "got %r" % (type(desc),))
63 |
64 | # Python 2 mandates __repr__ to be an ASCII string,
65 | # so if Unicode is passed (usually due to unicode_literals),
66 | # it should be ASCII-encodable.
67 | if not IS_PY3 and isinstance(desc, unicode):
68 | try:
69 | desc = desc.encode('ascii', errors='strict')
70 | except UnicodeEncodeError:
71 | raise TypeError("predicate description must be "
72 | "an ASCII string in Python 2")
73 |
74 | return desc
75 |
76 | def match(self, value):
77 | # Note that any possible exceptions from ``predicate``
78 | # are intentionally let through, to make it easier to diagnose errors
79 | # than a plain "no match" response would.
80 | return bool(self.predicate(value))
81 | # TODO: translate exceptions from the predicate into our own
82 | # exception type to not clutter user-visible stracktraces with our code
83 |
84 | def __repr__(self):
85 | """Return a representation of the matcher."""
86 | name = getattr(self.predicate, '__name__', None)
87 | desc = self.desc
88 |
89 | # When no user-provided description is available,
90 | # use function's own name or even its repr().
91 | if desc is None:
92 | # If not a lambda function, we can probably make the representation
93 | # more readable by showing just the function's own name.
94 | if name and name != '':
95 | # Where possible, make it a fully qualified name, including
96 | # the module path. This is either on Python 3.3+
97 | # (via __qualname__), or when the predicate is
98 | # a standalone function (not a method).
99 | qualname = getattr(self.predicate, '__qualname__', name)
100 | is_method = inspect.ismethod(self.predicate) or \
101 | isinstance(self.predicate, staticmethod)
102 | if qualname != name or not is_method:
103 | # Note that this shows inner functions (those defined
104 | # locally inside other functions) as if they were global
105 | # to the module.
106 | # This is why we use colon (:) as separator here, as to not
107 | # suggest this is an evaluatable identifier.
108 | name = '%s:%s' % (self.predicate.__module__, qualname)
109 | else:
110 | # For lambdas and other callable objects,
111 | # we'll just default to the Python repr().
112 | name = None
113 | else:
114 | # Quote and possibly ellipsize the provided description.
115 | if len(desc) > self.MAX_DESC_LENGTH:
116 | ellipsis = '...'
117 | desc = desc[:self.MAX_DESC_LENGTH - len(ellipsis)] + ellipsis
118 | desc = '"%s"' % desc
119 |
120 | return "" % (desc or name or repr(self.predicate))
121 |
122 | ArgThat = Matching
123 |
124 |
125 | class Captor(BaseMatcher):
126 | """Argument captor.
127 |
128 | You can use :class:`Captor` to "capture" the original argument
129 | that the mock was called with, and perform custom assertions on it.
130 |
131 | Example::
132 |
133 | captor = Captor()
134 | mock_foo.assert_called_with(captor)
135 |
136 | # captured value is available as the `arg` attribute
137 | self.assertEquals(captor.arg.some_method(), 42)
138 | self.assertEquals(captor.arg.some_other_method(), "foo")
139 |
140 | .. versionadded:: 0.2
141 | """
142 | __slots__ = ('matcher', 'value')
143 |
144 | def __init__(self, matcher=None):
145 | """
146 | :param matcher: Optional matcher to validate the argument against
147 | before it's captured
148 | """
149 | if matcher is None:
150 | matcher = Any()
151 |
152 | if not isinstance(matcher, BaseMatcher):
153 | raise TypeError("expected a matcher, got %r" % (type(matcher),))
154 | if isinstance(matcher, Captor):
155 | raise TypeError("cannot pass a captor to another captor")
156 |
157 | self.matcher = matcher
158 |
159 | def has_value(self):
160 | """Returns whether the :class:`Captor` has captured a value."""
161 | return hasattr(self, 'value')
162 |
163 | @property
164 | def arg(self):
165 | """The captured argument value."""
166 | if not self.has_value():
167 | raise ValueError("no value captured")
168 | return self.value
169 |
170 | def match(self, value):
171 | if self.has_value():
172 | raise ValueError("a value has already been captured")
173 |
174 | if not self.matcher.match(value):
175 | return False
176 | self.value = value
177 | return True
178 |
179 | def __repr__(self):
180 | """Return a representation of the captor."""
181 | return "" % (self.matcher,
182 | " (*)" if self.has_value() else "")
183 |
--------------------------------------------------------------------------------
/tests/test_objects.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for object matchers.
3 | """
4 | import io
5 | try:
6 | from StringIO import StringIO
7 | except ImportError:
8 | StringIO = io.StringIO
9 |
10 | from taipan.testing import skipIf, skipUnless
11 |
12 | from callee._compat import IS_PY3, asyncio
13 | import callee.objects as __unit__
14 | from tests import IS_PY34, IS_PY35, MatcherTestCase, python_code
15 |
16 |
17 | class Bytes(MatcherTestCase):
18 | test_none = lambda self: self.assert_no_match(None)
19 | test_empty_unicode = lambda self: self.assert_no_match(u'')
20 | test_some_unicode = lambda self: self.assert_no_match(u"Alice has a cat")
21 |
22 | @skipIf(IS_PY3, "requires Python 2.x")
23 | def test_some_string__py2(self):
24 | self.assert_match("Alice has a cat")
25 |
26 | @skipUnless(IS_PY3, "requires Python 3.x")
27 | def test_some_string__py3(self):
28 | self.assert_no_match("Alice has a cat")
29 |
30 | test_some_object = lambda self: self.assert_no_match(object())
31 | test_some_number = lambda self: self.assert_no_match(42)
32 |
33 | test_repr = lambda self: self.assert_repr(__unit__.Bytes())
34 |
35 | # Assertion functions
36 |
37 | def assert_match(self, value):
38 | return super(Bytes, self).assert_match(__unit__.Bytes(), value)
39 |
40 | def assert_no_match(self, value):
41 | return super(Bytes, self).assert_no_match(__unit__.Bytes(), value)
42 |
43 |
44 | class Coroutine(MatcherTestCase):
45 | test_none = lambda self: self.assert_no_match(None)
46 | test_zero = lambda self: self.assert_no_match(0)
47 | test_string = lambda self: self.assert_no_match("Alice has a cat")
48 | test_some_object = lambda self: self.assert_no_match(object())
49 |
50 | def test_function(self):
51 | def func():
52 | pass
53 | self.assert_no_match(func)
54 |
55 | test_method = lambda self: self.assert_no_match(str.upper)
56 | test_type = lambda self: self.assert_no_match(object)
57 |
58 | def test_callable_object(self):
59 | class Foo(object):
60 | def __call__(self):
61 | pass
62 | self.assert_no_match(Foo())
63 |
64 | @skipIf(IS_PY34, "requires Python 2.x or 3.3")
65 | def test_generator_function__py2(self):
66 | def func():
67 | yield
68 | self.assert_no_match(func)
69 | self.assert_no_match(func())
70 |
71 | @skipUnless(IS_PY34, "requires Python 3.4+")
72 | def test_generator_function__py34(self):
73 | def func():
74 | yield
75 | self.assert_no_match(func)
76 | self.assert_match(func())
77 |
78 | @skipUnless(IS_PY34, "requires Python 3.4+")
79 | def test_coroutine__decorator(self):
80 | @asyncio.coroutine
81 | def coro_func(loop):
82 | pass
83 | coro = self.await_(coro_func)
84 | self.assert_match(coro)
85 |
86 | @skipUnless(IS_PY35, "requires Python 3.5+")
87 | def test_coroutine__async_def(self):
88 | # This whole test uses the asynchronous coroutine definition syntax
89 | # which is invalid on Python <3.5 so it has to be executed from string.
90 | try:
91 | exec(python_code("""
92 | async def coro_func():
93 | pass
94 | coro = coro_func()
95 | self.await_(coro) # to prevent a warning
96 | self.assert_match(coro)
97 | """))
98 | except SyntaxError:
99 | pass
100 |
101 | @skipUnless(IS_PY34, "requires Python 3.4+")
102 | def test_coroutine_function__decorator(self):
103 | @asyncio.coroutine
104 | def coro_func(loop):
105 | pass
106 | self.assert_no_match(coro_func)
107 |
108 | @skipUnless(IS_PY35, "requires Python 3.5+")
109 | def test_coroutine_function__async_def(self):
110 | # This whole test uses the asynchronous coroutine definition syntax
111 | # which is invalid on Python <3.5 so it has to be executed from string.
112 | try:
113 | exec(python_code("""
114 | async def coro_func():
115 | pass
116 | self.assert_no_match(coro_func)
117 | """))
118 | except SyntaxError:
119 | pass
120 |
121 | test_repr = lambda self: self.assert_repr(__unit__.Coroutine())
122 |
123 | # Assertion functions
124 |
125 | def assert_match(self, value):
126 | return super(Coroutine, self) \
127 | .assert_match(__unit__.Coroutine(), value)
128 |
129 | def assert_no_match(self, value):
130 | return super(Coroutine, self) \
131 | .assert_no_match(__unit__.Coroutine(), value)
132 |
133 |
134 | class FileLike(MatcherTestCase):
135 | test_none = lambda self: self.assert_no_match(None)
136 | test_zero = lambda self: self.assert_no_match(0)
137 | test_string = lambda self: self.assert_no_match("Alice has a cat")
138 | test_some_object = lambda self: self.assert_no_match(object())
139 |
140 | def test_openfile__read(self):
141 | with open(__file__, 'r') as f:
142 | self.assert_match(f, read=True)
143 |
144 | def test_openfile__write(self):
145 | with open(__file__, 'a') as f:
146 | self.assert_match(f, write=True)
147 |
148 | def test_openfile__both(self):
149 | with open(__file__, 'r+') as f:
150 | self.assert_match(f, read=True, write=True)
151 |
152 | def test_io_open__read(self):
153 | with io.open(__file__, 'r') as f:
154 | self.assert_match(f, read=True)
155 |
156 | def test_io_open__write(self):
157 | with io.open(__file__, 'a') as f:
158 | self.assert_match(f, write=True)
159 |
160 | def test_io_open__both(self):
161 | with io.open(__file__, 'r+') as f:
162 | self.assert_match(f, read=True, write=True)
163 |
164 | def test_stringio(self):
165 | self.assert_match(StringIO(), read=True, write=True)
166 |
167 | def test_ctor(self):
168 | with self.assertRaises(ValueError):
169 | __unit__.FileLike(read=None, write=None)
170 |
171 | def test_repr(self):
172 | self.assertEquals("",
173 | repr(__unit__.FileLike(read=True, write=None)))
174 | self.assertEquals("",
175 | repr(__unit__.FileLike(read=True, write=True)))
176 | self.assertEquals("",
177 | repr(__unit__.FileLike(read=False, write=None)))
178 | self.assertEquals("",
179 | repr(__unit__.FileLike(read=False, write=False)))
180 |
181 | # Assertion functions
182 |
183 | def assert_match(self, value, *args, **kwargs):
184 | return super(FileLike, self) \
185 | .assert_match(__unit__.FileLike(*args, **kwargs), value)
186 |
187 | def assert_no_match(self, value, *args, **kwargs):
188 | return super(FileLike, self) \
189 | .assert_no_match(__unit__.FileLike(*args, **kwargs), value)
190 |
--------------------------------------------------------------------------------
/tests/test_numbers.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for numeric matchers.
3 | """
4 | from fractions import Fraction
5 |
6 | from taipan.testing import skipIf, skipUnless
7 |
8 | from callee._compat import IS_PY3
9 | import callee.numbers as __unit__
10 | from tests import MatcherTestCase
11 |
12 |
13 | class Number(MatcherTestCase):
14 | test_none = lambda self: self.assert_no_match(None)
15 | test_object = lambda self: self.assert_no_match(object())
16 | test_iterable = lambda self: self.assert_no_match([])
17 | test_integer = lambda self: self.assert_match(0)
18 |
19 | @skipIf(IS_PY3, "requires Python 2.x")
20 | def test_long(self):
21 | self.assert_match(eval('0l'))
22 |
23 | test_fraction = lambda self: self.assert_match(Fraction(3, 4))
24 | test_float = lambda self: self.assert_match(0.0)
25 | test_complex = lambda self: self.assert_match(complex(0, 1))
26 |
27 | test_repr = lambda self: self.assert_repr(__unit__.Number())
28 |
29 | # Assertion functions
30 |
31 | def assert_match(self, value):
32 | return super(Number, self).assert_match(__unit__.Number(), value)
33 |
34 | def assert_no_match(self, value):
35 | return super(Number, self).assert_no_match(__unit__.Number(), value)
36 |
37 |
38 | class Complex(MatcherTestCase):
39 | test_none = lambda self: self.assert_no_match(None)
40 | test_object = lambda self: self.assert_no_match(object())
41 | test_iterable = lambda self: self.assert_no_match([])
42 | test_integer = lambda self: self.assert_match(0)
43 |
44 | @skipIf(IS_PY3, "requires Python 2.x")
45 | def test_long(self):
46 | self.assert_match(eval('0l'))
47 |
48 | test_fraction = lambda self: self.assert_match(Fraction(5, 7))
49 | test_float = lambda self: self.assert_match(0.0)
50 | test_complex = lambda self: self.assert_match(complex(0, 1))
51 |
52 | test_repr = lambda self: self.assert_repr(__unit__.Complex())
53 |
54 | # Assertion functions
55 |
56 | def assert_match(self, value):
57 | return super(Complex, self).assert_match(__unit__.Complex(), value)
58 |
59 | def assert_no_match(self, value):
60 | return super(Complex, self).assert_no_match(__unit__.Complex(), value)
61 |
62 |
63 | class Real(MatcherTestCase):
64 | test_none = lambda self: self.assert_no_match(None)
65 | test_object = lambda self: self.assert_no_match(object())
66 | test_iterable = lambda self: self.assert_no_match([])
67 | test_integer = lambda self: self.assert_match(0)
68 |
69 | @skipIf(IS_PY3, "requires Python 2.x")
70 | def test_long(self):
71 | self.assert_match(eval('0l'))
72 |
73 | test_fraction = lambda self: self.assert_match(Fraction(7, 9))
74 | test_float = lambda self: self.assert_match(0.0)
75 | test_complex = lambda self: self.assert_no_match(complex(0, 1))
76 |
77 | test_repr = lambda self: self.assert_repr(__unit__.Real())
78 |
79 | # Assertion functions
80 |
81 | def assert_match(self, value):
82 | return super(Real, self).assert_match(__unit__.Real(), value)
83 |
84 | def assert_no_match(self, value):
85 | return super(Real, self).assert_no_match(__unit__.Real(), value)
86 |
87 |
88 | class Float(MatcherTestCase):
89 | test_none = lambda self: self.assert_no_match(None)
90 | test_object = lambda self: self.assert_no_match(object())
91 | test_iterable = lambda self: self.assert_no_match([])
92 | test_integer = lambda self: self.assert_no_match(0)
93 |
94 | @skipIf(IS_PY3, "requires Python 2.x")
95 | def test_long(self):
96 | self.assert_no_match(eval('0l'))
97 |
98 | test_fraction = lambda self: self.assert_no_match(Fraction(9, 11))
99 | test_float = lambda self: self.assert_match(0.0)
100 | test_complex = lambda self: self.assert_no_match(complex(0, 1))
101 |
102 | test_repr = lambda self: self.assert_repr(__unit__.Float())
103 |
104 | # Assertion functions
105 |
106 | def assert_match(self, value):
107 | return super(Float, self).assert_match(__unit__.Float(), value)
108 |
109 | def assert_no_match(self, value):
110 | return super(Float, self).assert_no_match(__unit__.Float(), value)
111 |
112 |
113 | class Integral(MatcherTestCase):
114 | test_none = lambda self: self.assert_no_match(None)
115 | test_object = lambda self: self.assert_no_match(object())
116 | test_iterable = lambda self: self.assert_no_match([])
117 | test_integer = lambda self: self.assert_match(0)
118 |
119 | @skipIf(IS_PY3, "requires Python 2.x")
120 | def test_long(self):
121 | self.assert_match(eval('0l'))
122 |
123 | test_fraction = lambda self: self.assert_no_match(Fraction(7, 9))
124 | test_float = lambda self: self.assert_no_match(0.0)
125 | test_complex = lambda self: self.assert_no_match(complex(0, 1))
126 |
127 | test_repr = lambda self: self.assert_repr(__unit__.Integral())
128 |
129 | # Assertion functions
130 |
131 | def assert_match(self, value):
132 | return super(Integral, self).assert_match(__unit__.Integral(), value)
133 |
134 | def assert_no_match(self, value):
135 | return super(Integral, self) \
136 | .assert_no_match(__unit__.Integral(), value)
137 |
138 |
139 | class Integer(MatcherTestCase):
140 | test_none = lambda self: self.assert_no_match(None)
141 | test_object = lambda self: self.assert_no_match(object())
142 | test_iterable = lambda self: self.assert_no_match([])
143 | test_integer = lambda self: self.assert_match(0)
144 |
145 | @skipIf(IS_PY3, "requires Python 2.x")
146 | def test_long(self):
147 | self.assert_no_match(eval('0l'))
148 |
149 | test_fraction = lambda self: self.assert_no_match(Fraction(9, 11))
150 | test_float = lambda self: self.assert_no_match(0.0)
151 | test_complex = lambda self: self.assert_no_match(complex(0, 1))
152 |
153 | test_repr = lambda self: self.assert_repr(__unit__.Integer())
154 |
155 | # Assertion functions
156 |
157 | def assert_match(self, value):
158 | return super(Integer, self).assert_match(__unit__.Integer(), value)
159 |
160 | def assert_no_match(self, value):
161 | return super(Integer, self).assert_no_match(__unit__.Integer(), value)
162 |
163 |
164 | class Long(MatcherTestCase):
165 | test_none = lambda self: self.assert_no_match(None)
166 | test_object = lambda self: self.assert_no_match(object())
167 | test_iterable = lambda self: self.assert_no_match([])
168 |
169 | @skipIf(IS_PY3, "requires Python 2.x")
170 | def test_integer__py2(self):
171 | self.assert_no_match(0)
172 |
173 | @skipUnless(IS_PY3, "requires Python 3.x")
174 | def test_integer__py3(self):
175 | self.assert_match(0)
176 |
177 | @skipIf(IS_PY3, "requires Python 2.x")
178 | def test_long(self):
179 | self.assert_match(eval('0l'))
180 |
181 | test_fraction = lambda self: self.assert_no_match(Fraction(9, 11))
182 | test_float = lambda self: self.assert_no_match(0.0)
183 | test_complex = lambda self: self.assert_no_match(complex(0, 1))
184 |
185 | test_repr = lambda self: self.assert_repr(__unit__.Long())
186 |
187 | # Assertion functions
188 |
189 | def assert_match(self, value):
190 | return super(Long, self).assert_match(__unit__.Long(), value)
191 |
192 | def assert_no_match(self, value):
193 | return super(Long, self).assert_no_match(__unit__.Long(), value)
194 |
--------------------------------------------------------------------------------
/callee/operators.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers based on Python operators.
3 | """
4 | from __future__ import absolute_import
5 |
6 | from numbers import Number
7 | import operator
8 |
9 | from callee.base import BaseMatcher, Eq, Is, IsNot
10 |
11 |
12 | __all__ = [
13 | # those are defined elsewhere but they fit in this module, too
14 | 'Eq', 'Is', 'IsNot',
15 |
16 | 'Less', 'LessThan', 'Lt',
17 | 'LessOrEqual', 'LessOrEqualTo', 'Le',
18 | 'Greater', 'GreaterThan', 'Gt',
19 | 'GreaterOrEqual', 'GreaterOrEqualTo', 'Ge',
20 |
21 | 'Shorter', 'ShorterThan', 'ShorterOrEqual', 'ShorterOrEqualTo',
22 | 'Longer', 'LongerThan', 'LongerOrEqual', 'LongerOrEqualTo',
23 |
24 | 'Contains', 'In',
25 | ]
26 |
27 |
28 | class OperatorMatcher(BaseMatcher):
29 | """Matches values based on comparison operator and a reference object.
30 | This class shouldn't be used directly.
31 | """
32 | #: Operator function to use for comparing a value with a reference object.
33 | #: Must be overridden in subclasses.
34 | OP = None
35 |
36 | #: Transformation function to apply to given value before comparison.
37 | TRANSFORM = None
38 |
39 | def __init__(self, *args, **kwargs):
40 | """Accepts a single argument: the reference object to compare against.
41 |
42 | It can be passed either as a single positional parameter,
43 | or as a single keyword argument -- preferably with a readable name,
44 | for example::
45 |
46 | some_mock.assert_called_with(Number() & LessOrEqual(to=42))
47 | """
48 | assert self.OP, "must specify comparison operator to use"
49 |
50 | # check that we've received exactly one argument,
51 | # either positional or keyword
52 | argcount = len(args) + len(kwargs)
53 | if argcount != 1:
54 | raise TypeError("a single argument expected, got %s" % argcount)
55 | if len(args) > 1:
56 | raise TypeError(
57 | "at most one positional argument expected, got %s" % len(args))
58 | if len(kwargs) > 1:
59 | raise TypeError(
60 | "at most one keyword argument expected, got %s" % len(kwargs))
61 |
62 | # extract the reference object from arguments
63 | ref = None
64 | if args:
65 | ref = args[0]
66 | elif kwargs:
67 | _, ref = kwargs.popitem()
68 |
69 | #: Reference object to compare given values to.
70 | self.ref = ref
71 |
72 | def match(self, value):
73 | # Note that any possible exceptions from either ``TRANSFORM`` or ``OP``
74 | # are intentionally let through, to make it easier to diagnose errors
75 | # than a plain "no match" response would.
76 | if self.TRANSFORM is not None:
77 | value = self.TRANSFORM(value)
78 | return self.OP(value, self.ref)
79 |
80 | def __repr__(self):
81 | """Provide an universal representation of the matcher."""
82 | # Mapping from operator functions to their symbols in Python.
83 | #
84 | # There is no point in including ``operator.contains`` due to lack of
85 | # equivalent ``operator.in_``.
86 | # These are handled by membership matchers directly.
87 | operator_symbols = {
88 | operator.eq: '==',
89 | operator.ge: '>=',
90 | operator.gt: '>',
91 | operator.le: '<=',
92 | operator.lt: '<',
93 | }
94 |
95 | # try to get the symbol for the operator, falling back to Haskell-esque
96 | # "infix function" representation
97 | op = operator_symbols.get(self.OP)
98 | if op is None:
99 | # TODO: convert CamelCase to either snake_case or kebab-case
100 | op = '`%s`' % (self.__class__.__name__.lower(),)
101 |
102 | return "<%s %s %r>" % (self._get_placeholder_repr(), op, self.ref)
103 |
104 | def _get_placeholder_repr(self):
105 | """Return the placeholder part of matcher's ``__repr__``."""
106 | placeholder = '...'
107 | if self.TRANSFORM is not None:
108 | placeholder = '%s(%s)' % (self.TRANSFORM.__name__, placeholder)
109 | return placeholder
110 |
111 |
112 | # Simple comparisons
113 |
114 | # TODO: AlmostEq() for approximate comparisons with epsilon
115 |
116 | class Less(OperatorMatcher):
117 | """Matches values that are smaller (as per ``<`` operator)
118 | than given object.
119 | """
120 | OP = operator.lt
121 |
122 | LessThan = Less
123 |
124 | Lt = Less
125 |
126 |
127 | class LessOrEqual(OperatorMatcher):
128 | """Matches values that are smaller than,
129 | or equal to (as per ``<=`` operator), given object.
130 | """
131 | OP = operator.le
132 |
133 | LessOrEqualTo = LessOrEqual
134 |
135 | Le = LessOrEqual
136 |
137 |
138 | class Greater(OperatorMatcher):
139 | """Matches values that are greater (as per ``>`` operator)
140 | than given object.
141 | """
142 | OP = operator.gt
143 |
144 | GreaterThan = Greater
145 |
146 | Gt = Greater
147 |
148 |
149 | class GreaterOrEqual(OperatorMatcher):
150 | """Matches values that are greater than,
151 | or equal to (as per ``>=`` operator), given object.
152 | """
153 | OP = operator.ge
154 |
155 | GreaterOrEqualTo = GreaterOrEqual
156 |
157 | Ge = GreaterOrEqual
158 |
159 |
160 | # Length comparisons
161 |
162 | class LengthMatcher(OperatorMatcher):
163 | """Matches values based on their length, as compared to a reference.
164 | This class shouldn't be used directly.
165 | """
166 | TRANSFORM = len
167 |
168 | def __init__(self, *args, **kwargs):
169 | super(LengthMatcher, self).__init__(*args, **kwargs)
170 |
171 | # allow the reference to be either a numeric length or another sequence
172 | # TODO: remember at least the sequence type to make it impossible
173 | # e.g. to accidentally compare strings and lists by length
174 | if not isinstance(self.ref, Number):
175 | self.ref = len(self.ref)
176 |
177 |
178 | class Shorter(LengthMatcher):
179 | """Matches values that are shorter (as per ``<`` comparison on ``len``)
180 | than given value.
181 | """
182 | OP = operator.lt
183 |
184 | ShorterThan = Shorter
185 |
186 |
187 | class ShorterOrEqual(LengthMatcher):
188 | """Matches values that are shorter than,
189 | or equal in ``len``\ gth to (as per ``<=`` operator), given object.
190 | """
191 | OP = operator.le
192 |
193 | ShorterOrEqualTo = ShorterOrEqual
194 |
195 |
196 | class Longer(LengthMatcher):
197 | """Matches values that are longer (as per ``>`` comparison on ``len``)
198 | than given value.
199 | """
200 | OP = operator.gt
201 |
202 | LongerThan = Longer
203 |
204 |
205 | class LongerOrEqual(LengthMatcher):
206 | """Matches values that are longer than,
207 | or equal in ``len``\ gth to (as per ``>=`` operator), given object.
208 | """
209 | OP = operator.ge
210 |
211 | LongerOrEqualTo = LongerOrEqual
212 |
213 |
214 | # Membership tests
215 |
216 | class Contains(OperatorMatcher):
217 | """Matches values that contain (as per the ``in`` operator)
218 | given reference object.
219 | """
220 | OP = operator.contains
221 |
222 | def __repr__(self):
223 | return "<%r in %s>" % (self.ref, self._get_placeholder_repr())
224 |
225 |
226 | class In(OperatorMatcher):
227 | """Matches values that are within the reference object
228 | (as per the ``in`` operator).
229 | """
230 | # There is no ``operator.in_``, so we must define the function ourselves.
231 | OP = staticmethod(lambda value, ref: value in ref)
232 |
233 | def __repr__(self):
234 | return "<%s in %r>" % (self._get_placeholder_repr(), self.ref)
235 |
--------------------------------------------------------------------------------
/tests/test_functions.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for function matchers.
3 | """
4 | import platform
5 |
6 | from taipan.testing import skipIf, skipUnless
7 |
8 | from callee._compat import IS_PY3, asyncio
9 | import callee.functions as __unit__
10 | from tests import IS_PY34, IS_PY35, MatcherTestCase, python_code
11 |
12 |
13 | IS_PYPY3 = IS_PY3 and platform.python_implementation() == 'PyPy'
14 |
15 |
16 | class Callable(MatcherTestCase):
17 | test_none = lambda self: self.assert_no_match(None)
18 | test_zero = lambda self: self.assert_no_match(0)
19 | test_string = lambda self: self.assert_no_match("Alice has a cat")
20 | test_some_object = lambda self: self.assert_no_match(object())
21 |
22 | def test_function(self):
23 | def func():
24 | pass
25 | self.assert_match(func)
26 |
27 | test_method = lambda self: self.assert_match(str.upper)
28 | test_type = lambda self: self.assert_match(object)
29 |
30 | def test_callable_object(self):
31 | class Foo(object):
32 | def __call__(self):
33 | pass
34 | self.assert_match(Foo())
35 |
36 | def test_generator_function(self):
37 | def func():
38 | yield
39 | self.assert_match(func)
40 | self.assert_no_match(func())
41 |
42 | test_lambda = lambda self: self.assert_match(lambda: ())
43 | test_generator = lambda self: self.assert_no_match(x for x in ())
44 |
45 | test_repr = lambda self: self.assert_repr(__unit__.Callable())
46 |
47 | # Assertion functions
48 |
49 | def assert_match(self, value):
50 | return super(Callable, self).assert_match(__unit__.Callable(), value)
51 |
52 | def assert_no_match(self, value):
53 | return super(Callable, self) \
54 | .assert_no_match(__unit__.Callable(), value)
55 |
56 |
57 | class Function(MatcherTestCase):
58 | test_none = lambda self: self.assert_no_match(None)
59 | test_zero = lambda self: self.assert_no_match(0)
60 | test_string = lambda self: self.assert_no_match("Alice has a cat")
61 | test_some_object = lambda self: self.assert_no_match(object())
62 |
63 | def test_function(self):
64 | def func():
65 | pass
66 | self.assert_match(func)
67 |
68 | @skipIf(IS_PYPY3, "requires non-PyPy3 interpreter")
69 | def test_method__non_pypy3(self):
70 | self.assert_no_match(str.upper)
71 | # TODO: accept unbound methods as functions
72 |
73 | @skipUnless(IS_PYPY3, "requires PyPy3")
74 | def test_method__pypy3(self):
75 | self.assert_match(str.upper)
76 |
77 | test_type = lambda self: self.assert_no_match(object)
78 |
79 | def test_callable_object(self):
80 | class Foo(object):
81 | def __call__(self):
82 | pass
83 | self.assert_no_match(Foo())
84 |
85 | def test_generator_function(self):
86 | def func():
87 | yield
88 | self.assert_match(func)
89 | self.assert_no_match(func())
90 |
91 | test_lambda = lambda self: self.assert_match(lambda: ())
92 | test_generator = lambda self: self.assert_no_match(x for x in ())
93 |
94 | test_repr = lambda self: self.assert_repr(__unit__.Function())
95 |
96 | # Assertion functions
97 |
98 | def assert_match(self, value):
99 | return super(Function, self).assert_match(__unit__.Function(), value)
100 |
101 | def assert_no_match(self, value):
102 | return super(Function, self) \
103 | .assert_no_match(__unit__.Function(), value)
104 |
105 |
106 | class GeneratorFunction(MatcherTestCase):
107 | test_none = lambda self: self.assert_no_match(None)
108 | test_zero = lambda self: self.assert_no_match(0)
109 | test_string = lambda self: self.assert_no_match("Alice has a cat")
110 | test_some_object = lambda self: self.assert_no_match(object())
111 |
112 | def test_function(self):
113 | def func():
114 | pass
115 | self.assert_no_match(func)
116 |
117 | test_method = lambda self: self.assert_no_match(str.upper)
118 | test_type = lambda self: self.assert_no_match(object)
119 |
120 | def test_callable_object(self):
121 | class Foo(object):
122 | def __call__(self):
123 | pass
124 | self.assert_no_match(Foo())
125 |
126 | def test_generator_function(self):
127 | def func():
128 | yield
129 | self.assert_match(func)
130 | self.assert_no_match(func())
131 |
132 | test_lambda = lambda self: self.assert_no_match(lambda: ())
133 | test_generator = lambda self: self.assert_no_match(x for x in ())
134 |
135 | test_repr = lambda self: self.assert_repr(__unit__.GeneratorFunction())
136 |
137 | # Assertion functions
138 |
139 | def assert_match(self, value):
140 | return super(GeneratorFunction, self) \
141 | .assert_match(__unit__.GeneratorFunction(), value)
142 |
143 | def assert_no_match(self, value):
144 | return super(GeneratorFunction, self) \
145 | .assert_no_match(__unit__.GeneratorFunction(), value)
146 |
147 |
148 | class CoroutineFunction(MatcherTestCase):
149 | test_none = lambda self: self.assert_no_match(None)
150 | test_zero = lambda self: self.assert_no_match(0)
151 | test_string = lambda self: self.assert_no_match("Alice has a cat")
152 | test_some_object = lambda self: self.assert_no_match(object())
153 |
154 | def test_function(self):
155 | def func():
156 | pass
157 | self.assert_no_match(func)
158 |
159 | test_method = lambda self: self.assert_no_match(str.upper)
160 | test_type = lambda self: self.assert_no_match(object)
161 |
162 | def test_callable_object(self):
163 | class Foo(object):
164 | def __call__(self):
165 | pass
166 | self.assert_no_match(Foo())
167 |
168 | def test_generator_function(self):
169 | def func():
170 | yield
171 | self.assert_no_match(func)
172 | self.assert_no_match(func())
173 |
174 | @skipUnless(IS_PY34, "requires Python 3.4+")
175 | def test_coroutine__decorator(self):
176 | @asyncio.coroutine
177 | def coro_func(loop):
178 | pass
179 | coro = self.await_(coro_func)
180 | self.assert_no_match(coro)
181 |
182 | @skipUnless(IS_PY35, "requires Python 3.5+")
183 | def test_coroutine__async_def(self):
184 | # This whole test uses the asynchronous coroutine definition syntax
185 | # which is invalid on Python <3.5 so it has to be executed from string.
186 | try:
187 | exec(python_code("""
188 | async def coro_func():
189 | pass
190 | coro = coro_func()
191 | self.await_(coro) # to prevent a warning
192 | self.assert_no_match(coro)
193 | """))
194 | except SyntaxError:
195 | pass
196 |
197 | @skipUnless(IS_PY34, "requires Python 3.4+")
198 | def test_coroutine_function__decorator(self):
199 | @asyncio.coroutine
200 | def coro_func(loop):
201 | pass
202 | self.assert_match(coro_func)
203 |
204 | @skipUnless(IS_PY35, "requires Python 3.5+")
205 | def test_coroutine_function__async_def(self):
206 | # This whole test uses the asynchronous coroutine definition syntax
207 | # which is invalid on Python <3.5 so it has to be executed from string.
208 | try:
209 | exec(python_code("""
210 | async def coro_func():
211 | pass
212 | self.assert_match(coro_func)
213 | """))
214 | except SyntaxError:
215 | pass
216 |
217 | test_repr = lambda self: self.assert_repr(__unit__.CoroutineFunction())
218 |
219 | # Assertion functions
220 |
221 | def assert_match(self, value):
222 | return super(CoroutineFunction, self) \
223 | .assert_match(__unit__.CoroutineFunction(), value)
224 |
225 | def assert_no_match(self, value):
226 | return super(CoroutineFunction, self) \
227 | .assert_no_match(__unit__.CoroutineFunction(), value)
228 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " applehelp to make an Apple Help Book"
34 | @echo " devhelp to make HTML files and a Devhelp project"
35 | @echo " epub to make an epub"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 | @echo " coverage to run coverage check of the documentation (if enabled)"
50 |
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | html:
55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
58 |
59 | dirhtml:
60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
61 | @echo
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
63 |
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | pickle:
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
71 | @echo
72 | @echo "Build finished; now you can process the pickle files."
73 |
74 | json:
75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
76 | @echo
77 | @echo "Build finished; now you can process the JSON files."
78 |
79 | htmlhelp:
80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
81 | @echo
82 | @echo "Build finished; now you can run HTML Help Workshop with the" \
83 | ".hhp project file in $(BUILDDIR)/htmlhelp."
84 |
85 | qthelp:
86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
87 | @echo
88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/callee.qhcp"
91 | @echo "To view the help file:"
92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/callee.qhc"
93 |
94 | applehelp:
95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
96 | @echo
97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
98 | @echo "N.B. You won't be able to view it unless you put it in" \
99 | "~/Library/Documentation/Help or install it in your application" \
100 | "bundle."
101 |
102 | devhelp:
103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
104 | @echo
105 | @echo "Build finished."
106 | @echo "To view the help file:"
107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/callee"
108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/callee"
109 | @echo "# devhelp"
110 |
111 | epub:
112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
113 | @echo
114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
115 |
116 | latex:
117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
118 | @echo
119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
121 | "(use \`make latexpdf' here to do that automatically)."
122 |
123 | latexpdf:
124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
125 | @echo "Running LaTeX files through pdflatex..."
126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
128 |
129 | latexpdfja:
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
131 | @echo "Running LaTeX files through platex and dvipdfmx..."
132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
134 |
135 | text:
136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
137 | @echo
138 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
139 |
140 | man:
141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
142 | @echo
143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
144 |
145 | texinfo:
146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
147 | @echo
148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
149 | @echo "Run \`make' in that directory to run these through makeinfo" \
150 | "(use \`make info' here to do that automatically)."
151 |
152 | info:
153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
154 | @echo "Running Texinfo files through makeinfo..."
155 | make -C $(BUILDDIR)/texinfo info
156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
157 |
158 | gettext:
159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
160 | @echo
161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
162 |
163 | changes:
164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
165 | @echo
166 | @echo "The overview file is in $(BUILDDIR)/changes."
167 |
168 | linkcheck:
169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
170 | @echo
171 | @echo "Link check complete; look for any errors in the above output " \
172 | "or in $(BUILDDIR)/linkcheck/output.txt."
173 |
174 | doctest:
175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
176 | @echo "Testing of doctests in the sources finished, look at the " \
177 | "results in $(BUILDDIR)/doctest/output.txt."
178 |
179 | coverage:
180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
181 | @echo "Testing of coverage in the sources finished, look at the " \
182 | "results in $(BUILDDIR)/coverage/python.txt."
183 |
184 | xml:
185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
186 | @echo
187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
188 |
189 | pseudoxml:
190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
191 | @echo
192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
193 |
--------------------------------------------------------------------------------
/callee/collections.py:
--------------------------------------------------------------------------------
1 | """
2 | Matchers for collections.
3 | """
4 | from __future__ import absolute_import
5 |
6 | try:
7 | import collections.abc
8 | abc = collections.abc
9 | except ImportError:
10 | import collections
11 | abc = collections
12 |
13 | import inspect
14 |
15 | from callee._compat import OrderedDict as _OrderedDict
16 | from callee.base import BaseMatcher
17 | from callee.general import Any
18 | from callee.types import InstanceOf
19 |
20 |
21 | __all__ = [
22 | 'Iterable', 'Generator',
23 | 'Sequence', 'List', 'Set',
24 | 'Mapping', 'Dict', 'OrderedDict',
25 | ]
26 |
27 |
28 | class CollectionMatcher(BaseMatcher):
29 | """Base class for collections' matchers.
30 | This class shouldn't be used directly.
31 | """
32 | #: Collection class to match.
33 | #: Must be overridden in subclasses.
34 | CLASS = None
35 |
36 | def __init__(self, of=None):
37 | """
38 | :param of: Optional matcher for the elements,
39 | or the expected type of the elements.
40 | """
41 | assert self.CLASS, "must specify collection type to match"
42 | self.of = self._validate_argument(of)
43 |
44 | def _validate_argument(self, arg):
45 | """Validate a type or matcher argument to the constructor."""
46 | if arg is None:
47 | return arg
48 |
49 | if isinstance(arg, type):
50 | return InstanceOf(arg)
51 | if not isinstance(arg, BaseMatcher):
52 | raise TypeError(
53 | "argument of %s can be a type or a matcher (got %r)" % (
54 | self.__class__.__name__, type(arg)))
55 |
56 | return arg
57 |
58 | def match(self, value):
59 | if not isinstance(value, self.CLASS):
60 | return False
61 | if self.of is not None:
62 | return all(self.of == item for item in value)
63 | return True
64 |
65 | def __repr__(self):
66 | """Return a readable representation of the matcher.
67 | Used mostly for AssertionError messages in failed tests.
68 |
69 | Example::
70 |
71 | ]>
72 | """
73 | of = "" if self.of is None else "[%r]" % (self.of,)
74 | return "<%s%s>" % (self.__class__.__name__, of)
75 |
76 |
77 | class Iterable(CollectionMatcher):
78 | """Matches any iterable."""
79 |
80 | CLASS = abc.Iterable
81 |
82 | def __init__(self):
83 | # Unfortunately, we can't allow an ``of`` argument to this matcher.
84 | #
85 | # An otherwise unspecified iterable can't be iterated upon
86 | # more than once safely, because it could be a one-off iterable
87 | # (e.g. generator comprehension) that's exhausted after a single pass.
88 | #
89 | # Thus the sole act of checking the element types would alter
90 | # the object we're trying to match, and potentially cause all sorts
91 | # of unexpected behaviors (e.g. tests passing/failing depending on
92 | # the order of assertions).
93 | #
94 | super(Iterable, self).__init__(of=None)
95 |
96 |
97 | class Generator(BaseMatcher):
98 | """Matches an iterable that's a generator.
99 |
100 | A generator can be a generator expression ("comprehension")
101 | or an invocation of a generator function (one that ``yield``\ s objects).
102 |
103 | .. note::
104 |
105 | To match a *generator function* itself, you should use the
106 | :class:`~callee.functions.GeneratorFunction` matcher instead.
107 | """
108 | def match(self, value):
109 | return inspect.isgenerator(value)
110 |
111 | def __repr__(self):
112 | return ""
113 |
114 |
115 | # Ordinary collections
116 |
117 | class Sequence(CollectionMatcher):
118 | """Matches a sequence of given items.
119 |
120 | A sequence is an iterable that has a length and can be indexed.
121 | """
122 | CLASS = abc.Sequence
123 |
124 |
125 | class List(CollectionMatcher):
126 | """Matches a :class:`list` of given items."""
127 |
128 | CLASS = list
129 |
130 |
131 | class Set(CollectionMatcher):
132 | """Matches a :class:`set` of given items."""
133 |
134 | CLASS = abc.Set
135 |
136 |
137 | # TODO: Tuple matcher, with of= that accepts a tuple of matchers
138 | # so that tuple elements can be also matched on
139 |
140 |
141 | # Mappings
142 |
143 | class MappingMatcher(CollectionMatcher):
144 | """Base class for mapping matchers.
145 | This class shouldn't be used directly.
146 | """
147 | #: Mapping class to match.
148 | #: Must be overridden in subclasses.
149 | CLASS = None
150 |
151 | def __init__(self, *args, **kwargs):
152 | """Constructor can be invoked either with parameters described below
153 | (given as keyword arguments), or with two positional arguments:
154 | matchers/types for dictionary keys & values::
155 |
156 | Dict(String(), int) # dict mapping strings to ints
157 |
158 | :param keys: Matcher for dictionary keys.
159 | :param values: Matcher for dictionary values.
160 | :param of: Matcher for dictionary items, or a tuple of matchers
161 | for keys & values, e.g. ``(String(), Integer())``.
162 | Cannot be provided if either ``keys`` or ``values``
163 | is also passed.
164 |
165 | """
166 | assert self.CLASS, "must specify mapping type to match"
167 | self._initialize(*args, **kwargs)
168 |
169 | def _initialize(self, *args, **kwargs):
170 | """Initiaize the mapping matcher with constructor arguments."""
171 | self.items = None
172 | self.keys = None
173 | self.values = None
174 |
175 | if args:
176 | if len(args) != 2:
177 | raise TypeError("expected exactly two positional arguments, "
178 | "got %s" % len(args))
179 | if kwargs:
180 | raise TypeError(
181 | "expected positional or keyword arguments, not both")
182 |
183 | # got positional arguments only
184 | self.keys, self.values = map(self._validate_argument, args)
185 | elif kwargs:
186 | has_kv = 'keys' in kwargs and 'values' in kwargs
187 | has_of = 'of' in kwargs
188 | if not (has_kv or has_of):
189 | raise TypeError("expected keys/values or items matchers, "
190 | "but got: %s" % list(kwargs.keys()))
191 | if has_kv and has_of:
192 | raise TypeError(
193 | "expected keys & values, or items matchers, not both")
194 |
195 | if has_kv:
196 | # got keys= and values= matchers
197 | self.keys = self._validate_argument(kwargs['keys'])
198 | self.values = self._validate_argument(kwargs['values'])
199 | else:
200 | # got of= matcher, which can be a tuple of matchers,
201 | # or a single matcher for dictionary items
202 | of = kwargs['of']
203 | if isinstance(of, tuple):
204 | try:
205 | # got of= as tuple of matchers
206 | self.keys, self.values = \
207 | map(self._validate_argument, of)
208 | except ValueError:
209 | raise TypeError(
210 | "of= tuple has to be a pair of matchers/types" % (
211 | self.__class__.__name__,))
212 | else:
213 | # got of= as a single matcher
214 | self.items = self._validate_argument(of)
215 |
216 | def match(self, value):
217 | if not isinstance(value, self.CLASS):
218 | return False
219 |
220 | if self.items is not None:
221 | return all(self.items == i for i in value.items())
222 | if self.keys is not None and self.values is not None:
223 | return all(self.keys == k and self.values == v
224 | for k, v in value.items())
225 |
226 | return True
227 |
228 | def __repr__(self):
229 | """Return a readable representation of the matcher
230 | Used mostly for AssertionError messages in failed tests.
231 |
232 | Example::
233 |
234 | => ]>
235 | """
236 | of = ""
237 |
238 | if self.items is not None:
239 | of = "[%r]" % self.items
240 |
241 | if self.keys is not None or self.values is not None:
242 | keys = repr(Any() if self.keys is None else self.keys)
243 | values = repr(Any() if self.values is None else self.values)
244 | of = "[%s => %s]" % (keys, values)
245 |
246 | return "<%s%s>" % (self.__class__.__name__, of)
247 |
248 |
249 | class Mapping(MappingMatcher):
250 | """Matches a mapping of given items."""
251 |
252 | CLASS = abc.Mapping
253 |
254 |
255 | class Dict(MappingMatcher):
256 | """Matches a dictionary (:class:`dict`) of given items."""
257 |
258 | CLASS = dict
259 |
260 |
261 | class OrderedDict(MappingMatcher):
262 | """Matches an ordered dictionary (:class:`collections.OrderedDict`)
263 | of given items.
264 |
265 | On Python 2.6, this requires the ordereddict backport package.
266 | Otherwise, no object will match this matcher.
267 | """
268 | CLASS = _OrderedDict
269 |
270 | def __init__(self, *args, **kwargs):
271 | """For more information about arguments,
272 | see the documentation of :class:`Dict`.
273 | """
274 | # Override the constructor from the base matcher class
275 | # without asserting that CLASS is not None, because it legimately will
276 | # be on Python 2.6 without the ordereddict package.
277 | self._initialize(*args, **kwargs)
278 |
279 | def match(self, value):
280 | if self.CLASS is None:
281 | return False
282 | return super(OrderedDict, self).match(value)
283 |
--------------------------------------------------------------------------------
/tests/test_strings.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for string matchers.
3 | """
4 | import fnmatch
5 | from itertools import combinations
6 | import re
7 |
8 | from taipan.testing import skipIf, skipUnless
9 |
10 | from callee._compat import IS_PY3
11 | import callee.strings as __unit__
12 | from tests import MatcherTestCase
13 |
14 |
15 | # String type matchers
16 |
17 | class String(MatcherTestCase):
18 | test_none = lambda self: self.assert_no_match(None)
19 | test_empty_string = lambda self: self.assert_match('')
20 | test_some_string = lambda self: self.assert_match("Alice has a cat")
21 |
22 | @skipIf(IS_PY3, "requires Python 2.x")
23 | def test_some_bytes__py2(self):
24 | self.assert_match(bytes("Alice has a cat"))
25 |
26 | @skipUnless(IS_PY3, "requires Python 3.x")
27 | def test_some_bytes__py3(self):
28 | self.assert_no_match(bytes("Alice has a cat", 'ascii'))
29 |
30 | test_some_object = lambda self: self.assert_no_match(object())
31 | test_some_number = lambda self: self.assert_no_match(42)
32 |
33 | test_repr = lambda self: self.assert_repr(__unit__.String)
34 |
35 | # Assertion functions
36 |
37 | def assert_match(self, value):
38 | return super(String, self).assert_match(__unit__.String(), value)
39 |
40 | def assert_no_match(self, value):
41 | return super(String, self).assert_no_match(__unit__.String(), value)
42 |
43 |
44 | class Unicode(MatcherTestCase):
45 | test_none = lambda self: self.assert_no_match(None)
46 | test_empty_unicode = lambda self: self.assert_match(u'')
47 | test_some_unicode = lambda self: self.assert_match(u"Alice has a cat")
48 |
49 | @skipIf(IS_PY3, "requires Python 2.x")
50 | def test_some_string__py2(self):
51 | self.assert_no_match("Alice has a cat")
52 |
53 | @skipUnless(IS_PY3, "requires Python 3.x")
54 | def test_some_string__py3(self):
55 | self.assert_match("Alice has a cat")
56 |
57 | test_some_object = lambda self: self.assert_no_match(object())
58 | test_some_number = lambda self: self.assert_no_match(42)
59 |
60 | test_repr = lambda self: self.assert_repr(__unit__.Unicode)
61 |
62 | # Assertion functions
63 |
64 | def assert_match(self, value):
65 | return super(Unicode, self).assert_match(__unit__.Unicode(), value)
66 |
67 | def assert_no_match(self, value):
68 | return super(Unicode, self).assert_no_match(__unit__.Unicode(), value)
69 |
70 |
71 | # Infix matchers
72 |
73 | class StartsWith(MatcherTestCase):
74 |
75 | def test_exact(self):
76 | self.assert_match('', '')
77 | self.assert_match(' ', ' ')
78 | self.assert_match('foo', 'foo')
79 |
80 | def test_prefix(self):
81 | self.assert_match('foo', '') # empty string is a prefix of everything
82 | self.assert_match('foo', 'f')
83 | self.assert_match('foo', 'fo')
84 | self.assert_match(' foo', ' ')
85 |
86 | def test_no_match(self):
87 | self.assert_no_match('foo', 'b')
88 | self.assert_no_match(' foo', 'f')
89 | self.assert_no_match('', 'foo')
90 |
91 | @skipIf(IS_PY3, "requires Python 2.x")
92 | def test_unicode(self):
93 | self.assert_match(u'', u'')
94 | self.assert_match(u' ', u' ')
95 | self.assert_match(u'foo', u'foo')
96 | self.assert_match(u'foo', u'')
97 | self.assert_match(u'foo', u'f')
98 | self.assert_match(u'foo', u'fo')
99 | self.assert_match(u' foo', u' ')
100 | self.assert_no_match(u' foo', u'f')
101 | self.assert_no_match(u'foo', u'b')
102 | self.assert_no_match(u'', u'foo')
103 |
104 | test_repr = lambda self: self.assert_repr(__unit__.StartsWith(''))
105 |
106 | # Assertion functions
107 |
108 | def assert_match(self, value, prefix):
109 | return super(StartsWith, self) \
110 | .assert_match(__unit__.StartsWith(prefix), value)
111 |
112 | def assert_no_match(self, value, prefix):
113 | return super(StartsWith, self) \
114 | .assert_no_match(__unit__.StartsWith(prefix), value)
115 |
116 |
117 | class EndsWith(MatcherTestCase):
118 |
119 | def test_exact(self):
120 | self.assert_match('', '')
121 | self.assert_match(' ', ' ')
122 | self.assert_match('bar', 'bar')
123 |
124 | def test_suffix(self):
125 | self.assert_match('bar', '') # empty string is a suffix of everything
126 | self.assert_match('bar', 'r')
127 | self.assert_match('bar', 'ar')
128 | self.assert_match('bar ', ' ')
129 |
130 | def test_no_match(self):
131 | self.assert_no_match('bar', 'o')
132 | self.assert_no_match('bar ', 'r')
133 | self.assert_no_match('', 'bar')
134 |
135 | @skipIf(IS_PY3, "requires Python 2.x")
136 | def test_unicode(self):
137 | self.assert_match(u'', u'')
138 | self.assert_match(u' ', u' ')
139 | self.assert_match(u'bar', u'bar')
140 | self.assert_match(u'bar', u'')
141 | self.assert_match(u'bar', u'r')
142 | self.assert_match(u'bar', u'ar')
143 | self.assert_match(u'bar ', u' ')
144 | self.assert_no_match(u'bar', u'o')
145 | self.assert_no_match(u'bar ', u'r')
146 | self.assert_no_match(u'', u'bar')
147 |
148 | test_repr = lambda self: self.assert_repr(__unit__.EndsWith(''))
149 |
150 | # Assertion functions
151 |
152 | def assert_match(self, value, suffix):
153 | return super(EndsWith, self) \
154 | .assert_match(__unit__.EndsWith(suffix), value)
155 |
156 | def assert_no_match(self, value, suffix):
157 | return super(EndsWith, self) \
158 | .assert_no_match(__unit__.EndsWith(suffix), value)
159 |
160 |
161 | # Pattern matchers
162 |
163 | class PatternTestCase(MatcherTestCase):
164 | ALPHABET = 'abcdefghijklmnopqrstvuwxyz'
165 |
166 | # Some tests is O(2^N) wrt to size of this set, so keep it short.
167 | LETTERS = 'abcdef'
168 |
169 | def suffixes(self):
170 | for l in range(len(self.LETTERS)):
171 | for suffix in combinations(self.LETTERS, l):
172 | return ''.join(suffix)
173 |
174 |
175 | class Glob(PatternTestCase):
176 |
177 | def test_exact(self):
178 | self.assert_match('', '')
179 | self.assert_match(' ', ' ')
180 | self.assert_match('foo', 'foo')
181 | self.assert_match('foo!', 'foo!') # ! is not special outside of []
182 |
183 | def test_escaping(self):
184 | self.assert_match('foo?', 'foo[?]')
185 | self.assert_match('foo*', 'foo[*]')
186 |
187 | def test_question_mark(self):
188 | text = 'foo'
189 | for char in self.ALPHABET:
190 | self.assert_match(text + char, text + '?')
191 |
192 | def test_asterisk(self):
193 | text = 'foo'
194 | for suffix in self.suffixes():
195 | self.assert_match(text + suffix, text + '*')
196 |
197 | def test_square_brackets(self):
198 | text = 'foo'
199 | for suffix in self.suffixes():
200 | square_pattern = ''.join('[%s]' % char for char in suffix)
201 | self.assert_match(text + suffix, text + square_pattern)
202 |
203 | def test_case_sensitive(self):
204 | self.assert_match('', '', case=True)
205 | self.assert_match(' ', ' ', case=True)
206 | self.assert_match('foo', 'foo', case=True)
207 | self.assert_no_match('foo', 'Foo', case=True)
208 | self.assert_no_match('Foo', 'foo', case=True)
209 |
210 | def test_case_insensitive(self):
211 | self.assert_match('', '', case=False)
212 | self.assert_match(' ', ' ', case=False)
213 | self.assert_match('foo', 'foo', case=False)
214 | self.assert_match('foo', 'Foo', case=False)
215 | self.assert_match('FoO', 'fOo', case=False)
216 |
217 | def test_system_case(self):
218 | # Just test that the fnmatch function is picked correctly,
219 | # since the actual match result is system-dependent by definition.
220 | self.assertIs(fnmatch.fnmatch, __unit__.Glob('').fnmatch)
221 | self.assertIs(fnmatch.fnmatch,
222 | __unit__.Glob('', case='system').fnmatch)
223 | self.assertIs(fnmatch.fnmatch, __unit__.Glob('', case=None).fnmatch)
224 |
225 | test_repr = lambda self: self.assert_repr(__unit__.Glob('*'))
226 |
227 | # Assertion functions
228 |
229 | def assert_match(self, value, pattern, case=__unit__.Glob.DEFAULT_CASE):
230 | return super(Glob, self) \
231 | .assert_match(__unit__.Glob(pattern, case), value)
232 |
233 | def assert_no_match(self, value, pattern, case=__unit__.Glob.DEFAULT_CASE):
234 | return super(Glob, self) \
235 | .assert_no_match(__unit__.Glob(pattern, case), value)
236 |
237 |
238 | class Regex(PatternTestCase):
239 |
240 | def test_exact(self):
241 | self.assert_match('', '')
242 | self.assert_match(' ', ' ')
243 | self.assert_match('foo', 'foo')
244 | self.assert_match('foo^', 'foo') # ^ is not special outside of []
245 |
246 | def test_escaping(self):
247 | self.assert_match('foo(', r'foo\(')
248 | self.assert_match('foo.', r'foo\.')
249 |
250 | def test_dot(self):
251 | text = 'foo'
252 | for char in self.ALPHABET:
253 | self.assert_match(text + char, re.escape(text) + '.')
254 |
255 | def test_dots(self):
256 | text = 'foo'
257 | for suffix in self.suffixes():
258 | self.assert_match(text + suffix, re.escape(text) + '.*')
259 |
260 | def test_square_brackets(self):
261 | text = 'foo'
262 | for suffix in self.suffixes():
263 | square_pattern = ''.join('[%s]' % char for char in suffix)
264 | self.assert_match(text + suffix, re.escape(text) + square_pattern)
265 |
266 | test_repr = lambda self: self.assert_repr(__unit__.Regex('.'))
267 |
268 | # Assertion functions
269 |
270 | def assert_match(self, value, pattern):
271 | return super(Regex, self).assert_match(__unit__.Regex(pattern), value)
272 |
273 | def assert_no_match(self, value, pattern):
274 | return super(Regex, self) \
275 | .assert_no_match(__unit__.Regex(pattern), value)
276 |
--------------------------------------------------------------------------------
/tests/test_general.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for general matchers.
3 | """
4 | import sys
5 |
6 | from taipan.testing import skipIf, skipUnless
7 |
8 | import callee.general as __unit__
9 | from tests import MatcherTestCase
10 |
11 |
12 | IS_PY33 = sys.version_info >= (3, 3)
13 |
14 |
15 | class Any(MatcherTestCase):
16 | test_none = lambda self: self.assert_match(None)
17 | test_zero = lambda self: self.assert_match(0)
18 | test_empty_string = lambda self: self.assert_match('')
19 | test_empty_list = lambda self: self.assert_match([])
20 | test_empty_tuple = lambda self: self.assert_match(())
21 | test_some_object = lambda self: self.assert_match(object())
22 | test_some_string = lambda self: self.assert_match("Alice has a cat")
23 | test_some_number = lambda self: self.assert_match(42)
24 | test_some_list = lambda self: self.assert_match([1, 2, 3, 5, 8, 13])
25 | test_some_tuple = lambda self: self.assert_match(('foo', -1, ['bar']))
26 |
27 | test_repr = lambda self: self.assert_repr(__unit__.Any())
28 |
29 | def assert_match(self, value):
30 | return super(Any, self).assert_match(__unit__.Any(), value)
31 |
32 |
33 | # Predicate matcher
34 |
35 | class Matching(MatcherTestCase):
36 | EVEN = staticmethod(lambda x: x % 2 == 0)
37 | ODD = staticmethod(lambda x: x % 2 != 0)
38 |
39 | SHORTER_THAN_THREE = staticmethod(lambda x: len(x) < 3)
40 | LONGER_THAN_THREE = staticmethod(lambda x: len(x) > 3)
41 |
42 | def test_invalid_predicate(self):
43 | with self.assertRaises(TypeError):
44 | __unit__.Matching(object())
45 |
46 | def test_none(self):
47 | # Exceptions from the matcher's predicate should be let through.
48 | with self.assertRaises(TypeError):
49 | self.assert_no_match(None, self.EVEN)
50 | with self.assertRaises(TypeError):
51 | self.assert_no_match(None, self.ODD)
52 |
53 | def test_zero(self):
54 | self.assert_match(0, self.EVEN)
55 | self.assert_no_match(0, self.ODD)
56 |
57 | def test_empty_string(self):
58 | self.assert_match('', self.SHORTER_THAN_THREE)
59 | self.assert_no_match('', self.LONGER_THAN_THREE)
60 |
61 | def test_empty_list(self):
62 | self.assert_match([], self.SHORTER_THAN_THREE)
63 | self.assert_no_match([], self.LONGER_THAN_THREE)
64 |
65 | def test_empty_tuple(self):
66 | self.assert_match((), self.SHORTER_THAN_THREE)
67 | self.assert_no_match((), self.LONGER_THAN_THREE)
68 |
69 | def test_some_string(self):
70 | s = "Alice has a cat"
71 | self.assert_no_match(s, self.SHORTER_THAN_THREE)
72 | self.assert_match(s, self.LONGER_THAN_THREE)
73 |
74 | # Assertion functions
75 |
76 | def assert_match(self, value, predicate):
77 | return super(Matching, self) \
78 | .assert_match(__unit__.Matching(predicate), value)
79 |
80 | def assert_no_match(self, value, predicate):
81 | return super(Matching, self) \
82 | .assert_no_match(__unit__.Matching(predicate), value)
83 |
84 |
85 | class MatchingRepr(MatcherTestCase):
86 | """Tests for the __repr__ method of Matching."""
87 |
88 | def tesc_desc__empty(self):
89 | matcher = __unit__.Matching(bool, "")
90 | self.assertIn('""', repr(matcher))
91 |
92 | def test_desc__nonempty(self):
93 | desc = "Truthy"
94 | matcher = __unit__.Matching(bool, desc)
95 | self.assertIn(desc, repr(matcher))
96 |
97 | def test_desc__trimmed(self):
98 | desc = "Long description with extraneous characters: %s" % (
99 | "x" * __unit__.Matching.MAX_DESC_LENGTH,)
100 | matcher = __unit__.Matching(bool, desc)
101 |
102 | self.assertNotIn(desc, repr(matcher))
103 | self.assertIn("...", repr(matcher))
104 |
105 | def test_lambda(self):
106 | self.assert_lambda_repr(lambda _: True)
107 |
108 | @skipIf(IS_PY33, "requires Python 2.x or 3.2")
109 | def test_local_function__py2(self):
110 | def predicate(_):
111 | return True
112 | self.assert_named_repr('predicate', predicate)
113 |
114 | @skipUnless(IS_PY33, "requires Python 3.3+")
115 | def test_local_function__py33(self):
116 | def predicate(_):
117 | return True
118 | matcher = __unit__.Matching(predicate)
119 | self.assertIn('.predicate', repr(matcher))
120 |
121 | def test_function(self):
122 | self.assert_named_repr('predicate', predicate)
123 |
124 | def test_staticmethod__lambda(self):
125 | self.assert_lambda_repr(MatchingRepr.staticmethod_lambda)
126 |
127 | @skipIf(IS_PY33, "requires Python 2.x or 3.2")
128 | def test_staticmethod__function__py2(self):
129 | # In Python 2, static methods are exactly the same as global functions.
130 | self.assert_named_repr('staticmethod_function',
131 | MatchingRepr.staticmethod_function)
132 |
133 | @skipUnless(IS_PY33, "requires Python 3.3+")
134 | def test_staticmethod__function__py33(self):
135 | self.assert_named_repr('MatchingRepr.staticmethod_function',
136 | MatchingRepr.staticmethod_function)
137 |
138 | def test_classmethod__lambda(self):
139 | self.assert_lambda_repr(MatchingRepr.classmethod_lambda)
140 |
141 | @skipIf(IS_PY33, "requires Python 2.x or 3.2")
142 | def test_classmethod__function__py2(self):
143 | matcher = __unit__.Matching(MatchingRepr.classmethod_function)
144 | self.assertIn(' classmethod_function', repr(matcher))
145 |
146 | @skipUnless(IS_PY33, "requires Python 3.3+")
147 | def test_classmethod__function__py33(self):
148 | self.assert_named_repr('MatchingRepr.classmethod_function',
149 | MatchingRepr.classmethod_function)
150 |
151 | def test_class(self):
152 | self.assert_named_repr('Class', Class)
153 |
154 | @skipIf(IS_PY33, "requires Python 2.x or 3.2")
155 | def test_inner_class__py2(self):
156 | self.assert_named_repr('Class', MatchingRepr.Class)
157 |
158 | @skipUnless(IS_PY33, "requires Python 3.3+")
159 | def test_inner_class__py33(self):
160 | self.assert_named_repr('MatchingRepr.Class', MatchingRepr.Class)
161 |
162 | @skipIf(IS_PY33, "requires Python 2.x or 3.2")
163 | def test_local_class__py2(self):
164 | class Class(object):
165 | def __call__(self, _):
166 | return True
167 | self.assert_named_repr('Class', Class)
168 |
169 | @skipUnless(IS_PY33, "requires Python 3.3+")
170 | def test_local_class__py33(self):
171 | class Class(object):
172 | def __call__(self, _):
173 | return True
174 | matcher = __unit__.Matching(Class)
175 | self.assertIn('.Class', repr(matcher))
176 |
177 | def test_callable_object(self):
178 | matcher = __unit__.Matching(Class())
179 | self.assertIn('object at', repr(matcher))
180 |
181 | # Utility functons
182 |
183 | def assert_lambda_repr(self, predicate):
184 | matcher = __unit__.Matching(predicate)
185 | self.assertIn(' ', repr(matcher)) # the space matters!
186 |
187 | def assert_named_repr(self, name, predicate):
188 | matcher = __unit__.Matching(predicate)
189 | self.assertIn(':' + name, repr(matcher))
190 |
191 | # Test predicates
192 |
193 | staticmethod_lambda = staticmethod(lambda _: True)
194 |
195 | @staticmethod
196 | def staticmethod_function(_):
197 | return True
198 |
199 | classmethod_lambda = classmethod(lambda cls, _: True)
200 |
201 | @classmethod
202 | def classmethod_function(cls, _):
203 | return True
204 |
205 | class Class(object):
206 | def __call__(self, _):
207 | return True
208 |
209 |
210 | def predicate(_):
211 | return True
212 |
213 |
214 | class Class(object):
215 | def __call__(self, _):
216 | return True
217 |
218 |
219 | # Argument captor
220 |
221 | class Captor(MatcherTestCase):
222 | ARG = object()
223 | FALSE_MATCHER = __unit__.Matching(lambda _: False)
224 |
225 | def test_ctor__no_args(self):
226 | captor = __unit__.Captor()
227 | self.assertIsInstance(captor.matcher, __unit__.Any)
228 |
229 | def test_ctor__invalid_matcher(self):
230 | not_matcher = object()
231 | with self.assertRaisesRegexp(TypeError, r'expected'):
232 | __unit__.Captor(not_matcher)
233 |
234 | def test_ctor__matcher(self):
235 | matcher = __unit__.Matching(bool)
236 | captor = __unit__.Captor(matcher)
237 | self.assertIs(matcher, captor.matcher)
238 |
239 | def test_ctor__captor(self):
240 | captor = __unit__.Captor()
241 | with self.assertRaisesRegexp(TypeError, r'captor'):
242 | __unit__.Captor(captor)
243 |
244 | def test_has_value__initial(self):
245 | captor = __unit__.Captor()
246 | self.assertFalse(captor.has_value())
247 |
248 | def test_has_value__not_captured(self):
249 | # When the argument fails the matcher test, it should not be captured.
250 | captor = __unit__.Captor(self.FALSE_MATCHER)
251 | captor.match(self.ARG)
252 | self.assertFalse(captor.has_value())
253 |
254 | def test_has_value__captured(self):
255 | captor = __unit__.Captor()
256 | captor.match(self.ARG)
257 | self.assertTrue(captor.has_value())
258 |
259 | def test_arg__initial(self):
260 | captor = __unit__.Captor()
261 | with self.assertRaisesRegexp(ValueError, r'no value'):
262 | captor.arg
263 |
264 | def test_arg__not_captured(self):
265 | # When the argument fails the matcher test, it should not be captured.
266 | captor = __unit__.Captor(self.FALSE_MATCHER)
267 | captor.match(self.ARG)
268 | with self.assertRaisesRegexp(ValueError, r'no value'):
269 | captor.arg
270 |
271 | def test_arg__captured(self):
272 | captor = __unit__.Captor()
273 | captor.match(self.ARG)
274 | self.assertIs(self.ARG, captor.arg)
275 |
276 | def test_match__captured(self):
277 | captor = __unit__.Captor()
278 | self.assertTrue(captor.match(self.ARG))
279 |
280 | def test_match___not_captured(self):
281 | captor = __unit__.Captor(self.FALSE_MATCHER)
282 | self.assertFalse(captor.match(self.ARG))
283 |
284 | def test_match__double_capture(self):
285 | captor = __unit__.Captor()
286 | captor.match(self.ARG)
287 | with self.assertRaisesRegexp(ValueError, r'already'):
288 | captor.match(self.ARG)
289 |
290 | def test_repr__not_captured(self):
291 | captor = __unit__.Captor()
292 | self.assertNotIn("(*)", repr(captor))
293 |
294 | def test_repr__captured(self):
295 | captor = __unit__.Captor()
296 | captor.match(self.ARG)
297 | self.assertIn("(*)", repr(captor))
298 |
--------------------------------------------------------------------------------
/tests/test_base.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | """
3 | Tests for matcher base classes.
4 | """
5 | import callee.base as __unit__
6 | from tests import MatcherTestCase, TestCase
7 |
8 |
9 | class BaseMatcherMetaclass(TestCase):
10 | """Tests for the BaseMatcherMetaclass."""
11 |
12 | def test_validate_class_definition(self):
13 | """Test for BaseMatcherMetaclass._validate_class_definition."""
14 | vcd = __unit__.BaseMatcherMetaclass._validate_class_definition
15 |
16 | bases = (object,)
17 | method = lambda self: None
18 |
19 | vcd('BaseMatcher', bases, __unit__.BaseMatcher.__dict__)
20 | vcd('Foo', bases, {}) # OK, empty definition
21 | vcd('Foo', bases, {'foo': 42}) # OK, no methods at all
22 | vcd('Foo', bases, {'__foo__': 42}) # OK, magic name but not method
23 | vcd('Foo', bases, {'__foo__': method}) # OK, not BaseMatcher's method
24 | vcd('Foo', bases, {'__init__': method}) # OK, explicitly allowed
25 | with self.assertRaises(RuntimeError):
26 | vcd('Foo', bases, {'__eq__': method}) # trying to mess up!
27 |
28 | def test_is_base_matcher_class_definition(self):
29 | """Test for BaseMatcherMetaclass._is_base_matcher_class_definition."""
30 | is_bmcd = \
31 | __unit__.BaseMatcherMetaclass._is_base_matcher_class_definition
32 |
33 | self.assertFalse(is_bmcd('Foo', {})) # wrong name
34 | self.assertFalse(is_bmcd('BaseMatcher', {})) # needs members
35 | self.assertFalse(is_bmcd('BaseMatcher', {'foo': 1})) # needs methods
36 | self.assertFalse( # needs methods from same module
37 | is_bmcd('BaseMatcher', {'foo': lambda self: None}))
38 |
39 | self.assertTrue(is_bmcd('BaseMatcher', __unit__.BaseMatcher.__dict__))
40 |
41 | def test_list_magic_methods(self):
42 | """Test for BaseMatcherMetaclass._list_magic_methods."""
43 | lmm = __unit__.BaseMatcherMetaclass._list_magic_methods
44 |
45 | class Foo(object):
46 | pass
47 | self.assertItemsEqual([], lmm(Foo))
48 |
49 | class Bar(object):
50 | def method(self):
51 | pass
52 | self.assertItemsEqual([], lmm(Bar))
53 |
54 | class Baz(object):
55 | def __init__(self):
56 | pass
57 | def __rdiv__(self, other):
58 | return self
59 | self.assertItemsEqual(['init', 'rdiv'], lmm(Baz))
60 |
61 | class Qux(object):
62 | def __init__(self):
63 | pass
64 | def foo(self):
65 | pass
66 | self.assertItemsEqual(['init'], lmm(Qux))
67 |
68 | class Thud(object):
69 | def __bool__(self):
70 | return False
71 | __nonzero__ = __bool__
72 | self.assertItemsEqual(['bool', 'nonzero'], lmm(Thud))
73 |
74 |
75 | class Matcher(TestCase):
76 | """Tests for the Matcher base class."""
77 |
78 | def test_match(self):
79 | """Test default match() is left to be implemented by subclasses."""
80 | class Custom(__unit__.Matcher):
81 | pass
82 | matcher = Custom()
83 | with self.assertRaises(NotImplementedError):
84 | matcher.match(None)
85 |
86 | def test_repr__no_ctor(self):
87 | """Test default __repr__ of Matcher subclass without a constructor."""
88 | class Custom(__unit__.Matcher):
89 | pass
90 | self.assertEquals("", "%r" % Custom())
91 |
92 | def test_repr__argless_ctor__no_state(self):
93 | """Test default __repr__ of Matcher subclass with argless ctor."""
94 | class Custom(__unit__.Matcher):
95 | def __init__(self):
96 | pass
97 | self.assertEquals("", "%r" % Custom())
98 |
99 | def test_repr__argless_ctor__with_state(self):
100 | """Test __repr__ of Matcher subclass with argless ctor & state."""
101 | class Custom(__unit__.Matcher):
102 | def __init__(self):
103 | self.foo = 42
104 | self.assertEquals("", "%r" % Custom())
105 |
106 | def test_repr__argful_ctor__no_state(self):
107 | """Test __repr__ with argful constructor but no actual fields."""
108 | class Custom(__unit__.Matcher):
109 | def __init__(self, unused):
110 | pass
111 | self.assertEquals("", "%r" % Custom('unused'))
112 |
113 | def test_repr__argful_ctor__with_state_from_args(self):
114 | """Test __repr__ with argful constructor & object fields."""
115 | class Custom(__unit__.Matcher):
116 | def __init__(self, foo):
117 | self.foo = foo
118 |
119 | foo = 'bar'
120 | self.assertEquals("" % (foo,),
121 | "%r" % Custom(foo='bar'))
122 |
123 | def test_repr__argful_ctor__with_unrelated_state(self):
124 | """Test __repr__ with argful ctor & unrelated object fields."""
125 | class Custom(__unit__.Matcher):
126 | def __init__(self, foo):
127 | self.bar = 42
128 |
129 | self.assertEquals("", "%r" % Custom(foo='unused'))
130 |
131 |
132 | class Eq(MatcherTestCase):
133 | """Tests for the Eq matcher."""
134 |
135 | def test_regular_objects(self):
136 | """Test that Eq is a no-op for regular objects."""
137 | self.assert_match(__unit__.Eq(None), None)
138 | self.assert_match(__unit__.Eq(0), 0)
139 | self.assert_match(__unit__.Eq(""), "")
140 | self.assert_match(__unit__.Eq([]), [])
141 | self.assert_match(__unit__.Eq(()), ())
142 |
143 | # Arbitary objects are only equal in the `is` sense.
144 | obj = object()
145 | self.assert_match(__unit__.Eq(obj), obj)
146 |
147 | def test_matchers(self):
148 | """Test that Eq allows to treat matchers as values."""
149 | # Hypothetical objects that we want to check the equality of,
150 | # where one is by some accident a Matcher.
151 | eq_by_x = lambda this, other: this.x == getattr(other, 'x', object())
152 | class RegularValue(object):
153 | def __init__(self, x):
154 | self.x = x
155 | def __eq__(self, other):
156 | return eq_by_x(self, other)
157 | class MatcherValue(__unit__.Matcher):
158 | def __init__(self, x):
159 | self.x = x
160 | def match(self, value):
161 | return eq_by_x(self, value)
162 |
163 | # Matching against a matcher object is an error if Eq isn't used.
164 | with self.assertRaises(TypeError) as r:
165 | self.assert_match(MatcherValue(42), MatcherValue(42))
166 | self.assertIn("incorrect use of matcher object", str(r.exception))
167 |
168 | # It's fine with Eq, though.
169 | self.assert_match(__unit__.Eq(RegularValue(42)), MatcherValue(42))
170 |
171 | def test_repr(self):
172 | """Test for the __repr__ method."""
173 | value = 42
174 | eq = __unit__.Eq(value)
175 | self.assert_repr(eq, value)
176 |
177 |
178 | class LogicalCombinators(MatcherTestCase):
179 | """Tests for the logical combinators (Not, And, etc.)."""
180 |
181 | def test_not(self):
182 | test_strings = ['', 'a', '42', 'a13', '99b', '!', '22 ?']
183 | not_no_digits = ~self.NoDigits() # i.e. HasDigits
184 | has_digits = self.HasDigits()
185 | for s in test_strings:
186 | self.assertEquals(
187 | has_digits.match(s), not_no_digits.match(s),
188 | msg="expected `%r` and `%r` to match %r equivalently" % (
189 | has_digits, not_no_digits, s))
190 |
191 | def test_not__repr(self):
192 | not_all_digits = ~self.AllDigits()
193 | self.assert_repr(not_all_digits)
194 |
195 | def test_and__impossible(self):
196 | test_strings = ['', 'a', '42', 'a13', '99b']
197 | impossible = self.AllDigits() & self.NoDigits()
198 | for s in test_strings:
199 | self.assertFalse(
200 | impossible.match(s),
201 | msg="%r matched an impossible matcher %r" % (s, impossible))
202 |
203 | def test_and__idempotent(self):
204 | test_strings = ['', 'a', '42', 'a13', '99b', '!', '22 ?']
205 | all_digits_and_all_digits = self.AllDigits() & self.AllDigits()
206 | all_digits = self.AllDigits()
207 | for s in test_strings:
208 | self.assertEquals(
209 | all_digits.match(s), all_digits_and_all_digits.match(s),
210 | msg="expected `%r` and `%r` to match %r equivalently" % (
211 | all_digits, all_digits_and_all_digits, s))
212 |
213 | def test_and__regular(self):
214 | test_strings = ['', '42', '31337', 'abcdef', 'a42', '22?']
215 | short_and_digits = self.Short() & self.AllDigits()
216 | short_digits = self.ShortDigits()
217 | for s in test_strings:
218 | self.assertEquals(
219 | short_digits.match(s), short_and_digits.match(s),
220 | msg="expected `%r` and `%r` to match %r equivalently" % (
221 | short_digits, short_and_digits, s))
222 |
223 | def test_and__repr(self):
224 | short_and_digits = self.Short() & self.AllDigits()
225 | self.assert_repr(short_and_digits)
226 |
227 | def test_or__trivially_true(self):
228 | test_strings = ['', 'abc', '123456789', 'qwerty?', '!!!!one']
229 | true = self.Short() | self.Long()
230 | for s in test_strings:
231 | self.assertTrue(
232 | true.match(s),
233 | msg="%r didn't match a trivially true matcher %r" % (s, true))
234 |
235 | def test_or__idempotent(self):
236 | test_strings = ['', '42', '31337', 'abcdef', 'a42', '22?']
237 | short_or_short = self.Short() | self.Short()
238 | short = self.Short()
239 | for s in test_strings:
240 | self.assertEquals(
241 | short.match(s), short_or_short.match(s),
242 | msg="expected `%r` and `%r` to match %r equivalently" % (
243 | short, short_or_short, s))
244 |
245 | def test_or__regular(self):
246 | test_strings = ['', '42', '31337', 'abcdef', 'qwerty55', 'a42', '22?']
247 | has_digits_or_long = self.HasDigits() | self.Long()
248 | long_or_has_digits = self.LongOrHasDigits()
249 | for s in test_strings:
250 | self.assertEquals(
251 | long_or_has_digits.match(s), has_digits_or_long.match(s),
252 | msg="expected `%r` and `%r` to match %r equivalently" % (
253 | long_or_has_digits, has_digits_or_long, s))
254 |
255 | def test_or__repr(self):
256 | has_digits_or_short = self.HasDigits() | self.Short()
257 | self.assert_repr(has_digits_or_short)
258 |
259 | def test_xor__impossible(self):
260 | test_strings = ['', 'a', '42', 'a13', '99b', '!', '22 ?']
261 | impossible = self.HasDigits() ^ self.HasDigits() # a^a <=> ~a
262 | for s in test_strings:
263 | self.assertFalse(
264 | impossible.match(s),
265 | msg="%r matched an impossible matcher" % (s,))
266 |
267 | def test_xor__trivially_true(self):
268 | test_strings = ['', 'abc', '123456789', 'qwerty?', '!!!!one']
269 | true = self.NoDigits() ^ self.HasDigits()
270 | for s in test_strings:
271 | self.assertTrue(
272 | true.match(s),
273 | msg="%r didn't match a trivially true matcher %r" % (s, true))
274 |
275 | def test_xor__as_and_not(self):
276 | test_strings = ['', '42', '31337', 'abcdef', 'a42', '22?']
277 | any_xor_all_digits = self.HasDigits() ^ self.AllDigits()
278 | only_some_digits = self.HasDigits() & ~self.AllDigits()
279 | for s in test_strings:
280 | # Note that the truth of assertion is specific to those predicates:
281 | # the second one implies the first one.
282 | self.assertEquals(
283 | only_some_digits.match(s), any_xor_all_digits.match(s),
284 | msg="expected `%r` and `%r` to match %r equivalently" % (
285 | only_some_digits, any_xor_all_digits, s))
286 |
287 | def test_xor__repr(self):
288 | all_digits_xor_short = self.AllDigits() ^ self.Short()
289 | self.assert_repr(all_digits_xor_short)
290 |
291 | # Utility code
292 |
293 | class NoDigits(__unit__.Matcher):
294 | def match(self, value):
295 | return all(not c.isdigit() for c in value)
296 |
297 | class HasDigits(__unit__.Matcher):
298 | def match(self, value):
299 | return any(c.isdigit() for c in value)
300 |
301 | class AllDigits(__unit__.Matcher):
302 | def match(self, value):
303 | return value.isdigit()
304 |
305 | class Short(__unit__.Matcher):
306 | def match(self, value):
307 | return len(value) < 5
308 |
309 | class ShortDigits(__unit__.Matcher):
310 | def match(self, value):
311 | return value.isdigit() and len(value) < 5
312 |
313 | class Long(__unit__.Matcher):
314 | def match(self, value):
315 | return len(value) >= 5
316 |
317 | class LongOrHasDigits(__unit__.Matcher):
318 | def match(self, value):
319 | return len(value) >= 5 or any(c.isdigit() for c in value)
320 |
--------------------------------------------------------------------------------
/callee/base.py:
--------------------------------------------------------------------------------
1 | """
2 | Base classes for argument matchers.
3 | """
4 | import inspect
5 | from operator import itemgetter
6 |
7 | from callee._compat import IS_PY3, metaclass
8 |
9 |
10 | __all__ = [
11 | 'Matcher',
12 | 'Eq', 'Is', 'IsNot',
13 | 'Not', 'And', 'Or', 'Either', 'OneOf', 'Xor',
14 | ]
15 |
16 |
17 | class BaseMatcherMetaclass(type):
18 | """Metaclass for :class:`BaseMatcher`."""
19 |
20 | #: What __magic__ methods of :class:`BaseMatcher`
21 | #: can be overriden by user-defined subclasses.
22 | #:
23 | #: Any method not on this list can only be overridden by classes defined
24 | #: within this module. This prevents users from accidentally interfering
25 | #: with fundamental matcher functionality while writing their own matchers.
26 | #
27 | #: The names are given without the leading or trailing underscores.
28 | #:
29 | USER_OVERRIDABLE_MAGIC_METHODS = ('init', 'repr')
30 |
31 | def __new__(meta, classname, bases, dict_):
32 | """Create a new matcher class."""
33 | meta._validate_class_definition(classname, bases, dict_)
34 | return super(BaseMatcherMetaclass, meta) \
35 | .__new__(meta, classname, bases, dict_)
36 |
37 | @classmethod
38 | def _validate_class_definition(meta, classname, bases, dict_):
39 | """Ensure the matcher class definition is acceptable.
40 | :raise RuntimeError: If there is a problem
41 | """
42 | # let the BaseMatcher class be created without hassle
43 | if meta._is_base_matcher_class_definition(classname, dict_):
44 | return
45 |
46 | # ensure that no important magic methods are being overridden
47 | for name, member in dict_.items():
48 | if not (name.startswith('__') and name.endswith('__')):
49 | continue
50 |
51 | # check if it's not a whitelisted magic method name
52 | name = name[2:-2]
53 | if not name:
54 | continue # unlikely case of a ``____`` function
55 | if name not in meta._list_magic_methods(BaseMatcher):
56 | continue
57 | if name in meta.USER_OVERRIDABLE_MAGIC_METHODS:
58 | continue
59 |
60 | # non-function attributes, like __slots__, are harmless
61 | if not inspect.isfunction(member):
62 | continue
63 |
64 | # classes in this very module are exempt, since they define
65 | # the very behavior of matchers we want to protect
66 | if member.__module__ == __name__:
67 | continue
68 |
69 | raise RuntimeError(
70 | "matcher class %s cannot override the __%s__ method" % (
71 | classname, name))
72 |
73 | @classmethod
74 | def _is_base_matcher_class_definition(meta, classname, dict_):
75 | """Checks whether given class name and dictionary
76 | define the :class:`BaseMatcher`.
77 | """
78 | if classname != 'BaseMatcher':
79 | return False
80 | methods = list(filter(inspect.isfunction, dict_.values()))
81 | return methods and all(m.__module__ == __name__ for m in methods)
82 |
83 | @classmethod
84 | def _list_magic_methods(meta, class_):
85 | """Return names of magic methods defined by a class.
86 | :return: Iterable of magic methods, each w/o the ``__`` prefix/suffix
87 | """
88 | return [
89 | name[2:-2] for name, member in class_.__dict__.items()
90 | if len(name) > 4 and name.startswith('__') and name.endswith('__')
91 | and inspect.isfunction(member)
92 | ]
93 |
94 | # TODO: consider making matcher classes interchangeable with matcher
95 | # objects created w/o ctor args, i.e. making Integer and Integer()
96 | # equivalent; it'd require this metaclass to implement the magic methods
97 | # from BaseMatcher and something better than `isinstance(x, BaseMatcher)`
98 |
99 |
100 | @metaclass(BaseMatcherMetaclass)
101 | class BaseMatcher(object):
102 | """Base class for all argument matchers.
103 |
104 | This class shouldn't be used directly by the clients.
105 | To create custom matchers, inherit from :class:`Matcher` instead.
106 | """
107 | __slots__ = ()
108 |
109 | def match(self, value):
110 | raise NotImplementedError("matching not implemented")
111 |
112 | def __repr__(self):
113 | return ""
114 |
115 | def __eq__(self, other):
116 | if isinstance(other, BaseMatcher):
117 | raise TypeError(
118 | "incorrect use of matcher object as a value to match on")
119 | return self.match(other)
120 |
121 | # TODO: make matcher objects callable
122 |
123 | def __invert__(self):
124 | return Not(self)
125 |
126 | def __and__(self, other):
127 | matchers = other._matchers if isinstance(other, And) else [other]
128 | return And(self, *matchers)
129 |
130 | def __or__(self, other):
131 | matchers = other._matchers if isinstance(other, Or) else [other]
132 | return Or(self, *matchers)
133 |
134 | def __xor__(self, other):
135 | matchers = other._matchers if isinstance(other, Either) else [other]
136 | return Either(self, *matchers)
137 |
138 |
139 | class Matcher(BaseMatcher):
140 | """Base class for custom (user-defined) argument matchers.
141 |
142 | To create a custom matcher, simply inherit from this class
143 | and implement the :meth:`match` method.
144 |
145 | If the matcher is more complicated (e.g. parametrized),
146 | you may also want to provide a :meth:`__repr__` method implementation
147 | for better error messages.
148 | """
149 | def __repr__(self):
150 | """Provides a default ``repr``\ esentation for custom matchers.
151 |
152 | This representation will include matcher class name
153 | and the values of its public attributes.
154 | If that's insufficient, consider overriding this method.
155 | """
156 | args = ""
157 |
158 | # check if the matcher class has a parametrized constructor
159 | has_argful_ctor = False
160 | if '__init__' in self.__class__.__dict__:
161 | argnames, vargargs, kwargs, _ = inspect.getargspec(
162 | self.__class__.__init__)
163 | has_argful_ctor = bool(argnames[1:] or vargargs or kwargs)
164 |
165 | # if so, then it probably means it has some interesting state
166 | # in its attributes which we can include in the default representation
167 | if has_argful_ctor:
168 | # TODO: __getstate__ instead of __dict__?
169 | fields = [(name, value) for name, value in self.__dict__.items()
170 | if not name.startswith('_')]
171 | if fields:
172 | def repr_value(value):
173 | """Safely represent a value as an ASCII string."""
174 | if isinstance(value, bytes):
175 | value = value.decode('ascii', 'ignore')
176 | if not IS_PY3 and isinstance(value, unicode):
177 | value = value.encode('ascii', 'replace')
178 | value = str(value)
179 | return repr(value)
180 |
181 | fields.sort(key=itemgetter(0))
182 | args = "(%s)" % ", ".join(
183 | "%s=%s" % (name, repr_value(value)[:32])
184 | for name, value in fields)
185 | else:
186 | args = "(...)"
187 |
188 | return "<%s%s>" % (self.__class__.__name__, args)
189 |
190 |
191 | # Special cases around equality/identity
192 |
193 | class Eq(BaseMatcher):
194 | """Matches a value exactly using the equality (``==``) operator.
195 |
196 | This is already the default mode of operation for ``assert_called_with``
197 | methods on mocks, making this matcher redundant in most situations::
198 |
199 | mock_foo.assert_called_with(bar)
200 | mock_foo.assert_called_with(Eq(bar)) # equivalent
201 |
202 | In very rare and specialized cases, however, if the **tested code** treats
203 | `callee` matcher objects in some special way, using :class:`Eq` may be
204 | necessary.
205 |
206 | Those situations shouldn't generally arise outside of writing tests
207 | for code that is itself a test library or helper.
208 | """
209 | def __init__(self, value):
210 | """:param value: Value to match against"""
211 | self.value = value
212 |
213 | def match(self, value):
214 | return self.value == value
215 |
216 | def __eq__(self, other):
217 | return self.match(other)
218 |
219 | def __repr__(self):
220 | # This representation matches the format of comparison operators
221 | # (such as :class:`Less`) defined in the ``.operators`` module.
222 | return "<... == %r>" % (self.value,)
223 |
224 |
225 | class Is(BaseMatcher):
226 | """Matches a value using the identity (``is``) operator."""
227 |
228 | def __init__(self, value):
229 | self.value = value
230 |
231 | def match(self, value):
232 | return value is self.value
233 |
234 | def __eq__(self, other):
235 | return self.match(other)
236 |
237 | def __repr__(self):
238 | # This representation matches the format of comparison operators
239 | # (such as :class:`Less`) defined in the ``.operators`` module.
240 | return "<... is %r>" % (self.value,)
241 |
242 |
243 | class IsNot(BaseMatcher):
244 | """Matches a value using the negated identity (``is not``) operator."""
245 |
246 | def __init__(self, value):
247 | self.value = value
248 |
249 | def match(self, value):
250 | return value is not self.value
251 |
252 | def __eq__(self, other):
253 | return self.match(other)
254 |
255 | def __repr__(self):
256 | # This representation matches the format of comparison operators
257 | # (such as :class:`Less`) defined in the ``.operators`` module.
258 | return "<... is not %r>" % (self.value,)
259 |
260 |
261 | # Logical combinators for matchers
262 |
263 | class Not(BaseMatcher):
264 | """Negates given matcher.
265 |
266 | :param matcher: Matcher object to negate the semantics of
267 | """
268 | def __init__(self, matcher):
269 | assert isinstance(matcher, BaseMatcher), "Not() expects a matcher"
270 | self._matcher = matcher
271 |
272 | def match(self, value):
273 | return not self._matcher.match(value)
274 |
275 | def __repr__(self):
276 | return "not %r" % (self._matcher,)
277 |
278 | def __invert__(self):
279 | return self._matcher
280 |
281 | def __and__(self, other):
282 | # convert (~a) & (~b) into ~(a | b) which is one operation less
283 | # but still equivalent as per de Morgan laws
284 | if isinstance(other, Not):
285 | return Not(self.matcher | other.matcher)
286 | return super(Not, self).__and__(other)
287 |
288 | def __or__(self, other):
289 | # convert (~a) | (~b) into ~(a & b) which is one operation less
290 | # but still equivalent as per de Morgan laws
291 | if isinstance(other, Not):
292 | return Not(self.matcher & other.matcher)
293 | return super(Not, self).__or__(other)
294 |
295 |
296 | class And(BaseMatcher):
297 | """Matches the argument only if all given matchers do."""
298 |
299 | def __init__(self, *matchers):
300 | assert matchers, "And() expects at least one matcher"
301 | assert all(isinstance(m, BaseMatcher)
302 | for m in matchers), "And() expects matchers"
303 | self._matchers = list(matchers)
304 |
305 | # TODO: coalesce a & b & c into single And(a, b, c)
306 |
307 | def match(self, value):
308 | return all(matcher.match(value) for matcher in self._matchers)
309 |
310 | def __repr__(self):
311 | return "<%s>" % " and ".join(map(repr, self._matchers))
312 |
313 |
314 | class Or(BaseMatcher):
315 | """Matches the argument only if at least one given matcher does."""
316 |
317 | def __init__(self, *matchers):
318 | assert matchers, "Or() expects at least one matcher"
319 | assert all(isinstance(m, BaseMatcher)
320 | for m in matchers), "Or() expects matchers"
321 | self._matchers = list(matchers)
322 |
323 | # TODO: coalesce a | b | c into single Or(a, b, c)
324 |
325 | def match(self, value):
326 | return any(matcher.match(value) for matcher in self._matchers)
327 |
328 | def __repr__(self):
329 | return "<%s>" % " or ".join(map(repr, self._matchers))
330 |
331 |
332 | class Either(BaseMatcher):
333 | """Matches the argument only if some (but not all) of given matchers do.
334 |
335 | .. versionadded:: 0.3
336 | """
337 | def __init__(self, *matchers):
338 | assert len(matchers) >= 2, "Either() expects at least two matchers"
339 | assert all(isinstance(m, BaseMatcher)
340 | for m in matchers), "Either() expects matchers"
341 | self._matchers = list(matchers)
342 |
343 | def match(self, value):
344 | any_matches = bool(self._matchers[0].match(value))
345 | for matcher in self._matchers[1:]:
346 | is_match = bool(matcher.match(value))
347 | if is_match != any_matches:
348 | return True
349 | any_matches |= is_match
350 | return False
351 |
352 | def __repr__(self):
353 | return "<%s>" % " xor ".join(map(repr, self._matchers))
354 |
355 | #: Alias for :class:`Either`.
356 | OneOf = Either
357 | #: Alias for :class:`Either`.
358 | Xor = Either
359 |
--------------------------------------------------------------------------------
/tests/test_operators.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for operators' matchers.
3 | """
4 | from itertools import chain, combinations
5 |
6 | import callee.operators as __unit__
7 | from tests import MatcherTestCase
8 |
9 |
10 | class OperatorTestCase(MatcherTestCase):
11 | """Base class for operator matchers' tests."""
12 |
13 | def subsets(self, s, strict=False):
14 | """Return a set of all subsets of set ``s``."""
15 | max_subset_size = len(s)
16 | if not strict:
17 | max_subset_size += 1
18 | return set(map(frozenset,
19 | chain.from_iterable(combinations(s, n)
20 | for n in range(max_subset_size))))
21 |
22 |
23 | # Simple comparisons
24 |
25 | class Less(OperatorTestCase):
26 |
27 | def test_numbers(self):
28 | ref = 42
29 |
30 | self.assert_match(0, ref)
31 | self.assert_match(-ref, ref)
32 | self.assert_match(3.14, ref)
33 |
34 | self.assert_no_match(ref, ref)
35 | self.assert_no_match(2 * ref, ref)
36 |
37 | def test_strings(self):
38 | ref = "Alice has a cat"
39 |
40 | for i in range(len(ref) - 1):
41 | self.assert_match(ref[:i], ref)
42 |
43 | self.assert_no_match(ref, ref)
44 | self.assert_no_match(2 * ref, ref)
45 | self.assert_no_match("Bob has a cat", ref)
46 |
47 | def test_sets(self):
48 | ref = set([1, 2, 3, 5])
49 |
50 | for s in self.subsets(ref, strict=True):
51 | self.assert_match(s, ref)
52 |
53 | self.assert_no_match(ref, ref)
54 | self.assert_no_match(ref | set([7]), ref)
55 | self.assert_no_match(set([0]), ref)
56 |
57 | test_repr = lambda self: self.assert_repr(__unit__.Less(42))
58 |
59 | # Assertion functions
60 |
61 | def assert_match(self, value, ref):
62 | return super(Less, self).assert_match(__unit__.Less(ref), value)
63 |
64 | def assert_no_match(self, value, ref):
65 | return super(Less, self).assert_no_match(__unit__.Less(ref), value)
66 |
67 |
68 | class LessOrEqual(OperatorTestCase):
69 |
70 | def test_numbers(self):
71 | ref = 42
72 |
73 | self.assert_match(0, ref)
74 | self.assert_match(-ref, ref)
75 | self.assert_match(3.14, ref)
76 | self.assert_match(ref, ref)
77 |
78 | self.assert_no_match(2 * ref, ref)
79 |
80 | def test_strings(self):
81 | ref = "Alice has a cat"
82 |
83 | for i in range(len(ref) - 1):
84 | self.assert_match(ref[:i], ref)
85 | self.assert_match(ref, ref)
86 |
87 | self.assert_no_match(2 * ref, ref)
88 | self.assert_no_match("Bob has a cat", ref)
89 |
90 | def test_sets(self):
91 | ref = set([1, 2, 3, 5])
92 |
93 | for s in self.subsets(ref):
94 | self.assert_match(s, ref)
95 |
96 | self.assert_no_match(ref | set([7]), ref)
97 | self.assert_no_match(set([0]), ref)
98 |
99 | test_repr = lambda self: self.assert_repr(__unit__.LessOrEqual(42))
100 |
101 | # Assertion functions
102 |
103 | def assert_match(self, value, ref):
104 | return super(LessOrEqual, self) \
105 | .assert_match(__unit__.LessOrEqual(ref), value)
106 |
107 | def assert_no_match(self, value, ref):
108 | return super(LessOrEqual, self) \
109 | .assert_no_match(__unit__.LessOrEqual(ref), value)
110 |
111 |
112 | class Greater(OperatorTestCase):
113 |
114 | def test_numbers(self):
115 | ref = 42
116 |
117 | self.assert_match(2 * ref, ref)
118 |
119 | self.assert_no_match(0, ref)
120 | self.assert_no_match(-ref, ref)
121 | self.assert_no_match(3.14, ref)
122 | self.assert_no_match(ref, ref)
123 |
124 | def test_strings(self):
125 | ref = "Alice has a cat"
126 |
127 | self.assert_match(2 * ref, ref)
128 | self.assert_match("Bob has a cat", ref)
129 |
130 | for i in range(len(ref) - 1):
131 | self.assert_no_match(ref[:i], ref)
132 | self.assert_no_match(ref, ref)
133 |
134 | def test_sets(self):
135 | ref = set([1, 2, 3, 5])
136 |
137 | self.assert_match(ref | set([7]), ref)
138 |
139 | for s in self.subsets(ref):
140 | self.assert_no_match(s, ref)
141 | self.assert_no_match(set([0]), ref)
142 |
143 | test_repr = lambda self: self.assert_repr(__unit__.Greater(42))
144 |
145 | # Assertion functions
146 |
147 | def assert_match(self, value, ref):
148 | return super(Greater, self).assert_match(__unit__.Greater(ref), value)
149 |
150 | def assert_no_match(self, value, ref):
151 | return super(Greater, self) \
152 | .assert_no_match(__unit__.Greater(ref), value)
153 |
154 |
155 | class GreaterOrEqual(OperatorTestCase):
156 |
157 | def test_numbers(self):
158 | ref = 42
159 |
160 | self.assert_match(ref, ref)
161 | self.assert_match(2 * ref, ref)
162 |
163 | self.assert_no_match(0, ref)
164 | self.assert_no_match(-ref, ref)
165 | self.assert_no_match(3.14, ref)
166 |
167 | def test_strings(self):
168 | ref = "Alice has a cat"
169 |
170 | self.assert_match(ref, ref)
171 | self.assert_match(2 * ref, ref)
172 | self.assert_match("Bob has a cat", ref)
173 |
174 | for i in range(len(ref) - 1):
175 | self.assert_no_match(ref[:i], ref)
176 |
177 | def test_sets(self):
178 | ref = set([1, 2, 3, 5])
179 |
180 | self.assert_match(ref, ref)
181 | self.assert_match(ref | set([7]), ref)
182 |
183 | for s in self.subsets(ref, strict=True):
184 | self.assert_no_match(s, ref)
185 | self.assert_no_match(set([0]), ref)
186 |
187 | test_repr = lambda self: self.assert_repr(__unit__.GreaterOrEqual(42))
188 |
189 | # Assertion functions
190 |
191 | def assert_match(self, value, ref):
192 | return super(GreaterOrEqual, self) \
193 | .assert_match(__unit__.GreaterOrEqual(ref), value)
194 |
195 | def assert_no_match(self, value, ref):
196 | return super(GreaterOrEqual, self) \
197 | .assert_no_match(__unit__.GreaterOrEqual(ref), value)
198 |
199 |
200 | # Length comparisons
201 |
202 | class Shorter(OperatorTestCase):
203 |
204 | def test_length_value(self):
205 | ref = 12
206 |
207 | self.assert_match([], ref)
208 | self.assert_match([1], ref)
209 | self.assert_match("Alice", ref)
210 |
211 | self.assert_no_match(range(ref), ref)
212 | self.assert_no_match('x' * ref, ref)
213 | self.assert_no_match("Alice has a cat", ref)
214 |
215 | def test_sequence(self):
216 | ref = [1, 2, 3]
217 |
218 | for s in self.subsets(ref, strict=True):
219 | self.assert_match(list(s), ref)
220 | self.assert_match([42] * (len(ref) - 1), ref)
221 |
222 | self.assert_no_match(ref, ref)
223 | self.assert_no_match(2 * ref, ref)
224 |
225 | test_repr = lambda self: self.assert_repr(__unit__.Shorter(42))
226 |
227 | # Assertion functions
228 |
229 | def assert_match(self, value, ref):
230 | return super(Shorter, self).assert_match(__unit__.Shorter(ref), value)
231 |
232 | def assert_no_match(self, value, ref):
233 | return super(Shorter, self) \
234 | .assert_no_match(__unit__.Shorter(ref), value)
235 |
236 |
237 | class ShorterOrEqual(OperatorTestCase):
238 |
239 | def test_length_value(self):
240 | ref = 12
241 |
242 | self.assert_match([], ref)
243 | self.assert_match([1], ref)
244 | self.assert_match("Alice", ref)
245 | self.assert_match(range(ref), ref)
246 | self.assert_match('x' * ref, ref)
247 |
248 | self.assert_no_match('x' * (ref + 1), ref)
249 | self.assert_no_match("Alice has a cat", ref)
250 |
251 | def test_sequence(self):
252 | ref = [1, 2, 3]
253 |
254 | for s in self.subsets(ref):
255 | self.assert_match(list(s), ref)
256 | self.assert_match([42] * (len(ref) - 1), ref)
257 |
258 | self.assert_no_match(2 * ref, ref)
259 |
260 | test_repr = lambda self: self.assert_repr(__unit__.ShorterOrEqual(42))
261 |
262 | # Assertion functions
263 |
264 | def assert_match(self, value, ref):
265 | return super(ShorterOrEqual, self) \
266 | .assert_match(__unit__.ShorterOrEqual(ref), value)
267 |
268 | def assert_no_match(self, value, ref):
269 | return super(ShorterOrEqual, self) \
270 | .assert_no_match(__unit__.ShorterOrEqual(ref), value)
271 |
272 |
273 | class Longer(OperatorTestCase):
274 |
275 | def test_length_value(self):
276 | ref = 12
277 |
278 | self.assert_match('x' * (ref + 1), ref)
279 | self.assert_match("Alice has a cat", ref)
280 |
281 | self.assert_no_match([], ref)
282 | self.assert_no_match([1], ref)
283 | self.assert_no_match("Alice", ref)
284 | self.assert_no_match(range(ref), ref)
285 | self.assert_no_match('x' * ref, ref)
286 |
287 | def test_sequence(self):
288 | ref = [1, 2, 3]
289 |
290 | self.assert_match(2 * ref, ref)
291 | self.assert_match([42] * (len(ref) + 1), ref)
292 |
293 | for s in self.subsets(ref):
294 | self.assert_no_match(list(s), ref)
295 | self.assert_no_match([42] * len(ref), ref)
296 |
297 | test_repr = lambda self: self.assert_repr(__unit__.Longer(42))
298 |
299 | # Assertion functions
300 |
301 | def assert_match(self, value, ref):
302 | return super(Longer, self).assert_match(__unit__.Longer(ref), value)
303 |
304 | def assert_no_match(self, value, ref):
305 | return super(Longer, self) \
306 | .assert_no_match(__unit__.Longer(ref), value)
307 |
308 |
309 | class LongerOrEqual(OperatorTestCase):
310 |
311 | def test_length_value(self):
312 | ref = 12
313 |
314 | self.assert_match('x' * (ref + 1), ref)
315 | self.assert_match("Alice has a cat", ref)
316 | self.assert_match(range(ref), ref)
317 | self.assert_match('x' * ref, ref)
318 |
319 | self.assert_no_match([], ref)
320 | self.assert_no_match([1], ref)
321 | self.assert_no_match("Alice", ref)
322 |
323 | def test_sequence(self):
324 | ref = [1, 2, 3]
325 |
326 | self.assert_match(ref, ref)
327 | self.assert_match(2 * ref, ref)
328 | self.assert_match([42] * len(ref), ref)
329 | self.assert_match([42] * (len(ref) + 1), ref)
330 |
331 | for s in self.subsets(ref, strict=True):
332 | self.assert_no_match(list(s), ref)
333 |
334 | test_repr = lambda self: self.assert_repr(__unit__.LongerOrEqual(42))
335 |
336 | # Assertion functions
337 |
338 | def assert_match(self, value, ref):
339 | return super(LongerOrEqual, self) \
340 | .assert_match(__unit__.LongerOrEqual(ref), value)
341 |
342 | def assert_no_match(self, value, ref):
343 | return super(LongerOrEqual, self) \
344 | .assert_no_match(__unit__.LongerOrEqual(ref), value)
345 |
346 |
347 | # Membership tests
348 |
349 | class Contains(OperatorTestCase):
350 |
351 | def test_lists(self):
352 | ref = 42
353 |
354 | self.assert_no_match([], ref)
355 | self.assert_no_match(list(range(ref)), ref)
356 |
357 | self.assert_match([ref], ref)
358 | self.assert_match([None, ref], ref)
359 | self.assert_match(list(range(ref + 1)), ref)
360 |
361 | def test_strings(self):
362 | ref = 'x'
363 |
364 | self.assert_no_match('', ref)
365 |
366 | self.assert_match(ref, ref)
367 | self.assert_match(ref + 'foo', ref)
368 |
369 | def test_sets(self):
370 | ref = 42
371 |
372 | self.assert_no_match(set(), ref)
373 | self.assert_no_match(set(range(ref)), ref)
374 |
375 | self.assert_match(set([ref]), ref)
376 | self.assert_match(set([None, ref]), ref)
377 | self.assert_match(set(range(ref + 1)), ref)
378 |
379 | test_repr = lambda self: self.assert_repr(__unit__.Contains(42))
380 |
381 | # Assertion functions
382 |
383 | def assert_match(self, value, ref):
384 | return super(Contains, self) \
385 | .assert_match(__unit__.Contains(ref), value)
386 |
387 | def assert_no_match(self, value, ref):
388 | return super(Contains, self) \
389 | .assert_no_match(__unit__.Contains(ref), value)
390 |
391 |
392 | class In(OperatorTestCase):
393 |
394 | def test_list(self):
395 | limit = 42
396 | ref = list(range(limit))
397 |
398 | self.assert_no_match(-1, ref)
399 | self.assert_no_match(limit + 1, ref)
400 | self.assert_no_match(None, ref)
401 |
402 | for num in ref:
403 | self.assert_match(num, ref)
404 |
405 | def test_string(self):
406 | ref = 'Alice has a cat'
407 |
408 | self.assert_no_match('_', ref)
409 | with self.assertRaises(TypeError):
410 | self.assert_no_match(42, ref)
411 | with self.assertRaises(TypeError):
412 | self.assert_no_match(None, ref)
413 |
414 | # every character should be inside the reference string
415 | for char in ref:
416 | self.assert_match(char, ref)
417 |
418 | # as well as every substring
419 | for i in range(len(ref)):
420 | for j in range(i + 1, len(ref)):
421 | self.assert_match(ref[i:j], ref)
422 |
423 | def test_set(self):
424 | limit = 42
425 | ref = set(range(limit))
426 |
427 | self.assert_no_match(-1, ref)
428 | self.assert_no_match(limit + 1, ref)
429 | self.assert_no_match(None, ref)
430 |
431 | for num in ref:
432 | self.assert_match(num, ref)
433 |
434 | test_repr = lambda self: self.assert_repr(__unit__.In(()))
435 |
436 | # Assertion functions
437 |
438 | def assert_match(self, value, ref):
439 | return super(In, self).assert_match(__unit__.In(ref), value)
440 |
441 | def assert_no_match(self, value, ref):
442 | return super(In, self).assert_no_match(__unit__.In(ref), value)
443 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # callee documentation build configuration file, created by
4 | # sphinx-quickstart on Wed Oct 14 11:24:04 2015.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | from datetime import date
16 | import os
17 | import re
18 | import sys
19 |
20 | from sphinx.errors import SphinxError
21 | from sphinx.util.docstrings import prepare_docstring
22 |
23 | sys.path.append(os.path.abspath('..'))
24 | import callee
25 |
26 | # If extensions (or modules to document with autodoc) are in another directory,
27 | # add these directories to sys.path here. If the directory is relative to the
28 | # documentation root, use os.path.abspath to make it absolute, like shown here.
29 | #sys.path.insert(0, os.path.abspath('.'))
30 |
31 |
32 | def setup(app):
33 | """Setup the :class:`Sphinx` app object to process source code
34 | and generate docs.
35 |
36 | We use this functions to connect the handlers to various Sphinx
37 | and extension events.
38 | """
39 | app.connect('autodoc-process-docstring', autodoc_process_docstring)
40 |
41 |
42 | # -- General configuration ------------------------------------------------
43 |
44 | # If your documentation needs a minimal Sphinx version, state it here.
45 | needs_sphinx = '1.1'
46 |
47 | # Add any Sphinx extension module names here, as strings. They can be
48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
49 | # ones.
50 | extensions = [
51 | 'alabaster',
52 | 'sphinx.ext.autodoc',
53 | 'sphinx.ext.coverage',
54 | 'sphinx.ext.intersphinx',
55 | 'sphinx.ext.viewcode',
56 | ]
57 |
58 | # Add any paths that contain templates here, relative to this directory.
59 | templates_path = ['_templates']
60 |
61 | # The suffix(es) of source filenames.
62 | # You can specify multiple suffix as a list of string:
63 | # source_suffix = ['.rst', '.md']
64 | source_suffix = '.rst'
65 |
66 | # The encoding of source files.
67 | #source_encoding = 'utf-8-sig'
68 |
69 | # The master toctree document.
70 | master_doc = 'index'
71 |
72 | # General information about the project.
73 | project = u'callee'
74 | copyright = u'%s, Karol Kuczmarski' % date.today().year
75 | author = u'Karol Kuczmarski'
76 |
77 | # The version info for the project you're documenting, acts as replacement for
78 | # |version| and |release|, also used in various other places throughout the
79 | # built documents.
80 | #
81 | # The short X.Y version.
82 | version = re.search(r"\d+\.\d+", callee.__version__).group()
83 | # The full version, including alpha/beta/rc tags.
84 | release = callee.__version__
85 |
86 | # The language for content autogenerated by Sphinx. Refer to documentation
87 | # for a list of supported languages.
88 | #
89 | # This is also used if you do content translation via gettext catalogs.
90 | # Usually you set "language" from the command line for these cases.
91 | language = None
92 |
93 | # There are two options for replacing |today|: either, you set today to some
94 | # non-false value, then it is used:
95 | #today = ''
96 | # Else, today_fmt is used as the format for a strftime call.
97 | #today_fmt = '%B %d, %Y'
98 |
99 | # List of patterns, relative to source directory, that match files and
100 | # directories to ignore when looking for source files.
101 | exclude_patterns = ['_build']
102 |
103 | # The reST default role (used for this markup: `text`) to use for all
104 | # documents.
105 | #default_role = None
106 |
107 | # If true, '()' will be appended to :func: etc. cross-reference text.
108 | #add_function_parentheses = True
109 |
110 | # If true, the current module name will be prepended to all description
111 | # unit titles (such as .. function::).
112 | #add_module_names = True
113 |
114 | # If true, sectionauthor and moduleauthor directives will be shown in the
115 | # output. They are ignored by default.
116 | #show_authors = False
117 |
118 | # The name of the Pygments (syntax highlighting) style to use.
119 | pygments_style = 'sphinx'
120 |
121 | # A list of ignored prefixes for module index sorting.
122 | #modindex_common_prefix = []
123 |
124 | # If true, keep warnings as "system message" paragraphs in the built documents.
125 | #keep_warnings = False
126 |
127 | # If true, `todo` and `todoList` produce output, else they produce nothing.
128 | todo_include_todos = False
129 |
130 |
131 | # -- Options for HTML output ----------------------------------------------
132 |
133 | # The theme to use for HTML and HTML Help pages. See the documentation for
134 | # a list of builtin themes.
135 | html_theme = 'alabaster'
136 |
137 | # Theme options are theme-specific and customize the look and feel of a theme
138 | # further. For a list of options available for each theme, see the
139 | # documentation.
140 | html_theme_options = {
141 | 'logo': 'logo.png', # handled by the logo.html template
142 | 'description': callee.__description__,
143 |
144 | 'github_user': 'Xion',
145 | 'github_repo': 'callee',
146 | 'github_type': 'star',
147 | 'github_count': False,
148 | 'github_banner': True, # "Fork me on GitHub" in the corner
149 |
150 | 'travis_button': True,
151 | }
152 |
153 | # Add any paths that contain custom themes here, relative to this directory.
154 | #html_theme_path = []
155 |
156 | # The name for this set of Sphinx documents. If None, it defaults to
157 | # " v documentation".
158 | #html_title = None
159 |
160 | # A shorter title for the navigation bar. Default is the same as html_title.
161 | #html_short_title = None
162 |
163 | # The name of an image file (relative to this directory) to place at the top
164 | # of the sidebar.
165 | #html_logo = None
166 |
167 | # The name of an image file (within the static path) to use as favicon of the
168 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
169 | # pixels large.
170 | #html_favicon = None
171 |
172 | # Add any paths that contain custom static files (such as style sheets) here,
173 | # relative to this directory. They are copied after the builtin static files,
174 | # so a file named "default.css" will overwrite the builtin "default.css".
175 | html_static_path = ['_static']
176 |
177 | # Add any extra paths that contain custom files (such as robots.txt or
178 | # .htaccess) here, relative to this directory. These files are copied
179 | # directly to the root of the documentation.
180 | #html_extra_path = []
181 |
182 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
183 | # using the given strftime format.
184 | #html_last_updated_fmt = '%b %d, %Y'
185 |
186 | # If true, SmartyPants will be used to convert quotes and dashes to
187 | # typographically correct entities.
188 | #html_use_smartypants = True
189 |
190 | # Custom sidebar templates, maps document names to template names.
191 | html_sidebars = {
192 | 'index': [
193 | 'about.html', # from Alabaster
194 | 'sidebar/index.html', # own
195 | 'searchbox.html', # from Sphinx
196 | ],
197 | '**': [
198 | 'about.html', # from Alabaster
199 | 'localtoc.html', # from Sphinx
200 | 'searchbox.html', # from Sphinx
201 | ],
202 | }
203 |
204 | # Additional templates that should be rendered to pages, maps page names to
205 | # template names.
206 | #html_additional_pages = {}
207 |
208 | # If false, no module index is generated.
209 | #html_domain_indices = True
210 |
211 | # If false, no index is generated.
212 | #html_use_index = True
213 |
214 | # If true, the index is split into individual pages for each letter.
215 | #html_split_index = False
216 |
217 | # If true, the reST sources are included in the HTML build as _sources/name.
218 | # The default is True.
219 | html_copy_source = False
220 |
221 | # If true, links to the reST sources are added to the pages.
222 | html_show_sourcelink = False
223 |
224 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
225 | html_show_sphinx = False
226 |
227 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
228 | #html_show_copyright = True
229 |
230 | # If true, an OpenSearch description file will be output, and all pages will
231 | # contain a tag referring to it. The value of this option must be the
232 | # base URL from which the finished HTML is served.
233 | #html_use_opensearch = ''
234 |
235 | # This is the file name suffix for HTML files (e.g. ".xhtml").
236 | #html_file_suffix = None
237 |
238 | # Language to be used for generating the HTML full-text search index.
239 | # Sphinx supports the following languages:
240 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
241 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
242 | #html_search_language = 'en'
243 |
244 | # A dictionary with options for the search language support, empty by default.
245 | # Now only 'ja' uses this config value
246 | #html_search_options = {'type': 'default'}
247 |
248 | # The name of a javascript file (relative to the configuration directory) that
249 | # implements a search results scorer. If empty, the default will be used.
250 | #html_search_scorer = 'scorer.js'
251 |
252 | # Output file base name for HTML help builder.
253 | htmlhelp_basename = 'calleedoc'
254 |
255 | # -- Options for LaTeX output ---------------------------------------------
256 |
257 | latex_elements = {
258 | # The paper size ('letterpaper' or 'a4paper').
259 | #'papersize': 'letterpaper',
260 |
261 | # The font size ('10pt', '11pt' or '12pt').
262 | #'pointsize': '10pt',
263 |
264 | # Additional stuff for the LaTeX preamble.
265 | #'preamble': '',
266 |
267 | # Latex figure (float) alignment
268 | #'figure_align': 'htbp',
269 | }
270 |
271 | # Grouping the document tree into LaTeX files. List of tuples
272 | # (source start file, target name, title,
273 | # author, documentclass [howto, manual, or own class]).
274 | latex_documents = [
275 | (master_doc, 'callee.tex', u'callee Documentation',
276 | u'Karol Kuczmarski', 'manual'),
277 | ]
278 |
279 | # The name of an image file (relative to this directory) to place at the top of
280 | # the title page.
281 | #latex_logo = None
282 |
283 | # For "manual" documents, if this is true, then toplevel headings are parts,
284 | # not chapters.
285 | #latex_use_parts = False
286 |
287 | # If true, show page references after internal links.
288 | #latex_show_pagerefs = False
289 |
290 | # If true, show URL addresses after external links.
291 | #latex_show_urls = False
292 |
293 | # Documents to append as an appendix to all manuals.
294 | #latex_appendices = []
295 |
296 | # If false, no module index is generated.
297 | #latex_domain_indices = True
298 |
299 |
300 | # -- Options for manual page output ---------------------------------------
301 |
302 | # One entry per manual page. List of tuples
303 | # (source start file, name, description, authors, manual section).
304 | man_pages = [
305 | (master_doc, 'callee', u'callee Documentation',
306 | [author], 1)
307 | ]
308 |
309 | # If true, show URL addresses after external links.
310 | #man_show_urls = False
311 |
312 |
313 | # -- Options for Texinfo output -------------------------------------------
314 |
315 | # Grouping the document tree into Texinfo files. List of tuples
316 | # (source start file, target name, title, author,
317 | # dir menu entry, description, category)
318 | texinfo_documents = [
319 | (master_doc, 'callee', u'callee Documentation',
320 | author, 'callee', 'One line description of project.',
321 | 'Miscellaneous'),
322 | ]
323 |
324 | # Documents to append as an appendix to all manuals.
325 | #texinfo_appendices = []
326 |
327 | # If false, no module index is generated.
328 | #texinfo_domain_indices = True
329 |
330 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
331 | #texinfo_show_urls = 'footnote'
332 |
333 | # If true, do not generate a @detailmenu in the "Top" node's menu.
334 | #texinfo_no_detailmenu = False
335 |
336 |
337 | # -- Options for autodoc extension ----------------------------------------
338 |
339 | # Whether `autoclass` should include class's docstring, __init__ method's
340 | # docstring, or both.
341 | autoclass_content = 'both'
342 |
343 | # Exceptions to the above setting, handled by ``autodoc_process_docstring``.
344 | # Any class names on this list will only have their class docstring included
345 | # (and not also the ``__init__`` method docstring).
346 | autoclass_content_exceptions = [
347 | 'callee.operators.Contains',
348 | 'callee.operators.In',
349 | ]
350 |
351 |
352 | def autodoc_process_docstring(app, what, name, obj, options, lines):
353 | """Handler for the event emitted when autodoc processes a docstring.
354 | See http://sphinx-doc.org/ext/autodoc.html#event-autodoc-process-docstring.
355 |
356 | The TL;DR is that we can modify ``lines`` in-place to influence the output.
357 | """
358 | # check that only symbols that can be directly imported from ``callee``
359 | # package are being documented
360 | _, symbol = name.rsplit('.', 1)
361 | if symbol not in callee.__all__:
362 | raise SphinxError(
363 | "autodoc'd '%s' is not a part of the public API!" % name)
364 |
365 | # for classes exempt from automatic merging of class & __init__ docs,
366 | # pretend their __init__ methods have no docstring at all,
367 | # so that nothing will be appended to the class's docstring
368 | if what == 'class' and name in autoclass_content_exceptions:
369 | # amusingly, when autodoc reads the constructor's docstring
370 | # for appending it to class docstring, it will report ``what``
371 | # as 'class' (again!); hence we must check what it actually read
372 | ctor_docstring_lines = prepare_docstring(obj.__init__.__doc__)
373 | if lines == ctor_docstring_lines:
374 | lines[:] = []
375 |
376 |
377 | # -- Options for intersphinx extension ------------------------------------
378 |
379 | intersphinx_mapping = {'python': ('https://docs.python.org/2.7', None)}
380 |
381 | # Since we point to Python 2.7 docs which basically don't change,
382 | # there is no reason not to cache the them indefinetely.
383 | intersphinx_cache_limit = -1
384 |
--------------------------------------------------------------------------------