Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collections and validator and types #93

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[bumpversion]
current_version = 2.0.1
current_version = 2.0.2
files = src/contracts/__init__.py
commit = True
tag = True

4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ matrix:
- python: 3.3
- python: 3.4
- python: 3.5
include:
- python: 3.7
dist: xenial
sudo: true

install:
- pip install -r requirements.txt
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pyparsing==2.2.2
decorator==4.0.10
six==1.10.0
backports.functools-lru-cache==1.5
qualname==0.1.0
future
numpy<1.16
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os

from setuptools import setup, find_packages

description = (
Expand Down Expand Up @@ -65,7 +64,7 @@ def get_version(filename):

package_dir={'': 'src'},
packages=find_packages('src'),
install_requires=['pyparsing', 'decorator', 'six', 'future'],
install_requires=['pyparsing', 'decorator', 'six', 'future', 'backports.functools-lru-cache', 'qualname', ],
tests_require=['nose'],
entry_points={},
)
42 changes: 33 additions & 9 deletions src/contracts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
__version__ = '2.0.1'
__version__ = "2.0.2"

import logging

#logging.basicConfig()
# logging.basicConfig()
logger = logging.getLogger(__name__)
from typing import Any, Callable, TypeVar, overload

from .interface import (Contract, ContractNotRespected,
CannotDecorateClassmethods,
ContractSyntaxError, ContractException)
from .interface import (
Contract,
ContractNotRespected,
CannotDecorateClassmethods,
ContractSyntaxError,
ContractException,
)

from .main import (check, fail, check_multiple, contract_decorator,
contracts_decorate as decorate,
parse_flexible_spec as parse)
from .main import (
check,
fail,
check_multiple,
contract_decorator,
contracts_decorate as decorate,
parse_flexible_spec as parse,
)

F = TypeVar("F", bound=Callable[..., Any])


@overload
def contract(func: F,) -> F:
...


@overload
def contract(func: F, *arg, **kwargs) -> Callable[[F], F]:
...


# Just make them appear as belonging to the "contracts" Module
# So that Eclipse and other IDEs will not get confused.
def contract(*args, **kwargs):
return contract_decorator(*args, **kwargs)


contract.__doc__ = contract_decorator.__doc__

from .main import new_contract as new_contract_main
Expand All @@ -38,9 +61,10 @@ def new_contract(*args):

from .metaclass import ContractsMeta, with_metaclass

ContractsMeta.__module__ = 'contracts'
ContractsMeta.__module__ = "contracts"

# And after everything else is loaded, load the utils
from .useful_contracts import *

# After everything is loaded, load aliases
# from .library import miscellaneous_aliases # @UnusedImport
58 changes: 58 additions & 0 deletions src/contracts/integrations/AttrValidator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import functools
try:
# python 3.5+
from functools import lru_cache
except ImportError:
# python 2
from backports.functools_lru_cache import lru_cache

try:
# python 3.3+

object.__qualname__

import operator
qualname = operator.attrgetter("__qualname__")
except AttributeError:
# python 2
from qualname import qualname

import attr # attr is only a dependency of pycontracts if this module is imported. Otherwise, it does not depend on it.
from contracts.interface import ContractNotRespected
from contracts.main import new_contract


@attr.s(repr=False, slots=True, hash=True)
class AttrValidator(object):
'''
Validator linking C{pycontracts} with C{attr}'s validators
'''

def __call__(self, inst, attr, value):
"""
Executes the validator by checking it against the contract it was configured with.
"""
try:
AttrValidator.__get_contract(inst.__class__, attr.name, self.contract).check(
value,
)
except ContractNotRespected as e:
# adding formatted debug information to the error
e.error = "{!r}.{}\n{}".format(inst, attr.name, e.error)
raise e

def __repr__(self):
return (
"<Validator linking attr's validators with pycontracts' contract: {contract!r}>"
.format(contract=self.contract)
)

