nti.testing 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nti/testing/zodb.py ADDED
@@ -0,0 +1,245 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Support for testing of ZODB applications.
4
+
5
+ .. versionadded:: 3.0.0
6
+
7
+ """
8
+ from __future__ import absolute_import
9
+ from __future__ import division
10
+ from __future__ import print_function
11
+
12
+ import gc
13
+ import sys
14
+
15
+
16
+ import transaction
17
+ from transaction.interfaces import NoTransaction
18
+
19
+ import ZODB
20
+ from ZODB.interfaces import IDatabase
21
+ from ZODB.DemoStorage import DemoStorage
22
+ from zope import component
23
+ from zope.exceptions import print_exception
24
+ from zope.exceptions import format_exception
25
+
26
+ from .layers import ZopeComponentLayer
27
+
28
+ PYPY = hasattr(sys, 'pypy_version_info')
29
+
30
+ __all__ = [
31
+ 'mock_db_trans',
32
+ 'ZODBLayer',
33
+ 'reset_db_caches',
34
+ ]
35
+
36
+ # The exceptions are not expected to be caught. They indicate errors
37
+ # to fix.
38
+ class _BaseException(Exception):
39
+ default_message = 'Unknown'
40
+ def __init__(self, t, v, tb):
41
+ msg = self.default_message
42
+ if t:
43
+ msg = ''.join(format_exception(t, v, tb))
44
+ Exception.__init__(self, msg)
45
+
46
+ class _TransactionManagerModeChanged(_BaseException):
47
+ default_message = 'The transaction manager explicit mode changed'
48
+
49
+ class _TransactionChanged(_BaseException):
50
+ default_message = 'The body changed the active transaction'
51
+
52
+ class mock_db_trans(object):
53
+ """
54
+ A context manager that begins and commits a database transaction.
55
+
56
+ This class may be subclassed; there are some extension points
57
+ to allow customization.
58
+
59
+ This class is not designed for concurrency.
60
+
61
+ Entering this context manager begins a transaction using the
62
+ thread-local global transaction manager and returns a connection
63
+ open to the database given in the *db* parameter. Exiting this
64
+ context manager commits the transaction (or aborts it if it is
65
+ doomed).
66
+
67
+ It is an error to enter this context manager with a transaction
68
+ already in progress. The global transaction may be committed or aborted
69
+ before exiting this transaction, but no new transaction may be opened. During
70
+ execution of the body, the transaction manager is in explicit mode;
71
+ it is an error if this is changed during the body.
72
+ """
73
+
74
+ #: The connection that was opened. Valid after entering and before exiting.
75
+ conn = None
76
+
77
+ #: The file to which unexpected exceptions that cannot be raised are printed.
78
+ #: If `None`, exceptions will be written to `sys.stderr`
79
+ exc_file = None
80
+
81
+ def __init__(self, db=None):
82
+ """
83
+ :param db: The :class:`ZODB.DB` to open. If none is given,
84
+ then the :attr:`ZODBLayer.db` will be used.
85
+ """
86
+ self.db = db if db is not None else ZODBLayer.db
87
+ self.__txm_was_explicit = None
88
+ self.__current_transaction = None
89
+
90
+ def on_connection_opened(self, conn):
91
+ """
92
+ Called when the connection to the DB has been opened.
93
+
94
+ Subclasses may override to perform initialization.
95
+ The DB may have been used before, so check its state and don't assume
96
+ a complete initialization must happen.
97
+ """
98
+
99
+ def __enter__(self):
100
+ txm = transaction.manager
101
+ self.__txm_was_explicit = txm.explicit
102
+ txm.explicit = True
103
+ try:
104
+ self.__current_transaction = txm.begin()
105
+ self.conn = conn = self.db.open()
106
+ self.on_connection_opened(conn)
107
+ except:
108
+ # Could be several things:
109
+ # Already in a transaction is definitely a user error.
110
+ # Failure to open the DB: Probably also a user error.
111
+ # Failure to setup the DB: Possibly a user error, or error in a
112
+ # subclass.
113
+ txm.explicit = self.__txm_was_explicit
114
+ if self.__current_transaction:
115
+ self._clean_up_one_transaction(self.__current_transaction, True)
116
+
117
+ if self.conn is not None:
118
+ self.conn.close()
119
+ self.conn = None
120
+ self.__current_transaction = None
121
+ raise
122
+ return self.conn
123
+
124
+ def _clean_up_one_transaction(self, tx, abort_only):
125
+ try:
126
+ if abort_only or tx.isDoomed():
127
+ tx.abort()
128
+ else:
129
+ tx.commit()
130
+ except:
131
+ tx.abort() # idempotent with transaction 3
132
+ raise
133
+
134
+ def _report_exception(self, msg, t, v, tb):
135
+ f = self.exc_file or sys.stderr
136
+ print(msg, file=f)
137
+ print_exception(t, v, tb, file=f, with_filenames=False)
138
+
139
+ def _close_connection(self, conn, ignore_errors):
140
+ catch = Exception if ignore_errors else ()
141
+ try:
142
+ conn.close()
143
+ except catch: # pylint:disable=broad-exception-caught
144
+ self._report_exception("Unexpected error closing connection; ignored.", *sys.exc_info())
145
+
146
+ def __exit__(self, t, v, tb):
147
+ # Returning a True value from __exit__ suppresses any
148
+ # exception raised in the body.
149
+ body_raised = t is not None
150
+ abort_only = body_raised # Should we commit our transaction, or only abort it?
151
+ error_in_body = None # Did the body do something wrong?
152
+ # Assuming we're still on the same thread and this hasn't been reset.
153
+ txm = transaction.manager
154
+ tx = self.__current_transaction
155
+ if not txm.explicit:
156
+ abort_only = True
157
+ error_in_body:Exception = _TransactionManagerModeChanged(t, v, tb)
158
+
159
+ try:
160
+ if txm.get() is not tx:
161
+ # Remember, .get() could raise.
162
+ self._clean_up_one_transaction(txm.get(), abort_only=True)
163
+ error_in_body = _TransactionChanged(t, v, tb)
164
+ except NoTransaction:
165
+ abort_only = True # It's already gone, so just abort it
166
+ error_in_body = _TransactionChanged(t, v, tb)
167
+
168
+ try:
169
+ self._clean_up_one_transaction(tx, abort_only)
170
+ except Exception: # pylint:disable=broad-except
171
+ if not body_raised:
172
+ # The body had no issue, but the commit did. Raise the commit error,
173
+ # but let the finally block know not to shadow it.
174
+ body_raised = True
175
+ raise
176
+ self._report_exception(
177
+ "Failed to cleanup trans, but body raised exception too. "
178
+ "This exception will be ignored.",
179
+ *sys.exc_info())
180
+ # So let the body exception propagate
181
+ finally:
182
+ txm.explicit = self.__txm_was_explicit
183
+ self._close_connection(self.conn, ignore_errors=body_raised)
184
+ self.conn = self.__current_transaction = None
185
+ reset_db_caches(self.db)
186
+ if error_in_body:
187
+ raise error_in_body # pylint:disable=raising-bad-type
188
+
189
+
190
+
191
+ def reset_db_caches(db=None, collect=False):
192
+ """
193
+ Minimize the caches of all connections found in the *db*.
194
+
195
+ If the *db* is not given, then the one from the :class:`ZODBLayer`
196
+ is used.
197
+
198
+ On PyPy, or if *collect* is true, this will invoke
199
+ :func:`gc.collect` to help remove any weak references to objects
200
+ that were ejected from the cache.
201
+ """
202
+ result = -1
203
+ if db is None:
204
+ db = ZODBLayer.db
205
+ if db is not None:
206
+ for conn in db.pool:
207
+ conn.cacheMinimize()
208
+ if PYPY or collect:
209
+ result = gc.collect()
210
+ return result
211
+
212
+ class ZODBLayer(ZopeComponentLayer):
213
+ """
214
+ Test layer that creates a ZODB database using
215
+ :class:`ZODB.DemoStorage.DemoStorage` and registers it as the
216
+ no-name :class:`ZODB.interfaces.IDatabase` in the global component
217
+ registry. It is also available in the :attr:`db` attribute of this
218
+ object.
219
+ """
220
+
221
+ #: The DB that was created.
222
+ db = None
223
+
224
+ @classmethod
225
+ def setUp(cls):
226
+ db = cls.db = ZODB.DB(DemoStorage())
227
+ component.getGlobalSiteManager().registerUtility(db, IDatabase)
228
+
229
+ @classmethod
230
+ def tearDown(cls):
231
+ db = cls.db
232
+ cls.db = None
233
+ if db is not None:
234
+ db.close()
235
+ reg_db = component.getGlobalSiteManager().queryUtility(IDatabase)
236
+ if reg_db is db:
237
+ component.getGlobalSiteManager().unregisterUtility(db, IDatabase)
238
+
239
+ @classmethod
240
+ def testSetUp(cls):
241
+ pass
242
+
243
+ @classmethod
244
+ def testTearDown(cls):
245
+ pass
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this software except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
@@ -0,0 +1,423 @@
1
+ Metadata-Version: 2.1
2
+ Name: nti.testing
3
+ Version: 4.2.0
4
+ Summary: Support for testing code
5
+ Home-page: https://github.com/OpenNTI/nti.testing
6
+ Author: Jason Madden
7
+ Author-email: jason@nextthought.com
8
+ License: Apache
9
+ Project-URL: Documentation, https://ntitesting.readthedocs.io/en/latest/
10
+ Keywords: nose2 testing zope3 ZTK hamcrest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Framework :: Zope3
25
+ Requires-Python: >=3.10
26
+ License-File: LICENSE
27
+ Requires-Dist: ZODB>=5.6.0
28
+ Requires-Dist: zope.interface>=5.4.0
29
+ Requires-Dist: pyhamcrest
30
+ Requires-Dist: six
31
+ Requires-Dist: transaction
32
+ Requires-Dist: zope.component
33
+ Requires-Dist: zope.configuration
34
+ Requires-Dist: zope.dottedname
35
+ Requires-Dist: zope.exceptions
36
+ Requires-Dist: zope.schema
37
+ Requires-Dist: zope.testing
38
+ Provides-Extra: docs
39
+ Requires-Dist: Sphinx; extra == "docs"
40
+ Requires-Dist: furo; extra == "docs"
41
+ Provides-Extra: test
42
+ Requires-Dist: Acquisition; extra == "test"
43
+ Requires-Dist: zope.site; extra == "test"
44
+ Requires-Dist: zope.testrunner; extra == "test"
45
+ Requires-Dist: testgres>=1.10; extra == "test"
46
+ Requires-Dist: psycopg2-binary; platform_python_implementation != "PyPy" and extra == "test"
47
+ Provides-Extra: testgres
48
+ Requires-Dist: testgres>=1.10; extra == "testgres"
49
+ Requires-Dist: psycopg2-binary; platform_python_implementation != "PyPy" and extra == "testgres"
50
+
51
+ =============
52
+ nti.testing
53
+ =============
54
+
55
+ .. image:: https://img.shields.io/pypi/v/nti.testing.svg
56
+ :target: https://pypi.python.org/pypi/nti.testing/
57
+ :alt: Latest release
58
+
59
+ .. image:: https://img.shields.io/pypi/pyversions/nti.testing.svg
60
+ :target: https://pypi.org/project/nti.testing/
61
+ :alt: Supported Python versions
62
+
63
+ .. image:: https://github.com/NextThought/nti.testing/actions/workflows/tests.yml/badge.svg
64
+ :target: https://github.com/NextThought/nti.testing/actions/workflows/tests.yml
65
+ :alt: Test Status
66
+
67
+ .. image:: https://coveralls.io/repos/github/NextThought/nti.testing/badge.svg
68
+ :target: https://coveralls.io/github/NextThought/nti.testing
69
+
70
+ .. image:: http://readthedocs.org/projects/ntitesting/badge/?version=latest
71
+ :target: http://ntitesting.readthedocs.io/en/latest/?badge=latest
72
+ :alt: Documentation Status
73
+
74
+ Support for writing tests, particularly in a Zope3/ZTK environment,
75
+ using zope.testing (nose2 may also work, but is not recommended).
76
+
77
+ Complete documentation is hosted at https://ntitesting.readthedocs.io/
78
+
79
+ Installation
80
+ ============
81
+
82
+ nti.testing can be installed using pip, either from the git repository
83
+ or from PyPI::
84
+
85
+ pip install nti.testing[testgres]
86
+
87
+ Use the ``testgres`` extra to be able to use `nti.testing.layers.postgres`.
88
+
89
+ PyHamcrest
90
+ ==========
91
+
92
+ nti.testing provides a group of `PyHamcrest`_ matchers. There are both
93
+ general-purpose matchers and matchers that are of use to users of
94
+ `zope.interface`_ and `zope.schema`_.
95
+
96
+
97
+ .. _PyHamcrest: https://pyhamcrest.readthedocs.io/en/latest/
98
+ .. _zope.interface: https://pypi.python.org/pypi/zope.interface
99
+ .. _zope.schema: https://pypi.python.org/pypi/zope.schema
100
+
101
+ .. NOTE: We rely on the Sphinx 'default_role' to turn single back quotes into links,
102
+ while still being compatible with rendering with plain docutils/readme_renderer
103
+ for PyPI.
104
+
105
+ Matchers can be imported from the `nti.testing.matchers` module; the most commonly used matchers
106
+ can be directly imported from `nti.testing`.
107
+
108
+ Basic Matchers
109
+ --------------
110
+
111
+ `is_true` and `is_false` check the `bool` value of a supplied
112
+ object (we're using literals for explanation purposes, but it
113
+ obviously makes more sense, and reads better, when the matched object
114
+ is a variable, often of a more complex type):
115
+
116
+ >>> from hamcrest import assert_that, is_
117
+ >>> from nti.testing import is_true, is_false
118
+ >>> assert_that("Hi", is_true())
119
+ >>> assert_that(0, is_false())
120
+
121
+ Interface Matchers
122
+ ------------------
123
+
124
+ Next we come to matchers that support basic use of ``zope.interface``.
125
+
126
+ We can check that an object provides an interface and that a factory
127
+ implements it:
128
+
129
+ >>> from zope.interface import Interface, Attribute, implementer
130
+ >>> class IThing1(Interface):
131
+ ... pass
132
+ >>> class IThing2(Interface):
133
+ ... pass
134
+ >>> class IThings(IThing1, IThing2):
135
+ ... got_that_thing_i_sent_you = Attribute("Did you get that thing?")
136
+ >>> @implementer(IThings)
137
+ ... class Thing(object):
138
+ ... def __repr__(self): return "<object Thing>"
139
+
140
+ >>> from nti.testing import provides, implements
141
+ >>> assert_that(Thing(), provides(IThings))
142
+ >>> assert_that(Thing, implements(IThings))
143
+
144
+ The attentive reader will have noticed that ``IThings`` defines an
145
+ attribute that our implementation doesn't *actually* provide. This is
146
+ where the next stricter check comes in. `verifiably_provides` uses
147
+ the interface machinery to determine that all attributes and methods
148
+ specified by the interface are present as described:
149
+
150
+ >>> from nti.testing import verifiably_provides
151
+ >>> assert_that(Thing(), verifiably_provides(IThing2, IThing1))
152
+ >>> assert_that(Thing(), verifiably_provides(IThings))
153
+ Traceback (most recent call last):
154
+ ...
155
+ AssertionError:...
156
+ Expected: object verifiably providing <...IThings>
157
+ but: Using class <class 'Thing'> the object <object Thing> has failed to implement interface ....IThings: The ....IThings.got_that_thing_i_sent_you attribute was not provided.
158
+ <BLANKLINE>
159
+
160
+ If multiple attributes or methods are not provided, all such missing
161
+ information is reported:
162
+
163
+ >>> class IThingReceiver(IThings):
164
+ ... def receive(some_thing):
165
+ ... """Get the thing"""
166
+ >>> @implementer(IThingReceiver)
167
+ ... class ThingReceiver(object):
168
+ ... def __repr__(self): return "<object ThingReceiver>"
169
+ >>> assert_that(ThingReceiver(), verifiably_provides(IThingReceiver))
170
+ Traceback (most recent call last):
171
+ ...
172
+ AssertionError:...
173
+ Expected: object verifiably providing <...IThingReceiver>
174
+ but: Using class <class 'ThingReceiver'> the object <object ThingReceiver> has failed to implement interface ....IThingReceiver:
175
+ The ....IThings.got_that_thing_i_sent_you attribute was not provided
176
+ The ....IThingReceiver.receive(some_thing) attribute was not provided
177
+ <BLANKLINE>
178
+
179
+ ``zope.interface`` can only check whether or not an attribute or
180
+ method is present. To place (arbitrary) tighter constraints on the
181
+ values of the attributes, we can step up to ``zope.schema`` and the
182
+ `validly_provides` matcher:
183
+
184
+ >>> from zope.schema import Bool
185
+ >>> class IBoolThings(IThing1, IThing2):
186
+ ... got_that_thing_i_sent_you = Bool(required=True)
187
+ >>> @implementer(IBoolThings)
188
+ ... class BoolThing(object):
189
+ ... def __repr__(self): return "<object BoolThing>"
190
+
191
+ `validly_provides` is a superset of `verifiably_provides`:
192
+
193
+ >>> from nti.testing import validly_provides
194
+ >>> assert_that(BoolThing(), validly_provides(IThing1, IThing2))
195
+ >>> assert_that(BoolThing(), validly_provides(IBoolThings))
196
+ Traceback (most recent call last):
197
+ ...
198
+ AssertionError:...
199
+ Expected: (object verifiably providing <...IBoolThings> and object validly providing ....IBoolThings)
200
+ but: object verifiably providing <....IBoolThings> Using class <class 'BoolThing'> the object <object BoolThing> has failed to implement interface ....IBoolThings: The ....IBoolThings.got_that_thing_i_sent_you attribute was not provided.
201
+ <BLANKLINE>
202
+
203
+ For finer grained control, we can compare data against schema fields
204
+ using `validated_by` and `not_validated_by`:
205
+
206
+ >>> from nti.testing import validated_by, not_validated_by
207
+ >>> field = IBoolThings.get('got_that_thing_i_sent_you')
208
+ >>> assert_that(True, is_(validated_by(field)))
209
+ >>> assert_that(None, is_(not_validated_by(field)))
210
+
211
+ Parent/Child Relationships
212
+ --------------------------
213
+
214
+ The `aq_inContextOf` matcher uses the concepts from `Acquisition` to
215
+ check parent/child relationships:
216
+
217
+ >>> from nti.testing import aq_inContextOf
218
+ >>> class Parent(object):
219
+ ... pass
220
+ >>> class Child(object):
221
+ ... __parent__ = None
222
+ >>> parent = Parent()
223
+ >>> child = Child()
224
+ >>> child.__parent__ = parent
225
+
226
+ >>> assert_that(child, aq_inContextOf(parent))
227
+
228
+ Test Fixtures
229
+ =============
230
+
231
+ Support for test fixtures can be found in `nti.testing.base` and
232
+ `nti.testing.layers`. The ``base`` package includes fully-fleshed
233
+ out base classes for direct use, while the ``layers`` package mostly includes
234
+ mixins that can be used to construct your own test layers.
235
+
236
+ The ``base`` package makes a distinction between "normal" and "shared"
237
+ fixtures. Normal fixtures are those that are used for a single test
238
+ case. They are established via ``setUp`` and torn down via
239
+ ``tearDown``.
240
+
241
+ In contrast, shared fixtures are expected to endure for the duration
242
+ of all the tests in the class or all the tests in the layer. These are
243
+ best used when the fixture is expensive to create. Anything that
244
+ extends from `nti.testing.base.AbstractSharedTestBase` creates a shared fixture.
245
+ Through the magic of metaclasses, such a subclass can also be assigned
246
+ as the ``layer`` property of another class to be used as a test layer
247
+ that can be shared across more than one class.
248
+
249
+ The most important bases are `nti.testing.base.ConfiguringTestBase` and
250
+ `nti.testing.base.SharedConfiguringTestBase`. These are both fixtures for
251
+ configuring ZCML, either from existing packages or complete file
252
+ paths. To use these, subclass them and define class attributes
253
+ ``set_up_packages`` and (if necessary) ``features``:
254
+
255
+ >>> from nti.testing.base import ConfiguringTestBase
256
+ >>> import zope.security
257
+ >>> class MyConfiguringTest(ConfiguringTestBase):
258
+ ... set_up_packages = (
259
+ ... 'zope.component', # the default configuration by name
260
+ ... # a named file in a named package
261
+ ... ('ftesting.zcml', 'zope.traversing.tests'),
262
+ ... # an imported module
263
+ ... zope.security,
264
+ ... # Our own package; in a test, this will mean the parent
265
+ ... # package
266
+ ... ".")
267
+
268
+ We would then proceed to write our test methods. The packages that we
269
+ specified will be set up and torn down around every test method. In
270
+ addition, the ``zope.testing`` cleanup functions will also run around
271
+ every test method.
272
+
273
+ Time
274
+ ====
275
+
276
+ Having a clock that's guaranteed to move in a positive increasing way
277
+ in every call to ``time.time`` is useful. `nti.testing.time`
278
+ provides a decorator to accomplish this that ensures values always are
279
+ at least the current time and always are increasing. (It is not thread
280
+ safe.) It can be applied to functions or methods, and optionally takes
281
+ a ``granularity`` argument:
282
+
283
+ >>> from nti.testing import time_monotonically_increases
284
+ >>> from nti.testing.time import reset_monotonic_time
285
+ >>> @time_monotonically_increases(0.1) # increment by 0.1
286
+ ... def test():
287
+ ... import time
288
+ ... t1 = time.time()
289
+ ... t2 = time.time()
290
+ ... assert t2 == t1 + 0.1, (t2, t1)
291
+
292
+ >>> test()
293
+
294
+ And The Rest
295
+ ============
296
+
297
+ There are some other assorted utilities, including support for working with
298
+ ZODB in `nti.testing.zodb`. See the API documentation for details.
299
+
300
+
301
+ =========
302
+ Changes
303
+ =========
304
+
305
+ 4.2.0 (2024-11-06)
306
+ ==================
307
+
308
+ - Add support for Python 3.13.
309
+ - Use native namespace packages.
310
+
311
+
312
+ 4.1.0 (2024-04-10)
313
+ ==================
314
+
315
+ - Add support for, and require, testgres 1.10. This is needed because
316
+ they changed the signature for ``get_pg_version``.
317
+ - Drop support for Python 3.8 and 3.9.
318
+
319
+
320
+ 4.0.0 (2023-10-24)
321
+ ==================
322
+
323
+ - Add support for Python 3.10, 3.11 and 3.12.
324
+ - Drop support for Python 2 and Python 3.6 and 3.7.
325
+ - Add a layer for working with a ``testgres`` Postgres instance.
326
+ - Add methods to the test base classes to support ``unittest.mock`` patches.
327
+
328
+
329
+ 3.1.0 (2021-09-08)
330
+ ==================
331
+
332
+ - Add support for Python 3.9.
333
+
334
+ - Drop support for Python 3.5.
335
+
336
+ - Add the module alias ``nti.testing.mock``, which is either the
337
+ standard library ``unittest.mock``, or the backport ``mock``. This
338
+ allows easy imports when backwards compatibility matters.
339
+
340
+ - Make ``mock``, ``mock.Mock`` and various other API attributes,
341
+ like ``is_true``, available directly from the ``nti.testing`` namespace.
342
+
343
+ 3.0.0 (2020-06-16)
344
+ ==================
345
+
346
+ - Add support for Python 3.8.
347
+
348
+ - Require zope.interface 5.1. This lets the interface matchers produce
349
+ much more informative error messages.
350
+
351
+ - Add ``nti.testing.zodb`` with helpers for dealing with ZODB. This
352
+ makes ZODB 5.6 or above a required dependency.
353
+
354
+ 2.2.1 (2019-09-10)
355
+ ==================
356
+
357
+ - Make transaction cleanup safer if the default transaction manager
358
+ has been made explicit.
359
+
360
+ Also, reset the default transaction manager to implicit.
361
+
362
+ See `issue 17 <https://github.com/NextThought/nti.testing/issues/17>`_.
363
+
364
+
365
+ 2.2.0 (2018-08-24)
366
+ ==================
367
+
368
+ - Add support for Python 3.7.
369
+
370
+ - Make ``time_monotonically_increases`` also handle ``time.gmtime``
371
+ and add a helper for using it in layers.
372
+
373
+
374
+ 2.1.0 (2017-10-23)
375
+ ==================
376
+
377
+ - Make ``Acquisition`` an optional dependency. If it is not installed,
378
+ the ``aq_inContextOf`` matcher will always return False.
379
+
380
+ - Remove dependency on ``fudge``. Instead, we now use ``unittest.mock`` on
381
+ Python 3, or its backport ``mock`` on Python 2. See `issue 11
382
+ <https://github.com/NextThought/nti.testing/issues/11>`_.
383
+
384
+ - Refactor ZCML configuration support to share more code and
385
+ documentation. See `issue 10
386
+ <https://github.com/NextThought/nti.testing/issues/10>`_.
387
+
388
+ - The layer ``ConfiguringLayerMixin`` and the base class
389
+ ``SharedConfiguringTestBase`` now default to running
390
+ configuration in the package the subclass is defined in, just as
391
+ subclasses of ``ConfiguringTestBase`` did.
392
+
393
+ 2.0.1 (2017-10-18)
394
+ ==================
395
+
396
+ - The validation matchers (``validated_by`` and ``not_validated_by``)
397
+ now consider it a failure (by default) if the validate method raises
398
+ anything other than ``zope.interface.exceptions.Invalid`` (which
399
+ includes the ``zope.schema`` exceptions like ``WrongType``).
400
+ Previously, they accepted any exception as meaning the object was
401
+ invalid, but this could hide bugs in the actual validation method
402
+ itself. You can supply the ``invalid`` argument to the matchers to
403
+ loosen or tighten this as desired. (Giving ``invalid=Exception``
404
+ will restore the old behaviour.)
405
+ See `issue 7 <https://github.com/NextThought/nti.testing/issues/7>`_.
406
+
407
+
408
+ 2.0.0 (2017-04-12)
409
+ ==================
410
+
411
+ - Add support for Python 3.6.
412
+
413
+ - Remove ``unicode_literals``.
414
+
415
+ - Substantially rework ``time_monotonically_increases`` for greater
416
+ safety. Fixes `issue 5 <https://github.com/NextThought/nti.testing/issues/5>`_.
417
+
418
+ 1.0.0 (2016-07-28)
419
+ ==================
420
+
421
+ - Add Python 3 support.
422
+
423
+ - Initial PyPI release.