pax_global_header00006660000000000000000000000064136250622760014523gustar00rootroot0000000000000052 comment=c77b527493dba9dc52974e492999caafdcc33a16 re-assert-1.1.0/000077500000000000000000000000001362506227600134275ustar00rootroot00000000000000re-assert-1.1.0/.coveragerc000066400000000000000000000012431362506227600155500ustar00rootroot00000000000000[run] branch = True source = . omit = .tox/* /usr/* setup.py # Don't complain if non-runnable code isn't run */__main__.py [report] show_missing = True skip_covered = True exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover # We optionally substitute this ${COVERAGE_IGNORE_WINDOWS} # Don't complain if tests don't hit defensive assertion code: ^\s*raise AssertionError\b ^\s*raise NotImplementedError\b ^\s*return NotImplemented\b ^\s*raise$ # Don't complain if non-runnable code isn't run: ^if __name__ == ['"]__main__['"]:$ [html] directory = coverage-html # vim:ft=dosini re-assert-1.1.0/.gitignore000066400000000000000000000001051362506227600154130ustar00rootroot00000000000000*.egg-info *.pyc /.mypy_cache /.pytest_cache /.coverage /.tox /venv* re-assert-1.1.0/.pre-commit-config.yaml000066400000000000000000000020261362506227600177100ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-docstring-first - id: check-yaml - id: debug-statements - id: requirements-txt-fixer - id: double-quote-string-fixer - id: name-tests-test - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.5 hooks: - id: autopep8 - repo: https://github.com/asottile/reorder_python_imports rev: v1.9.0 hooks: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v1.5.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade rev: v1.26.2 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.761 hooks: - id: mypy re-assert-1.1.0/LICENSE000066400000000000000000000020431362506227600144330ustar00rootroot00000000000000Copyright (c) 2019 Anthony Sottile Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. re-assert-1.1.0/README.md000066400000000000000000000046761362506227600147230ustar00rootroot00000000000000[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.re-assert?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=31&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/31/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=31&branchName=master) re-assert ========= show where your regex match assertion failed! ## installation `pip install re-assert` ## usage `re-assert` provides a helper class to make assertions of regexes simpler. ### `re_assert.Matches(pattern: str, *args, **kwargs)` construct a `Matches` object. _note_: under the hood, `re-assert` uses the [`regex`] library for matching, any `*args` / `**kwargs` that `regex.compile` supports will work. in general, the `regex` library is 100% compatible with the `re` library (and will even accept its flags, etc.) [`regex`]: https://pypi.org/project/regex/ ### `re_assert.Matches.from_pattern(pattern: Pattern[str]) -> Matches` construct a `Matches` object from an already-compiled regex. this is useful (for instance) if you're testing an existing compiled regex. ```pycon >>> import re >>> reg = re.compile('foo') >>> Matches.from_pattern(reg) == 'fork' False >>> Matches.from_pattern(reg) == 'food' True ``` ### `Matches.__eq__(other)` (`==`) the equality operator is overridden for use with assertion frameworks such as pytest ```pycon >>> pat = Matches('foo') >>> pat == 'bar' False >>> pat == 'food' True ``` ### `Matches.__repr__()` (`repr(...)`) a side-effect of an equality failure changes the `repr(...)` of a `Matches` object. this allows for useful pytest assertion messages: ```pytest > assert Matches('foo') == 'fork' E AssertionError: assert Matches('foo'...ork\n # ^ == 'fork' E -Matches('foo')\n E - # regex failed to match at:\n E - #\n E - #> fork\n E - # ^ E +'fork' ``` ### `Matches.assert_matches(s: str)` if you're using some other test framework, this method is useful for producing a readable traceback ```pycon >>> Matches('foo').assert_matches('food') >>> Matches('foo').assert_matches('fork') Traceback (most recent call last): File "", line 1, in File "/home/asottile/workspace/re-assert/re_assert.py", line 63, in assert_matches assert self == s, self._fail AssertionError: regex failed to match at: > fork ^ ``` re-assert-1.1.0/azure-pipelines.yml000066400000000000000000000006131362506227600172660ustar00rootroot00000000000000trigger: branches: include: [master, test-me-*] tags: include: ['*'] resources: repositories: - repository: asottile type: github endpoint: github name: asottile/azure-pipeline-templates ref: refs/tags/v1.0.0 jobs: - template: job--pre-commit.yml@asottile - template: job--python-tox.yml@asottile parameters: toxenvs: [py36, py37] os: linux re-assert-1.1.0/re_assert.py000066400000000000000000000044521362506227600157750ustar00rootroot00000000000000from typing import Any from typing import Optional from typing import Pattern import regex class Matches: # TODO: Generic[AnyStr] (binary pattern support) def __init__(self, pattern: str, *args: Any, **kwargs: Any) -> None: self._pattern = regex.compile(pattern, *args, **kwargs) self._fail: Optional[str] = None self._type = type(pattern) def _fail_message(self, fail: str) -> str: # binary search to find the longest substring match pos, bound = 0, len(fail) while pos < bound: pivot = pos + (bound - pos + 1) // 2 match = self._pattern.match(fail[:pivot], partial=True) if match: pos = pivot else: bound = pivot - 1 retv = [' regex failed to match at:', ''] for line in fail.splitlines(True): line_noeol = line.rstrip('\r\n') retv.append(f'> {line_noeol}') if 0 <= pos <= len(line_noeol): indent = ''.join(c if c.isspace() else ' ' for c in line[:pos]) retv.append(f' {indent}^') pos = -1 else: pos -= len(line) if pos >= 0: retv.append('>') retv.append(' ^') return '\n'.join(retv) def __eq__(self, other: object) -> bool: if not isinstance(other, self._type): raise TypeError(f'expected {self._type}, got {type(other)}') if not self._pattern.match(other): self._fail = self._fail_message(other) return False else: self._fail = None return True def __repr__(self) -> str: pattern_repr = repr(self._pattern) params = pattern_repr[pattern_repr.index('(') + 1:-1] boring_flag = ', flags=regex.V0' if params.endswith(boring_flag): params = params[:-1 * len(boring_flag)] if self._fail is not None: fail_msg = ' #'.join(['\n'] + self._fail.splitlines(True)) else: fail_msg = '' return f'{type(self).__name__}({params}){fail_msg}' def assert_matches(self, s: str) -> None: assert self == s, self._fail @classmethod def from_pattern(cls, pattern: Pattern[str]) -> 'Matches': return cls(pattern.pattern, pattern.flags) re-assert-1.1.0/requirements-dev.txt000066400000000000000000000000331362506227600174630ustar00rootroot00000000000000coverage pre-commit pytest re-assert-1.1.0/setup.cfg000066400000000000000000000017031362506227600152510ustar00rootroot00000000000000[metadata] name = re_assert version = 1.1.0 description = show where your regex match assertion failed! long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/asottile/re-assert author = Anthony Sottile author_email = asottile@umich.edu license = MIT license_file = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] py_modules = re_assert install_requires = regex python_requires = >=3.6.1 [bdist_wheel] universal = True [mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true no_implicit_optional = true [mypy-testing.*] disallow_untyped_defs = false [mypy-tests.*] disallow_untyped_defs = false re-assert-1.1.0/setup.py000066400000000000000000000000451362506227600151400ustar00rootroot00000000000000from setuptools import setup setup() re-assert-1.1.0/tests/000077500000000000000000000000001362506227600145715ustar00rootroot00000000000000re-assert-1.1.0/tests/__init__.py000066400000000000000000000000001362506227600166700ustar00rootroot00000000000000re-assert-1.1.0/tests/re_assert_test.py000066400000000000000000000050731362506227600201760ustar00rootroot00000000000000import re import pytest from re_assert import Matches def test_typeerror_equality_different_type(): with pytest.raises(TypeError): Matches('foo') == b'foo' def test_matches_repr_plain(): assert repr(Matches('^foo')) == "Matches('^foo')" def test_matches_repr_with_flags(): ret = repr(Matches('^foo', re.I)) assert ret == "Matches('^foo', flags=regex.I | regex.V0)" def test_repr_with_failure(): obj = Matches('^foo') assert obj != 'fa' assert repr(obj) == ( "Matches('^foo')\n" ' # regex failed to match at:\n' ' #\n' ' #> fa\n' ' # ^' ) def test_assert_success(): obj = Matches('foo') assert obj == 'food' obj.assert_matches('food') def test_fail_at_beginning(): with pytest.raises(AssertionError) as excinfo: Matches('foo').assert_matches('bar') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> bar\n' ' ^' ) def test_fail_at_end_of_line(): with pytest.raises(AssertionError) as excinfo: Matches('foo').assert_matches('fo') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> fo\n' ' ^' ) def test_fail_multiple_lines(): with pytest.raises(AssertionError) as excinfo: Matches('foo.bar', re.DOTALL).assert_matches('foo\nbr') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> foo\n' '> br\n' ' ^' ) def test_fail_end_of_line_with_newline(): with pytest.raises(AssertionError) as excinfo: Matches('foo.bar', re.DOTALL).assert_matches('foo\n') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> foo\n' '>\n' ' ^' ) def test_fail_at_end_of_line_mismatching_newline(): with pytest.raises(AssertionError) as excinfo: Matches('foo.', re.DOTALL).assert_matches('foo') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> foo\n' ' ^' ) def test_match_with_tabs(): with pytest.raises(AssertionError) as excinfo: Matches('f.o.o').assert_matches('f\to\tx\n') msg, = excinfo.value.args assert msg == ( ' regex failed to match at:\n' '\n' '> f\to\tx\n' ' \t \t^' ) def test_from_pattern(): pattern = re.compile('^foo', flags=re.I) assert Matches.from_pattern(pattern) == 'FOO' re-assert-1.1.0/tox.ini000066400000000000000000000005541362506227600147460ustar00rootroot00000000000000[tox] envlist = py36,py37,pre-commit [testenv] deps = -rrequirements-dev.txt commands = coverage erase coverage run -m pytest {posargs:tests} coverage report --fail-under 100 pre-commit install [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [pep8] ignore = E265,E501,W504