@staticmethod
@lru_cache(typed=True)
def __get_contract(inst_class, name, contract):
# compiling the contract takes time, we want to cache active contracts as they come up
return new_contract("{}___{}".format(qualname(inst_class).replace('.', '___'), name), contract,)

contract = attr.ib()
""" Contract honoured by the given validator"""

1 change: 1 addition & 0 deletions src/contracts/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This space for rent
35 changes: 17 additions & 18 deletions src/contracts/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class Where(object):

All parsed elements contain a reference to a :py:class:`Where` object
so that we can output pretty error messages.


Character should be >= len(string) (possibly outside the string).
Character_end should be >= character (so that you can splice with
Character_end should be >= character (so that you can splice with
string[character:character_end])
"""

Expand All @@ -38,7 +38,7 @@ def __init__(self, string, character, character_end=None):
# or (character <= character_end - 1)):
# character += 1
# else:
# break
# break
self.line, self.col = line_and_col(character, string)

if character_end is not None:
Expand Down Expand Up @@ -93,7 +93,7 @@ def __str__(self):
# mark = 'here or nearby'
def format_where(w, context_before=3, mark=None, arrow=True,
use_unicode=True, no_mark_arrow_if_longer_than=3):
s = u''
s = ''
if w.filename:
s += 'In file %r:\n' % w.filename
lines = w.string.split('\n')
Expand All @@ -108,7 +108,7 @@ def format_where(w, context_before=3, mark=None, arrow=True,
for i in range(start, w.line + 1):
# suppress empty lines
if one_written or lines[i].strip():
s += (u"%s%s\n" % (pattern % (i + 1), lines[i]))
s += ("%s%s\n".format(pattern % (i + 1), lines[i],))
one_written = True

fill = len(pattern % maxi)
Expand All @@ -119,29 +119,29 @@ def format_where(w, context_before=3, mark=None, arrow=True,
space_before = Where(w.string, char0, char0_end)

nindent = printable_length_where(space_before)
space = u' ' * fill + u' ' * nindent
space = ' ' * fill + ' ' * nindent
if w.col_end is not None:
if w.line == w.line_end:
num_highlight = printable_length_where(w)
s += space + u'~' * num_highlight + '\n'
space += u' ' * (num_highlight / 2)
s += space + '~' * num_highlight + '\n'
space += ' ' * (num_highlight / 2)
else:
# cannot highlight if on different lines
num_highlight = None
pass
else:
num_highlight = None
# Do not add the arrow and the mark if we have a long underline string
# Do not add the arrow and the mark if we have a long underline string

disable_mark_arrow = (num_highlight is not None) and (no_mark_arrow_if_longer_than < num_highlight)

if not disable_mark_arrow:
if arrow:
if use_unicode:
s += space + u'↑\n'
s += space + '↑\n'
else:
s += space + u'^\n'
s += space + u'|\n'
s += space + '^\n'
s += space + '|\n'

if mark is not None:
s += space + mark
Expand Down Expand Up @@ -173,7 +173,7 @@ def line_and_col(loc, strg):
from .utils import check_isinstance
check_isinstance(loc, int)
check_isinstance(strg, six.string_types)
# first find the line
# first find the line
lines = strg.split('\n')

if loc == len(strg):
Expand Down Expand Up @@ -433,8 +433,8 @@ def check_contract(self, context, value, silent): # @UnusedVariable
context. This is the function that subclasses must implement.

If silent = False, do not bother with creating detailed error messages yet.
This is for performance optimization.
This is for performance optimization.

:param context: The context in which expressions are evaluated.
:type context:
"""
Expand Down Expand Up @@ -548,7 +548,6 @@ def clipped_repr(x, clip):
s = "%s%s" % (s[:cut], clip_tag)
return s


# TODO: add checks for these functions


Expand Down Expand Up @@ -620,7 +619,7 @@ def describe_value_multiline(x):
try:
# This fails for classes
final = "{}\n{}".format(desc, x.__repr__())
except: # XXX
except: # XXX
final = "%s\n%s" % (desc, x)

return final
10 changes: 7 additions & 3 deletions src/contracts/library/map.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from ..interface import Contract, ContractNotRespected
from ..syntax import (W, contract_expression, O, S, add_contract, add_keyword,
Keyword)
import collections

try:
import collections.abc as collectionsAbc # python 3.6+
except ImportError:
import collections as collectionsAbc


class Map(Contract):
Expand All @@ -13,7 +17,7 @@ def __init__(self, length=None, key_c=None, value_c=None, where=None):
self.value_c = value_c

def check_contract(self, context, value, silent):
if not isinstance(value, collections.Mapping):
if not isinstance(value, collectionsAbc.Mapping):
error = 'Expected a Mapping, got %r.' % value.__class__.__name__
raise ContractNotRespected(contract=self, error=error,
value=value, context=context)
Expand Down Expand Up @@ -57,7 +61,7 @@ def parse_action(s, loc, tokens):

length_spec = S('[') - contract_expression('length') - S(']')
kv_spec = ('(' - O(contract_expression('key')) + ':'
+ O(contract_expression('value')) - ')')
+O(contract_expression('value')) - ')')
dict_contract = Keyword('map') + O(length_spec) + O(kv_spec)

