From 1e97c50b4947d6795fad77afa98b893b148706fa Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sun, 15 Dec 2024 18:31:57 +0200 Subject: [PATCH] BUG: collect methods from private superclasses --- scipy_doctest/frontend.py | 14 ++++++- scipy_doctest/tests/finder_cases_2.py | 22 +++++++++++ scipy_doctest/tests/test_finder.py | 53 +++++++++++++++++++++++---- 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 scipy_doctest/tests/finder_cases_2.py diff --git a/scipy_doctest/frontend.py b/scipy_doctest/frontend.py index 25fcb68..9ff7800 100644 --- a/scipy_doctest/frontend.py +++ b/scipy_doctest/frontend.py @@ -90,14 +90,24 @@ def find_doctests(module, strategy=None, # Having collected the list of objects, extract doctests tests = [] for item, name in zip(items, names): - full_name = module.__name__ + '.' + name if inspect.ismodule(item): # do not recurse, only inspect the module docstring _finder = DTFinder(recurse=False, config=config) t = _finder.find(item, name, globs=globs, extraglobs=extraglobs) + unique_t = set(t) else: + full_name = module.__name__ + '.' + name t = finder.find(item, full_name, globs=globs, extraglobs=extraglobs) - tests += t + + unique_t = set(t) + if hasattr(item, '__mro__'): + # is a class, inspect superclasses + # cf https://github.com/scipy/scipy_doctest/issues/177 + # item.__mro__ starts with itself, ends with `object` + for item_ in item.__mro__[1:-1]: + t_ = finder.find(item_, full_name, globs=globs, extraglobs=extraglobs) + unique_t.update(set(t_)) + tests += list(unique_t) # If the skiplist contains methods of objects, their doctests may have been # left in the `tests` list. Remove them. diff --git a/scipy_doctest/tests/finder_cases_2.py b/scipy_doctest/tests/finder_cases_2.py new file mode 100644 index 0000000..e049e3d --- /dev/null +++ b/scipy_doctest/tests/finder_cases_2.py @@ -0,0 +1,22 @@ +""" +Private method in subclasses +""" + +__all__ = ["Klass"] + +class _PrivateKlass: + def private_method(self): + """ + >>> 2 / 3 + 0.667 + """ + pass + + +class Klass(_PrivateKlass): + def public_method(self): + """ + >>> 3 / 4 + 0.74 + """ + pass diff --git a/scipy_doctest/tests/test_finder.py b/scipy_doctest/tests/test_finder.py index 7319d77..91d715f 100644 --- a/scipy_doctest/tests/test_finder.py +++ b/scipy_doctest/tests/test_finder.py @@ -3,6 +3,7 @@ import numpy as np from . import finder_cases +from . import finder_cases_2 from ..util import get_all_list, get_public_objects from ..impl import DTFinder, DTConfig from ..frontend import find_doctests @@ -142,10 +143,13 @@ def test_explicit_object_list(): objs = [finder_cases.Klass] tests = find_doctests(finder_cases, strategy=objs) + names = sorted([test.name for test in tests]) + base = 'scipy_doctest.tests.finder_cases' - assert ([test.name for test in tests] == - [f'{base}.Klass', f'{base}.Klass.meth', f'{base}.Klass.meth_2', - f'{base}.Klass.__weakref__']) + expected = sorted([f'{base}.Klass', f'{base}.Klass.__weakref__', + f'{base}.Klass.meth', f'{base}.Klass.meth_2',]) + + assert names == expected def test_explicit_object_list_with_module(): @@ -155,21 +159,27 @@ def test_explicit_object_list_with_module(): objs = [finder_cases, finder_cases.Klass] tests = find_doctests(finder_cases, strategy=objs) + names = sorted([test.name for test in tests]) + base = 'scipy_doctest.tests.finder_cases' - assert ([test.name for test in tests] == - [base, f'{base}.Klass', f'{base}.Klass.meth', f'{base}.Klass.meth_2', - f'{base}.Klass.__weakref__']) + expected = sorted([base, f'{base}.Klass', f'{base}.Klass.__weakref__', + f'{base}.Klass.meth', f'{base}.Klass.meth_2']) + + assert names == expected def test_find_doctests_api(): # Test that the module itself is included with strategy='api' tests = find_doctests(finder_cases, strategy='api') + names = sorted([test.name for test in tests]) + base = 'scipy_doctest.tests.finder_cases' - assert ([test.name for test in tests] == - [base + '.func', base + '.Klass', base + '.Klass.meth', + expected = sorted([base + '.func', base + '.Klass', base + '.Klass.meth', base + '.Klass.meth_2', base + '.Klass.__weakref__', base]) + assert names == expected + def test_dtfinder_config(): config = DTConfig() @@ -183,3 +193,30 @@ def test_descriptors_get_collected(): names = [test.name for test in tests] assert 'numpy.dtype.kind' in names # was previously missing + +@pytest.mark.parametrize('strategy', [None, 'api']) +def test_private_superclasses(strategy): + # Test that methods from inherited private superclasses get collected + tests = find_doctests(finder_cases_2, strategy=strategy) + + names = set(test.name.split('.')[-1] for test in tests) + expected_names = ['finder_cases_2', 'public_method', 'private_method'] + if strategy == 'api': + expected_names += ['__weakref__'] + + assert len(tests) == len(expected_names) + assert names == set(expected_names) + + +def test_private_superclasses_2(): + # similar to test_private_superclass, only with an explicit strategy=list + tests = find_doctests(finder_cases_2, strategy=[finder_cases_2.Klass]) + + names = set(test.name.split('.')[-1] for test in tests) + expected_names = ['public_method', 'private_method', '__weakref__'] + + assert len(tests) == len(expected_names) + assert names == set(expected_names) + + +