diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5072cf --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Fork from https://github.com/barbuza/contract to improve the documentation layout and add some new contracts. diff --git a/contract.py b/contract.py index c7a4d15..a8880b8 100644 --- a/contract.py +++ b/contract.py @@ -3,6 +3,7 @@ import functools import inspect import re +from dateutil.parser import parse as dateutil_parse """ Contract is tiny library for data validation @@ -36,9 +37,9 @@ class ContractMeta(type): on instances but on classes >>> IntC | StringC - , )> + Integer or String >>> IntC | StringC | NullC - , , )> + Integer or String or Null """ def __or__(cls, other): @@ -86,14 +87,21 @@ def _contract(self, contract): def __or__(self, other): return OrC(self, other) + def get_full_condition_name(self, condition): + conditions = {"gt": "greater than", + "lt": "less than", + "gte": "greater or equal than", + "lte": "less or equal than"} + return conditions.get(condition) + class TypeC(Contract): """ >>> TypeC(int) - + >>> TypeC[int] - + >>> c = TypeC[int] >>> c.check(1) >>> c.check("foo") @@ -115,14 +123,14 @@ def check(self, value): self._failure("value is not %s" % self.type_.__name__) def __repr__(self): - return "" % self.type_.__name__ + return "" % self.type_.__name__ class AnyC(Contract): """ >>> AnyC() - + Any >>> AnyC().check(object()) """ @@ -130,7 +138,7 @@ def check(self, value): pass def __repr__(self): - return "" + return "Any" class OrCMeta(ContractMeta): @@ -139,7 +147,7 @@ class OrCMeta(ContractMeta): Allows to use "<<" operator on OrC class >>> OrC << IntC << StringC - , )> + Integer or String """ def __lshift__(cls, other): @@ -151,7 +159,7 @@ class OrC(Contract): """ >>> nullString = OrC(StringC, NullC) >>> nullString - , )> + String or Null >>> nullString.check(None) >>> nullString.check("test") >>> nullString.check(1) @@ -184,14 +192,15 @@ def __or__(self, contract): return self def __repr__(self): - return "" % (", ".join(map(repr, self.contracts))) + #return "\n\n\t- %s\n" % ("\n\t- ".join(map(repr, self.contracts))) + return "%s" % (" or ".join(map(repr, self.contracts))) class NullC(Contract): """ >>> NullC() - + Null >>> NullC().check(None) >>> NullC().check(1) Traceback (most recent call last): @@ -204,14 +213,14 @@ def check(self, value): self._failure("value should be None") def __repr__(self): - return "" + return "Null" class BoolC(Contract): """ >>> BoolC() - + Boolean >>> BoolC().check(True) >>> BoolC().check(False) >>> BoolC().check(1) @@ -225,7 +234,7 @@ def check(self, value): self._failure("value should be True or False") def __repr__(self): - return "" + return "Boolean" class NumberCMeta(ContractMeta): @@ -235,17 +244,17 @@ class NumberCMeta(ContractMeta): number contracts >>> IntC[1:] - + Integer (greater or equal than 1) >>> IntC[1:10] - + Integer (greater or equal than 1, less or equal than 10) >>> IntC[:10] - + Integer (less or equal than 10) >>> FloatC[1:] - + Float (greater or equal than 1) >>> IntC > 3 - + Integer (greater than 3) >>> 1 < (FloatC < 10) - + Float (greater than 1, less than 10) >>> (IntC > 5).check(10) >>> (IntC > 5).check(1) Traceback (most recent call last): @@ -272,13 +281,13 @@ class FloatC(Contract): """ >>> FloatC() - + Float >>> FloatC(gte=1) - + Float (greater or equal than 1) >>> FloatC(lte=10) - + Float (less or equal than 10) >>> FloatC(gte=1, lte=10) - + Float (greater or equal than 1, less or equal than 10) >>> FloatC().check(1.0) >>> FloatC().check(1) Traceback (most recent call last): @@ -324,15 +333,21 @@ def __lt__(self, lt): def __gt__(self, gt): return type(self)(gte=self.gte, lte=self.lte, gt=gt, lt=self.lt) + def __reprname__(self): + return type(self).__name__.lower()[:-1] + def __repr__(self): - r = "<%s" % type(self).__name__ + if type(self) is IntC: + r = "Integer" + else: + r = "Float" options = [] for param in ("gte", "lte", "gt", "lt"): if getattr(self, param) is not None: - options.append("%s=%s" % (param, getattr(self, param))) + condition = self.get_full_condition_name(param) + options.append("%s %s" % (condition, getattr(self, param))) if options: - r += "(%s)" % (", ".join(options)) - r += ">" + r += " (%s)" % (", ".join(options)) return r @@ -340,7 +355,7 @@ class IntC(FloatC): """ >>> IntC() - + Integer >>> IntC().check(5) >>> IntC().check(1.1) Traceback (most recent call last): @@ -355,9 +370,9 @@ class StringC(Contract): """ >>> StringC() - + String >>> StringC(allow_blank=True) - + String (could be blank) >>> StringC().check("foo") >>> StringC().check("") Traceback (most recent call last): @@ -380,12 +395,23 @@ def check(self, value): self._failure("blank value is not allowed") def __repr__(self): - return "" if self.allow_blank else "" + return "String (could be blank)" if self.allow_blank else "String" class EmailC(Contract): """ + >>> EmailC() + String with email format + >>> EmailC().check('alex.gonzalez@paylogic.eu') + >>> EmailC().check(1) + Traceback (most recent call last): + ... + ContractValidationError: value is not email + >>> EmailC().check('alex') + Traceback (most recent call last): + ... + ContractValidationError: value is not email """ email_re = re.compile( @@ -398,7 +424,7 @@ def __init__(self): pass def check(self, value): - if not value: + if not value or not isinstance(value, basestring): self._failure("value is not email") if self.email_re.search(value): return @@ -415,7 +441,22 @@ def check(self, value): self._failure('value is not email') def __repr__(self): - return "" + return "String with email format" + +class IsoDateC(Contract): + def _rant(self, value): + self._failure("value is not an iso formatted date: %r" % value) + + def check(self, value): + if not value: + self._rant(value) + try: + dateutil_parse(value) + except: + self._rant(value) + + def __repr__(self): + return "ISO formatted date" class SquareBracketsMeta(ContractMeta): @@ -424,11 +465,11 @@ class SquareBracketsMeta(ContractMeta): Allows usage of square brackets for ListC initialization >>> ListC[IntC] - )> + List of Integer >>> ListC[IntC, 1:] - )> + List of Integer (minimum length of 1) >>> ListC[:10, IntC] - )> + List of Integer (maximum length of 10) >>> ListC[1:10] Traceback (most recent call last): ... @@ -458,11 +499,11 @@ class ListC(Contract): """ >>> ListC(IntC) - )> + List of Integer >>> ListC(IntC, min_length=1) - )> + List of Integer (minimum length of 1) >>> ListC(IntC, min_length=1, max_length=10) - )> + List of Integer (minimum length of 1, maximum length of 10) >>> ListC(IntC).check(1) Traceback (most recent call last): ... @@ -507,17 +548,14 @@ def check(self, value): raise ContractValidationError(err.msg, name) def __repr__(self): - r = ">> contract.allow_extra("eggs") - , foo=)> + >>> contract.check({"foo": 1, "bar": "spam", "eggs": None}) >>> contract.check({"foo": 1, "bar": "spam"}) >>> contract.check({"foo": 1, "bar": "spam", "ham": 100}) @@ -547,7 +585,7 @@ class DictC(Contract): ... ContractValidationError: ham is not allowed key >>> contract.allow_extra("*") - , foo=)> + >>> contract.check({"foo": 1, "bar": "spam", "ham": 100}) >>> contract.check({"foo": 1, "bar": "spam", "ham": 100, "baz": None}) >>> contract.check({"foo": 1, "ham": 100, "baz": None}) @@ -555,7 +593,7 @@ class DictC(Contract): ... ContractValidationError: bar is required >>> contract.allow_optionals("bar") - , foo=)> + >>> contract.check({"foo": 1, "ham": 100, "baz": None}) >>> contract.check({"bar": 1, "ham": 100, "baz": None}) Traceback (most recent call last): @@ -638,7 +676,7 @@ class MappingC(Contract): """ >>> contract = MappingC(StringC, IntC) >>> contract - => )> + Integer> >>> contract.check({"foo": 1, "bar": 2}) >>> contract.check({"foo": 1, "bar": None}) Traceback (most recent call last): @@ -667,7 +705,7 @@ def check(self, mapping): raise ContractValidationError(err.msg, "(value for key %r)" % key) def __repr__(self): - return " %r)>" % (self.keyC, self.valueC) + return "<%r => %r>" % (self.keyC, self.valueC) class EnumC(Contract): @@ -675,7 +713,7 @@ class EnumC(Contract): """ >>> contract = EnumC("foo", "bar", 1) >>> contract - + 'foo' or 'bar' or 1 >>> contract.check("foo") >>> contract.check(1) >>> contract.check(2) @@ -692,7 +730,7 @@ def check(self, value): self._failure("value doesn't match any variant") def __repr__(self): - return "" % (", ".join(map(repr, self.variants))) + return "%s" % (" or ".join(map(repr, self.variants))) class CallableC(Contract): @@ -710,7 +748,7 @@ def check(self, value): self._failure("value is not callable") def __repr__(self): - return "" + return "" class CallC(Contract): @@ -754,7 +792,7 @@ class ForwardC(Contract): >>> nodeC = ForwardC() >>> nodeC << DictC(name=StringC, children=ListC[nodeC]) >>> nodeC - )>, name=)>)> + , name=String)>)> >>> nodeC.check({"name": "foo", "children": []}) >>> nodeC.check({"name": "foo", "children": [1]}) Traceback (most recent call last): @@ -797,6 +835,15 @@ class GuardValidationError(ContractValidationError): pass +def get_array_from_contract(doc_contract): + contracts = {} + for attribute in doc_contract.split(','): + (key, value) = attribute.split('=') + key = key.strip() + contracts.update({key: value}) + return contracts + + def guard(contract=None, **kwargs): """ Decorator for protecting function with contracts @@ -811,7 +858,11 @@ def guard(contract=None, **kwargs): Help on function fn: fn(*args, **kwargs) - guarded with , b=, c=)> + Guarded with: + + - ``a``: String + - ``c``: String + - ``b``: Integer docstring @@ -858,29 +909,67 @@ def decor(*args, **kwargs): try: call_args = dict(zip(fnargs, checkargs) + kwargs.items()) for name, default in zip(reversed(fnargs), - argspec.defaults or ()): + argspec.defaults or ()): if name not in call_args: call_args[name] = default contract.check(call_args) except ContractValidationError as err: raise GuardValidationError(unicode(err)) return fn(*args, **kwargs) - decor.__doc__ = "guarded with %r\n\n" % contract + (decor.__doc__ or "") + + doc_contract = repr(contract) + + # find the first ( and strip anything around it + min_garbage_index = doc_contract.index("(") + 1 + max_garbage_index = doc_contract.index(")") + doc_contract = doc_contract[min_garbage_index:max_garbage_index] + guarded_with = get_array_from_contract(doc_contract) + + try: + pattern = re.compile('^( ){8}', flags=re.MULTILINE) + old_documentation = pattern.sub('', decor.__doc__) + except TypeError: + old_documentation = '' + decor.__doc__ = "Guarded with:\n\n" + for param in guarded_with: + decor.__doc__ += '- ``%s``: %s\n' % (param, guarded_with[param]) + if len(guarded_with) > 0: + decor.__doc__ += '\n' + decor.__doc__ += old_documentation + return decor return wrapper class NumberC(StringC): + """ + >>> NumberC() + Digit + >>> NumberC().check(5) + >>> NumberC().check('alex') + Traceback (most recent call last): + ... + ContractValidationError: value is not a number + """ def __init__(self): super(NumberC, self).__init__(allow_blank=False) def check(self, value): - super(NumberC, self).check(value) + if value == None: + self._failure("value is None") + try: + super(NumberC, self).check(value) + except ContractValidationError as e: + try: + float(value) + return + except ValueError: + raise e if not value.isdigit(): self._failure("value is not a number") def __repr__(self): - return '' + return 'Digit' if __name__ == "__main__": diff --git a/setup.py b/setup.py index d7164e6..a0909ec 100755 --- a/setup.py +++ b/setup.py @@ -11,8 +11,9 @@ name='contractplus', description='contract forked from https://github.com/barbuza/contract', license='none', - version='1.0', + version='1.2', author='barbuza', author_email='', py_modules=['contract'], + install_requires=['python-dateutil>=1.5.0,<2.0.0'], )