From 97256e3091e78fefa4d3d89533a95adeee78fdb5 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi Date: Thu, 12 May 2022 07:51:31 -0400 Subject: [PATCH] feat: support snapshots in doc tests (#525) --- pyproject.toml | 5 ++- script/bootstrap | 2 +- src/syrupy/location.py | 41 ++++++++++++++++--- tests/syrupy/__snapshots__/test_doctest.ambr | 37 +++++++++++++++++ tests/syrupy/test_doctest.py | 42 ++++++++++++++++++++ tests/syrupy/test_doctest.txt | 14 +++++++ 6 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 tests/syrupy/__snapshots__/test_doctest.ambr create mode 100644 tests/syrupy/test_doctest.py create mode 100644 tests/syrupy/test_doctest.txt diff --git a/pyproject.toml b/pyproject.toml index f659c21c..0fa43715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,10 @@ dist, ''' [tool.pytest.ini_options] -addopts = '-p syrupy' +addopts = '-p syrupy --doctest-modules' +testpaths = [ + "tests", +] [tool.coverage.run] source = ['./src'] diff --git a/script/bootstrap b/script/bootstrap index 5356a691..2731f0b5 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -19,7 +19,7 @@ fi if [[ -z $CI ]]; then # FIXME: There must be a better way to install this per project rather than globally? curl -sSL https://install.python-poetry.org | python3 - --version 1.2.0a2 - poetry shell + . ./.venv/bin/activate fi if [[ -z $SKIP_DEPS ]]; then diff --git a/src/syrupy/location.py b/src/syrupy/location.py index 91b920f2..31207e75 100644 --- a/src/syrupy/location.py +++ b/src/syrupy/location.py @@ -23,6 +23,11 @@ class PyTestLocation: filepath: str = field(init=False) def __post_init__(self) -> None: + if self.is_doctest: + return self.__attrs_post_init_doc__() + self.__attrs_post_init_def__() + + def __attrs_post_init_def__(self) -> None: self.filepath = getattr(self._node, "fspath") # noqa: B009 obj = getattr(self._node, "obj") # noqa: B009 self.modulename = obj.__module__ @@ -30,16 +35,35 @@ def __post_init__(self) -> None: self.nodename = getattr(self._node, "name", None) self.testname = self.nodename or self.methodname + def __attrs_post_init_doc__(self) -> None: + doctest = getattr(self._node, "dtest") # noqa: B009 + self.filepath = doctest.filename + test_relfile, test_node = self.nodeid.split(PYTEST_NODE_SEP) + test_relpath = Path(test_relfile) + self.modulename = ".".join([*test_relpath.parent.parts, test_relpath.stem]) + self.nodename = test_node.replace(f"{self.modulename}.", "") + self.testname = self.nodename or self.methodname + @property def classname(self) -> Optional[str]: + if self.is_doctest: + return None + return ".".join(self.nodeid.split(PYTEST_NODE_SEP)[1:-1]) or None + + @property + def nodeid(self) -> str: """ Pytest node names contain file path and module members delimited by `::` - Example tests/grouping/test_file.py::TestClass::TestSubClass::test_method + + Examples: + - tests/grouping/test_file.py::TestClass::TestSubClass::test_method + - tests/grouping/test_file.py::DocTestClass.doc_test_method + - tests/grouping/test_file.py::doctestfile.txt + + :raises: `AttributeError` if node has no node id + :return: test node id """ - nodeid: Optional[str] = getattr(self._node, "nodeid", None) - if nodeid is None: - return None - return ".".join(nodeid.split(PYTEST_NODE_SEP)[1:-1]) or None + return str(getattr(self._node, "nodeid")) # noqa: B009 @property def filename(self) -> str: @@ -51,6 +75,13 @@ def snapshot_name(self) -> str: return f"{self.classname}.{self.testname}" return str(self.testname) + @property + def is_doctest(self) -> bool: + return self.__is_doctest(self._node) + + def __is_doctest(self, node: "pytest.Item") -> bool: + return hasattr(node, "dtest") + def __valid_id(self, name: str) -> str: """ Take characters from the name while the result would be a valid python diff --git a/tests/syrupy/__snapshots__/test_doctest.ambr b/tests/syrupy/__snapshots__/test_doctest.ambr new file mode 100644 index 00000000..4de8a0f5 --- /dev/null +++ b/tests/syrupy/__snapshots__/test_doctest.ambr @@ -0,0 +1,37 @@ +# name: DocTestClass + DocTestClass( + obj_attr='test class attr', + ) +# --- +# name: DocTestClass.1 + DocTestClass( + obj_attr='test class attr', + ) +# --- +# name: DocTestClass.NestedDocTestClass + NestedDocTestClass( + nested_obj_attr='nested doc test class attr', + ) +# --- +# name: DocTestClass.NestedDocTestClass.doctest_method + 'nested doc test method return value' +# --- +# name: DocTestClass.doctest_method + 'doc test method return value' +# --- +# name: doctest_fn + 'doc test fn return value' +# --- +# name: test_doctest.txt + 'There must be a break after every snapshot assertion' +# --- +# name: test_doctest.txt.1 + 'constant value' +# --- +# name: test_doctest.txt.2 + set({ + 1, + 2, + 3, + }) +# --- diff --git a/tests/syrupy/test_doctest.py b/tests/syrupy/test_doctest.py new file mode 100644 index 00000000..bb0ea6e9 --- /dev/null +++ b/tests/syrupy/test_doctest.py @@ -0,0 +1,42 @@ +def doctest_fn(): + """a doctest in a function docstring + >>> doctest_fn() == getfixture('snapshot') + True + """ + return "doc test fn return value" + + +class DocTestClass: + """ + >>> DocTestClass() == getfixture('snapshot') + True + + a doctest in a class docstring + >>> DocTestClass() == getfixture('snapshot') + True + """ + + obj_attr = "test class attr" + + def doctest_method(self): + """a doctest in a method docstring + >>> DocTestClass().doctest_method() == getfixture('snapshot') + True + """ + return "doc test method return value" + + class NestedDocTestClass: + """a doctest in a nested class docstring + >>> DocTestClass.NestedDocTestClass() == getfixture('snapshot') + True + """ + + nested_obj_attr = "nested doc test class attr" + + def doctest_method(self): + """a doctest in a nested method docstring + >>> nested_obj = DocTestClass.NestedDocTestClass() + >>> nested_obj.doctest_method() == getfixture('snapshot') + True + """ + return "nested doc test method return value" diff --git a/tests/syrupy/test_doctest.txt b/tests/syrupy/test_doctest.txt new file mode 100644 index 00000000..7fc859f0 --- /dev/null +++ b/tests/syrupy/test_doctest.txt @@ -0,0 +1,14 @@ +>>> "There must be a break after every snapshot assertion" == getfixture('snapshot') +True + +doctest x + +doctest y +>>> y = "constant value" +>>> y == getfixture('snapshot') +True + +doctest z +>>> z = {1, 2, 3} +>>> z == getfixture('snapshot') +True