├── 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 | --------------------------------------------------------------------------------