From 1f097551d5383c8b705af520458996ef50803e1d Mon Sep 17 00:00:00 2001 From: cyberco Date: Mon, 3 Oct 2011 18:21:16 +0300 Subject: [PATCH 01/10] Better email contract --- contract.py | 259 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 159 insertions(+), 100 deletions(-) diff --git a/contract.py b/contract.py index b68a3a2..f9fdf1e 100644 --- a/contract.py +++ b/contract.py @@ -2,6 +2,7 @@ import functools import inspect +import re """ Contract is tiny library for data validation @@ -11,15 +12,16 @@ __all__ = ("ContractValidationError", "Contract", "AnyC", "IntC", "StringC", "ListC", "DictC", "OrC", "NullC", "FloatC", "EnumC", "CallableC", - "CallC", "ForwardC", "BoolC", "TypeC", "MappingC", "guard", ) + "CallC", "ForwardC", "BoolC", "TypeC", "MappingC", "guard", + "EmailC") class ContractValidationError(TypeError): - + """ Basic contract validation error """ - + def __init__(self, msg, name=None): message = msg if not name else "%s: %s" % (name, msg) super(ContractValidationError, self).__init__(message) @@ -28,30 +30,30 @@ def __init__(self, msg, name=None): class ContractMeta(type): - + """ Metaclass for contracts to make using "|" operator possible not only on instances but on classes - + >>> IntC | StringC , )> >>> IntC | StringC | NullC , , )> """ - + def __or__(cls, other): return cls() | other class Contract(object): - + """ Base class for contracts, provides only one method for contract validation failure reporting """ - + __metaclass__ = ContractMeta - + def check(self, value): """ Implement this method in Contract subclasses @@ -59,13 +61,13 @@ def check(self, value): cls = "%s.%s" % (type(self).__module__, type(self).__name__) raise NotImplementedError("method check is not implemented in" " '%s'" % cls) - + def _failure(self, message): """ Shortcut method for raising validation error """ raise ContractValidationError(message) - + def _contract(self, contract): """ Helper for complex contracts, takes contract instance or class @@ -80,13 +82,13 @@ def _contract(self, contract): else: raise RuntimeError("%r should be instance or subclass" " of Contract" % contract) - + def __or__(self, other): return OrC(self, other) class TypeC(Contract): - + """ >>> TypeC(int) @@ -99,53 +101,53 @@ class TypeC(Contract): ... ContractValidationError: value is not int """ - + class __metaclass__(ContractMeta): - + def __getitem__(self, type_): return self(type_) - + def __init__(self, type_): self.type_ = type_ - + def check(self, value): if not isinstance(value, self.type_): self._failure("value is not %s" % self.type_.__name__) - + def __repr__(self): return "" % self.type_.__name__ class AnyC(Contract): - + """ >>> AnyC() >>> AnyC().check(object()) """ - + def check(self, value): pass - + def __repr__(self): return "" class OrCMeta(ContractMeta): - + """ Allows to use "<<" operator on OrC class - + >>> OrC << IntC << StringC , )> """ - + def __lshift__(cls, other): return cls() << other class OrC(Contract): - + """ >>> nullString = OrC(StringC, NullC) >>> nullString @@ -157,12 +159,12 @@ class OrC(Contract): ... ContractValidationError: no one contract matches """ - + __metaclass__ = OrCMeta - + def __init__(self, *contracts): self.contracts = map(self._contract, contracts) - + def check(self, value): for contract in self.contracts: try: @@ -172,21 +174,21 @@ def check(self, value): else: return self._failure("no one contract matches") - + def __lshift__(self, contract): self.contracts.append(self._contract(contract)) return self - + def __or__(self, contract): self << contract return self - + def __repr__(self): return "" % (", ".join(map(repr, self.contracts))) class NullC(Contract): - + """ >>> NullC() @@ -196,17 +198,17 @@ class NullC(Contract): ... ContractValidationError: value should be None """ - + def check(self, value): if value is not None: self._failure("value should be None") - + def __repr__(self): return "" class BoolC(Contract): - + """ >>> BoolC() @@ -217,21 +219,21 @@ class BoolC(Contract): ... ContractValidationError: value should be True or False """ - + def check(self, value): if not isinstance(value, bool): self._failure("value should be True or False") - + def __repr__(self): return "" class NumberCMeta(ContractMeta): - + """ Allows slicing syntax for min and max arguments for number contracts - + >>> IntC[1:] >>> IntC[1:10] @@ -255,19 +257,19 @@ class NumberCMeta(ContractMeta): ... ContractValidationError: value should be less than 3 """ - + def __getitem__(self, slice_): return self(gte=slice_.start, lte=slice_.stop) - + def __lt__(self, lt): return self(lt=lt) - + def __gt__(self, gt): return self(gt=gt) class FloatC(Contract): - + """ >>> FloatC() @@ -293,17 +295,17 @@ class FloatC(Contract): ... ContractValidationError: value is greater than 3 """ - + __metaclass__ = NumberCMeta - + value_type = float - + def __init__(self, gte=None, lte=None, gt=None, lt=None): self.gte = gte self.lte = lte self.gt = gt self.lt = lt - + def check(self, value): if not isinstance(value, self.value_type): self._failure("value is not %s" % self.value_type.__name__) @@ -315,13 +317,13 @@ def check(self, value): self._failure("value should be less than %s" % self.lt) if self.gt is not None and value <= self.gt: self._failure("value should be greater than %s" % self.gt) - + def __lt__(self, lt): return type(self)(gte=self.gte, lte=self.lte, gt=self.gt, lt=lt) - + def __gt__(self, gt): return type(self)(gte=self.gte, lte=self.lte, gt=gt, lt=self.lt) - + def __repr__(self): r = "<%s" % type(self).__name__ options = [] @@ -335,7 +337,7 @@ def __repr__(self): class IntC(FloatC): - + """ >>> IntC() @@ -345,12 +347,12 @@ class IntC(FloatC): ... ContractValidationError: value is not int """ - + value_type = int class StringC(Contract): - + """ >>> StringC() @@ -367,25 +369,60 @@ class StringC(Contract): ... ContractValidationError: value is not string """ - + def __init__(self, allow_blank=False): self.allow_blank = allow_blank - + def check(self, value): if not isinstance(value, basestring): self._failure("value is not string") if not self.allow_blank and len(value) is 0: self._failure("blank value is not allowed") - + def __repr__(self): return "" if self.allow_blank else "" +class EmailC(Contract): + + """ + """ + + email_re = re.compile( + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string + r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain + r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) + + def __init__(self): + pass + + def check(self, value): + if not value: + self._failure("value is not email") + if self.email_re.search(value): + return + # Trivial case failed. Try for possible IDN domain-part + if value and u'@' in value: + parts = value.split(u'@') + try: + parts[-1] = parts[-1].encode('idna') + except UnicodeError: + pass + else: + if self.email_re.search(u'@'.join(parts)): + return + self._failure('value is not email') + + def __repr__(self): + return "" + + class SquareBracketsMeta(ContractMeta): - + """ Allows usage of square brackets for ListC initialization - + >>> ListC[IntC] )> >>> ListC[IntC, 1:] @@ -397,7 +434,7 @@ class SquareBracketsMeta(ContractMeta): ... RuntimeError: Contract is required for ListC initialization """ - + def __getitem__(self, args): slice_ = None contract = None @@ -418,7 +455,7 @@ def __getitem__(self, args): class ListC(Contract): - + """ >>> ListC(IntC) )> @@ -447,14 +484,14 @@ class ListC(Contract): ... ContractValidationError: list length is greater than 2 """ - + __metaclass__ = SquareBracketsMeta - + def __init__(self, contract, min_length=0, max_length=None): self.contract = self._contract(contract) self.min_length = min_length self.max_length = max_length - + def check(self, value): if not isinstance(value, list): self._failure("value is not list") @@ -468,7 +505,7 @@ def check(self, value): except ContractValidationError as err: name = "%i.%s" % (index, err.name) if err.name else str(index) raise ContractValidationError(err.msg, name) - + def __repr__(self): r = ">> contract = DictC(foo=IntC, bar=StringC) >>> contract.check({"foo": 1, "bar": "spam"}) @@ -529,7 +566,7 @@ class DictC(Contract): ... ContractValidationError: bar: value is not string """ - + def __init__(self, **contracts): self.optionals = [] self.extras = [] @@ -537,7 +574,7 @@ def __init__(self, **contracts): self.contracts = {} for key, contract in contracts.items(): self.contracts[key] = self._contract(contract) - + def allow_extra(self, *names): for name in names: if name == "*": @@ -545,7 +582,7 @@ def allow_extra(self, *names): else: self.extras.append(name) return self - + def allow_optionals(self, *names): for name in names: if name == "*": @@ -553,18 +590,18 @@ def allow_optionals(self, *names): else: self.optionals.append(name) return self - + def check(self, value): if not isinstance(value, dict): self._failure("value is not dict") self.check_presence(value) map(self.check_item, value.items()) - + def check_presence(self, value): for key in self.contracts: if key not in self.optionals and key not in value: self._failure("%s is required" % key) - + def check_item(self, item): key, value = item if key in self.contracts: @@ -575,7 +612,7 @@ def check_item(self, item): raise ContractValidationError(err.msg, name) elif not self.allow_any and key not in self.extras: self._failure("%s is not allowed key" % key) - + def __repr__(self): r = ">> contract = MappingC(StringC, IntC) >>> contract @@ -612,11 +649,11 @@ class MappingC(Contract): ... ContractValidationError: (key 2): value is not string """ - + def __init__(self, keyC, valueC): self.keyC = self._contract(keyC) self.valueC = self._contract(valueC) - + def check(self, mapping): for key in mapping: value = mapping[key] @@ -628,13 +665,13 @@ def check(self, mapping): self.valueC.check(value) except ContractValidationError as err: raise ContractValidationError(err.msg, "(value for key %r)" % key) - + def __repr__(self): return " %r)>" % (self.keyC, self.valueC) class EnumC(Contract): - + """ >>> contract = EnumC("foo", "bar", 1) >>> contract @@ -646,20 +683,20 @@ class EnumC(Contract): ... ContractValidationError: value doesn't match any variant """ - + def __init__(self, *variants): self.variants = variants[:] - + def check(self, value): if value not in self.variants: self._failure("value doesn't match any variant") - + def __repr__(self): return "" % (", ".join(map(repr, self.variants))) class CallableC(Contract): - + """ >>> CallableC().check(lambda: 1) >>> CallableC().check(1) @@ -667,17 +704,17 @@ class CallableC(Contract): ... ContractValidationError: value is not callable """ - + def check(self, value): if not callable(value): self._failure("value is not callable") - + def __repr__(self): return "" class CallC(Contract): - + """ >>> def validator(value): ... if value != "foo": @@ -692,7 +729,7 @@ class CallC(Contract): ... ContractValidationError: I want only foo! """ - + def __init__(self, fn): if not callable(fn): raise RuntimeError("CallC argument should be callable") @@ -701,18 +738,18 @@ def __init__(self, fn): raise RuntimeError("CallC argument should be" " one argument function") self.fn = fn - + def check(self, value): error = self.fn(value) if error is not None: self._failure(error) - + def __repr__(self): return "" % self.fn.__name__ class ForwardC(Contract): - + """ >>> nodeC = ForwardC() >>> nodeC << DictC(name=StringC, children=ListC[nodeC]) @@ -727,19 +764,19 @@ class ForwardC(Contract): {"name": "bar", "children": []} \ ]}) """ - + def __init__(self): self.contract = None self._recur_repr = False - + def __lshift__(self, contract): if self.contract: raise RuntimeError("contract for ForwardC is already specified") self.contract = self._contract(contract) - + def check(self, value): self.contract.check(value) - + def __repr__(self): # XXX not threadsafe if self._recur_repr: @@ -751,19 +788,19 @@ def __repr__(self): class GuardValidationError(ContractValidationError): - + """ Raised when guarded function gets invalid arguments, inherits error message from corresponding ContractValidationError """ - + pass def guard(contract=None, **kwargs): """ Decorator for protecting function with contracts - + >>> @guard(a=StringC, b=IntC, c=StringC) ... def fn(a, b, c="default"): ... '''docstring''' @@ -805,25 +842,47 @@ def guard(contract=None, **kwargs): " contract or kwargs") if not contract: contract = DictC(**kwargs) + def wrapper(fn): argspec = inspect.getargspec(fn) + @functools.wraps(fn) def decor(*args, **kwargs): + fnargs = argspec.args + if fnargs[0] == 'self': + fnargs = fnargs[1:] + checkargs = args[1:] + else: + checkargs = args + try: - call_args = dict(zip(argspec.args, args) + kwargs.items()) - for name, default in zip(reversed(argspec.args), argspec.defaults): + call_args = dict(zip(fnargs, checkargs) + kwargs.items()) + for name, default in zip(reversed(fnargs), + 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 "") + decor.__doc__ = "guarded with %r\n\n" % contract + (decor.__doc__ or "") return decor return wrapper +class NumberC(StringC): + def __init__(self): + super(NumberC, self).__init__(allow_blank=False) + + def check(self, value): + super(NumberC, self).check(value) + if not value.isdigit(): + self._failure("value is not a number") + + def __repr__(self): + return '' + + if __name__ == "__main__": import doctest - doctest.testmod() + doctest.testmod() \ No newline at end of file From 2a34e1d96e4fca223e9703b65b188a9c3acef19f Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 08:55:19 +0200 Subject: [PATCH 02/10] Updated --- contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract.py b/contract.py index f9fdf1e..c7a4d15 100644 --- a/contract.py +++ b/contract.py @@ -885,4 +885,4 @@ def __repr__(self): if __name__ == "__main__": import doctest - doctest.testmod() \ No newline at end of file + doctest.testmod() From 22544fe003bb6a60263a9597bda7b754774f828b Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 09:42:30 +0200 Subject: [PATCH 03/10] added setup.py file --- setup.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 setup.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..ad9abd4 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import sys + +from setuptools import setup, find_packages + +if 'register' in sys.argv or 'upload' in sys.argv: + raise Exception('I don\'t want to be on PyPI!') + +setup( + name='contract', + description='contract forked from https://github.com/barbuza/contract', + license='none', + version='1.0', + author='barbuza', + author_email='', + packages=find_packages(), + include_package_data=True, + ) From 7d7aa121f1c6d92a46a6c16bbf3ea8f77f8cddbd Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 13:31:26 +0200 Subject: [PATCH 04/10] Changed repo name in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad9abd4..381c4b0 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ raise Exception('I don\'t want to be on PyPI!') setup( - name='contract', + name='contrac-plus', description='contract forked from https://github.com/barbuza/contract', license='none', version='1.0', From f9a67759193f33156d775bdf39eee4a784a65a09 Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 13:52:36 +0200 Subject: [PATCH 05/10] Fixed typo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 381c4b0..f8bb075 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ raise Exception('I don\'t want to be on PyPI!') setup( - name='contrac-plus', + name='contract-plus', description='contract forked from https://github.com/barbuza/contract', license='none', version='1.0', From d8e988a2f01e4775ae03a53d44f9e340c8389386 Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 14:00:12 +0200 Subject: [PATCH 06/10] Renamed project (once again) since dashes are not allowed... --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8bb075..f65e8f4 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ raise Exception('I don\'t want to be on PyPI!') setup( - name='contract-plus', + name='contractplus', description='contract forked from https://github.com/barbuza/contract', license='none', version='1.0', From 84528ca81319d160f646507d84a4aae6d9b0eba1 Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 14:05:53 +0200 Subject: [PATCH 07/10] Changed name in setup.py since that should be the name of the python module --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f65e8f4..ad9abd4 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ raise Exception('I don\'t want to be on PyPI!') setup( - name='contractplus', + name='contract', description='contract forked from https://github.com/barbuza/contract', license='none', version='1.0', From fa455445531d1d9fca674851de14dc42e1eef77c Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 14:15:32 +0200 Subject: [PATCH 08/10] Update setup.py to not use packages --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ad9abd4..c955e72 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import sys -from setuptools import setup, find_packages +from setuptools import setup if 'register' in sys.argv or 'upload' in sys.argv: raise Exception('I don\'t want to be on PyPI!') @@ -14,6 +14,5 @@ version='1.0', author='barbuza', author_email='', - packages=find_packages(), - include_package_data=True, + packages=['contract.py'], ) From 120027acef5dfd3a40515ad00722c0937c961375 Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 14:16:49 +0200 Subject: [PATCH 09/10] Better setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c955e72..130eac0 100755 --- a/setup.py +++ b/setup.py @@ -14,5 +14,5 @@ version='1.0', author='barbuza', author_email='', - packages=['contract.py'], + py_modules=['contract'], ) From c288af19b764e108275d7fc90d8f488aaee2696d Mon Sep 17 00:00:00 2001 From: Berco Beute Date: Wed, 5 Oct 2011 14:18:23 +0200 Subject: [PATCH 10/10] renamed project --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 130eac0..d7164e6 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ raise Exception('I don\'t want to be on PyPI!') setup( - name='contract', + name='contractplus', description='contract forked from https://github.com/barbuza/contract', license='none', version='1.0',