├── .travis.yml
├── LICENSE
├── README.rst
├── README.txt
├── dpcontracts.py
├── setup.py
└── tox.ini
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - 3.5
5 | - 3.6
6 | - 3.7
7 | - 3.8
8 | - pypy3
9 |
10 | before_install:
11 |
12 | install:
13 | - pip install tox tox-travis
14 |
15 | script:
16 | - tox
17 |
18 | notifications:
19 | email: false
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 | This module provides a collection of decorators that makes it easy to
4 | write software using contracts.
5 |
6 | Contracts are a debugging and verification tool. They are declarative
7 | statements about what states a program must be in to be considered
8 | "correct" at runtime. They are similar to assertions, and are verified
9 | automatically at various well-defined points in the program. Contracts can
10 | be specified on functions and on classes.
11 |
12 | Contracts serve as a form of documentation and a way of formally
13 | specifying program behavior. Good practice often includes writing all of
14 | the contracts first, with these contract specifying the exact expected
15 | state before and after each function or method call and the things that
16 | should always be true for a given class of object.
17 |
18 | Contracts consist of two parts: a description and a condition. The
19 | description is simply a human-readable string that describes what the
20 | contract is testing, while the condition is a single function that tests
21 | that condition. The condition is executed automatically and passed certain
22 | arguments (which vary depending on the type of contract), and must return
23 | a boolean value: True if the condition has been met, and False otherwise.
24 |
25 | Legacy Python Support
26 | =====================
27 | This module supports versions of Python >= 3.5; that is, versions with
28 | support for "async def" functions. There is a branch of this module that
29 | is kept compatible to the greatest possible degree for versions of Python
30 | earlier than 3.5 (including Python 2.7).
31 |
32 | The Python 2 and <= 3.5 branch is available at
33 | https://github.com/deadpixi/contracts/tree/python2
34 |
35 | This legacy-compatible version is also distributed on PyPI along the 0.5.x
36 | branch; this branch will kept compatible with newer versions to the greatest
37 | extent possible.
38 |
39 | That branch is a drop-in replacement for this module and includes most
40 | of the functionality, except support for "async def" functions and a few
41 | other things.
42 |
43 | Preconditions and Postconditions
44 | ================================
45 |
46 | >>> from dpcontracts import require, ensure
47 |
48 | Contracts on functions consist of preconditions and postconditions.
49 | A precondition is declared using the ``requires`` decorator, and describes
50 | what must be true upon entrance to the function. The condition function
51 | is passed an arguments object, which has as its attributes the arguments
52 | to the decorated function:
53 |
54 | >>> @require("`i` must be an integer", lambda args: isinstance(args.i, int))
55 | ... @require("`j` must be an integer", lambda args: isinstance(args.j, int))
56 | ... def add2(i, j):
57 | ... return i + j
58 |
59 | Note that an arbitrary number of preconditions can be stacked on top of
60 | each other.
61 |
62 | These decorators have declared that the types of both arguments must be
63 | integers. Calling the ``add2`` function with the correct types of arguments
64 | works:
65 |
66 | >>> add2(1, 2)
67 | 3
68 |
69 | But calling with incorrect argument types (violating the contract) fails
70 | with a ``PreconditionError`` (a subtype of ``AssertionError``):
71 |
72 | >>> add2("foo", 2)
73 | Traceback (most recent call last):
74 | dpcontracts.PreconditionError: `i` must be an integer
75 |
76 | Functions can also have postconditions, specified using the ``ensure``
77 | decorator. Postconditions describe what must be true after the function
78 | has successfully returned. Like the ``require`` decorator, the ``ensure``
79 | decorator is passed an argument object. It is also passed an additional
80 | argument, which is the result of the function invocation. For example:
81 |
82 | >>> @require("`i` must be a positive integer",
83 | ... lambda args: isinstance(args.i, int) and args.i > 0)
84 | ... @require("`j` must be a positive integer",
85 | ... lambda args: isinstance(args.j, int) and args.j > 0)
86 | ... @ensure("the result must be greater than either `i` or `j`",
87 | ... lambda args, result: result > args.i and result > args.j)
88 | ... def add2(i, j):
89 | ... if i == 7:
90 | ... i = -7 # intentionally broken for purposes of example
91 | ... return i + j
92 |
93 | We can now call the function and ensure that everything is working correctly:
94 |
95 | >>> add2(1, 3)
96 | 4
97 |
98 | Except that the function is broken in unexpected ways:
99 |
100 | >>> add2(7, 4)
101 | Traceback (most recent call last):
102 | dpcontracts.PostconditionError: the result must be greater than either `i` or `j`
103 |
104 | The function specifying the condition doesn't have to be a lambda; it can be
105 | any function, and pre- and postconditions don't have to actually reference
106 | the arguments or results of the function at all. They can simply check
107 | the function's environments and effects:
108 |
109 | >>> names = set()
110 | >>> def exists_in_database(x):
111 | ... return x in names
112 | >>> @require("`name` must be a string", lambda args: isinstance(args.name, str))
113 | ... @require("`name` must not already be in the database",
114 | ... lambda args: not exists_in_database(args.name.strip()))
115 | ... @ensure("the normalized version of the name must be added to the database",
116 | ... lambda args, result: exists_in_database(args.name.strip()))
117 | ... def add_to_database(name):
118 | ... if name not in names and name != "Rob": # intentionally broken
119 | ... names.add(name.strip())
120 |
121 | >>> add_to_database("James")
122 | >>> add_to_database("Marvin")
123 | >>> add_to_database("Marvin")
124 | Traceback (most recent call last):
125 | dpcontracts.PreconditionError: `name` must not already be in the database
126 | >>> add_to_database("Rob")
127 | Traceback (most recent call last):
128 | dpcontracts.PostconditionError: the normalized version of the name must be added to the database
129 |
130 | All of the various calling conventions of Python are supported:
131 |
132 | >>> @require("`a` is an integer", lambda args: isinstance(args.a, int))
133 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
134 | ... @require("every member of `c` should be a boolean",
135 | ... lambda args: all(isinstance(x, bool) for x in args.c))
136 | ... def func(a, b="Foo", *c):
137 | ... pass
138 |
139 | >>> func(1, "foo", True, True, False)
140 | >>> func(b="Foo", a=7)
141 | >>> args = {"a": 8, "b": "foo"}
142 | >>> func(**args)
143 | >>> args = (1, "foo", True, True, False)
144 | >>> func(*args)
145 | >>> args = {"a": 9}
146 | >>> func(**args)
147 | >>> func(10)
148 |
149 | A common contract is to validate the types of arguments. To that end,
150 | there is an additional decorator, ``types``, that can be used
151 | to validate arguments' types:
152 |
153 | >>> from dpcontracts import types
154 |
155 | >>> class ExampleClass:
156 | ... pass
157 |
158 | >>> @types(a=int, b=str, c=(type(None), ExampleClass)) # or types.NoneType, if you prefer
159 | ... @require("a must be nonzero", lambda args: args.a != 0)
160 | ... def func(a, b, c=38):
161 | ... return " ".join(str(x) for x in [a, b])
162 |
163 | >>> func(1, "foo", ExampleClass())
164 | '1 foo'
165 |
166 | >>> func(1.0, "foo", ExampleClass) # invalid type for `a`
167 | Traceback (most recent call last):
168 | dpcontracts.PreconditionError: the types of arguments must be valid
169 |
170 | >>> func(1, "foo") # invalid type (the default) for `c`
171 | Traceback (most recent call last):
172 | dpcontracts.PreconditionError: the types of arguments must be valid
173 |
174 | Contracts on Classes
175 | ====================
176 | The ``require`` and ``ensure`` decorators can be used on class methods too,
177 | not just bare functions:
178 |
179 | >>> class Foo:
180 | ... @require("`name` should be nonempty", lambda args: len(args.name) > 0)
181 | ... def __init__(self, name):
182 | ... self.name = name
183 |
184 | >>> foo = Foo()
185 | Traceback (most recent call last):
186 | TypeError: __init__ missing required positional argument: 'name'
187 |
188 | >>> foo = Foo("")
189 | Traceback (most recent call last):
190 | dpcontracts.PreconditionError: `name` should be nonempty
191 |
192 | Classes may also have an additional sort of contract specified over them:
193 | the invariant. An invariant, created using the ``invariant`` decorator,
194 | specifies a condition that must always be true for instances of that class.
195 | In this case, "always" means "before invocation of any method and after
196 | its return" -- methods are allowed to violate invariants so long as they
197 | are restored prior to return.
198 |
199 | >>> from dpcontracts import invariant
200 |
201 | Invariant contracts are passed a single variable, a reference to the
202 | instance of the class. For example:
203 |
204 | >>> @invariant("inner list can never be empty", lambda self: len(self.lst) > 0)
205 | ... @invariant("inner list must consist only of integers",
206 | ... lambda self: all(isinstance(x, int) for x in self.lst))
207 | ... class NonemptyList:
208 | ... @require("initial list must be a list", lambda args: isinstance(args.initial, list))
209 | ... @require("initial list cannot be empty", lambda args: len(args.initial) > 0)
210 | ... @ensure("the list instance variable is equal to the given argument",
211 | ... lambda args, result: args.self.lst == args.initial)
212 | ... @ensure("the list instance variable is not an alias to the given argument",
213 | ... lambda args, result: args.self.lst is not args.initial)
214 | ... def __init__(self, initial):
215 | ... self.lst = initial[:]
216 | ...
217 | ... def get(self, i):
218 | ... return self.lst[i]
219 | ...
220 | ... def pop(self):
221 | ... self.lst.pop()
222 | ...
223 | ... def as_string(self):
224 | ... # Build up a string representation using the `get` method,
225 | ... # to illustrate methods calling methods with invariants.
226 | ... return ",".join(str(self.get(i)) for i in range(0, len(self.lst)))
227 |
228 | >>> nl = NonemptyList([1,2,3])
229 | >>> nl.pop()
230 | >>> nl.pop()
231 | >>> nl.pop()
232 | Traceback (most recent call last):
233 | dpcontracts.PostconditionError: inner list can never be empty
234 |
235 | >>> nl = NonemptyList(["a", "b", "c"])
236 | Traceback (most recent call last):
237 | dpcontracts.PostconditionError: inner list must consist only of integers
238 |
239 | Violations of invariants are ignored in the following situations:
240 |
241 | - before calls to ``__init__`` and ``__new__`` (since the object is still
242 | being initialized)
243 |
244 | - before and after calls to any method whose name begins with "__",
245 | except for methods implementing arithmetic and comparison operations
246 | and container type emulation (because such methods are private and
247 | expected to manipulate the object's inner state, plus things get hairy
248 | with certain applications of ``__getattr(ibute)?__``)
249 |
250 | - before and after calls to methods added from outside the initial
251 | class definition (because invariants are processed only at class
252 | definition time)
253 |
254 | - before and after calls to classmethods, since they apply to the class
255 | as a whole and not any particular instance
256 |
257 | For example:
258 |
259 | >>> @invariant("`always` should be True", lambda self: self.always)
260 | ... class Foo:
261 | ... always = True
262 | ...
263 | ... def get_always(self):
264 | ... return self.always
265 | ...
266 | ... @classmethod
267 | ... def break_everything(cls):
268 | ... cls.always = False
269 |
270 | >>> x = Foo()
271 | >>> x.get_always()
272 | True
273 | >>> x.break_everything()
274 | >>> x.get_always()
275 | Traceback (most recent call last):
276 | dpcontracts.PreconditionError: `always` should be True
277 |
278 | Also note that if a method invokes another method on the same object,
279 | all of the invariants will be tested again:
280 |
281 | >>> nl = NonemptyList([1,2,3])
282 | >>> nl.as_string() == '1,2,3'
283 | True
284 |
285 | Automatically Generated Descriptions
286 | ====================================
287 | Some might find that providing a human-readable description for a contract
288 | in addition to a function implementing that contract is a bit too verbose.
289 |
290 | For the `require`, `ensure`, and `invariant` decorators, a single-argument
291 | version exists. If only a function is passed in, a description will be
292 | automatically generated based on the code of that function:
293 |
294 | >>> import math
295 | >>> @require("x must be an integer", lambda args: isinstance(args.x, int))
296 | ... @require(lambda args: args.x > 0)
297 | ... @ensure("result must be a float", lambda args, result: isinstance(result, float))
298 | ... def square_root(x):
299 | ... return math.sqrt(x)
300 | >>> square_root(-1)
301 | Traceback (most recent call last):
302 | PreconditionError: @require(lambda args: args.x > 0) failed
303 |
304 | This is true for postconditions as well:
305 |
306 | >>> @ensure(lambda args, result: result > 0)
307 | ... def sub(x, y):
308 | ... return x - y
309 | >>> sub(10, 100)
310 | Traceback (most recent call last):
311 | PostconditionError: @ensure(lambda args, result: result > 0) failed
312 |
313 | And of course for invariants:
314 |
315 | >>> @invariant(lambda self: self.counter >= 0)
316 | ... class Counter:
317 | ... def __init__(self, initial_value):
318 | ... self.counter = initial_value
319 | ... def increment(self, value):
320 | ... self.counter += value
321 | >>> counter = Counter(10)
322 | >>> counter.increment(-100)
323 | Traceback (most recent call last):
324 | PostconditionError: @invariant(lambda self: self.counter >= 0) failed
325 |
326 | Tests can span more than one line as well:
327 |
328 | >>> @ensure(lambda args, result: result < 1000)
329 | ... @ensure(lambda args, result: all([
330 | ... result > 0]))
331 | ... @ensure(lambda args, result: isinstance(result, int))
332 | ... def sub2(x, y):
333 | ... return x - y
334 | >>> sub2(10, 100)
335 | Traceback (most recent call last):
336 | PostconditionError: @ensure(lambda args, result: all([
337 | result > 0])) failed
338 |
339 | Preserving Old Values
340 | =====================
341 | Sometimes it's important to be able to compare the results of a function with the
342 | previous state of the program. Earlier states can be preserved using the
343 | `preserve` decorator:
344 |
345 | >>> class Counter:
346 | ... def __init__(self, initial_value):
347 | ... self.value = initial_value
348 | ...
349 | ... @preserve(lambda args: {"old_value": args.self.value})
350 | ... @require("value > 0", lambda args: args.value > 0)
351 | ... @ensure("counter is incremented by value",
352 | ... lambda args, res, old: args.self.value == old.old_value + args.value)
353 | ... def increment(self, value):
354 | ... if value == 9:
355 | ... self.value += 2 # broken for purposes of example
356 | ... self.value += value
357 |
358 | >>> counter = Counter(100)
359 | >>> counter.increment(10)
360 | >>> counter.increment(9)
361 | Traceback (most recent call last):
362 | PostconditionError: counter is incremented by value
363 |
364 | Note that Python's pass-by-reference semantics still apply, so if you need to
365 | preserve an old value, you might have to copy it.
366 |
367 | Transforming Data in Contracts
368 | ==============================
369 | In general, you should avoid transforming data inside a contract; contracts
370 | themselves are supposed to be side-effect-free.
371 |
372 | However, this is not always possible in Python.
373 |
374 | Take, for example, iterables passed as arguments. We might want to verify
375 | that a given set of properties hold for every item in the iterable. The
376 | obvious solution would be to do something like this:
377 |
378 | >>> @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
379 | ... def my_func(l):
380 | ... return sum(l)
381 |
382 | This works well in most situations:
383 |
384 | >>> my_func([1, 2, 3])
385 | 6
386 | >>> my_func([0, -1, 2])
387 | Traceback (most recent call last):
388 | dpcontracts.PreconditionError: every item in `l` must be > 0
389 |
390 | But it fails in the case of a generator:
391 |
392 | >>> def iota(n):
393 | ... for i in range(1, n):
394 | ... yield i
395 |
396 | >>> sum(iota(5))
397 | 10
398 | >>> my_func(iota(5))
399 | 0
400 |
401 | The call to ``my_func`` has a result of 0 because the generator was consumed
402 | inside the ``all`` call inside the contract. Obviously, this is problematic.
403 |
404 | Sadly, there is no generic solution to this problem. In a statically-typed
405 | language, the compiler can verify that some properties of infinite lists
406 | (though not all of them, and what exactly depends on the type system).
407 |
408 | We get around that limitation here using an additional decorator, called
409 | ``transform`` that transforms the arguments to a function, and a function
410 | called ``rewrite`` that rewrites argument tuples.
411 |
412 | >>> from dpcontracts import transform, rewrite
413 |
414 | For example:
415 |
416 | >>> @transform(lambda args: rewrite(args, l=list(args.l)))
417 | ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
418 | ... def my_func(l):
419 | ... return sum(l)
420 | >>> my_func(iota(5))
421 | 10
422 |
423 | Note that this does not completely solve the problem of infinite sequences,
424 | but it does allow for verification of any desired prefix of such a sequence.
425 |
426 | This works for class methods too, of course:
427 |
428 | >>> class TestClass:
429 | ... @transform(lambda args: rewrite(args, l=list(args.l)))
430 | ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
431 | ... def my_func(self, l):
432 | ... return sum(l)
433 | >>> TestClass().my_func(iota(5))
434 | 10
435 |
436 | Contracts on Asynchronous Functions (aka coroutine functions)
437 | =============================================================
438 | Contracts can be placed on coroutines (that is, async functions):
439 |
440 | >>> import asyncio
441 | >>> @require("`a` is an integer", lambda args: isinstance(args.a, int))
442 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
443 | ... @require("every member of `c` should be a boolean",
444 | ... lambda args: all(isinstance(x, bool) for x in args.c))
445 | ... async def func(a, b="Foo", *c):
446 | ... await asyncio.sleep(1)
447 |
448 | >>> asyncio.get_event_loop().run_until_complete(
449 | ... func( 1, "foo", True, True, False))
450 |
451 | Predicates functions themselves cannot be coroutines, as this could
452 | influence the run loop:
453 |
454 | >>> async def coropred_aisint(e):
455 | ... await asyncio.sleep(1)
456 | ... return isinstance(getattr(e, 'a'), int)
457 | >>> @require("`a` is an integer", coropred_aisint)
458 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
459 | ... @require("every member of `c` should be a boolean",
460 | ... lambda args: all(isinstance(x, bool) for x in args.c))
461 | ... async def func(a, b="Foo", *c):
462 | ... await asyncio.sleep(1)
463 | Traceback (most recent call last):
464 | AssertionError: contract predicates cannot be coroutines
465 |
466 | Contracts and Debugging
467 | =======================
468 | Contracts are a documentation and testing tool; they are not intended
469 | to be used to validate user input or implement program logic. Indeed,
470 | running Python with ``__debug__`` set to False (e.g. by calling the Python
471 | intrepreter with the "-O" option) disables contracts.
472 |
473 | Testing This Module
474 | ===================
475 | This module has embedded doctests that are run with the module is invoked
476 | from the command line. Simply run the module directly to run the tests.
477 |
478 | Contact Information and Licensing
479 | =================================
480 | This module has a home page at `GitHub `_.
481 |
482 | This module was written by Rob King (jking@deadpixi.com).
483 |
484 | This program is free software: you can redistribute it and/or modify
485 | it under the terms of the GNU Lesser General Public License as published by
486 | the Free Software Foundation, either version 3 of the License, or
487 | (at your option) any later version.
488 |
489 | This program is distributed in the hope that it will be useful,
490 | but WITHOUT ANY WARRANTY; without even the implied warranty of
491 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
492 | GNU Lesser General Public License for more details.
493 |
494 | You should have received a copy of the GNU Lesser General Public License
495 | along with this program. If not, see .
496 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | README.rst
--------------------------------------------------------------------------------
/dpcontracts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Introduction
5 | ============
6 | This module provides a collection of decorators that makes it easy to
7 | write software using contracts.
8 |
9 | Contracts are a debugging and verification tool. They are declarative
10 | statements about what states a program must be in to be considered
11 | "correct" at runtime. They are similar to assertions, and are verified
12 | automatically at various well-defined points in the program. Contracts can
13 | be specified on functions and on classes.
14 |
15 | Contracts serve as a form of documentation and a way of formally
16 | specifying program behavior. Good practice often includes writing all of
17 | the contracts first, with these contract specifying the exact expected
18 | state before and after each function or method call and the things that
19 | should always be true for a given class of object.
20 |
21 | Contracts consist of two parts: a description and a condition. The
22 | description is simply a human-readable string that describes what the
23 | contract is testing, while the condition is a single function that tests
24 | that condition. The condition is executed automatically and passed certain
25 | arguments (which vary depending on the type of contract), and must return
26 | a boolean value: True if the condition has been met, and False otherwise.
27 |
28 | Legacy Python Support
29 | =====================
30 | This module supports versions of Python >= 3.5; that is, versions with
31 | support for "async def" functions. There is a branch of this module that
32 | is in maintenance mode for versions of Python earlier than 3.5
33 | (including Python 2.7).
34 |
35 | The Python 2 and <= 3.5 branch is available at
36 | https://github.com/deadpixi/contracts/tree/python2
37 |
38 | This legacy-compatible version is also distributed on PyPI along the 0.5.x
39 | branch; this branch will kept compatible with newer versions to the greatest
40 | extent possible.
41 |
42 | That branch is a drop-in replacement for this module and includes most
43 | of the functionality, except support for "async def" functions and a few
44 | other things.
45 |
46 | Preconditions and Postconditions
47 | ================================
48 | Contracts on functions consist of preconditions and postconditions.
49 | A precondition is declared using the `requires` decorator, and describes
50 | what must be true upon entrance to the function. The condition function
51 | is passed an arguments object, which as as its attributes the arguments
52 | to the decorated function:
53 |
54 | >>> @require("`i` must be an integer", lambda args: isinstance(args.i, int))
55 | ... @require("`j` must be an integer", lambda args: isinstance(args.j, int))
56 | ... def add2(i, j):
57 | ... return i + j
58 |
59 | Note that an arbitrary number of preconditions can be stacked on top of
60 | each other.
61 |
62 | These decorators have declared that the types of both arguments must be
63 | integers. Calling the `add2` function with the correct types of arguments
64 | works:
65 |
66 | >>> add2(1, 2)
67 | 3
68 |
69 | But calling with incorrect argument types (violating the contract) fails
70 | with a PreconditionError (a subtype of AssertionError):
71 |
72 | >>> add2("foo", 2)
73 | Traceback (most recent call last):
74 | PreconditionError: `i` must be an integer
75 |
76 | Functions can also have postconditions, specified using the `ensure`
77 | decorator. Postconditions describe what must be true after the function
78 | has successfully returned. Like the `require` decorator, the `ensure`
79 | decorator is passed an argument object. It is also passed an additional
80 | argument, which is the result of the function invocation. For example:
81 |
82 | >>> @require("`i` must be a positive integer",
83 | ... lambda args: isinstance(args.i, int) and args.i > 0)
84 | ... @require("`j` must be a positive integer",
85 | ... lambda args: isinstance(args.j, int) and args.j > 0)
86 | ... @ensure("the result must be greater than either `i` or `j`",
87 | ... lambda args, result: result > args.i and result > args.j)
88 | ... def add2(i, j):
89 | ... if i == 7:
90 | ... i = -7 # intentionally broken for purposes of example
91 | ... return i + j
92 |
93 | We can now call the function and ensure that everything is working correctly:
94 |
95 | >>> add2(1, 3)
96 | 4
97 |
98 | Except that the function is broken in unexpected ways:
99 |
100 | >>> add2(7, 4)
101 | Traceback (most recent call last):
102 | PostconditionError: the result must be greater than either `i` or `j`
103 |
104 | The function specifying the condition doesn't have to be a lambda; it can be
105 | any function, and pre- and postconditions don't have to actually reference
106 | the arguments or results of the function at all. They can simply check
107 | the function's environments and effects:
108 |
109 | >>> names = set()
110 | >>> def exists_in_database(x):
111 | ... return x in names
112 | >>> @require("`name` must be a string", lambda args: isinstance(args.name, str))
113 | ... @require("`name` must not already be in the database",
114 | ... lambda args: not exists_in_database(args.name.strip()))
115 | ... @ensure("the normalized version of the name must be added to the database",
116 | ... lambda args, result: exists_in_database(args.name.strip()))
117 | ... def add_to_database(name):
118 | ... if name not in names and name != "Rob": # intentionally broken
119 | ... names.add(name.strip())
120 |
121 | >>> add_to_database("James")
122 | >>> add_to_database("Marvin")
123 | >>> add_to_database("Marvin")
124 | Traceback (most recent call last):
125 | PreconditionError: `name` must not already be in the database
126 | >>> add_to_database("Rob")
127 | Traceback (most recent call last):
128 | PostconditionError: the normalized version of the name must be added to the database
129 |
130 | All of the various calling conventions of Python are supported:
131 |
132 | >>> @require("`a` is an integer", lambda args: isinstance(args.a, int))
133 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
134 | ... @require("every member of `c` should be a boolean",
135 | ... lambda args: all(isinstance(x, bool) for x in args.c))
136 | ... def func(a, b="Foo", *c):
137 | ... pass
138 |
139 | >>> func(1, "foo", True, True, False)
140 | >>> func(b="Foo", a=7)
141 | >>> args = {"a": 8, "b": "foo"}
142 | >>> func(**args)
143 | >>> args = (1, "foo", True, True, False)
144 | >>> func(*args)
145 | >>> args = {"a": 9}
146 | >>> func(**args)
147 | >>> func(10)
148 |
149 | A common contract is to validate the types of arguments. To that end,
150 | there is an additional decorator, `types`, that can be used
151 | to validate arguments' types:
152 |
153 | >>> class ExampleClass:
154 | ... pass
155 |
156 | >>> @types(a=int, b=str, c=(type(None), ExampleClass)) # or types.NoneType, if you prefer
157 | ... @require("a must be nonzero", lambda args: args.a != 0)
158 | ... def func(a, b, c=38):
159 | ... return " ".join(str(x) for x in [a, b])
160 |
161 | >>> func(1, "foo", ExampleClass())
162 | '1 foo'
163 |
164 | >>> func(1.0, "foo", ExampleClass) # invalid type for `a`
165 | Traceback (most recent call last):
166 | PreconditionError: the types of arguments must be valid
167 |
168 | >>> func(1, "foo") # invalid type (the default) for `c`
169 | Traceback (most recent call last):
170 | PreconditionError: the types of arguments must be valid
171 |
172 | Contracts on Classes
173 | ====================
174 | The `require` and `ensure` decorators can be used on class methods too,
175 | not just bare functions:
176 |
177 | >>> class Foo:
178 | ... @require("`name` should be nonempty", lambda args: len(args.name) > 0)
179 | ... def __init__(self, name):
180 | ... self.name = name
181 |
182 | >>> foo = Foo()
183 | Traceback (most recent call last):
184 | TypeError: __init__ missing required positional argument: 'name'
185 |
186 | >>> foo = Foo("")
187 | Traceback (most recent call last):
188 | PreconditionError: `name` should be nonempty
189 |
190 | Classes may also have an additional sort of contract specified over them:
191 | the invariant. An invariant, created using the `invariant` decorator,
192 | specifies a condition that must always be true for instances of that class.
193 | In this case, "always" means "before invocation of any method and after
194 | its return" -- methods are allowed to violate invariants so long as they
195 | are restored prior to return.
196 |
197 | Invariant contracts are passed a single variable, a reference to the
198 | instance of the class. For example:
199 |
200 | >>> @invariant("inner list can never be empty", lambda self: len(self.lst) > 0)
201 | ... @invariant("inner list must consist only of integers",
202 | ... lambda self: all(isinstance(x, int) for x in self.lst))
203 | ... class NonemptyList:
204 | ... @require("initial list must be a list", lambda args: isinstance(args.initial, list))
205 | ... @require("initial list cannot be empty", lambda args: len(args.initial) > 0)
206 | ... @ensure("the list instance variable is equal to the given argument",
207 | ... lambda args, result: args.self.lst == args.initial)
208 | ... @ensure("the list instance variable is not an alias to the given argument",
209 | ... lambda args, result: args.self.lst is not args.initial)
210 | ... def __init__(self, initial):
211 | ... self.lst = initial[:]
212 | ...
213 | ... def get(self, i):
214 | ... return self.lst[i]
215 | ...
216 | ... def pop(self):
217 | ... self.lst.pop()
218 | ...
219 | ... def as_string(self):
220 | ... # Build up a string representation using the `get` method,
221 | ... # to illustrate methods calling methods with invariants.
222 | ... return ",".join(str(self.get(i)) for i in range(0, len(self.lst)))
223 |
224 | >>> nl = NonemptyList([1,2,3])
225 | >>> nl.pop()
226 | >>> nl.pop()
227 | >>> nl.pop()
228 | Traceback (most recent call last):
229 | PostconditionError: inner list can never be empty
230 |
231 | >>> nl = NonemptyList(["a", "b", "c"])
232 | Traceback (most recent call last):
233 | PostconditionError: inner list must consist only of integers
234 |
235 | Violations of invariants are ignored in the following situations:
236 |
237 | - before calls to __init__ and __new__ (since the object is still
238 | being initialized)
239 |
240 | - before and after calls to any method whose name begins with "__",
241 | except for methods implementing arithmetic and comparison operations
242 | and container type emulation (because such methods are private and
243 | expected to manipulate the object's inner state, plus things get hairy
244 | with certain applications of `__getattr(ibute)?__`)
245 |
246 | - before and after calls to methods added from outside the initial
247 | class definition (because invariants are processed only at class
248 | definition time)
249 |
250 | - before and after calls to classmethods, since they apply to the class
251 | as a whole and not any particular instance
252 |
253 | For example:
254 |
255 | >>> @invariant("`always` should be True", lambda self: self.always)
256 | ... class Foo:
257 | ... always = True
258 | ...
259 | ... def get_always(self):
260 | ... return self.always
261 | ...
262 | ... @classmethod
263 | ... def break_everything(cls):
264 | ... cls.always = False
265 |
266 | >>> x = Foo()
267 | >>> x.get_always()
268 | True
269 | >>> x.break_everything()
270 | >>> x.get_always()
271 | Traceback (most recent call last):
272 | PreconditionError: `always` should be True
273 |
274 | Also note that if a method invokes another method on the same object,
275 | all of the invariants will be tested again:
276 |
277 | >>> nl = NonemptyList([1,2,3])
278 | >>> nl.as_string() == '1,2,3'
279 | True
280 |
281 | Automatically Generated Descriptions
282 | ====================================
283 | Some might find that providing a human-readable description for a contract
284 | in addition to a function implementing that contract is a bit too verbose.
285 |
286 | For the `require`, `ensure`, and `invariant` decorators, a single-argument
287 | version exists. If only a function is passed in, a description will be
288 | automatically generated based on the code of that function:
289 |
290 | >>> import math
291 | >>> @require("x must be an integer", lambda args: isinstance(args.x, int))
292 | ... @require(lambda args: args.x > 0)
293 | ... @ensure("result must be a float", lambda args, result: isinstance(result, float))
294 | ... def square_root(x):
295 | ... return math.sqrt(x)
296 | >>> square_root(-1)
297 | Traceback (most recent call last):
298 | PreconditionError: @require(lambda args: args.x > 0) failed
299 |
300 | This is true for postconditions as well:
301 |
302 | >>> @ensure(lambda args, result: result > 0)
303 | ... def sub(x, y):
304 | ... return x - y
305 | >>> sub(10, 100)
306 | Traceback (most recent call last):
307 | PostconditionError: @ensure(lambda args, result: result > 0) failed
308 |
309 | And of course for invariants:
310 |
311 | >>> @invariant(lambda self: self.counter >= 0)
312 | ... class Counter:
313 | ... def __init__(self, initial_value):
314 | ... self.counter = initial_value
315 | ... def increment(self, value):
316 | ... self.counter += value
317 | >>> counter = Counter(10)
318 | >>> counter.increment(-100)
319 | Traceback (most recent call last):
320 | PostconditionError: @invariant(lambda self: self.counter >= 0) failed
321 |
322 | Tests can span more than one line as well:
323 |
324 | >>> @ensure(lambda args, result: result < 1000)
325 | ... @ensure(lambda args, result: all([
326 | ... result > 0]))
327 | ... @ensure(lambda args, result: isinstance(result, int))
328 | ... def sub2(x, y):
329 | ... return x - y
330 | >>> sub2(10, 100)
331 | Traceback (most recent call last):
332 | PostconditionError: @ensure(lambda args, result: all([
333 | result > 0])) failed
334 |
335 | Preserving Old Values
336 | =====================
337 | Sometimes it's important to be able to compare the results of a function with the
338 | previous state of the program. Earlier states can be preserved using the
339 | `preserve` decorator:
340 |
341 | >>> class Counter:
342 | ... def __init__(self, initial_value):
343 | ... self.value = initial_value
344 | ...
345 | ... @preserve(lambda args: {"old_value": args.self.value})
346 | ... @require("value > 0", lambda args: args.value > 0)
347 | ... @ensure("counter is incremented by value",
348 | ... lambda args, res, old: args.self.value == old.old_value + args.value)
349 | ... def increment(self, value):
350 | ... if value == 9:
351 | ... self.value += 2 # broken for purposes of example
352 | ... self.value += value
353 |
354 | >>> counter = Counter(100)
355 | >>> counter.increment(10)
356 | >>> counter.increment(9)
357 | Traceback (most recent call last):
358 | PostconditionError: counter is incremented by value
359 |
360 | Note that Python's pass-by-reference semantics still apply, so if you need to
361 | preserve an old value, you might have to copy it.
362 |
363 | Transforming Data in Contracts
364 | ==============================
365 | In general, you should avoid transforming data inside a contract; contracts
366 | themselves are supposed to be side-effect-free.
367 |
368 | However, this is not always possible in Python.
369 |
370 | Take, for example, iterables passed as arguments. We might want to verify
371 | that a given set of properties hold for every item in the iterable. The
372 | obvious solution would be to do something like this:
373 |
374 | >>> @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
375 | ... def my_func(l):
376 | ... return sum(l)
377 |
378 | This works well in most situations:
379 |
380 | >>> my_func([1, 2, 3])
381 | 6
382 | >>> my_func([0, -1, 2])
383 | Traceback (most recent call last):
384 | PreconditionError: every item in `l` must be > 0
385 |
386 | But it fails in the case of a generator:
387 |
388 | >>> def iota(n):
389 | ... for i in range(1, n):
390 | ... yield i
391 |
392 | >>> sum(iota(5))
393 | 10
394 | >>> my_func(iota(5))
395 | 0
396 |
397 | The call to `my_func` has a result of 0 because the generator was consumed
398 | inside the `all` call inside the contract. Obviously, this is problematic.
399 |
400 | Sadly, there is no generic solution to this problem. In a statically-typed
401 | language, the compiler can verify that some properties of infinite lists
402 | (though not all of them, and what exactly depends on the type system).
403 |
404 | We get around that limitation here using an additional decorator, called
405 | `transform` that transforms the arguments to a function, and a function
406 | called `rewrite` that rewrites argument tuples.
407 |
408 | For example:
409 |
410 | >>> @transform(lambda args: rewrite(args, l=list(args.l)))
411 | ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
412 | ... def my_func(l):
413 | ... return sum(l)
414 | >>> my_func(iota(5))
415 | 10
416 |
417 | Note that this does not completely solve the problem of infinite sequences,
418 | but it does allow for verification of any desired prefix of such a sequence.
419 |
420 | This works for class methods too, of course:
421 |
422 | >>> class TestClass:
423 | ... @transform(lambda args: rewrite(args, l=list(args.l)))
424 | ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l))
425 | ... def my_func(self, l):
426 | ... return sum(l)
427 | >>> TestClass().my_func(iota(5))
428 | 10
429 |
430 | Contracts on Asynchronous Functions (aka coroutine functions)
431 | =============================================================
432 | Contracts can be placed on coroutines (that is, async functions):
433 |
434 | >>> import asyncio
435 | >>> @require("`a` is an integer", lambda args: isinstance(args.a, int))
436 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
437 | ... @require("every member of `c` should be a boolean",
438 | ... lambda args: all(isinstance(x, bool) for x in args.c))
439 | ... async def func(a, b="Foo", *c):
440 | ... await asyncio.sleep(1)
441 |
442 | >>> asyncio.get_event_loop().run_until_complete(
443 | ... func( 1, "foo", True, True, False))
444 |
445 | Predicates functions themselves cannot be coroutines, as this could
446 | influence the run loop:
447 |
448 | >>> async def coropred_aisint(e):
449 | ... await asyncio.sleep(1)
450 | ... return isinstance(getattr(e, 'a'), int)
451 | >>> @require("`a` is an integer", coropred_aisint)
452 | ... @require("`b` is a string", lambda args: isinstance(args.b, str))
453 | ... @require("every member of `c` should be a boolean",
454 | ... lambda args: all(isinstance(x, bool) for x in args.c))
455 | ... async def func(a, b="Foo", *c):
456 | ... await asyncio.sleep(1)
457 | Traceback (most recent call last):
458 | AssertionError: contract predicates cannot be coroutines
459 |
460 | Contracts and Debugging
461 | =======================
462 | Contracts are a documentation and testing tool; they are not intended
463 | to be used to validate user input or implement program logic. Indeed,
464 | running Python with `__debug__` set to False (e.g. by calling the Python
465 | interpreter with the "-O" option) disables contracts.
466 |
467 | Testing This Module
468 | ===================
469 | This module has embedded doctests that are run with the module is invoked
470 | from the command line. Simply run the module directly to run the tests.
471 |
472 | Contact Information and Licensing
473 | =================================
474 | This module has a home page at `GitHub `_.
475 |
476 | This module was written by Rob King (jking@deadpixi.com).
477 |
478 | This program is free software: you can redistribute it and/or modify
479 | it under the terms of the GNU Lesser General Public License as published by
480 | the Free Software Foundation, either version 3 of the License, or
481 | (at your option) any later version.
482 |
483 | This program is distributed in the hope that it will be useful,
484 | but WITHOUT ANY WARRANTY; without even the implied warranty of
485 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
486 | GNU Lesser General Public License for more details.
487 |
488 | You should have received a copy of the GNU Lesser General Public License
489 | along with this program. If not, see .
490 | """
491 |
492 | __all__ = ["ensure", "invariant", "require", "transform", "rewrite",
493 | "preserve", "PreconditionError", "PostconditionError"]
494 | __author__ = "Rob King"
495 | __copyright__ = "Copyright (C) 2015-2018 Rob King"
496 | __license__ = "LGPL"
497 | __version__ = "$Id$"
498 | __email__ = "jking@deadpixi.com"
499 | __status__ = "Alpha"
500 |
501 | from ast import parse
502 | from collections import namedtuple
503 | from functools import wraps
504 | from inspect import isfunction, ismethod, iscoroutinefunction, getfullargspec, getsource
505 | from sys import version_info
506 |
507 | if version_info[:2] < (3, 5):
508 | raise ImportError('dpcontracts >= 0.6 requires Python 3.5 or later.')
509 |
510 | class PreconditionError(AssertionError):
511 | """An AssertionError raised due to violation of a precondition."""
512 |
513 | class PostconditionError(AssertionError):
514 | """An AssertionError raised due to violation of a postcondition."""
515 |
516 | def get_function_source(func):
517 | try:
518 | source = getsource(func)
519 | tree = parse(source)
520 | decorators = tree.body[0].decorator_list
521 | function = tree.body[0]
522 | first_line = decorators[0].lineno
523 | following_line = first_line + 1
524 | if len(decorators) > 1:
525 | following_line = decorators[1].lineno
526 | elif len(function.body) > 0:
527 | following_line = function.body[0].lineno - 1
528 | return "\n".join(source.split("\n")[first_line - 1:following_line - first_line]) + " failed"
529 |
530 | except (SyntaxError, OSError):
531 | return str(func)
532 |
533 | def get_wrapped_func(func):
534 | while hasattr(func, '__contract_wrapped_func__'):
535 | func = func.__contract_wrapped_func__
536 | return func
537 |
538 | def build_call(func, *args, **kwargs):
539 | """
540 | Build an argument dictionary suitable for passing via `**` expansion given
541 | function `f`, positional arguments `args`, and keyword arguments `kwargs`.
542 | """
543 |
544 | func = get_wrapped_func(func)
545 | named, vargs, _, defs, kwonly, kwonlydefs, _ = getfullargspec(func)
546 |
547 | nonce = object()
548 | actual = dict((name, nonce) for name in named)
549 |
550 | defs = defs or ()
551 | kwonlydefs = kwonlydefs or {}
552 |
553 | actual.update(kwonlydefs)
554 | actual.update(dict(zip(reversed(named), reversed(defs))))
555 | actual.update(dict(zip(named, args)))
556 |
557 | if vargs:
558 | actual[vargs] = tuple(args[len(named):])
559 |
560 | actual.update(kwargs)
561 |
562 | for name, value in actual.items():
563 | if value is nonce:
564 | raise TypeError("%s missing required positional argument: '%s'" % (func.__name__, name))
565 |
566 | return tuple_of_dict(actual)
567 |
568 | def tuple_of_dict(dictionary, name="Args"):
569 | assert isinstance(dictionary, dict), "dictionary must be a dict instance"
570 | return namedtuple(name, dictionary.keys())(**dictionary)
571 |
572 | def arg_count(func):
573 | named, vargs, _, defs, kwonly, kwonlydefs, _ = getfullargspec(func)
574 | return len(named) + len(kwonly) + (1 if vargs else 0)
575 |
576 | def condition(description, predicate, precondition=False, postcondition=False, instance=False):
577 | assert isinstance(description, str), "contract descriptions must be strings"
578 | assert len(description) > 0, "contracts must have nonempty descriptions"
579 | assert isfunction(predicate), "contract predicates must be functions"
580 | assert not iscoroutinefunction(predicate), "contract predicates cannot be coroutines"
581 | assert precondition or postcondition, "contracts must be at least one of pre- or post-conditional"
582 | if instance or precondition:
583 | assert arg_count(predicate) == 1, "invariant predicates must take one argument"
584 | elif postcondition:
585 | assert arg_count(predicate) in (2, 3), "postcondition predicates must take two or three arguments"
586 |
587 | def require(f):
588 | wrapped = get_wrapped_func(f)
589 |
590 | if iscoroutinefunction(f):
591 | @wraps(f)
592 | async def inner(*args, **kwargs):
593 | rargs = build_call(f, *args, **kwargs) if not instance else args[0]
594 |
595 | if precondition and not predicate(rargs):
596 | raise PreconditionError(description)
597 |
598 | preserved_values = {}
599 | for preserver in getattr(wrapped, "__contract_preserver__", [lambda x: {}]):
600 | preserved_values.update(preserver(rargs))
601 | result = await f(*args, **kwargs)
602 |
603 | if instance:
604 | if not predicate(rargs):
605 | raise PostconditionError(description)
606 | elif postcondition:
607 | check = None
608 | if arg_count(predicate) == 3:
609 | check = predicate(rargs, result, tuple_of_dict(preserved_values))
610 | else:
611 | check = predicate(rargs, result)
612 | if not check:
613 | raise PostconditionError(description)
614 |
615 | return result
616 |
617 | elif isfunction(f):
618 | @wraps(f)
619 | def inner(*args, **kwargs):
620 | rargs = build_call(f, *args, **kwargs) if not instance else args[0]
621 |
622 | if precondition and not predicate(rargs):
623 | raise PreconditionError(description)
624 |
625 | preserved_values = {}
626 | for preserver in getattr(wrapped, "__contract_preserver__", [lambda x: {}]):
627 | preserved_values.update(preserver(rargs))
628 | result = f(*args, **kwargs)
629 |
630 | if instance:
631 | if not predicate(rargs):
632 | raise PostconditionError(description)
633 | elif postcondition:
634 | check = None
635 | if arg_count(predicate) == 3:
636 | check = predicate(rargs, result, tuple_of_dict(preserved_values))
637 | else:
638 | check = predicate(rargs, result)
639 | if not check:
640 | raise PostconditionError(description)
641 |
642 | return result
643 |
644 | else:
645 | raise NotImplementedError
646 |
647 | inner.__contract_wrapped_func__ = wrapped
648 | return inner
649 | return require
650 |
651 | def require(arg1, arg2=None):
652 | """
653 | Specify a precondition described by `description` and tested by
654 | `predicate`.
655 | """
656 |
657 | assert (isinstance(arg1, str) and isfunction(arg2)) or (isfunction(arg1) and arg2 is None)
658 |
659 | description = ""
660 | predicate = lambda x: x
661 |
662 | if isinstance(arg1, str):
663 | description = arg1
664 | predicate = arg2
665 | else:
666 | description = get_function_source(arg1)
667 | predicate = arg1
668 |
669 | return condition(description, predicate, True, False)
670 |
671 | def rewrite(args, **kwargs):
672 | return args._replace(**kwargs)
673 |
674 | def preserve(preserver):
675 | assert isfunction(preserver), "preservers must be functions"
676 | assert arg_count(preserver) == 1, "preservers can only take a single argument"
677 |
678 | def func(f):
679 | wrapped = get_wrapped_func(f)
680 | @wraps(f)
681 | def inner(*args, **kwargs):
682 | return f(*args, **kwargs)
683 | if not hasattr(wrapped, "__contract_preserver__"):
684 | wrapped.__contract_preserver__ = []
685 | wrapped.__contract_preserver__.append(preserver)
686 | return inner
687 | return func
688 |
689 | def transform(transformer):
690 | assert isfunction(transformer), "transformers must be functions"
691 | assert arg_count(transformer) == 1, "transformers can only take a single argument"
692 |
693 | def func(f):
694 | @wraps(f)
695 | def inner(*args, **kwargs):
696 | rargs = transformer(build_call(f, *args, **kwargs))
697 | return f(**(rargs._asdict()))
698 | return inner
699 | return func
700 |
701 | def types(**requirements):
702 | """
703 | Specify a precondition based on the types of the function's
704 | arguments.
705 | """
706 |
707 | def predicate(args):
708 | for name, kind in sorted(requirements.items()):
709 | assert hasattr(args, name), "missing required argument `%s`" % name
710 |
711 | if not isinstance(kind, tuple):
712 | kind = (kind,)
713 |
714 | if not any(isinstance(getattr(args, name), k) for k in kind):
715 | return False
716 |
717 | return True
718 |
719 | return condition("the types of arguments must be valid", predicate, True)
720 |
721 | def ensure(arg1, arg2=None):
722 | """
723 | Specify a precondition described by `description` and tested by
724 | `predicate`.
725 | """
726 |
727 | assert (isinstance(arg1, str) and isfunction(arg2)) or (isfunction(arg1) and arg2 is None)
728 |
729 | description = ""
730 | predicate = lambda x: x
731 |
732 | if isinstance(arg1, str):
733 | description = arg1
734 | predicate = arg2
735 | else:
736 | description = get_function_source(arg1)
737 | predicate = arg1
738 |
739 | return condition(description, predicate, False, True)
740 |
741 | def invariant(arg1, arg2=None):
742 | """
743 | Specify a class invariant described by `description` and tested
744 | by `predicate`.
745 | """
746 |
747 | desc = ""
748 | predicate = lambda x: x
749 |
750 | if isinstance(arg1, str):
751 | desc = arg1
752 | predicate = arg2
753 | else:
754 | desc = get_function_source(arg1)
755 | predicate = arg1
756 |
757 | def invariant(c):
758 | def check(name, func):
759 | exceptions = ("__getitem__", "__setitem__", "__lt__", "__le__", "__eq__",
760 | "__ne__", "__gt__", "__ge__", "__init__")
761 |
762 | if name.startswith("__") and name.endswith("__") and name not in exceptions:
763 | return False
764 |
765 | if not ismethod(func) and not isfunction(func):
766 | return False
767 |
768 | if getattr(func, "__self__", None) is c:
769 | return False
770 |
771 | return True
772 |
773 | class InvariantContractor(c):
774 | pass
775 |
776 | for name, value in [(name, getattr(c, name)) for name in dir(c)]:
777 | if check(name, value):
778 | setattr(InvariantContractor, name,
779 | condition(desc, predicate, name != "__init__", True, True)(value))
780 | return InvariantContractor
781 | return invariant
782 |
783 | if not __debug__:
784 | def require(description, predicate):
785 | def func(f):
786 | return f
787 | return func
788 |
789 | def ensure(description, predicate):
790 | def func(f):
791 | return f
792 | return func
793 |
794 | def invariant(description, predicate):
795 | def func(c):
796 | return c
797 | return func
798 |
799 | def transform(transformer):
800 | def func(c):
801 | return c
802 | return func
803 |
804 | def preserve(preserver):
805 | def func(c):
806 | return c
807 | return func
808 |
809 | if __name__ == "__main__":
810 | import doctest
811 | doctest.testmod()
812 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import sys
4 |
5 | if sys.version_info[:2] < (3, 5):
6 | sys.stderr.write(
7 | 'This version of dpcontracts requires Python 3.5 - either upgrade '
8 | 'to a newer version of pip that handles this automatically, or '
9 | 'explicitly "pip install dpcontracts<0.6".'
10 | )
11 | sys.exit(1)
12 |
13 | from setuptools import setup
14 | import dpcontracts
15 |
16 | setup(name="dpcontracts",
17 | version="0.6.0",
18 | author="Rob King",
19 | author_email="jking@deadpixi.com",
20 | url="https://github.com/deadpixi/contracts",
21 | description="A simple implementation of contracts for Python.",
22 | py_modules=['dpcontracts'],
23 | python_requires='>=3.5',
24 | long_description=dpcontracts.__doc__,
25 | license="https://www.gnu.org/licenses/lgpl.txt",
26 | classifiers=["Development Status :: 3 - Alpha",
27 | "Intended Audience :: Developers",
28 | "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
29 | "Operating System :: OS Independent",
30 | "Programming Language :: Python",
31 | "Programming Language :: Python :: 3",
32 | "Topic :: Software Development :: Libraries"])
33 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py35, py36, py37, py38, pypy3
3 | skip_missing_interpreters=true
4 |
5 | [travis]
6 | 3.5 = py35
7 | 3.6 = py36
8 | 3.7 = py37
9 | 3.8 = py38
10 | pypy3 = pypy3
11 |
12 | [testenv]
13 | passenv = CI TRAVIS TRAVIS_*
14 |
15 | # to always force recreation and avoid unexpected side effects
16 | recreate = True
17 |
18 | deps = pytest
19 |
20 | commands = python -m pytest README.rst
21 |
22 |
--------------------------------------------------------------------------------