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