diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..af4ba8b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + sha: 'master' + hooks: + - id: check-added-large-files + args: [--maxkb=1024] + - id: check-byte-order-marker + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: pretty-format-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: fix-encoding-pragma + exclude: test_data/.*\.py$ + - id: flake8 + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: local + hooks: + - id: importanize + name: importanize + entry: python -m importanize + language: system + language_version: python3 + types: [python] + args: [-v] + + - repo: https://github.com/ambv/black + sha: 'master' + hooks: + - id: black + args: [--line-length=80, --safe] + language_version: python3 + exclude: test_data/.*\.py$ diff --git a/.travis.yml b/.travis.yml index 0c7756b..7bd9a81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ # Config file for automatic testing at travis-ci.org -language: python dist: xenial +sudo: false + +language: python cache: directories: - $HOME/.cache/pip @@ -12,15 +14,14 @@ python: - "3.6" - "2.7" - "pypy" + - "pypy3" +# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: - - pip install -U -r requirements-dev.txt + - pip install tox-travis - pip install coveralls -script: - make check - -after_success: - coveralls +# command to run tests, e.g. python setup.py test +script: tox -sudo: false +after_success: coveralls diff --git a/AUTHORS.rst b/AUTHORS.rst index 7db1941..8669466 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,3 @@ Contributors ~~~~~~~~~~~~ * Serkan Hoscai - https://github.com/shosca - diff --git a/Makefile b/Makefile index ec8b7e9..12c6d24 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ .PHONY: clean-pyc clean-build clean # automatic help generator -help: - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' +help: ## show help + @grep -E '^[a-zA-Z_\-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + cut -d':' -f1- | \ + sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' install: ## install all requirements including dev dependencies pip install -U -r requirements-dev.txt @@ -14,22 +17,25 @@ clean-build: ## remove build artifacts @rm -rf dist/ @rm -rf *.egg-info -clean-pyc: ## remove Python file artifacts - -@find . -name '*.pyc' -follow -print0 | xargs -0 rm -f - -@find . -name '*.pyo' -follow -print0 | xargs -0 rm -f - -@find . -name '__pycache__' -type d -follow -print0 | xargs -0 rm -rf +clean-pyc: ## clean pyc files + -@find . -path ./.tox -prune -o -name '*.pyc' -follow -print0 | xargs -0 rm -f + -@find . -path ./.tox -prune -o -name '*.pyo' -follow -print0 | xargs -0 rm -f + -@find . -path ./.tox -prune -o -name '__pycache__' -type d -follow -print0 | xargs -0 rm -rf clean-test: ## remove test and coverage artifacts @rm -rf .coverage coverage* @rm -rf htmlcov/ @rm -rf .cache -clean-all: ## remove tox test artifacts +clean-all: clean ## remove tox test artifacts rm -rf .tox -lint: ## check style with flake8 and importanize - flake8 alchemy_mock - python -m importanize --ci alchemy_mock/ +lint: clean ## lint whole library + if python -c "import sys; exit(1) if sys.version[:3] < '3.6' else exit(0)"; \ + then \ + pre-commit run --all-files ; \ + fi + python setup.py checkdocs test: clean ## run all tests pytest --doctest-modules --cov=alchemy_mock/ --cov-report=term-missing alchemy_mock/ @@ -42,12 +48,9 @@ test-all: clean ## run all tests with tox check: lint clean test ## run all necessary steps to check validity of project -release: clean ## package and upload a release - # python setup.py checkdocs - python setup.py sdist upload - python setup.py bdist_wheel upload +release: clean ## push release to pypi + python setup.py sdist bdist_wheel upload -dist: clean ## package - python setup.py sdist - python setup.py bdist_wheel +dist: clean ## create distribution of the library + python setup.py sdist bdist_wheel ls -l dist diff --git a/alchemy_mock/__init__.py b/alchemy_mock/__init__.py index dc538c8..9ae53ea 100644 --- a/alchemy_mock/__init__.py +++ b/alchemy_mock/__init__.py @@ -2,6 +2,6 @@ from __future__ import absolute_import, print_function, unicode_literals -__version__ = '0.4.0' -__description__ = 'SQLAlchemy mock helpers.' -__author__ = 'Miroslav Shubernetskiy' +__version__ = "0.4.0" +__description__ = "SQLAlchemy mock helpers." +__author__ = "Miroslav Shubernetskiy" diff --git a/alchemy_mock/comparison.py b/alchemy_mock/comparison.py index d2f0dba..05f2d45 100644 --- a/alchemy_mock/comparison.py +++ b/alchemy_mock/comparison.py @@ -10,10 +10,10 @@ from .utils import match_type -ALCHEMY_UNARY_EXPRESSION_TYPE = type(column('').asc()) -ALCHEMY_BINARY_EXPRESSION_TYPE = type(column('') == '') -ALCHEMY_BOOLEAN_CLAUSE_LIST = type(or_(column('') == '', column('').is_(None))) -ALCHEMY_FUNC_TYPE = type(func.dummy(column(''))) +ALCHEMY_UNARY_EXPRESSION_TYPE = type(column("").asc()) +ALCHEMY_BINARY_EXPRESSION_TYPE = type(column("") == "") +ALCHEMY_BOOLEAN_CLAUSE_LIST = type(or_(column("") == "", column("").is_(None))) +ALCHEMY_FUNC_TYPE = type(func.dummy(column(""))) ALCHEMY_TYPES = ( ALCHEMY_UNARY_EXPRESSION_TYPE, ALCHEMY_BINARY_EXPRESSION_TYPE, @@ -36,9 +36,8 @@ class PrettyExpression(object): >>> PrettyExpression(PrettyExpression(15)) 15 """ - __slots__ = [ - 'expr', - ] + + __slots__ = ["expr"] def __init__(self, e): if isinstance(e, PrettyExpression): @@ -51,9 +50,9 @@ def __repr__(self): compiled = self.expr.compile() - return '{}(sql={!r}, params={!r})'.format( + return "{}(sql={!r}, params={!r})".format( self.expr.__class__.__name__, - match_type(six.text_type(compiled).replace('\n', ' '), str), + match_type(six.text_type(compiled).replace("\n", " "), str), {match_type(k, str): v for k, v in compiled.params.items()}, ) @@ -107,11 +106,15 @@ def __eq__(self, other): # if the right hand side is mock.ANY, # mocks comparison will not be used hence # we hard-code comparison here - if isinstance(self.expr, type(mock.ANY)) or isinstance(other, type(mock.ANY)): + if isinstance(self.expr, type(mock.ANY)) or isinstance( + other, type(mock.ANY) + ): return True # handle string comparison bytes vs unicode in dict keys - if isinstance(self.expr, six.string_types) and isinstance(other, six.string_types): + if isinstance(self.expr, six.string_types) and isinstance( + other, six.string_types + ): other = match_type(other, type(self.expr)) # compare sqlalchemy public api attributes @@ -119,15 +122,21 @@ def __eq__(self, other): return False if not isinstance(self.expr, ALCHEMY_TYPES): + def _(v): return type(self)(v) if isinstance(self.expr, (list, tuple)): - return all(_(i) == j for i, j in six.moves.zip_longest(self.expr, other)) + return all( + _(i) == j + for i, j in six.moves.zip_longest(self.expr, other) + ) elif isinstance(self.expr, collections.Mapping): same_keys = self.expr.keys() == other.keys() - return same_keys and all(_(self.expr[k]) == other[k] for k in self.expr.keys()) + return same_keys and all( + _(self.expr[k]) == other[k] for k in self.expr.keys() + ) else: return self.expr is other or self.expr == other diff --git a/alchemy_mock/mocking.py b/alchemy_mock/mocking.py index 5eb635e..a16dda6 100644 --- a/alchemy_mock/mocking.py +++ b/alchemy_mock/mocking.py @@ -28,6 +28,7 @@ class UnorderedTuple(tuple): >>> UnorderedTuple((1, 2, 3)) == (3, 2, 1) True """ + def __eq__(self, other): if len(self) != len(other): return False @@ -51,10 +52,14 @@ class UnorderedCall(Call): >>> UnorderedCall(((1, 2, 3), {'hello': 'world'})) == Call(((3, 2, 1), {'hello': 'world'})) True """ + def __eq__(self, other): _other = list(other) _other[-2] = UnorderedTuple(other[-2]) - other = Call(tuple(_other), **{k.replace('_mock_', ''): v for k, v in vars(other).items()}) + other = Call( + tuple(_other), + **{k.replace("_mock_", ""): v for k, v in vars(other).items()} + ) return super(UnorderedCall, self).__eq__(other) @@ -76,7 +81,7 @@ def sqlalchemy_call(call, with_name=False, base_call=Call): except ValueError: name, args, kwargs = call else: - name = '' + name = "" args = tuple([ExpressionMatcher(i) for i in args]) kwargs = {k: ExpressionMatcher(v) for k, v in kwargs.items()} @@ -114,11 +119,11 @@ class AlchemyMagicMock(mock.MagicMock): """ def __init__(self, *args, **kwargs): - kwargs.setdefault('__name__', 'Session') + kwargs.setdefault("__name__", "Session") super(AlchemyMagicMock, self).__init__(*args, **kwargs) def _format_mock_call_signature(self, args, kwargs): - name = self._mock_name or 'mock' + name = self._mock_name or "mock" args, kwargs = sqlalchemy_call(mock.call(*args, **kwargs)) return mock._format_call_signature(name, args, kwargs) @@ -128,13 +133,27 @@ def assert_called_with(self, *args, **kwargs): def assert_any_call(self, *args, **kwargs): args, kwargs = sqlalchemy_call(mock.call(*args, **kwargs)) - with setattr_tmp(self, 'call_args_list', [sqlalchemy_call(i) for i in self.call_args_list]): - return super(AlchemyMagicMock, self).assert_any_call(*args, **kwargs) + with setattr_tmp( + self, + "call_args_list", + [sqlalchemy_call(i) for i in self.call_args_list], + ): + return super(AlchemyMagicMock, self).assert_any_call( + *args, **kwargs + ) def assert_has_calls(self, calls, any_order=False): calls = [sqlalchemy_call(i) for i in calls] - with setattr_tmp(self, 'mock_calls', type(self.mock_calls)([sqlalchemy_call(i) for i in self.mock_calls])): - return super(AlchemyMagicMock, self).assert_has_calls(calls, any_order) + with setattr_tmp( + self, + "mock_calls", + type(self.mock_calls)( + [sqlalchemy_call(i) for i in self.mock_calls] + ), + ): + return super(AlchemyMagicMock, self).assert_has_calls( + calls, any_order + ) class UnifiedAlchemyMagicMock(AlchemyMagicMock): @@ -265,74 +284,74 @@ class UnifiedAlchemyMagicMock(AlchemyMagicMock): """ boundary = { - 'all': lambda x: x, - '__iter__': lambda x: iter(x), - 'count': lambda x: len(x), - 'first': lambda x: next(iter(x), None), - 'one': lambda x: ( - x[0] if len(x) == 1 else - raiser(MultipleResultsFound, 'Multiple rows were found for one()') if x else - raiser(NoResultFound, 'No row was found for one()') + "all": lambda x: x, + "__iter__": lambda x: iter(x), + "count": lambda x: len(x), + "first": lambda x: next(iter(x), None), + "one": lambda x: ( + x[0] + if len(x) == 1 + else raiser( + MultipleResultsFound, "Multiple rows were found for one()" + ) + if x + else raiser(NoResultFound, "No row was found for one()") ), - 'get': lambda x, idmap: build_identity_map(x).get(idmap), + "get": lambda x, idmap: build_identity_map(x).get(idmap), } unify = { - 'query': None, - 'add_columns': None, - 'join': None, - 'options': None, - 'group_by': None, - 'filter': UnorderedCall, - 'filter_by': UnorderedCall, - 'order_by': None, - 'limit': None, + "query": None, + "add_columns": None, + "join": None, + "options": None, + "group_by": None, + "filter": UnorderedCall, + "filter_by": UnorderedCall, + "order_by": None, + "limit": None, } - mutate = { - 'add', - 'add_all', - } + mutate = {"add", "add_all"} def __init__(self, *args, **kwargs): - kwargs['_mock_default'] = kwargs.pop('default', []) - kwargs['_mock_data'] = kwargs.pop('data', None) - - kwargs.update({ - k: AlchemyMagicMock( - side_effect=partial( - self._get_data, - _mock_name=k, - ), - ) - for k in self.boundary - }) - - kwargs.update({ - k: AlchemyMagicMock( - return_value=self, - side_effect=partial( - self._unify, - _mock_name=k, - ), - ) - for k in self.unify - }) - - kwargs.update({ - k: AlchemyMagicMock( - return_value=None, - side_effect=partial( - self._mutate_data, - _mock_name=k, - ), - ) - for k in self.mutate - }) + kwargs["_mock_default"] = kwargs.pop("default", []) + kwargs["_mock_data"] = kwargs.pop("data", None) + + kwargs.update( + { + k: AlchemyMagicMock( + side_effect=partial(self._get_data, _mock_name=k) + ) + for k in self.boundary + } + ) + + kwargs.update( + { + k: AlchemyMagicMock( + return_value=self, + side_effect=partial(self._unify, _mock_name=k), + ) + for k in self.unify + } + ) + + kwargs.update( + { + k: AlchemyMagicMock( + return_value=None, + side_effect=partial(self._mutate_data, _mock_name=k), + ) + for k in self.mutate + } + ) super(UnifiedAlchemyMagicMock, self).__init__(*args, **kwargs) def _get_previous_calls(self, calls): - return iter(takewhile(lambda i: i[0] not in self.boundary, reversed(calls))) + return iter( + takewhile(lambda i: i[0] not in self.boundary, reversed(calls)) + ) def _get_previous_call(self, name, calls): # get all previous session calls within same session query @@ -344,11 +363,15 @@ def _get_previous_call(self, name, calls): return next(iter(filter(lambda i: i[0] == name, previous_calls)), None) def _unify(self, *args, **kwargs): - _mock_name = kwargs.pop('_mock_name') + _mock_name = kwargs.pop("_mock_name") submock = getattr(self, _mock_name) - previous_method_call = self._get_previous_call(_mock_name, self.method_calls) - previous_mock_call = self._get_previous_call(_mock_name, self.mock_calls) + previous_method_call = self._get_previous_call( + _mock_name, self.method_calls + ) + previous_mock_call = self._get_previous_call( + _mock_name, self.mock_calls + ) if previous_mock_call is None: return submock.return_value @@ -373,7 +396,7 @@ def _unify(self, *args, **kwargs): submock.call_args = Call((args, kwargs), two=True) submock.call_args_list.append(Call((args, kwargs), two=True)) - submock.mock_calls.append(Call(('', args, kwargs))) + submock.mock_calls.append(Call(("", args, kwargs))) self.method_calls.append(Call((name, args, kwargs))) self.mock_calls.append(Call((name, args, kwargs))) @@ -381,51 +404,71 @@ def _unify(self, *args, **kwargs): return submock.return_value def _get_data(self, *args, **kwargs): - _mock_name = kwargs.pop('_mock_name') + _mock_name = kwargs.pop("_mock_name") _mock_default = self._mock_default _mock_data = self._mock_data if _mock_data is not None: previous_calls = [ - sqlalchemy_call(i, with_name=True, base_call=self.unify.get(i[0]) or Call) + sqlalchemy_call( + i, with_name=True, base_call=self.unify.get(i[0]) or Call + ) for i in self._get_previous_calls(self.mock_calls[:-1]) ] - sorted_mock_data = sorted(_mock_data, key=lambda x: len(x[0]), reverse=True) + sorted_mock_data = sorted( + _mock_data, key=lambda x: len(x[0]), reverse=True + ) - if _mock_name == 'get': - query_call = [c for c in previous_calls if c[0] == 'query'][0] - results = list(chain(*[result for calls, result in sorted_mock_data if query_call in calls])) + if _mock_name == "get": + query_call = [c for c in previous_calls if c[0] == "query"][0] + results = list( + chain( + *[ + result + for calls, result in sorted_mock_data + if query_call in calls + ] + ) + ) return self.boundary[_mock_name](results, *args, **kwargs) else: for calls, result in sorted_mock_data: calls = [ - sqlalchemy_call(i, with_name=True, base_call=self.unify.get(i[0]) or Call) + sqlalchemy_call( + i, + with_name=True, + base_call=self.unify.get(i[0]) or Call, + ) for i in calls ] if all(c in previous_calls for c in calls): - return self.boundary[_mock_name](result, *args, **kwargs) + return self.boundary[_mock_name]( + result, *args, **kwargs + ) return self.boundary[_mock_name](_mock_default, *args, **kwargs) def _mutate_data(self, *args, **kwargs): - _mock_name = kwargs.get('_mock_name') + _mock_name = kwargs.get("_mock_name") _mock_data = self._mock_data = self._mock_data or [] - if _mock_name == 'add': + if _mock_name == "add": to_add = args[0] query_call = mock.call.query(type(to_add)) - mocked_data = next(iter(filter(lambda i: i[0] == [query_call], _mock_data)), None) + mocked_data = next( + iter(filter(lambda i: i[0] == [query_call], _mock_data)), None + ) if mocked_data: mocked_data[1].append(to_add) else: _mock_data.append(([query_call], [to_add])) - elif _mock_name == 'add_all': + elif _mock_name == "add_all": to_add = args[0] _kwargs = kwargs.copy() - _kwargs['_mock_name'] = 'add' + _kwargs["_mock_name"] = "add" for i in to_add: self._mutate_data(i, *args[1:], **_kwargs) diff --git a/alchemy_mock/unittests.py b/alchemy_mock/unittests.py index c919b04..af8688f 100644 --- a/alchemy_mock/unittests.py +++ b/alchemy_mock/unittests.py @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs): # add sqlalchemy expression type which will allow to # use self.assertEqual for t in ALCHEMY_TYPES: - self.addTypeEqualityFunc(t, 'assertSQLAlchemyExpressionEqual') + self.addTypeEqualityFunc(t, "assertSQLAlchemyExpressionEqual") def assertSQLAlchemyExpressionEqual(self, left, right, msg=None): """ @@ -43,7 +43,9 @@ def assertSQLAlchemyExpressionEqual(self, left, right, msg=None): as determined by SQLAlchemyExpressionMatcher """ if ExpressionMatcher(left) != right: - raise self.failureException(msg or '{!r} != {!r}'.format( - PrettyExpression(left), - PrettyExpression(right) - )) + raise self.failureException( + msg + or "{!r} != {!r}".format( + PrettyExpression(left), PrettyExpression(right) + ) + ) diff --git a/alchemy_mock/utils.py b/alchemy_mock/utils.py index d729b9a..9251ac3 100644 --- a/alchemy_mock/utils.py +++ b/alchemy_mock/utils.py @@ -20,9 +20,9 @@ def match_type(s, t): if isinstance(s, t): return s if t is six.text_type: - return s.decode('utf-8') + return s.decode("utf-8") else: - return s.encode('utf-8') + return s.encode("utf-8") def copy_and_update(target, updater): @@ -63,7 +63,7 @@ def indexof(needle, haystack): for i, item in enumerate(haystack): if needle is item: return i - raise ValueError('{!r} is not in {!r}'.format(needle, haystack)) + raise ValueError("{!r} is not in {!r}".format(needle, haystack)) @contextmanager @@ -146,8 +146,7 @@ def build_identity_map(items): for i in items: mapper = inspect(type(i)).mapper pk_keys = tuple( - mapper.get_property_by_column(c).key - for c in mapper.primary_key + mapper.get_property_by_column(c).key for c in mapper.primary_key ) pk = tuple(getattr(i, k) for k in pk_keys) idmap[pk] = i diff --git a/requirements-dev.txt b/requirements-dev.txt index b5cc337..be399d7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,14 @@ -r requirements.txt +black; python_version >= '3.6' collective.checkdocs coverage flake8 +flake8-bugbear; python_version >= '3.5' importanize mock pdbpp +pre-commit +pre-commit-hooks ptpython pytest pytest-cov diff --git a/setup.py b/setup.py index 7102ceb..7d7f05b 100755 --- a/setup.py +++ b/setup.py @@ -9,48 +9,45 @@ def read(fname): - return (open(os.path.join(os.path.dirname(__file__), fname), 'rb') - .read().decode('utf-8')) + return ( + open(os.path.join(os.path.dirname(__file__), fname), "rb") + .read() + .decode("utf-8") + ) -authors = read('AUTHORS.rst') -history = read('HISTORY.rst').replace('.. :changelog:', '') -licence = read('LICENSE.rst') -readme = read('README.rst') +authors = read("AUTHORS.rst") +history = read("HISTORY.rst").replace(".. :changelog:", "") +licence = read("LICENSE.rst") +readme = read("README.rst") -requirements = read('requirements.txt').splitlines() + [ - 'setuptools', -] +requirements = read("requirements.txt").splitlines() + ["setuptools"] test_requirements = ( - read('requirements.txt').splitlines() + - read('requirements-dev.txt').splitlines()[1:] + read("requirements.txt").splitlines() + + read("requirements-dev.txt").splitlines()[1:] ) setup( - name='alchemy-mock', + name="alchemy-mock", version=__version__, author=__author__, description=__description__, - long_description='\n\n'.join([readme, history, authors, licence]), - url='https://github.com/miki725/alchemy-mock', - license='MIT', - packages=find_packages(exclude=['test', 'test.*']), + long_description="\n\n".join([readme, history, authors, licence]), + url="https://github.com/miki725/alchemy-mock", + license="MIT", + packages=find_packages(exclude=["test", "test.*"]), install_requires=requirements, tests_require=test_requirements, - keywords=' '.join([ - 'sqlalchemy', - 'mock', - 'testing', - ]), + keywords=" ".join(["sqlalchemy", "mock", "testing"]), classifiers=[ - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Development Status :: 2 - Pre-Alpha', + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Development Status :: 2 - Pre-Alpha", ], ) diff --git a/tox.ini b/tox.ini index 7543b6f..e0c15ad 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py36, pypy, py27mock1 +envlist = py27, py36, py37, pypy, pypy3, py27-mock1 [testenv] setenv = @@ -12,14 +12,14 @@ deps = whitelist_externals = make -[testenv:py27mock1] +[testenv:py27-mock1] commands = pip install mock==1.0.1 pip freeze make check [flake8] -ignore=E501 +ignore=E501,W503 exclude = .tox [pytest]