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

feat: support snapshots in doc tests #525

Merged
merged 1 commit into from
May 12, 2022
Merged
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
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ dist,
'''

[tool.pytest.ini_options]
addopts = '-p syrupy'
addopts = '-p syrupy --doctest-modules'
testpaths = [
"tests",
]

[tool.coverage.run]
source = ['./src']
Expand Down
2 changes: 1 addition & 1 deletion script/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 36 additions & 5 deletions src/syrupy/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,47 @@ 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__
self.methodname = obj.__name__
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:
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/syrupy/__snapshots__/test_doctest.ambr
Original file line number Diff line number Diff line change
@@ -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,
})
# ---
42 changes: 42 additions & 0 deletions tests/syrupy/test_doctest.py
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 14 additions & 0 deletions tests/syrupy/test_doctest.txt
Original file line number Diff line number Diff line change
@@ -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