{ "info": { "author": "Rob King", "author_email": "jking@deadpixi.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries" ], "description": "Introduction\n============\nThis module provides a collection of decorators that makes it easy to\nwrite software using contracts.\n\nContracts are a debugging and verification tool. They are declarative\nstatements about what states a program must be in to be considered\n\"correct\" at runtime. They are similar to assertions, and are verified\nautomatically at various well-defined points in the program. Contracts can\nbe specified on functions and on classes.\n\nContracts serve as a form of documentation and a way of formally\nspecifying program behavior. Good practice often includes writing all of\nthe contracts first, with these contract specifying the exact expected\nstate before and after each function or method call and the things that\nshould always be true for a given class of object.\n\nContracts consist of two parts: a description and a condition. The\ndescription is simply a human-readable string that describes what the\ncontract is testing, while the condition is a single function that tests\nthat condition. The condition is executed automatically and passed certain\narguments (which vary depending on the type of contract), and must return\na boolean value: True if the condition has been met, and False otherwise.\n\nLegacy Python Support\n=====================\nThis module supports versions of Python >= 3.5; that is, versions with\nsupport for \"async def\" functions. There is a branch of this module that\nis kept compatible to the greatest possible degree for versions of Python\nearlier than 3.5 (including Python 2.7).\n\nThe Python 2 and <= 3.5 branch is available at\nhttps://github.com/deadpixi/contracts/tree/python2\n\nThis legacy-compatible version is also distributed on PyPI along the 0.5.x\nbranch; this branch will kept compatible with newer versions to the greatest\nextent possible.\n\nThat branch is a drop-in replacement for this module and includes all\nfunctionality except support for \"async def\" functions.\n\nPreconditions and Postconditions\n================================\nContracts on functions consist of preconditions and postconditions.\nA precondition is declared using the `requires` decorator, and describes\nwhat must be true upon entrance to the function. The condition function\nis passed an arguments object, which as as its attributes the arguments\nto the decorated function:\n\n >>> @require(\"`i` must be an integer\", lambda args: isinstance(args.i, int))\n ... @require(\"`j` must be an integer\", lambda args: isinstance(args.j, int))\n ... def add2(i, j):\n ... return i + j\n\nNote that an arbitrary number of preconditions can be stacked on top of\neach other.\n\nThese decorators have declared that the types of both arguments must be\nintegers. Calling the `add2` function with the correct types of arguments\nworks:\n\n >>> add2(1, 2)\n 3\n\nBut calling with incorrect argument types (violating the contract) fails\nwith a PreconditionError (a subtype of AssertionError):\n\n >>> add2(\"foo\", 2)\n Traceback (most recent call last):\n PreconditionError: `i` must be an integer\n\nFunctions can also have postconditions, specified using the `ensure`\ndecorator. Postconditions describe what must be true after the function\nhas successfully returned. Like the `require` decorator, the `ensure`\ndecorator is passed an argument object. It is also passed an additional\nargument, which is the result of the function invocation. For example:\n\n >>> @require(\"`i` must be a positive integer\",\n ... lambda args: isinstance(args.i, int) and args.i > 0)\n ... @require(\"`j` must be a positive integer\",\n ... lambda args: isinstance(args.j, int) and args.j > 0)\n ... @ensure(\"the result must be greater than either `i` or `j`\",\n ... lambda args, result: result > args.i and result > args.j)\n ... def add2(i, j):\n ... if i == 7:\n ... i = -7 # intentionally broken for purposes of example\n ... return i + j\n\nWe can now call the function and ensure that everything is working correctly:\n\n >>> add2(1, 3)\n 4\n\nExcept that the function is broken in unexpected ways:\n\n >>> add2(7, 4)\n Traceback (most recent call last):\n PostconditionError: the result must be greater than either `i` or `j`\n\nThe function specifying the condition doesn't have to be a lambda; it can be\nany function, and pre- and postconditions don't have to actually reference\nthe arguments or results of the function at all. They can simply check\nthe function's environments and effects:\n\n >>> names = set()\n >>> def exists_in_database(x):\n ... return x in names\n >>> @require(\"`name` must be a string\", lambda args: isinstance(args.name, str))\n ... @require(\"`name` must not already be in the database\",\n ... lambda args: not exists_in_database(args.name.strip()))\n ... @ensure(\"the normalized version of the name must be added to the database\",\n ... lambda args, result: exists_in_database(args.name.strip()))\n ... def add_to_database(name):\n ... if name not in names and name != \"Rob\": # intentionally broken\n ... names.add(name.strip())\n\n >>> add_to_database(\"James\")\n >>> add_to_database(\"Marvin\")\n >>> add_to_database(\"Marvin\")\n Traceback (most recent call last):\n PreconditionError: `name` must not already be in the database\n >>> add_to_database(\"Rob\")\n Traceback (most recent call last):\n PostconditionError: the normalized version of the name must be added to the database\n\nAll of the various calling conventions of Python are supported:\n\n >>> @require(\"`a` is an integer\", lambda args: isinstance(args.a, int))\n ... @require(\"`b` is a string\", lambda args: isinstance(args.b, str))\n ... @require(\"every member of `c` should be a boolean\",\n ... lambda args: all(isinstance(x, bool) for x in args.c))\n ... def func(a, b=\"Foo\", *c):\n ... pass\n\n >>> func(1, \"foo\", True, True, False)\n >>> func(b=\"Foo\", a=7)\n >>> args = {\"a\": 8, \"b\": \"foo\"}\n >>> func(**args)\n >>> args = (1, \"foo\", True, True, False)\n >>> func(*args)\n >>> args = {\"a\": 9}\n >>> func(**args)\n >>> func(10)\n\nA common contract is to validate the types of arguments. To that end,\nthere is an additional decorator, `types`, that can be used\nto validate arguments' types:\n\n >>> class ExampleClass:\n ... pass\n\n >>> @types(a=int, b=str, c=(type(None), ExampleClass)) # or types.NoneType, if you prefer\n ... @require(\"a must be nonzero\", lambda args: args.a != 0)\n ... def func(a, b, c=38):\n ... return \" \".join(str(x) for x in [a, b])\n\n >>> func(1, \"foo\", ExampleClass())\n '1 foo'\n\n >>> func(1.0, \"foo\", ExampleClass) # invalid type for `a`\n Traceback (most recent call last):\n PreconditionError: the types of arguments must be valid\n\n >>> func(1, \"foo\") # invalid type (the default) for `c`\n Traceback (most recent call last):\n PreconditionError: the types of arguments must be valid\n\nContracts on Classes\n====================\nThe `require` and `ensure` decorators can be used on class methods too,\nnot just bare functions:\n\n >>> class Foo:\n ... @require(\"`name` should be nonempty\", lambda args: len(args.name) > 0)\n ... def __init__(self, name):\n ... self.name = name\n\n >>> foo = Foo()\n Traceback (most recent call last):\n TypeError: __init__ missing required positional argument: 'name'\n\n >>> foo = Foo(\"\")\n Traceback (most recent call last):\n PreconditionError: `name` should be nonempty\n\nClasses may also have an additional sort of contract specified over them:\nthe invariant. An invariant, created using the `invariant` decorator,\nspecifies a condition that must always be true for instances of that class.\nIn this case, \"always\" means \"before invocation of any method and after\nits return\" -- methods are allowed to violate invariants so long as they\nare restored prior to return.\n\nInvariant contracts are passed a single variable, a reference to the\ninstance of the class. For example:\n\n >>> @invariant(\"inner list can never be empty\", lambda self: len(self.lst) > 0)\n ... @invariant(\"inner list must consist only of integers\",\n ... lambda self: all(isinstance(x, int) for x in self.lst))\n ... class NonemptyList:\n ... @require(\"initial list must be a list\", lambda args: isinstance(args.initial, list))\n ... @require(\"initial list cannot be empty\", lambda args: len(args.initial) > 0)\n ... @ensure(\"the list instance variable is equal to the given argument\",\n ... lambda args, result: args.self.lst == args.initial)\n ... @ensure(\"the list instance variable is not an alias to the given argument\",\n ... lambda args, result: args.self.lst is not args.initial)\n ... def __init__(self, initial):\n ... self.lst = initial[:]\n ...\n ... def get(self, i):\n ... return self.lst[i]\n ...\n ... def pop(self):\n ... self.lst.pop()\n ...\n ... def as_string(self):\n ... # Build up a string representation using the `get` method,\n ... # to illustrate methods calling methods with invariants.\n ... return \",\".join(str(self.get(i)) for i in range(0, len(self.lst)))\n\n >>> nl = NonemptyList([1,2,3])\n >>> nl.pop()\n >>> nl.pop()\n >>> nl.pop()\n Traceback (most recent call last):\n PostconditionError: inner list can never be empty\n\n >>> nl = NonemptyList([\"a\", \"b\", \"c\"])\n Traceback (most recent call last):\n PostconditionError: inner list must consist only of integers\n\nViolations of invariants are ignored in the following situations:\n\n - before calls to __init__ and __new__ (since the object is still\n being initialized)\n\n - before and after calls to any method whose name begins with \"__\",\n except for methods implementing arithmetic and comparison operations\n and container type emulation (because such methods are private and\n expected to manipulate the object's inner state, plus things get hairy\n with certain applications of `__getattr(ibute)?__`)\n\n - before and after calls to methods added from outside the initial\n class definition (because invariants are processed only at class\n definition time)\n\n - before and after calls to classmethods, since they apply to the class\n as a whole and not any particular instance\n\nFor example:\n\n >>> @invariant(\"`always` should be True\", lambda self: self.always)\n ... class Foo:\n ... always = True\n ...\n ... def get_always(self):\n ... return self.always\n ...\n ... @classmethod\n ... def break_everything(cls):\n ... cls.always = False\n\n >>> x = Foo()\n >>> x.get_always()\n True\n >>> x.break_everything()\n >>> x.get_always()\n Traceback (most recent call last):\n PreconditionError: `always` should be True\n\nAlso note that if a method invokes another method on the same object,\nall of the invariants will be tested again:\n\n >>> nl = NonemptyList([1,2,3])\n >>> nl.as_string() == '1,2,3'\n True\n\nTransforming Data in Contracts\n==============================\nIn general, you should avoid transforming data inside a contract; contracts\nthemselves are supposed to be side-effect-free.\n\nHowever, this is not always possible in Python.\n\nTake, for example, iterables passed as arguments. We might want to verify\nthat a given set of properties hold for every item in the iterable. The\nobvious solution would be to do something like this:\n\n >>> @require(\"every item in `l` must be > 0\", lambda args: all(x > 0 for x in args.l))\n ... def my_func(l):\n ... return sum(l)\n\nThis works well in most situations:\n\n >>> my_func([1, 2, 3])\n 6\n >>> my_func([0, -1, 2])\n Traceback (most recent call last):\n PreconditionError: every item in `l` must be > 0\n\nBut it fails in the case of a generator:\n\n >>> def iota(n):\n ... for i in range(1, n):\n ... yield i\n\n >>> sum(iota(5))\n 10\n >>> my_func(iota(5))\n 0\n\nThe call to `my_func` has a result of 0 because the generator was consumed\ninside the `all` call inside the contract. Obviously, this is problematic.\n\nSadly, there is no generic solution to this problem. In a statically-typed\nlanguage, the compiler can verify that some properties of infinite lists\n(though not all of them, and what exactly depends on the type system).\n\nWe get around that limitation here using an additional decorator, called\n`transform` that transforms the arguments to a function, and a function\ncalled `rewrite` that rewrites argument tuples.\n\nFor example:\n\n >>> @transform(lambda args: rewrite(args, l=list(args.l)))\n ... @require(\"every item in `l` must be > 0\", lambda args: all(x > 0 for x in args.l))\n ... def my_func(l):\n ... return sum(l)\n >>> my_func(iota(5))\n 10\n\nNote that this does not completely solve the problem of infinite sequences,\nbut it does allow for verification of any desired prefix of such a sequence.\n\nThis works for class methods too, of course:\n\n >>> class TestClass:\n ... @transform(lambda args: rewrite(args, l=list(args.l)))\n ... @require(\"every item in `l` must be > 0\", lambda args: all(x > 0 for x in args.l))\n ... def my_func(self, l):\n ... return sum(l)\n >>> TestClass().my_func(iota(5))\n 10\n\nContracts on Asynchronous Functions (aka coroutine functions)\n=============================================================\nContracts can be placed on coroutines (that is, async functions):\n\n >>> import asyncio\n >>> @require(\"`a` is an integer\", lambda args: isinstance(args.a, int))\n ... @require(\"`b` is a string\", lambda args: isinstance(args.b, str))\n ... @require(\"every member of `c` should be a boolean\",\n ... lambda args: all(isinstance(x, bool) for x in args.c))\n ... async def func(a, b=\"Foo\", *c):\n ... await asyncio.sleep(1)\n\n >>> asyncio.get_event_loop().run_until_complete(\n ... func( 1, \"foo\", True, True, False))\n\nPredicates functions themselves cannot be coroutines, as this could\ninfluence the run loop:\n\n >>> async def coropred_aisint(e):\n ... await asyncio.sleep(1)\n ... return isinstance(getattr(e, 'a'), int)\n >>> @require(\"`a` is an integer\", coropred_aisint)\n ... @require(\"`b` is a string\", lambda args: isinstance(args.b, str))\n ... @require(\"every member of `c` should be a boolean\",\n ... lambda args: all(isinstance(x, bool) for x in args.c))\n ... async def func(a, b=\"Foo\", *c):\n ... await asyncio.sleep(1)\n Traceback (most recent call last):\n AssertionError: contract predicates cannot be coroutines\n\nContracts and Debugging\n=======================\nContracts are a documentation and testing tool; they are not intended\nto be used to validate user input or implement program logic. Indeed,\nrunning Python with `__debug__` set to False (e.g. by calling the Python\nintrepreter with the \"-O\" option) disables contracts.\n\nTesting This Module\n===================\nThis module has embedded doctests that are run with the module is invoked\nfrom the command line. Simply run the module directly to run the tests.\n\nContact Information and Licensing\n=================================\nThis module has a home page at `GitHub `_.\n\nThis module was written by Rob King (jking@deadpixi.com).\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU Lesser General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU Lesser General Public License for more details.\n\nYou should have received a copy of the GNU Lesser General Public License\nalong with this program. If not, see .", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/deadpixi/contracts", "keywords": "", "license": "https://www.gnu.org/licenses/lgpl.txt", "maintainer": "", "maintainer_email": "", "name": "dpcontracts", "package_url": "https://pypi.org/project/dpcontracts/", "platform": "", "project_url": "https://pypi.org/project/dpcontracts/", "project_urls": { "Homepage": "https://github.com/deadpixi/contracts" }, "release_url": "https://pypi.org/project/dpcontracts/0.6.0/", "requires_dist": null, "requires_python": ">=3.5", "summary": "A simple implementation of contracts for Python.", "version": "0.6.0" }, "last_serial": 4254327, "releases": { "0.2.0": [], "0.3.0": [ { "comment_text": "", "digests": { "md5": "797e2d26b9fc7418dc77ab713ef661cf", "sha256": "fca9a6a509b4cc8750197d3ffe60c12d56fbdd62dbcc7419fb44994698a54259" }, "downloads": -1, "filename": "dpcontracts-0.3.0.tar.gz", "has_sig": false, "md5_digest": "797e2d26b9fc7418dc77ab713ef661cf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10048, "upload_time": "2018-05-23T15:24:01", "url": "https://files.pythonhosted.org/packages/d5/4d/3879d7597d77a2adf6bc1a7ee4419286112236ed0fb489b12d4dbf1ff66a/dpcontracts-0.3.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "e7c451da9ca5ab61f82f8f325e13e7e9", "sha256": "3d0e27cfdb754193be19e4803c92b3b70c22736dce7b52d2c218117fda135603" }, "downloads": -1, "filename": "dpcontracts-0.4.0.tar.gz", "has_sig": false, "md5_digest": "e7c451da9ca5ab61f82f8f325e13e7e9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10199, "upload_time": "2018-08-23T20:26:45", "url": "https://files.pythonhosted.org/packages/62/e2/6169afc26075cd3779660c23a1f141e86fcca50baf9ca60d4b1d71090b2b/dpcontracts-0.4.0.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "5fe40434996e25014ad97bb55dfc678b", "sha256": "557fe79202e58695aab1a059549300e5e2a3496e6b40b79cbc34a7f1588b5a5c" }, "downloads": -1, "filename": "dpcontracts-0.5.0.tar.gz", "has_sig": false, "md5_digest": "5fe40434996e25014ad97bb55dfc678b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10177, "upload_time": "2018-09-09T19:26:13", "url": "https://files.pythonhosted.org/packages/49/96/a275310293e8db46b0dff6d323f2aff2a017d1dc2c197e3c097e727fd81a/dpcontracts-0.5.0.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "c35b2bb2dd2ec389dbd10f6f1d1b8dee", "sha256": "6cf9df1f16beaa48523b798b41170dabf7a536a6133328731665cdb29c42234a" }, "downloads": -1, "filename": "dpcontracts-0.6.0.tar.gz", "has_sig": false, "md5_digest": "c35b2bb2dd2ec389dbd10f6f1d1b8dee", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 11156, "upload_time": "2018-09-09T19:27:28", "url": "https://files.pythonhosted.org/packages/aa/e2/cad64673297a634a623808045d416ed85bad1c470ccc99e0cdc7b13b9774/dpcontracts-0.6.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "c35b2bb2dd2ec389dbd10f6f1d1b8dee", "sha256": "6cf9df1f16beaa48523b798b41170dabf7a536a6133328731665cdb29c42234a" }, "downloads": -1, "filename": "dpcontracts-0.6.0.tar.gz", "has_sig": false, "md5_digest": "c35b2bb2dd2ec389dbd10f6f1d1b8dee", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 11156, "upload_time": "2018-09-09T19:27:28", "url": "https://files.pythonhosted.org/packages/aa/e2/cad64673297a634a623808045d416ed85bad1c470ccc99e0cdc7b13b9774/dpcontracts-0.6.0.tar.gz" } ] }