dict_contract.setParseAction(Map.parse_action)
Expand Down
45 changes: 24 additions & 21 deletions src/contracts/library/miscellaneous_aliases.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
import collections

try:
import collections.abc as collectionsAbc # python 3.6+
except ImportError:
import collections as collectionsAbc


def ist(C):

def f(x):
f.__name__ = 'isinstance_of_%s' % C.__name__
if not isinstance(x, C):
raise ValueError('Value is not an instance of %s.' % C.__name__)

return f


def m_new_contract(name, f):
from contracts.library.extensions import CheckCallable
from contracts.library.extensions import Extension
Extension.registrar[name] = CheckCallable(f)


m_new_contract('Container', ist(collections.Container))
# todo: Iterable(x)
m_new_contract('Iterable', ist(collections.Iterable))

m_new_contract('Hashable', ist(collections.Hashable))

m_new_contract('Container', ist(collectionsAbc.Container))
# todo: Iterable(x)
m_new_contract('Iterable', ist(collectionsAbc.Iterable))

m_new_contract('Hashable', ist(collectionsAbc.Hashable))

m_new_contract('Iterator', ist(collections.Iterator))
m_new_contract('Sized', ist(collections.Sized))
m_new_contract('Callable', ist(collections.Callable))
m_new_contract('Sequence', ist(collections.Sequence))
m_new_contract('Set', ist(collections.Set))
m_new_contract('MutableSequence', ist(collections.MutableSequence))
m_new_contract('MutableSet', ist(collections.MutableSet))
m_new_contract('Mapping', ist(collections.Mapping))
m_new_contract('MutableMapping', ist(collections.MutableMapping))
#new_contract('MappingView', ist(collections.MappingView))
#new_contract('ItemsView', ist(collections.ItemsView))
#new_contract('ValuesView', ist(collections.ValuesView))
m_new_contract('Iterator', ist(collectionsAbc.Iterator))
m_new_contract('Sized', ist(collectionsAbc.Sized))
m_new_contract('Callable', ist(collectionsAbc.Callable))
m_new_contract('Sequence', ist(collectionsAbc.Sequence))
m_new_contract('Set', ist(collectionsAbc.Set))
m_new_contract('MutableSequence', ist(collectionsAbc.MutableSequence))
m_new_contract('MutableSet', ist(collectionsAbc.MutableSet))
m_new_contract('Mapping', ist(collectionsAbc.Mapping))
m_new_contract('MutableMapping', ist(collectionsAbc.MutableMapping))
# new_contract('MappingView', ist(collections.MappingView))
# new_contract('ItemsView', ist(collections.ItemsView))
# new_contract('ValuesView', ist(collections.ValuesView))


# Not a lambda to have better messages
def is_None(x):
def is_None(x):
return x is None


m_new_contract('None', is_None)
m_new_contract('NoneType', is_None)
Loading