diff -Nru python-testfixtures-6.10.1/CHANGELOG.rst python-testfixtures-6.14.1/CHANGELOG.rst --- python-testfixtures-6.10.1/CHANGELOG.rst 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/CHANGELOG.rst 2020-04-20 12:23:41.000000000 +0000 @@ -3,13 +3,77 @@ .. currentmodule:: testfixtures +6.14.1 (20 Apr 2020) +-------------------- + +- Fix bugs in comparison of :func:`~unittest.mock.call` objects where the :func:`repr` of the + :func:`~unittest.mock.call` arguments were the same even when their attributes were not. + +6.14.0 (24 Feb 2020) +-------------------- + +- Add support for non-deterministic logging order when using :meth:`twisted.LogCapture`. + +6.13.1 (20 Feb 2020) +-------------------- + +- Fix for using :func:`compare` to compare two-element :func:`~unittest.mock.call` + objects. + +Thanks to Daniel Fortunov for the fix. + +6.13.0 (18 Feb 2020) +-------------------- + +- Allow any attributes that need to be ignored to be specified directly when calling + :func:`~testfixtures.comparison.compare_object`. This is handy when writing + comparers for :func:`compare`. + +6.12.1 (16 Feb 2020) +-------------------- + +- Fix a bug that occured when using :func:`compare` to compare a string with a + slotted object that had the same :func:`repr` as the string. + +6.12.0 (6 Feb 2020) +------------------- + +- Add support for ``universal_newlines``, ``text``, ``encoding`` and ``errors`` to + :class:`popen.MockPopen`, but only for Python 3. + +6.11.0 (29 Jan 2020) +-------------------- + +- :class:`decimal.Decimal` now has better representation when :func:`compare` displays a failed + comparison, particularly on Python 2. + +- Add support to :func:`compare` for explicitly naming objects to be compared as ``x`` and ``y``. + This allows symmetry with the ``x_label`` and ``y_label`` parameters that are now documented. + +- Restore ability for :class:`Comparison` to compare properties and methods, although these uses + are not recommended. + +Thanks to Daniel Fortunov for all of the above. + +6.10.3 (22 Nov 2019) +-------------------- + +- Fix bug where new-style classes had their attributes checked with :func:`compare` even + when they were of different types. + +6.10.2 (15 Nov 2019) +-------------------- + +- Fix bugs in :func:`compare` when comparing objects which have both ``__slots__`` + and a ``__dict__``. + 6.10.1 (1 Nov 2019) ------------------- - Fix edge case where string interning made dictionary comparison output much less useful. 6.10.0 (19 Jun 2019) -------------------- +-------------------- - Better feedback where objects do not :func:`compare` equal but do have the same representation. diff -Nru python-testfixtures-6.10.1/.circleci/config.yml python-testfixtures-6.14.1/.circleci/config.yml --- python-testfixtures-6.10.1/.circleci/config.yml 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/.circleci/config.yml 2020-04-20 12:23:41.000000000 +0000 @@ -38,6 +38,9 @@ name: python37 image: circleci/python:3.7 - python/pip-run-tests: + name: python38 + image: circleci/python:3.8 + - python/pip-run-tests: name: python36-mock-backport # so we test the mock monkey patches aren't used: image: circleci/python:3.6.4 @@ -71,6 +74,7 @@ - python27 - python36 - python37 + - python38 - python36-mock-backport - python37-mock-backport - python27-django-1-9 @@ -99,8 +103,8 @@ - package - check-package: - name: check-package-python37 - image: circleci/python:3.7 + name: check-package-python38 + image: circleci/python:3.8 requires: - package @@ -113,8 +117,8 @@ - package - check-package: - name: check-package-python37-mock - image: circleci/python:3.7 + name: check-package-python38-mock + image: circleci/python:3.8 extra_package: mock imports: "testfixtures, testfixtures.mock" requires: @@ -129,8 +133,8 @@ - package - check-package: - name: check-package-python37-django - image: circleci/python:3.7 + name: check-package-python38-django + image: circleci/python:3.8 extra_package: django imports: "testfixtures, testfixtures.django" requires: @@ -143,9 +147,9 @@ - check-package-python27 - check-package-python27-mock - check-package-python27-django - - check-package-python37 - - check-package-python37-mock - - check-package-python37-django + - check-package-python38 + - check-package-python38-mock + - check-package-python38-django workflows: push: diff -Nru python-testfixtures-6.10.1/debian/changelog python-testfixtures-6.14.1/debian/changelog --- python-testfixtures-6.10.1/debian/changelog 2019-11-13 15:15:05.000000000 +0000 +++ python-testfixtures-6.14.1/debian/changelog 2020-06-29 16:07:55.000000000 +0000 @@ -1,3 +1,18 @@ +python-testfixtures (6.14.1-1) unstable; urgency=medium + + * Team upload. + + [ Debian Janitor ] + * Update standards version to 4.5.0, no changes needed. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Andrey Rahmatullin ] + * New upstream version. + * Add an upstream fix for Python 3.8.3 (Closes: #963369). + + -- Andrey Rahmatullin Mon, 29 Jun 2020 21:07:55 +0500 + python-testfixtures (6.10.1-1) unstable; urgency=medium * Team upload. diff -Nru python-testfixtures-6.10.1/debian/control python-testfixtures-6.14.1/debian/control --- python-testfixtures-6.10.1/debian/control 2019-11-13 15:15:05.000000000 +0000 +++ python-testfixtures-6.14.1/debian/control 2020-06-29 16:07:55.000000000 +0000 @@ -20,7 +20,7 @@ python3-sybil, python3-twisted, python3-zope.component, -Standards-Version: 4.4.1 +Standards-Version: 4.5.0 Homepage: https://github.com/Simplistix/testfixtures Vcs-Browser: https://salsa.debian.org/python-team/modules/python-testfixtures Vcs-Git: https://salsa.debian.org/python-team/modules/python-testfixtures.git diff -Nru python-testfixtures-6.10.1/debian/patches/0004-fix-for-python-3.8.3.patch python-testfixtures-6.14.1/debian/patches/0004-fix-for-python-3.8.3.patch --- python-testfixtures-6.10.1/debian/patches/0004-fix-for-python-3.8.3.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-testfixtures-6.14.1/debian/patches/0004-fix-for-python-3.8.3.patch 2020-06-29 16:07:55.000000000 +0000 @@ -0,0 +1,21 @@ +From: Chris Withers +Date: Sun, 17 May 2020 08:01:57 +0100 +Subject: fix for python 3.8.3 + +--- + testfixtures/utils.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/testfixtures/utils.py b/testfixtures/utils.py +index 9584db3..00aa9da 100644 +--- a/testfixtures/utils.py ++++ b/testfixtures/utils.py +@@ -68,7 +68,7 @@ def wrap(before, after=None): + to_add = len(getargspec(func).args[len(args):]) + added = 0 + +- exc_info = tuple() ++ exc_info = (None, None, None) + try: + for patching in patched.patchings: + arg = patching.__enter__() diff -Nru python-testfixtures-6.10.1/debian/patches/series python-testfixtures-6.14.1/debian/patches/series --- python-testfixtures-6.10.1/debian/patches/series 2019-11-13 15:15:05.000000000 +0000 +++ python-testfixtures-6.14.1/debian/patches/series 2020-06-29 16:07:55.000000000 +0000 @@ -1,3 +1,4 @@ 0001-Use-local-objects.inv-where-possible.patch 0002-Do-not-duplicate-license-in-documentation.patch 0003-Remove-external-image-links-from-README.rst.patch +0004-fix-for-python-3.8.3.patch diff -Nru python-testfixtures-6.10.1/debian/upstream/metadata python-testfixtures-6.14.1/debian/upstream/metadata --- python-testfixtures-6.10.1/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-testfixtures-6.14.1/debian/upstream/metadata 2020-06-29 16:07:55.000000000 +0000 @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/Simplistix/testfixtures/issues +Bug-Submit: https://github.com/Simplistix/testfixtures/issues/new +Repository: https://github.com/Simplistix/testfixtures.git +Repository-Browse: https://github.com/Simplistix/testfixtures diff -Nru python-testfixtures-6.10.1/docs/api.txt python-testfixtures-6.14.1/docs/api.txt --- python-testfixtures-6.10.1/docs/api.txt 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/docs/api.txt 2020-04-20 12:23:41.000000000 +0000 @@ -3,6 +3,8 @@ .. currentmodule:: testfixtures +.. autofunction:: compare(x, y, prefix=None, suffix=None, raises=True, recursive=True, strict=False, comparers=None, **kw) + .. autoclass:: Comparison .. autoclass:: LogCapture @@ -42,30 +44,6 @@ .. autoclass:: TempDirectory :members: -.. autofunction:: compare(x, y, prefix=None, suffix=None, raises=True, recursive=True, strict=False, comparers=None, **kw) - -.. autofunction:: testfixtures.comparison.register - -.. autofunction:: testfixtures.comparison.compare_simple - -.. autofunction:: testfixtures.comparison.compare_object - -.. autofunction:: testfixtures.comparison.compare_exception - -.. autofunction:: testfixtures.comparison.compare_with_type - -.. autofunction:: testfixtures.comparison.compare_sequence - -.. autofunction:: testfixtures.comparison.compare_generator - -.. autofunction:: testfixtures.comparison.compare_tuple - -.. autofunction:: testfixtures.comparison.compare_dict - -.. autofunction:: testfixtures.comparison.compare_set - -.. autofunction:: testfixtures.comparison.compare_text - .. autofunction:: diff .. autofunction:: generator @@ -347,6 +325,32 @@ A singleton used to represent the absence of a particular attribute. + +.. automodule:: testfixtures.comparison + +.. autofunction:: testfixtures.comparison.register + +.. autofunction:: testfixtures.comparison.compare_simple + +.. autofunction:: testfixtures.comparison.compare_object + +.. autofunction:: testfixtures.comparison.compare_exception + +.. autofunction:: testfixtures.comparison.compare_with_type + +.. autofunction:: testfixtures.comparison.compare_sequence + +.. autofunction:: testfixtures.comparison.compare_generator + +.. autofunction:: testfixtures.comparison.compare_tuple + +.. autofunction:: testfixtures.comparison.compare_dict + +.. autofunction:: testfixtures.comparison.compare_set + +.. autofunction:: testfixtures.comparison.compare_text + + .. currentmodule:: testfixtures.popen .. automodule:: testfixtures.popen @@ -361,8 +365,10 @@ automatically set to make comparing django :class:`~django.db.models.Model` instances easier. + .. automodule:: testfixtures.mock + .. automodule:: testfixtures.twisted :member-order: bysource :members: diff -Nru python-testfixtures-6.10.1/docs/development.txt python-testfixtures-6.14.1/docs/development.txt --- python-testfixtures-6.10.1/docs/development.txt 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/docs/development.txt 2020-04-20 12:23:41.000000000 +0000 @@ -3,15 +3,6 @@ .. highlight:: bash -This package is developed using continuous integration which can be -found here: - -https://travis-ci.org/Simplistix/testfixtures - -The latest development version of the documentation can be found here: - -http://testfixtures.readthedocs.org/en/latest/ - If you wish to contribute to this project, then you should fork the repository found here: diff -Nru python-testfixtures-6.10.1/LICENSE.txt python-testfixtures-6.14.1/LICENSE.txt --- python-testfixtures-6.10.1/LICENSE.txt 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/LICENSE.txt 2020-04-20 12:23:41.000000000 +0000 @@ -1,5 +1,5 @@ Copyright (c) 2008-2015 Simplistix Ltd -Copyright (c) 2015-2019 Chris Withers +Copyright (c) 2015-2020 Chris Withers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff -Nru python-testfixtures-6.10.1/.readthedocs.yml python-testfixtures-6.14.1/.readthedocs.yml --- python-testfixtures-6.10.1/.readthedocs.yml 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/.readthedocs.yml 2020-04-20 12:23:41.000000000 +0000 @@ -5,4 +5,6 @@ - method: pip path: . extra_requirements: - - build + - docs +sphinx: + fail_on_warning: true diff -Nru python-testfixtures-6.10.1/setup.cfg python-testfixtures-6.14.1/setup.cfg --- python-testfixtures-6.10.1/setup.cfg 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/setup.cfg 2020-04-20 12:23:41.000000000 +0000 @@ -6,3 +6,4 @@ DJANGO_SETTINGS_MODULE=testfixtures.tests.test_django.settings filterwarnings = ignore::DeprecationWarning + ignore::PendingDeprecationWarning diff -Nru python-testfixtures-6.10.1/setup.py python-testfixtures-6.14.1/setup.py --- python-testfixtures-6.10.1/setup.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/setup.py 2020-04-20 12:23:41.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2014 Simplistix Ltd, 2015-2019 Chris Withers +# Copyright (c) 2008-2014 Simplistix Ltd, 2015-2020 Chris Withers # See license.txt for license details. import os @@ -8,6 +8,15 @@ name = 'testfixtures' base_dir = os.path.dirname(__file__) +optional = [ + 'mock;python_version<"3"', + 'zope.component', + 'django<2;python_version<"3"', + 'django;python_version>="3"', + 'sybil', + 'twisted' +] + setup( name=name, version=open(os.path.join(base_dir, name, 'version.txt')).read().strip(), @@ -27,6 +36,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], packages=find_packages(), zip_safe=False, @@ -35,13 +45,8 @@ test=['pytest>=3.6', 'pytest-cov', 'pytest-django', - 'mock;python_version<"3"', - 'sybil', - 'zope.component', - 'django<2;python_version<"3"', - 'django;python_version>="3"', - 'twisted'], - docs=['sphinx'], + ]+optional, + docs=['sphinx']+optional, build=['setuptools-git', 'wheel', 'twine'] ) ) diff -Nru python-testfixtures-6.10.1/testfixtures/comparison.py python-testfixtures-6.14.1/testfixtures/comparison.py --- python-testfixtures-6.10.1/testfixtures/comparison.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/comparison.py 2020-04-20 12:23:41.000000000 +0000 @@ -1,3 +1,9 @@ +""" +testfixtures.comparison +----------------------- +""" + +from decimal import Decimal from difflib import unified_diff from functools import partial from pprint import pformat @@ -19,6 +25,8 @@ repr_x = repr(x) repr_y = repr(y) if repr_x == repr_y: + if type(x) is not type(y): + return compare_with_type(x, y, context) x_attrs = _extract_attrs(x) y_attrs = _extract_attrs(y) diff = _compare_mapping(x_attrs, y_attrs, context, x, @@ -32,33 +40,45 @@ def _extract_attrs(obj, ignore=None): + try: + attrs = vars(obj).copy() + except TypeError: + attrs = None + else: + if isinstance(obj, BaseException): + attrs['args'] = obj.args + has_slots = getattr(obj, '__slots__', not_there) is not not_there if has_slots: slots = set() for cls in type(obj).__mro__: slots.update(getattr(cls, '__slots__', ())) - attrs = {} + if slots and attrs is None: + attrs = {} for n in slots: value = getattr(obj, n, not_there) if value is not not_there: attrs[n] = value - else: - try: - attrs = vars(obj).copy() - except TypeError: - return None - else: - if isinstance(obj, BaseException): - attrs['args'] = obj.args + + if attrs is None: + return None + if ignore is not None: - if isinstance(ignore, dict): - ignore = ignore.get(type(obj), ()) for attr in ignore: attrs.pop(attr, None) return attrs -def compare_object(x, y, context): +def _attrs_to_ignore(context, ignore_attributes, obj): + ignore = context.get_option('ignore_attributes', ()) + if isinstance(ignore, dict): + ignore = ignore.get(type(obj), ()) + ignore = set(ignore) + ignore.update(ignore_attributes) + return ignore + + +def compare_object(x, y, context, ignore_attributes=()): """ Compare the two supplied objects based on their type and attributes. @@ -67,12 +87,17 @@ Either a sequence of strings containing attribute names to be ignored when comparing or a mapping of type to sequence of strings containing attribute names to be ignored when comparing that type. + + This may be specified as either a parameter to this function or in the + ``context``. If specified in both, they will both apply with precedence + given to whatever is specified is specified as a parameter. + If specified as a parameter to this fucntion, it may only be a list of + strings. """ - ignore_attributes = context.get_option('ignore_attributes', ()) - if type(x) is not type(y) or isinstance(x, ClassType): + if type(x) is not type(y) or isinstance(x, (ClassType, type)): return compare_simple(x, y, context) - x_attrs = _extract_attrs(x, ignore_attributes) - y_attrs = _extract_attrs(y, ignore_attributes) + x_attrs = _extract_attrs(x, _attrs_to_ignore(context, ignore_attributes, x)) + y_attrs = _extract_attrs(y, _attrs_to_ignore(context, ignore_attributes, y)) if x_attrs is None or y_attrs is None or not (x_attrs and y_attrs): return compare_simple(x, y, context) if x_attrs != y_attrs: @@ -341,11 +366,34 @@ def compare_call(x, y, context): if x == y: return - x_name, x_args, x_kw = x - y_name, y_args, y_kw = y + + def extract(call): + try: + name, args, kwargs = call + except ValueError: + name = None + args, kwargs = call + return name, args, kwargs + + x_name, x_args, x_kw = extract(x) + y_name, y_args, y_kw = extract(y) + if x_name == y_name and x_args == y_args and x_kw == y_kw: return compare_call(getattr(x, parent_name), getattr(y, parent_name), context) - return compare_text(repr(x), repr(y), context) + + if repr(x) != repr(y): + return compare_text(repr(x), repr(y), context) + + different = ( + context.different(x_name, y_name, ' function name') or + context.different(x_args, y_args, ' args') or + context.different(x_kw, y_kw, ' kw') + ) + if not different: + return + + return 'mock.call not as expected:' + def compare_partial(x, y, context): @@ -372,6 +420,7 @@ Unicode: compare_text, int: compare_simple, float: compare_simple, + Decimal: compare_simple, GeneratorType: compare_generator, mock_call.__class__: compare_call, unittest_mock_call.__class__: compare_call, @@ -450,6 +499,13 @@ if actual is not not_there: possible.append(actual) + x = self.options.pop('x', not_there) + if x is not not_there: + possible.append(x) + y = self.options.pop('y', not_there) + if y is not not_there: + possible.append(y) + if len(possible) != 2: message = 'Exactly two objects needed, you supplied:' if possible: @@ -550,14 +606,17 @@ def compare(*args, **kw): """ - Compare the two arguments passed either positionally or using - explicit ``expected`` and ``actual`` keyword paramaters. An - :class:`AssertionError` will be raised if they are not the same. - The :class:`AssertionError` raised will attempt to provide + Compare two objects, raising an :class:`AssertionError` if they are not + the same. The :class:`AssertionError` raised will attempt to provide descriptions of the differences found. + The two objects to compare can be passed either positionally or using + explicit keyword arguments named ``x`` and ``y``, or ``expected`` and + ``actual``. + Any other keyword parameters supplied will be passed to the functions - that end up doing the comparison. See the API documentation below + that end up doing the comparison. See the + :mod:`API documentation below ` for details of these. :param prefix: If provided, in the event of an :class:`AssertionError` @@ -568,6 +627,18 @@ being raised, the suffix supplied will be appended to the message in the :class:`AssertionError`. + :param x_label: If provided, in the event of an :class:`AssertionError` + being raised, the object passed as the first positional + argument, or ``x`` keyword argument, will be labelled + with this string in the message in the + :class:`AssertionError`. + + :param x_label: If provided, in the event of an :class:`AssertionError` + being raised, the object passed as the second positional + argument, or ``y`` keyword argument, will be labelled + with this string in the message in the + :class:`AssertionError`. + :param raises: If ``False``, the message that would be raised in the :class:`AssertionError` will be returned instead of the exception being raised. @@ -669,15 +740,19 @@ if self.v is None: return True + remaining_keys = set(self.v.keys()) if self.strict: v = _extract_attrs(other) + remaining_keys -= set(v.keys()) else: v = {} - for k in self.v.keys(): - try: - v[k] = getattr(other, k) - except AttributeError: - pass + + while remaining_keys: + k = remaining_keys.pop() + try: + v[k] = getattr(other, k) + except AttributeError: + pass kw = {'x_label': 'Comparison', 'y_label': 'actual'} context = CompareContext(kw) diff -Nru python-testfixtures-6.10.1/testfixtures/compat.py python-testfixtures-6.14.1/testfixtures/compat.py --- python-testfixtures-6.10.1/testfixtures/compat.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/compat.py 2020-04-20 12:23:41.000000000 +0000 @@ -28,6 +28,7 @@ from itertools import zip_longest from functools import reduce from collections.abc import Iterable + from abc import ABC else: @@ -50,3 +51,5 @@ from itertools import izip_longest as zip_longest reduce = reduce from collections import Iterable + from abc import ABCMeta + ABC = ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3 diff -Nru python-testfixtures-6.10.1/testfixtures/django.py python-testfixtures-6.14.1/testfixtures/django.py --- python-testfixtures-6.10.1/testfixtures/django.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/django.py 2020-04-20 12:23:41.000000000 +0000 @@ -1,3 +1,7 @@ +""" +testfixtures.django +------------------- +""" from __future__ import absolute_import from functools import partial diff -Nru python-testfixtures-6.10.1/testfixtures/popen.py python-testfixtures-6.14.1/testfixtures/popen.py --- python-testfixtures-6.10.1/testfixtures/popen.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/popen.py 2020-04-20 12:23:41.000000000 +0000 @@ -1,9 +1,15 @@ +""" +testfixtures.popen +------------------ +""" + import pipes from functools import wraps, partial +from io import TextIOWrapper from itertools import chain from subprocess import STDOUT, PIPE from tempfile import TemporaryFile -from testfixtures.compat import basestring, PY3, zip_longest, reduce +from testfixtures.compat import basestring, PY3, zip_longest, reduce, PY2 from testfixtures.utils import extend_docstring from .mock import Mock, call @@ -61,7 +67,7 @@ env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), - encoding=None, errors=None): + encoding=None, errors=None, text=None): self.mock = Mock() self.class_instance_mock = mock_class.mock.Popen_instance #: A :func:`unittest.mock.call` representing the call made to instantiate @@ -105,6 +111,8 @@ value.write(mock_value) value.flush() value.seek(0) + if PY3 and (universal_newlines or text or encoding): + value = TextIOWrapper(value, encoding=encoding, errors=errors) setattr(self, name, value) if stdin == PIPE: diff -Nru python-testfixtures-6.10.1/testfixtures/tests/test_compare.py python-testfixtures-6.14.1/testfixtures/tests/test_compare.py --- python-testfixtures-6.10.1/testfixtures/tests/test_compare.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/tests/test_compare.py 2020-04-20 12:23:41.000000000 +0000 @@ -1,4 +1,5 @@ from datetime import date, datetime +from decimal import Decimal from functools import partial @@ -19,9 +20,9 @@ from testfixtures.compat import ( class_type_name, exception_module, PY3, xrange, BytesLiteral, UnicodeLiteral, - PY2, PY_37_PLUS + PY2, PY_37_PLUS, ABC ) -from testfixtures.comparison import compare_sequence +from testfixtures.comparison import compare_sequence, compare_object from unittest import TestCase hexaddr = compile('0x[0-9A-Fa-f]+') @@ -36,31 +37,41 @@ _compare = compare -class CompareHelper(object): - def check_raises(self, x=marker, y=marker, message=None, regex=None, - compare=compare, **kw): - args = [] - for value in x, y: - if value is not marker: - args.append(value) - try: - compare(*args, **kw) - except Exception as e: - if not isinstance(e, AssertionError): # pragma: no cover - raise - actual = hexsub(e.args[0]) - if message is not None: - # handy for debugging, but can't be relied on for tests! - _compare(actual, expected=message, show_whitespace=True) - assert actual == message - else: - if not regex.match(actual): # pragma: no cover - raise AssertionError( - '%r did not match %r' % (actual, regex.pattern) - ) +def check_raises(x=marker, y=marker, message=None, regex=None, + compare=compare, **kw): + args = [] + for value in x, y: + if value is not marker: + args.append(value) + for value in 'x', 'y': + explicit = 'explicit_{}'.format(value) + if explicit in kw: + kw[value] = kw[explicit] + del kw[explicit] + try: + compare(*args, **kw) + except Exception as e: + if not isinstance(e, AssertionError): # pragma: no cover + raise + actual = hexsub(e.args[0]) + if message is not None: + # handy for debugging, but can't be relied on for tests! + _compare(actual, expected=message, show_whitespace=True) + assert actual == message else: - raise AssertionError('No exception raised!') + if not regex.match(actual): # pragma: no cover + raise AssertionError( + '%r did not match %r' % (actual, regex.pattern) + ) + else: + raise AssertionError('No exception raised!') + + +class CompareHelper(object): + + def check_raises(self, *args, **kw): + check_raises(*args, **kw) class TestCompare(CompareHelper, TestCase): @@ -84,6 +95,10 @@ def test_number_different(self): self.check_raises(1, 2, '1 != 2') + def test_decimal_different(self): + self.check_raises(Decimal(1), Decimal(2), + "Decimal('1') != Decimal('2')") + def test_different_with_labels(self): self.check_raises(1, 2, '1 (expected) != 2 (actual)', x_label='expected', y_label='actual') @@ -291,6 +306,30 @@ "[4]" ) + def test_list_different_float(self): + self.check_raises( + [1, 2, 3.0], [1, 2, 4.0], + "sequence not as expected:\n\n" + "same:\n" + "[1, 2]\n\n" + "first:\n" + "[3.0]\n\n" + "second:\n" + "[4.0]" + ) + + def test_list_different_decimal(self): + self.check_raises( + [1, 2, Decimal(3)], [1, 2, Decimal(4)], + "sequence not as expected:\n\n" + "same:\n" + "[1, 2]\n\n" + "first:\n" + "[Decimal('3')]\n\n" + "second:\n" + "[Decimal('4')]" + ) + def test_list_totally_different(self): self.check_raises( [1], [2], @@ -748,6 +787,34 @@ pass self.check_raises(X, Y, expected) + def test_new_style_classes_same(self): + class X(object): + pass + compare(X, X) + + def test_new_style_classes_different(self): + if PY3: + expected = ( + ".X'>" + " != " + ".Y'>" + ) + else: + expected = ( + "" + " != " + "" + ) + + class X(object): + pass + + class Y(object): + pass + self.check_raises(X, Y, expected) + def test_show_whitespace(self): # does nothing! ;-) self.check_raises( @@ -1325,13 +1392,18 @@ message="'x' (expected) != 'y' (actual)") def test_explicit_both(self): - self.check_raises(message="'x' (expected) != 'y' (actual)", - expected='x', actual='y') + self.check_raises(expected='x', actual='y', + message="'x' (expected) != 'y' (actual)") + + def test_implicit_and_labels(self): + self.check_raises('x', 'y', + x_label='x_label', y_label='y_label', + message="'x' (x_label) != 'y' (y_label)") def test_explicit_and_labels(self): - self.check_raises(message="'x' (x_label) != 'y' (y_label)", - expected='x', actual='y', - x_label='x_label', y_label='y_label') + self.check_raises(explicit_x='x', explicit_y='y', + x_label='x_label', y_label='y_label', + message="'x' (x_label) != 'y' (y_label)") def test_invalid_two_args_expected(self): with ShouldRaise(TypeError( @@ -1447,8 +1519,8 @@ compare(m.mock_calls, m.mock_calls, strict=True) def test_calls_different(self): - m1 =Mock() - m2 =Mock() + m1 = Mock() + m2 = Mock() m1.foo(1, 2, x=3, y=4) m2.bar(1, 3, x=7, y=4) @@ -1472,45 +1544,73 @@ "'call.bar(1, 3, x=7, y=4)'" ) - def test_compare_arbitrary_nested_same(self): - compare(SampleClassA([SampleClassB()]), - SampleClassA([SampleClassB()])) - - def test_compare_different_vars(self): - obj1 = SampleClassB(1) - obj1.same = 42 - obj1.foo = '1' - obj2 = SampleClassB(2) - obj2.same = 42 - obj2.bar = '2' + def test_call_args_different(self): + m = Mock() + m.foo(1) + + self.check_raises( + m.foo.call_args, + call(2), + "'call(1)' != 'call(2)'" + ) + + def test_calls_args_different_but_same_repr(self): + class Annoying(object): + def __init__(self, x): + self.x = x + def __repr__(self): + return '' + m1 = Mock() + m2 = Mock() + m1.foo(Annoying(1)) + m2.foo(Annoying(3)) + self.check_raises( - obj1, obj2, - "SampleClassB not as expected:\n" - "\n" - "attributes same:\n" - "['same']\n" - "\n" - 'attributes in first but not second:\n' - "'foo': '1'\n" - "\n" - 'attributes in second but not first:\n' - "'bar': '2'\n" + m1.mock_calls, + m2.mock_calls, + 'sequence not as expected:\n' '\n' - 'attributes differ:\n' - "'args': (1,) != (2,)\n" + 'same:\n' + '[]\n' '\n' - "While comparing .args: sequence not as expected:\n" + 'first:\n' + '[call.foo()]\n' + '\n' + 'second:\n' + '[call.foo()]\n' + '\n' + 'While comparing [0]: mock.call not as expected:\n' + '\n' + 'While comparing [0] args: sequence not as expected:\n' '\n' 'same:\n' '()\n' '\n' 'first:\n' - '(1,)\n' + '(,)\n' '\n' 'second:\n' - '(2,)' + '(,)\n' + '\n' + 'While comparing [0] args[0]: Annoying not as expected:\n' + '\n' + 'attributes differ:\n' + "'x': 1 != 3" ) + def test_calls_nested_equal_sub_attributes(self): + class Annoying(object): + def __init__(self, x): + self.x = x + def __repr__(self): + return '' + m1 = Mock() + m2 = Mock() + m1.foo(x=[Annoying(1)]) + m2.foo(x=[Annoying(1)]) + + compare(m1.mock_calls, m2.mock_calls) + def test_compare_arbitrary_nested_diff(self): class OurClass: def __init__(self, *args): @@ -1619,6 +1719,26 @@ compare(Child(1), Child(1)) + def test_slots_and_attrs(self): + + class Parent(object): + __slots__ = ('a',) + + class Child(Parent): + def __init__(self, a, b): + self.a = a + self.b = b + + self.check_raises(Child(1, 2), Child(1, 3), message=( + 'Child not as expected:\n' + '\n' + 'attributes same:\n' + "['a']\n" + '\n' + 'attributes differ:\n' + "'b': 2 != 3" + )) + def test_partial_callable_different(self): def foo(x): pass @@ -1719,6 +1839,21 @@ message="Both expected and actual appear as 'Wut', but are not equal!" ) + def test_string_with_slotted(self): + + class Slotted(object): + __slots__ = ['foo'] + def __init__(self, foo): + self.foo = foo + def __repr__(self): + return repr(self.foo) + + self.check_raises( + 'foo', + Slotted('foo'), + "'foo' (%s) != 'foo' (%s)" % (repr(str), repr(Slotted)) + ) + def test_not_recursive(self): self.check_raises( {1: 'foo', 2: 'foo'}, @@ -1772,3 +1907,85 @@ "'id': 1 != 2", ignore_attributes=ignore ) + + +class TestCompareObject(object): + + class Thing(object): + def __init__(self, **kw): + for k, v in kw.items(): + setattr(self, k, v) + + def test_ignore(self): + def compare_thing(x, y, context): + return compare_object(x, y, context, ignore_attributes=['y']) + compare(self.Thing(x=1, y=2), self.Thing(x=1, y=3), + comparers={self.Thing: compare_thing}) + + def test_ignore_dict_context_list_param(self): + def compare_thing(x, y, context): + return compare_object(x, y, context, ignore_attributes=['y']) + compare(self.Thing(x=1, y=2, z=3), self.Thing(x=1, y=4, z=5), + comparers={self.Thing: compare_thing}, + ignore_attributes={self.Thing: ['z']}) + + def test_ignore_list_context_list_param(self): + def compare_thing(x, y, context): + return compare_object(x, y, context, ignore_attributes=['y']) + compare(self.Thing(x=1, y=2, z=3), self.Thing(x=1, y=4, z=5), + comparers={self.Thing: compare_thing}, + ignore_attributes=['z']) + + +class BaseClass(ABC): + pass + + +class MyDerivedClass(BaseClass): + + def __init__(self, thing): + self.thing = thing + + +class ConcreteBaseClass(object): pass + + +class ConcreteDerivedClass(ConcreteBaseClass): + + def __init__(self, thing): + self.thing = thing + + +class TestBaseClasses(CompareHelper): + + def test_abc_equal(self): + thing1 = MyDerivedClass(1) + thing2 = MyDerivedClass(1) + + compare(thing1, thing2) + + def test_abc_unequal(self): + thing1 = MyDerivedClass(1) + thing2 = MyDerivedClass(2) + + self.check_raises(thing1, thing2, message=( + "MyDerivedClass not as expected:\n\n" + "attributes differ:\n" + "'thing': 1 != 2" + )) + + def test_concrete_equal(self): + thing1 = ConcreteDerivedClass(1) + thing2 = ConcreteDerivedClass(1) + + compare(thing1, thing2) + + def test_concrete_unequal(self): + thing1 = ConcreteDerivedClass(1) + thing2 = ConcreteDerivedClass(2) + + self.check_raises(thing1, thing2, message=( + "ConcreteDerivedClass not as expected:\n\n" + "attributes differ:\n" + "'thing': 1 != 2" + )) diff -Nru python-testfixtures-6.10.1/testfixtures/tests/test_comparison.py python-testfixtures-6.14.1/testfixtures/tests/test_comparison.py --- python-testfixtures-6.10.1/testfixtures/tests/test_comparison.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/tests/test_comparison.py 2020-04-20 12:23:41.000000000 +0000 @@ -427,6 +427,85 @@ AClass(1, 2), ) + def run_property_equal_test(self, strict): + class SomeClass(object): + @property + def prop(self): + return 1 + + self.assertEqual( + C(SomeClass, prop=1, strict=strict), + SomeClass() + ) + + def test_property_equal_strict(self): + self.run_property_equal_test(strict=True) + + def test_property_equal_not_strict(self): + self.run_property_equal_test(strict=False) + + def run_property_not_equal_test(self, strict): + class SomeClass(object): + @property + def prop(self): + return 1 + + c = C(SomeClass, prop=2, strict=strict) + self.assertNotEqual(c, SomeClass()) + compare_repr( + c, + "\n" + "\n" + "attributes differ:\n" + "'prop': 2 (Comparison) != 1 (actual)\n" + "") + + def test_property_not_equal_strict(self): + self.run_property_not_equal_test(strict=True) + + def test_property_not_equal_not_strict(self): + self.run_property_not_equal_test(strict=False) + + def run_method_equal_test(self, strict): + class SomeClass(object): + def method(self): + pass # pragma: no cover + + instance = SomeClass() + self.assertEqual( + C(SomeClass, method=instance.method, strict=strict), + instance + ) + + def test_method_equal_strict(self): + self.run_method_equal_test(strict=True) + + def test_method_equal_not_strict(self): + self.run_method_equal_test(strict=False) + + def run_method_not_equal_test(self, strict): + class SomeClass(object): pass + instance = SomeClass() + instance.method = min + + c = C(SomeClass, method=max, strict=strict) + self.assertNotEqual(c, instance) + compare_repr( + c, + "\n" + "\n" + "attributes differ:\n" + "'method': (Comparison)" + " != (actual)\n" + "" + ) + + def test_method_not_equal_strict(self): + self.run_method_not_equal_test(strict=True) + + def test_method_not_equal_not_strict(self): + self.run_method_not_equal_test(strict=False) + def test_exception(self): self.assertEqual( ValueError('foo'), diff -Nru python-testfixtures-6.10.1/testfixtures/tests/test_popen.py python-testfixtures-6.14.1/testfixtures/tests/test_popen.py --- python-testfixtures-6.10.1/testfixtures/tests/test_popen.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/tests/test_popen.py 2020-04-20 12:23:41.000000000 +0000 @@ -185,6 +185,54 @@ call.Popen('a command', shell=True, stderr=PIPE, stdout=PIPE), ], Popen.mock.method_calls) + def test_communicate_text_mode(self): + Popen = MockPopen() + Popen.set_command('a command', stdout=b'foo', stderr=b'bar') + # usage + process = Popen('a command', stdout=PIPE, stderr=PIPE, text=True) + actual = process.communicate() + # check + compare(actual, expected=(u'foo', u'bar')) + + def test_communicate_universal_newlines(self): + Popen = MockPopen() + Popen.set_command('a command', stdout=b'foo', stderr=b'bar') + # usage + process = Popen('a command', stdout=PIPE, stderr=PIPE, universal_newlines=True) + actual = process.communicate() + # check + compare(actual, expected=(u'foo', u'bar')) + + def test_communicate_encoding(self): + Popen = MockPopen() + Popen.set_command('a command', stdout=b'foo', stderr=b'bar') + # usage + process = Popen('a command', stdout=PIPE, stderr=PIPE, encoding='ascii') + actual = process.communicate() + # check + compare(actual, expected=(u'foo', u'bar')) + + def test_communicate_encoding_with_errors(self): + Popen = MockPopen() + Popen.set_command('a command', stdout=b'\xa3', stderr=b'\xa3') + # usage + process = Popen('a command', stdout=PIPE, stderr=PIPE, encoding='ascii', errors='ignore') + actual = process.communicate() + # check + if PY2: + compare(actual, expected=(b'\xa3', b'\xa3')) + else: + compare(actual, expected=(u'', u'')) + + def test_read_from_stdout_and_stderr_text_mode(self): + Popen = MockPopen() + Popen.set_command('a command', stdout=b'foo', stderr=b'bar') + # usage + process = Popen('a command', stdout=PIPE, stderr=PIPE, text=True) + actual = process.stdout.read(), process.stderr.read() + # check + compare(actual, expected=(u'foo', u'bar')) + def test_write_to_stdin(self): # setup Popen = MockPopen() diff -Nru python-testfixtures-6.10.1/testfixtures/tests/test_twisted.py python-testfixtures-6.14.1/testfixtures/tests/test_twisted.py --- python-testfixtures-6.10.1/testfixtures/tests/test_twisted.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/tests/test_twisted.py 2020-04-20 12:23:41.000000000 +0000 @@ -2,7 +2,8 @@ from twisted.python.failure import Failure from twisted.trial.unittest import TestCase -from testfixtures import compare, ShouldRaise +from testfixtures import compare, ShouldRaise, StringComparison as S, ShouldAssert +from testfixtures.compat import PY3 from testfixtures.twisted import LogCapture, INFO log = Logger() @@ -80,3 +81,87 @@ capture.raise_logged_failure(start_index=1) compare(s.raised.value, expected=TypeError('all gone wrong')) self.flushLoggedErrors() + + def test_order_doesnt_matter_ok(self): + capture = LogCapture.make(self) + log.info('Failed to send BAR') + log.info('Sent FOO, length 1234') + log.info('Sent 1 Messages') + capture.check( + (INFO, S('Sent FOO, length \d+')), + (INFO, 'Failed to send BAR'), + (INFO, 'Sent 1 Messages'), + order_matters=False + ) + + def test_order_doesnt_matter_failure(self): + capture = LogCapture.make(self) + log.info('Failed to send BAR') + log.info('Sent FOO, length 1234') + log.info('Sent 1 Messages') + with ShouldAssert( + "entries not as expected:\n" + "\n" + "expected and found:\n" + "[(, 'Failed to send BAR'), (, 'Sent 1 Messages')]\n" + "\n" + "expected but not found:\n" + "[(, )]\n" + "\n" + "other entries:\n" + "[(, {}'Sent FOO, length 1234')]".format('' if PY3 else 'u') + ): + capture.check( + (INFO, S('Sent FOO, length abc')), + (INFO, 'Failed to send BAR'), + (INFO, 'Sent 1 Messages'), + order_matters=False + ) + + def test_order_doesnt_matter_extra_in_expected(self): + capture = LogCapture.make(self) + log.info('Failed to send BAR') + log.info('Sent FOO, length 1234') + with ShouldAssert( + "entries not as expected:\n" + "\n" + "expected and found:\n" + "[(, 'Failed to send BAR'),\n" + " (, )]\n" + "\n" + "expected but not found:\n" + "[(, 'Sent 1 Messages')]\n" + "\n" + "other entries:\n" + "[]" + ): + capture.check( + (INFO, S('Sent FOO, length 1234')), + (INFO, 'Failed to send BAR'), + (INFO, 'Sent 1 Messages'), + order_matters=False + ) + + def test_order_doesnt_matter_extra_in_actual(self): + capture = LogCapture.make(self) + log.info('Failed to send BAR') + log.info('Sent FOO, length 1234') + log.info('Sent 1 Messages') + with ShouldAssert( + "entries not as expected:\n" + "\n" + "expected and found:\n" + "[(, 'Failed to send BAR'), (, 'Sent 1 Messages')]\n" + "\n" + "expected but not found:\n" + "[(, )]\n" + "\n" + "other entries:\n" + "[(, {}'Sent FOO, length 1234')]".format('' if PY3 else 'u') + ): + capture.check( + (INFO, S('Sent FOO, length abc')), + (INFO, 'Failed to send BAR'), + (INFO, 'Sent 1 Messages'), + order_matters=False + ) diff -Nru python-testfixtures-6.10.1/testfixtures/twisted.py python-testfixtures-6.14.1/testfixtures/twisted.py --- python-testfixtures-6.10.1/testfixtures/twisted.py 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/twisted.py 2020-04-20 12:23:41.000000000 +0000 @@ -6,6 +6,8 @@ """ from __future__ import absolute_import +from pprint import pformat + from . import compare from twisted.logger import globalLogPublisher, formatEvent, LogLevel @@ -40,19 +42,44 @@ "Stop capturing." globalLogPublisher._observers = self.original_observers - def check(self, *expected): + def check(self, *expected, **kw): """ Check captured events against those supplied. Please see the ``fields`` parameter to the constructor to see how "actual" events are built. + + :param order_matters: + This defaults to ``True``. If ``False``, the order of expected logging versus + actual logging will be ignored. """ + order_matters = kw.pop('order_matters', True) + assert not kw, 'order_matters is the only keyword parameter' actual = [] for event in self.events: - actual_event = [field(event) if callable(field) else event.get(field) - for field in self.fields] + actual_event = tuple(field(event) if callable(field) else event.get(field) + for field in self.fields) if len(actual_event) == 1: actual_event = actual_event[0] actual.append(actual_event) - compare(expected=expected, actual=actual) + if order_matters: + compare(expected=expected, actual=actual) + else: + expected = list(expected) + matched = [] + unmatched = [] + for entry in actual: + try: + index = expected.index(entry) + except ValueError: + unmatched.append(entry) + else: + matched.append(expected.pop(index)) + if expected: + raise AssertionError(( + 'entries not as expected:\n\n' + 'expected and found:\n%s\n\n' + 'expected but not found:\n%s\n\n' + 'other entries:\n%s' + ) % (pformat(matched), pformat(expected), pformat(unmatched))) def check_failure_text(self, expected, index=-1, attribute='value'): """ diff -Nru python-testfixtures-6.10.1/testfixtures/version.txt python-testfixtures-6.14.1/testfixtures/version.txt --- python-testfixtures-6.10.1/testfixtures/version.txt 2019-11-01 07:57:01.000000000 +0000 +++ python-testfixtures-6.14.1/testfixtures/version.txt 2020-04-20 12:23:41.000000000 +0000 @@ -1 +1 @@ -6.10.1 +6.14.1