pax_global_header00006660000000000000000000000064147263300770014523gustar00rootroot0000000000000052 comment=4c4f88bda509c73cf26628b86902887cfdd6fedd expecttest-0.3.0/000077500000000000000000000000001472633007700137135ustar00rootroot00000000000000expecttest-0.3.0/.flake8000066400000000000000000000014531472633007700150710ustar00rootroot00000000000000[flake8] select = B,C,E,F,P,T4,W,B9 max-line-length = 150 ### DEFAULT IGNORES FOR 4-space INDENTED PROJECTS ### # E127, E128 are hard to silence in certain nested formatting situations. # E203 doesn't work for slicing # E265, E266 talk about comment formatting which is too opinionated. # E402 warns on imports coming after statements. There are important use cases # like demandimport (https://fburl.com/demandimport) that require statements # before imports. # E501 is not flexible enough, we're using B950 instead. # E722 is a duplicate of B001. # P207 is a duplicate of B003. # P208 is a duplicate of C403. # W503 talks about operator formatting which is too opinionated. ignore = E127, E128, E203, E265, E266, E402, E501, E722, P207, P208, W503 exclude = .git, .hg, __pycache__, max-complexity = 12 expecttest-0.3.0/.github/000077500000000000000000000000001472633007700152535ustar00rootroot00000000000000expecttest-0.3.0/.github/workflows/000077500000000000000000000000001472633007700173105ustar00rootroot00000000000000expecttest-0.3.0/.github/workflows/ci.yml000066400000000000000000000014511472633007700204270ustar00rootroot00000000000000name: CI on: pull_request: push: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: ci: strategy: fail-fast: false max-parallel: 5 matrix: python-version: - 3.8 - 3.9 - "3.10" - 3.11 - 3.12 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry uses: snok/install-poetry@v1 - name: Install dependencies run: poetry install - name: Run tests run: poetry run ./run_tests.sh expecttest-0.3.0/.gitignore000066400000000000000000000000541472633007700157020ustar00rootroot00000000000000__pycache__/ /.hypothesis/ /.vscode/ /dist/ expecttest-0.3.0/CODE_OF_CONDUCT.md000066400000000000000000000067211472633007700165200ustar00rootroot00000000000000# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when there is a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq expecttest-0.3.0/CONTRIBUTING.md000066400000000000000000000023461472633007700161510ustar00rootroot00000000000000# Contributing to expecttest We want to make contributing to this project as easy and transparent as possible. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## License By contributing to expecttest, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree.expecttest-0.3.0/LICENSE000066400000000000000000000020561472633007700147230ustar00rootroot00000000000000Copyright (c) Edward Z. Yang 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. expecttest-0.3.0/README.md000066400000000000000000000061121472633007700151720ustar00rootroot00000000000000# expecttest [![PyPI version](https://badge.fury.io/py/expecttest.svg)](https://badge.fury.io/py/expecttest) This library implements expect tests (also known as "golden" tests). Expect tests are a method of writing tests where instead of hard-coding the expected output of a test, you run the test to get the output, and the test framework automatically populates the expected output. If the output of the test changes, you can rerun the test with the environment variable `EXPECTTEST_ACCEPT=1` to automatically update the expected output. Somewhat unusually, this library implements *inline* expect tests: that is to say, the expected output isn't saved to an external file, it is saved directly in the Python file (and we modify your Python file when updating the expect test.) The general recipe for how to use this is as follows: 1. Write your test and use `assertExpectedInline()` instead of a normal `assertEqual`. Leave the expected argument blank with an empty string: ```py self.assertExpectedInline(some_func(), """""") ``` 2. Run your test. It should fail, and you get an error message about accepting the output with `EXPECTTEST_ACCEPT=1` 3. Rerun the test with `EXPECTTEST_ACCEPT=1`. Now the previously blank string literal will contain the expected value of the test. ```py self.assertExpectedInline(some_func(), """my_value""") ``` ## A minimal working example ```python # test.py import unittest from expecttest import TestCase class TestStringMethods(TestCase): def test_split(self): s = 'hello world' self.assertExpectedInline(str(s.split()), """""") if __name__ == '__main__': unittest.main() ``` Run `EXPECTTEST_ACCEPT=1 python test.py` , and the content in triple-quoted string will be automatically updated. For people who use `pytest`: ```python from expecttest import assert_expected_inline def test_split(): s = 'hello world' assert_expected_inline(str(s.split()), """""") ``` Run `EXPECTTEST_ACCEPT=1 pytest test.py` , and the content in triple-quoted string will be automatically updated. For parameterized tests, advanced fixturing and other cases where the expectation is in a different place than the assertion, use `Expect`: ```python from expecttest import Expect def test_removing_spaces(s: str, expected: Expect) -> None: expected.assert_expected(s.replace(" ", "")) test_removing_spaces("foo bar", Expect("""foobar""")) test_removing_spaces("foo bar !!", Expect("""""")) ``` Run `EXPECTTEST_ACCEPT=1 pytest test.py` , and the content in triple-quoted string will be automatically updated. ## Some tips and tricks - Often, you will want to expect test on a multiline string. This framework understands triple-quoted strings, so you can just write `"""my_value"""` and it will turn into triple-quoted strings. - Take some time thinking about how exactly you want to design the output format of the expect test. It is often profitable to design an output representation specifically for expect tests. ## Similar libraries - [expect-test](https://docs.rs/expect-test) for Rust expecttest-0.3.0/expecttest/000077500000000000000000000000001472633007700161035ustar00rootroot00000000000000expecttest-0.3.0/expecttest/__init__.py000066400000000000000000000347051472633007700202250ustar00rootroot00000000000000import ast import os import re import string import sys import traceback import unittest import difflib import dataclasses from typing import Any, Callable, Dict, List, Match, Tuple, Optional # NB: We do not internally use this property for anything, but it # is preserved for BC reasons ACCEPT = os.getenv('EXPECTTEST_ACCEPT') LINENO_AT_START = sys.version_info >= (3, 8) def nth_line(src: str, lineno: int) -> int: """ Compute the starting index of the n-th line (where n is 1-indexed) >>> nth_line("aaa\\nbb\\nc", 2) 4 """ assert lineno >= 1 pos = 0 for _ in range(lineno - 1): pos = src.find("\n", pos) + 1 return pos def nth_eol(src: str, lineno: int) -> int: """ Compute the ending index of the n-th line (before the newline, where n is 1-indexed) >>> nth_eol("aaa\\nbb\\nc", 2) 6 """ assert lineno >= 1 pos = -1 for _ in range(lineno): pos = src.find("\n", pos + 1) if pos == -1: return len(src) return pos def normalize_nl(t: str) -> str: return t.replace("\r\n", "\n").replace("\r", "\n") def escape_trailing_quote(s: str, quote: str) -> str: if s and s[-1] == quote: return s[:-1] + "\\" + quote else: return s class EditHistory: state: Dict[str, List[Tuple[int, int]]] seen: Dict[str, Dict[int, str]] def __init__(self) -> None: self.state = {} self.seen = {} def reload_file(self, fn: str) -> None: """ The idea is that if you reload a file, the line numbers from traceback are now up to date, but we do NOT want to clear out seen list as it will tell us if we are expecting the same line multiple times. Instead, we need to adjust the seen list for the new world order. """ new_seen = {} for seen_loc, seen_str in self.seen.get(fn, {}).items(): new_seen[self.adjust_lineno(fn, seen_loc)] = seen_str self.seen[fn] = new_seen self.state.pop(fn, None) def adjust_lineno(self, fn: str, lineno: int) -> int: if fn not in self.state: return lineno for edit_loc, edit_diff in self.state[fn]: if lineno > edit_loc: lineno += edit_diff return lineno def seen_file(self, fn: str) -> bool: return fn in self.state def seen_edit(self, fn: str, lineno: int) -> Optional[str]: return self.seen.get(fn, {}).get(lineno, None) def record_edit(self, fn: str, lineno: int, delta: int, expect: str) -> None: self.state.setdefault(fn, []).append((lineno, delta)) self.seen.setdefault(fn, {})[lineno] = expect EDIT_HISTORY = EditHistory() def ok_for_raw_triple_quoted_string(s: str, quote: str) -> bool: """ Is this string representable inside a raw triple-quoted string? Due to the fact that backslashes are always treated literally, some strings are not representable. >>> ok_for_raw_triple_quoted_string("blah", quote="'") True >>> ok_for_raw_triple_quoted_string("'", quote="'") False >>> ok_for_raw_triple_quoted_string("a ''' b", quote="'") False """ return quote * 3 not in s and (not s or s[-1] not in [quote, "\\"]) RE_EXPECT = re.compile( (r"(?Pr?)" r"(?P'''|" r'""")' r"(?P.*?)" r"(?P=quote)"), re.DOTALL ) def replace_string_literal( src: str, start_lineno: int, end_lineno: int, new_string: str ) -> Tuple[str, int]: r""" Replace a triple quoted string literal with new contents. Only handles printable ASCII correctly at the moment. This will preserve the quote style of the original string, and makes a best effort to preserve raw-ness (unless it is impossible to do so.) Returns a tuple of the replaced string, as well as a delta of number of lines added/removed. >>> replace_string_literal("'''arf'''", 1, 1, "barf") ("'''barf'''", 0) >>> r = replace_string_literal(" moo = '''arf'''", 1, 1, "'a'\n\\b\n") >>> print(r[0]) moo = '''\ 'a' \\b ''' >>> r[1] 3 >>> replace_string_literal(" moo = '''\\\narf'''", 1, 2, "'a'\n\\b\n")[1] 2 >>> print(replace_string_literal(" f('''\"\"\"''')", 1, 1, "a ''' b")[0]) f('''a \'\'\' b''') """ # Haven't implemented correct escaping for non-printable characters assert all(c in string.printable for c in new_string), repr(new_string) new_string = normalize_nl(new_string) delta = new_string.count("\n") if delta > 0: delta += 1 # handle the extra \\\n assert start_lineno <= end_lineno start = nth_line(src, start_lineno) end = nth_eol(src, end_lineno) assert start <= end def replace(m: Match[str]) -> str: nonlocal delta s = new_string raw = m.group("raw") == "r" if not raw or not ok_for_raw_triple_quoted_string(s, quote=m.group("quote")[0]): raw = False s = s.replace("\\", "\\\\") if m.group("quote") == "'''": s = escape_trailing_quote(s, "'").replace("'''", r"\'\'\'") else: s = escape_trailing_quote(s, '"').replace('"""', r"\"\"\"") new_body = "\\\n" + s if "\n" in s and not raw else s delta -= m.group("body").count("\n") return "".join( [ "r" if raw else "", m.group("quote"), new_body, m.group("quote"), ] ) return ( src[:start] + RE_EXPECT.sub(replace, src[start:end], count=1) + src[end:], delta, ) def replace_many(rep: Dict[str, str], text: str) -> str: rep = {re.escape(k): v for k, v in rep.items()} pattern = re.compile("|".join(rep.keys())) return pattern.sub(lambda m: rep[re.escape(m.group(0))], text) def assert_eq(expect: str, actual: str, *, msg: str) -> None: # TODO: improve this if actual != expect: diff = "".join( difflib.unified_diff( expect.splitlines(True), actual.splitlines(True), fromfile="expect.txt", tofile="actual.txt", ) ) raise AssertionError( f"Mismatch between actual and expect strings:\n\n{diff}\n\n{msg}" ) class Expect: """ An expected string literal, analogous to expect_test::expect! in Rust. This saves its position so that you can pass it around for e.g. parameterized tests and similar. Example: >>> e = Expect("value") # expected value >>> e.assert_expected("value") # actual value """ def __init__(self, expected: str, *, skip: int = 0): """ Creates an expectation of the given value. """ # n.b. this is not a dataclass because it seems like it would expose # us to being broken by dataclasses internals changes. self.expected = expected # current frame and parent frame, plus any requested skip tb = traceback.extract_stack(limit=2 + skip) fn, lineno, _, _ = tb[0] self.pos = PositionInfo(fn, lineno) def assert_expected(self, actual: str) -> None: assert_expected_inline(actual, self.expected, pos=self.pos) def __repr__(self) -> str: return f"Expect({self.expected!r})" def __str__(self) -> str: return self.expected @dataclasses.dataclass class PositionInfo: filename: str lineno: int def __str__(self) -> str: return f"{self.filename}:{self.lineno}" def _accept_output( pos: PositionInfo, actual: str, debug_suffix: str, ) -> None: """ Replaces the string literal at "pos" (according to the interpreter) with the new string "actual". """ print("Accepting new output{} at {}".format(debug_suffix, pos)) with open(pos.filename, "r+") as f: old = f.read() old_ast = ast.parse(old) # NB: it's only the traceback line numbers that are wrong; # we reread the file every time we write to it, so AST's # line numbers are correct lineno = EDIT_HISTORY.adjust_lineno(pos.filename, pos.lineno) # Conservative assumption to start start_lineno = lineno end_lineno = lineno # Try to give a more accurate bounds based on AST # NB: this walk is in no specified order (in practice it's # breadth first) for n in ast.walk(old_ast): if isinstance(n, ast.Expr): if hasattr(n, "end_lineno") and n.end_lineno: assert LINENO_AT_START if n.lineno == start_lineno: end_lineno = n.end_lineno # type: ignore[attr-defined] else: if n.lineno == end_lineno: start_lineno = n.lineno new, delta = replace_string_literal( old, start_lineno, end_lineno, actual ) assert old != new, ( f"Failed to substitute string at {pos}; did you use triple quotes? " "If this is unexpected, please file a bug report at " "https://github.com/ezyang/expecttest/issues/new " f"with the contents of the source file near {pos}" ) # Only write the backup file the first time we hit the # file if not EDIT_HISTORY.seen_file(pos.filename): with open(pos.filename + ".bak", "w") as f_bak: f_bak.write(old) f.seek(0) f.truncate(0) f.write(new) EDIT_HISTORY.record_edit(pos.filename, lineno, delta, actual) def assert_expected_inline( actual: str, expect: str, skip: int = 0, *, pos: Optional[PositionInfo] = None, expect_filters: Optional[Dict[str, str]] = None, assert_eq: Any = assert_eq, debug_id: str = "", ) -> None: """ Assert that actual is equal to expect. The expect argument MUST be a string literal (triple-quoted strings OK), and will get updated directly in source when you run the test suite with EXPECTTEST_ACCEPT=1. If you want to write a helper function that makes use of assertExpectedInline (e.g., expect is not a string literal), set the skip argument to how many function calls we should skip to find the string literal to update. """ if expect_filters is not None: actual = replace_many(expect_filters, actual) # NB: Intentionally do not use ACCEPT global variable; # reaccessing environment here allows for modification # of os.environ to be picked up if os.getenv("EXPECTTEST_ACCEPT"): if actual != expect: if not pos: # current frame and parent frame, plus any requested skip tb = traceback.extract_stack(limit=2 + skip) fn, lineno, _, _ = tb[0] pos = PositionInfo(fn, lineno) debug_suffix = "" if not debug_id else f" for {debug_id}" if (prev_actual := EDIT_HISTORY.seen_edit(pos.filename, pos.lineno)) is not None: assert_eq( actual, prev_actual, msg=f"Uh oh, accepting different values{debug_suffix} at {pos}. Are you running a parametrized test? If so, you need a separate assertExpectedInline invocation per distinct output.", ) print( "Skipping already accepted output{} at {}".format( debug_suffix, pos ) ) return _accept_output(pos=pos, actual=actual, debug_suffix=debug_suffix) else: help_text = ( "To accept the new output, re-run test with " "envvar EXPECTTEST_ACCEPT=1 (we recommend " "staging/committing your changes before doing this)" ) assert_eq(expect, actual, msg=help_text) class TestCase(unittest.TestCase): longMessage = True _expect_filters: Dict[str, str] def substituteExpected(self, pattern: str, replacement: str) -> None: if not hasattr(self, "_expect_filters"): self._expect_filters = {} def expect_filters_cleanup() -> None: del self._expect_filters self.addCleanup(expect_filters_cleanup) if pattern in self._expect_filters: raise RuntimeError( "Cannot remap {} to {} (existing mapping is {})".format( pattern, replacement, self._expect_filters[pattern] ) ) self._expect_filters[pattern] = replacement def assertExpectedInline(self, actual: str, expect: str, skip: int = 0) -> None: """ Assert that actual is equal to expect. The expect argument MUST be a string literal (triple-quoted strings OK), and will get updated directly in source when you run the test suite with EXPECTTEST_ACCEPT=1. If you want to write a helper function that makes use of assertExpectedInline (e.g., expect is not a string literal), set the skip argument to how many function calls we should skip to find the string literal to update. """ assert_expected_inline( actual, expect, skip=skip + 1, expect_filters=getattr(self, "_expect_filters", None), debug_id=self.id(), assert_eq=self.assertMultiLineEqualMaybeCppStack, ) def assertExpectedRaisesInline( self, exc_type: Any, callable: Callable[..., Any], expect: str, *args: Any, **kwargs: Any, ) -> None: """ Like assertExpectedInline, but tests the str() representation of the raised exception from callable. The raised exeption must be exc_type. """ try: callable(*args, **kwargs) except exc_type as e: self.assertExpectedInline(str(e), expect, skip=1) return # Don't put this in the try block; the AssertionError will catch it self.fail(msg="Did not raise when expected to") def assertMultiLineEqualMaybeCppStack( self, expect: str, actual: str, *args: Any, **kwargs: Any ) -> None: cpp_stack_header = "\nException raised from" if cpp_stack_header in actual: actual = actual.rsplit(cpp_stack_header, maxsplit=1)[0] if hasattr(self, "assertMultiLineEqual"): self.assertMultiLineEqual(expect, actual, *args, **kwargs) else: self.assertEqual(expect, actual, *args, **kwargs) if __name__ == "__main__": import doctest doctest.testmod() expecttest-0.3.0/expecttest/py.typed000066400000000000000000000000001472633007700175700ustar00rootroot00000000000000expecttest-0.3.0/poetry.lock000066400000000000000000000232601472633007700161120ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] [package.extras] dev = ["coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] tests-no-zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] [[package]] name = "flake8" version = "7.0.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "hypothesis" version = "6.14.0" description = "A library for property-based testing" optional = false python-versions = ">=3.6" files = [ {file = "hypothesis-6.14.0-py3-none-any.whl", hash = "sha256:27aa2af763af06b8b61ce65c09626cf1da6d3a6ff155900f3c581837b453313a"}, {file = "hypothesis-6.14.0.tar.gz", hash = "sha256:9bdee01ae260329b16117e9b0229a839b4a77747a985922653f595bd2a6a541a"}, ] [package.dependencies] attrs = ">=19.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "importlib-resources (>=3.3.0)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2020.4)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=2.2)", "pytz (>=2014.1)"] dpcontracts = ["dpcontracts (>=0.4)"] ghostwriter = ["black (>=19.10b0)"] lark = ["lark-parser (>=0.6.5)"] numpy = ["numpy (>=1.9.0)"] pandas = ["pandas (>=0.25)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] zoneinfo = ["backports.zoneinfo (>=0.2.1)", "importlib-resources (>=3.3.0)", "tzdata (>=2020.4)"] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] name = "mypy" version = "0.910" description = "Optional static typing for Python" optional = false python-versions = ">=3.5" files = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." optional = false python-versions = "*" files = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] [[package]] name = "pycodestyle" version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] [[package]] name = "pyflakes" version = "3.2.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" files = [ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[package]] name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] [[package]] name = "typing-extensions" version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" optional = false python-versions = "*" files = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] [metadata] lock-version = "2.0" python-versions = "^3.8.1" content-hash = "ce4c82db39c101879d16296f21bfd659dc84909afc6eabe8d1133f7dd3e80abe" expecttest-0.3.0/pyproject.toml000066400000000000000000000022571472633007700166350ustar00rootroot00000000000000[tool.poetry] name = "expecttest" version = "0.3.0" description = "" readme = "README.md" repository = "https://github.com/ezyang/expecttest" authors = ["Edward Z. Yang "] include = ["expecttest/py.typed"] license = "MIT" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] [tool.poetry.dependencies] python = "^3.8.1" [tool.poetry.dev-dependencies] flake8 = "^7" hypothesis = "^6" mypy = "^0.910" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.mypy] python_version = "3.8" strict_optional = true show_column_numbers = true show_error_codes = true warn_no_return = true disallow_any_unimported = true # --strict warn_unused_configs = true disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = false # purposely disabled warn_return_any = true no_implicit_reexport = true strict_equality = true expecttest-0.3.0/run_tests.sh000077500000000000000000000001171472633007700162770ustar00rootroot00000000000000#!/bin/sh set -ex flake8 mypy --exclude=smoketests . python test_expecttest.py expecttest-0.3.0/smoketests/000077500000000000000000000000001472633007700161145ustar00rootroot00000000000000expecttest-0.3.0/smoketests/accept_expected.py000066400000000000000000000002141472633007700216030ustar00rootroot00000000000000from expecttest import Expect exps = [ Expect("""ok"""), Expect("""changeme"""), ] for exp in exps: exp.assert_expected("ok") expecttest-0.3.0/smoketests/accept_twice.py000066400000000000000000000006421472633007700211220ustar00rootroot00000000000000import expecttest import unittest S1 = "a\nb" S2 = "c\nd" class Test(expecttest.TestCase): def bar(self): self.assertExpectedInline( S1, """\ w""", ) def test_a(self): self.bar() self.bar() def test_b(self): self.assertExpectedInline( S2, """\ x y z""", ) if __name__ == "__main__": unittest.main() expecttest-0.3.0/smoketests/accept_twice_clobber.py000066400000000000000000000002511472633007700226060ustar00rootroot00000000000000import expecttest from expecttest import assert_expected_inline S1 = "a\nb" S2 = "a\nb\nc" assert_expected_inline(S2 if hasattr(expecttest, "_TEST2") else S1, """""") expecttest-0.3.0/smoketests/accept_twice_reload.py000066400000000000000000000003231472633007700224440ustar00rootroot00000000000000import expecttest from expecttest import assert_expected_inline S1 = "a\nb" S2 = "a\nb\nc" if hasattr(expecttest, "_TEST1"): assert_expected_inline(S1, """""") else: assert_expected_inline(S2, """""") expecttest-0.3.0/smoketests/no_unittest.py000066400000000000000000000001371472633007700210420ustar00rootroot00000000000000from expecttest import assert_expected_inline S1 = "a\nb" assert_expected_inline(S1, """""") expecttest-0.3.0/test_expecttest.py000066400000000000000000000177401472633007700175250ustar00rootroot00000000000000import doctest import sys import string import shutil import subprocess import os import textwrap import traceback import unittest import tempfile import runpy from typing import Any, Dict, Tuple, Generator from contextlib import contextmanager from pathlib import Path import hypothesis from hypothesis.strategies import booleans, composite, integers, sampled_from, text import expecttest def sh(file: str, accept: bool = False) -> subprocess.CompletedProcess: # type: ignore[type-arg] env = "" if accept: env = "EXPECTTEST_ACCEPT=1 " print(f" $ {env}python {file}") r = subprocess.run( [sys.executable, file], capture_output=True, # NB: Always OVERRIDE EXPECTTEST_ACCEPT variable, so outer usage doesn't # get confused! env={**os.environ, "EXPECTTEST_ACCEPT": "1" if accept else ""}, text=True, ) if r.stderr: print(textwrap.indent(r.stderr, " ")) return r @composite def text_lineno(draw: Any) -> Tuple[str, int]: t = draw(text("a\n")) lineno = draw(integers(min_value=1, max_value=t.count("\n") + 1)) return (t, lineno) @contextmanager def smoketest(name: str) -> Generator[str, None, None]: with tempfile.TemporaryDirectory() as d: dst = Path(d) / "test.py" code_dir = Path(__file__).parent shutil.copy( code_dir / "smoketests" / name, dst, ) yield str(dst) class TestExpectTest(expecttest.TestCase): @hypothesis.given(text_lineno()) def test_nth_line_ref(self, t_lineno: Tuple[str, int]) -> None: t, lineno = t_lineno hypothesis.event("lineno = {}".format(lineno)) def nth_line_ref(src: str, lineno: int) -> int: xs = src.split("\n")[:lineno] xs[-1] = "" return len("\n".join(xs)) self.assertEqual(expecttest.nth_line(t, lineno), nth_line_ref(t, lineno)) @hypothesis.given(text(string.printable), booleans(), sampled_from(['"', "'"])) def test_replace_string_literal_roundtrip( self, t: str, raw: bool, quote: str ) -> None: if raw: hypothesis.assume( expecttest.ok_for_raw_triple_quoted_string(t, quote=quote) ) prog = """\ r = {r}{quote}placeholder{quote} r2 = {r}{quote}placeholder2{quote} r3 = {r}{quote}placeholder3{quote} """.format( r="r" if raw else "", quote=quote * 3 ) new_prog = expecttest.replace_string_literal(textwrap.dedent(prog), 2, 2, t)[0] ns: Dict[str, Any] = {} exec(new_prog, ns) msg = "program was:\n{}".format(new_prog) self.assertEqual(ns["r"], "placeholder", msg=msg) # noqa: F821 self.assertEqual(ns["r2"], expecttest.normalize_nl(t), msg=msg) # noqa: F821 self.assertEqual(ns["r3"], "placeholder3", msg=msg) # noqa: F821 def test_sample_lineno(self) -> None: prog = r""" single_single('''0''') single_multi('''1''') multi_single('''\ 2 ''') multi_multi_less('''\ 3 4 ''') multi_multi_same('''\ 5 ''') multi_multi_more('''\ 6 ''') different_indent( RuntimeError, '''7''' ) """ edits = [ (2, 2, "a"), (3, 3, "b\n"), (4, 6, "c"), (7, 10, "d\n"), (11, 13, "e\n"), (14, 16, "f\ng\n"), (17, 20, "h"), ] history = expecttest.EditHistory() fn = "not_a_real_file.py" for start_lineno, end_lineno, actual in edits: start_lineno = history.adjust_lineno(fn, start_lineno) end_lineno = history.adjust_lineno(fn, end_lineno) prog, delta = expecttest.replace_string_literal( prog, start_lineno, end_lineno, actual ) # NB: it doesn't really matter start/end you record edit at history.record_edit(fn, start_lineno, delta, actual) self.assertExpectedInline( prog, r""" single_single('''a''') single_multi('''\ b ''') multi_single('''c''') multi_multi_less('''\ d ''') multi_multi_same('''\ e ''') multi_multi_more('''\ f g ''') different_indent( RuntimeError, '''h''' ) """, ) def test_lineno_assumptions(self) -> None: def get_tb(s: str) -> traceback.StackSummary: return traceback.extract_stack(limit=2) tb1 = get_tb("") tb2 = get_tb( """a b c""" ) assert isinstance(tb1[0].lineno, int) if expecttest.LINENO_AT_START: # tb2's stack starts on the next line self.assertEqual(tb1[0].lineno + 1, tb2[0].lineno) else: # starts at the end here self.assertEqual(tb1[0].lineno + 1 + 2, tb2[0].lineno) def test_smoketest_accept_twice(self) -> None: with smoketest("accept_twice.py") as test_py: r = sh(test_py) self.assertNotEqual(r.returncode, 0) r = sh(test_py, accept=True) self.assertExpectedInline( r.stdout.replace(test_py, "test.py"), """\ Accepting new output for __main__.Test.test_a at test.py:10 Skipping already accepted output for __main__.Test.test_a at test.py:10 Accepting new output for __main__.Test.test_b at test.py:21 """, ) r = sh(test_py) self.assertEqual(r.returncode, 0) def test_smoketest_no_unittest(self) -> None: with smoketest("no_unittest.py") as test_py: r = sh(test_py) self.assertNotEqual(r.returncode, 0) r = sh(test_py, accept=True) self.assertExpectedInline( r.stdout.replace(str(test_py), "test.py"), """\ Accepting new output at test.py:5 """, ) r = sh(test_py) self.assertEqual(r.returncode, 0) def test_smoketest_accept_twice_reload(self) -> None: with smoketest("accept_twice_reload.py") as test_py: env = os.environ.copy() try: os.environ["EXPECTTEST_ACCEPT"] = "1" runpy.run_path(test_py) expecttest.EDIT_HISTORY.reload_file(test_py) try: expecttest._TEST1 = True # type: ignore[attr-defined] runpy.run_path(test_py) finally: delattr(expecttest, "_TEST1") finally: os.environ.clear() os.environ.update(env) # Should pass runpy.run_path(test_py) try: expecttest._TEST1 = True # type: ignore[attr-defined] runpy.run_path(test_py) finally: delattr(expecttest, "_TEST1") def test_smoketest_accept_twice_clobber(self) -> None: with smoketest("accept_twice_clobber.py") as test_py: env = os.environ.copy() try: os.environ["EXPECTTEST_ACCEPT"] = "1" runpy.run_path(test_py) expecttest.EDIT_HISTORY.reload_file(test_py) try: expecttest._TEST2 = True # type: ignore[attr-defined] self.assertRaises(AssertionError, lambda: runpy.run_path(test_py)) finally: delattr(expecttest, "_TEST2") finally: os.environ.clear() os.environ.update(env) def test_smoketest_expected_objects(self) -> None: with smoketest("accept_expected.py") as test_py: r = sh(test_py) self.assertNotEqual(r.returncode, 0) r = sh(test_py, accept=True) self.assertExpectedInline( r.stdout.replace(test_py, "test.py"), """\ Accepting new output at test.py:5 """, ) r = sh(test_py) self.assertEqual(r.returncode, 0) def load_tests( loader: unittest.TestLoader, tests: unittest.TestSuite, ignore: Any ) -> unittest.TestSuite: tests.addTests(doctest.DocTestSuite(expecttest)) return tests if __name__ == "__main__": unittest.main()