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

Docstring checker #237

Merged
merged 21 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d6fcaa9
added TODO
pauladkisson Sep 7, 2023
df509ce
added Cody's comparison script
pauladkisson Sep 29, 2023
f7bfa10
revamped test_docstrings to use Cody's cleaner implementation + minor…
pauladkisson Sep 29, 2023
1afea15
removed docstring comparison script
pauladkisson Oct 19, 2023
00f5af6
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Sep 12, 2023
d878a6b
Made run-tests workflow dependent on update-testing-data workflow
pauladkisson Sep 28, 2023
ea9bb38
update awscli version
pauladkisson Sep 28, 2023
e7182d8
pin wheel version to avoid future dependency conflicts
pauladkisson Sep 28, 2023
c7fa284
updated awscli version in run-tests
pauladkisson Sep 28, 2023
ad0133d
removed dependent run of run-tests
pauladkisson Sep 28, 2023
fbdd782
commented out awscli commands only
pauladkisson Sep 28, 2023
f6d5e1d
refactored to trigger run-tests after update-testing-data
pauladkisson Sep 28, 2023
04ecacc
added success/failure logic
pauladkisson Sep 28, 2023
45f59a2
return 0 for failing case
pauladkisson Sep 28, 2023
8abb097
fixed indentation
pauladkisson Sep 28, 2023
6016b58
Fixed broken badge
pauladkisson Sep 28, 2023
2cdf51b
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Oct 9, 2023
b67dbd4
removed unnecessary imports
pauladkisson Oct 19, 2023
ed5e2b7
Merge branch 'main' into docstring_checker
pauladkisson Oct 19, 2023
a0029f3
Merge branch 'main' into docstring_checker
pauladkisson Oct 19, 2023
3d653df
Merge branch 'main' into docstring_checker
CodyCBakerPhD Oct 19, 2023
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
195 changes: 195 additions & 0 deletions tests/compare_docstring_testers.py
pauladkisson marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import inspect
import os
import importlib
from pathlib import Path
from types import ModuleType, FunctionType
from typing import List, Iterable
from pprint import pprint

import roiextractors


def traverse_class(cls, objs):
"""Traverse a class and its methods and append them to objs."""
predicate = lambda x: inspect.isfunction(x) or inspect.ismethod(x)
for name, obj in inspect.getmembers(cls, predicate=predicate):
objs.append(obj)


def traverse_module(module, objs):
"""Traverse all classes and functions in a module and append them to objs."""
objs.append(module)
predicate = lambda x: inspect.isclass(x) or inspect.isfunction(x) or inspect.ismethod(x)
for name, obj in inspect.getmembers(module, predicate=predicate):
parent_package = obj.__module__.split(".")[0]
if parent_package != "roiextractors": # avoid traversing external dependencies
continue
objs.append(obj)
if inspect.isclass(obj):
traverse_class(obj, objs)


def traverse_package(package, objs):
"""Traverse all modules and subpackages in a package to append all members to objs."""
for child in os.listdir(package.__path__[0]):
if child.startswith(".") or child == "__pycache__":
continue
elif child.endswith(".py"):
module_name = child[:-3]
module = importlib.import_module(f"{package.__name__}.{module_name}")
traverse_module(module, objs)
elif Path(child).is_dir(): # subpackage - I did change this one line b/c error otherwise when hit a .json
subpackage = importlib.import_module(f"{package.__name__}.{child}")
traverse_package(subpackage, objs)


def traverse_class_2(class_object: type, parent: str) -> List[FunctionType]:
"""Traverse the class dictionary and return the methods overridden by this module."""
class_functions = list()
for attribute_name, attribute_value in class_object.__dict__.items():
if isinstance(attribute_value, FunctionType) and attribute_value.__module__.startswith(parent):
class_functions.append(attribute_value)
return class_functions


def traverse_module_2(module: ModuleType, parent: str) -> Iterable[FunctionType]:
"""Traverse the module directory and return all submodules, classes, and functions defined along the way."""
local_modules_classes_and_functions = list()

for name in dir(module):
if name.startswith("__") and name.endswith("__"): # skip all magic methods
continue

object_ = getattr(module, name)

if isinstance(object_, ModuleType) and object_.__package__.startswith(parent):
submodule = object_

submodule_functions = traverse_module_2(module=submodule, parent=parent)

local_modules_classes_and_functions.append(submodule)
local_modules_classes_and_functions.extend(submodule_functions)
elif isinstance(object_, type) and object_.__module__.startswith(parent): # class
class_object = object_

class_functions = traverse_class_2(class_object=class_object, parent=parent)

local_modules_classes_and_functions.append(class_object)
local_modules_classes_and_functions.extend(class_functions)
elif isinstance(object_, FunctionType) and object_.__module__.startswith(parent):
function = object_

local_modules_classes_and_functions.append(function)

return local_modules_classes_and_functions


def traverse_class3(class_object: type, parent: str) -> List[FunctionType]:
"""Traverse the class dictionary and return the methods overridden by this module."""
class_functions = []
for attribute_name, attribute_value in class_object.__dict__.items():
if isinstance(attribute_value, FunctionType) and attribute_value.__module__.startswith(parent):
if attribute_name.startswith("__") and attribute_name.endswith("__"):
continue
class_functions.append(attribute_value)
return class_functions


def traverse_module3(module: ModuleType, parent: str) -> List:
local_classes_and_functions = []

for name in dir(module):
if name.startswith("__") and name.endswith("__"): # skip all magic methods
continue

object_ = getattr(module, name)

if isinstance(object_, type) and object_.__module__.startswith(parent): # class
class_object = object_
class_functions = traverse_class3(class_object=class_object, parent=parent)
local_classes_and_functions.append(class_object)
local_classes_and_functions.extend(class_functions)

elif isinstance(object_, FunctionType) and object_.__module__.startswith(parent):
function = object_
local_classes_and_functions.append(function)

return local_classes_and_functions


def traverse_package3(package: ModuleType, parent: str) -> List[ModuleType]:
"""Traverse the package and return all subpackages and modules defined along the way.

Parameters
----------
package : ModuleType
The package, subpackage, or module to traverse.
parent : str
The parent package name.

Returns
-------
local_packages_and_modules : List[ModuleType]
A list of all subpackages and modules defined in the given package.
"""
local_packages_and_modules = []

for name in dir(package):
if name.startswith("__") and name.endswith("__"): # skip all magic methods
continue

object_ = getattr(package, name)

if (
isinstance(object_, ModuleType)
and object_.__file__[-11:] == "__init__.py"
and object_.__package__.startswith(parent)
):
subpackage = object_
subpackage_members = traverse_package3(package=subpackage, parent=parent)
local_packages_and_modules.append(subpackage)
local_packages_and_modules.extend(subpackage_members)

elif isinstance(object_, ModuleType) and object_.__package__.startswith(parent):
module = object_
module_members = traverse_module3(module=module, parent=parent)
local_packages_and_modules.append(module)
local_packages_and_modules.extend(module_members)

return local_packages_and_modules


list_1 = list()
traverse_package(package=roiextractors, objs=list_1)

list_2 = traverse_module_2(module=roiextractors, parent="roiextractors")
list_3 = traverse_package3(package=roiextractors, parent="roiextractors")

# Analyze and compare - note that for set comparison, the lists must have been run in the same kernel
# to give all imports the same address in memory
unique_list_1 = set(list_1)
unique_list_2 = set(list_2)
unique_list_3 = set(list_3)

found_by_2_and_not_by_1 = unique_list_2 - unique_list_1
print("found by 2 and not by 1:")
pprint(found_by_2_and_not_by_1)

# Summary: A series of nested submodules under `checks` and `tools`; some various private functions scattered around
# not really clear why Paul's missed these

found_by_1_and_not_by_2 = unique_list_1 - unique_list_2
print("found by 1 and not by 2:")
pprint(found_by_1_and_not_by_2)

# Summary: All of these are bound methods of the Enum's (Importance/Severity) or JSONEncoder
# and are not methods that we actually override in the codebase (they strictly inherit)
# It did, however, find the outermost package __init__ (does that really need a docstring though?)

found_by_3_and_not_by_2 = unique_list_3 - unique_list_2
print("found by 3 and not by 2:")
pprint(found_by_3_and_not_by_2)

found_by_2_and_not_by_3 = unique_list_2 - unique_list_3
print("found by 2 and not by 3:")
pprint(found_by_2_and_not_by_3)
129 changes: 98 additions & 31 deletions tests/test_docstrings.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,115 @@
import inspect
import os
import importlib
import roiextractors
from types import ModuleType, FunctionType
from typing import List, Iterable
import pytest
import roiextractors


def traverse_class(class_object: type, parent: str) -> List[FunctionType]:
"""Traverse the class dictionary and return the methods overridden by this module.

Parameters
----------
class_object : type
The class to traverse.
parent : str
The parent package name.

Returns
-------
class_methods : List[FunctionType]
A list of all methods defined in the given class.
"""
class_methods = []
for attribute_name, attribute_value in class_object.__dict__.items():
if isinstance(attribute_value, FunctionType) and attribute_value.__module__.startswith(parent):
if attribute_name.startswith("__") and attribute_name.endswith("__"):
continue
class_methods.append(attribute_value)
return class_methods

def traverse_class(cls, objs):
"""Traverse a class and its methods and append them to objs."""
predicate = lambda x: inspect.isfunction(x) or inspect.ismethod(x)
for name, obj in inspect.getmembers(cls, predicate=predicate):
objs.append(obj)

def traverse_module(module: ModuleType, parent: str) -> List:
"""Traverse the module and return all classes and functions defined along the way.

def traverse_module(module, objs):
"""Traverse all classes and functions in a module and append them to objs."""
objs.append(module)
predicate = lambda x: inspect.isclass(x) or inspect.isfunction(x) or inspect.ismethod(x)
for name, obj in inspect.getmembers(module, predicate=predicate):
parent_package = obj.__module__.split(".")[0]
if parent_package != "roiextractors": # avoid traversing external dependencies
Parameters
----------
module : ModuleType
The module to traverse.
parent : str
The parent package name.

Returns
-------
local_classes_and_functions : List
A list of all classes and functions defined in the given module.
"""
local_classes_and_functions = []

for name in dir(module):
if name.startswith("__") and name.endswith("__"): # skip all magic methods
continue
objs.append(obj)
if inspect.isclass(obj):
traverse_class(obj, objs)

object_ = getattr(module, name)

if isinstance(object_, type) and object_.__module__.startswith(parent): # class
class_object = object_
class_functions = traverse_class(class_object=class_object, parent=parent)
local_classes_and_functions.append(class_object)
local_classes_and_functions.extend(class_functions)

elif isinstance(object_, FunctionType) and object_.__module__.startswith(parent):
function = object_
local_classes_and_functions.append(function)

def traverse_package(package, objs):
"""Traverse all modules and subpackages in a package to append all members to objs."""
for child in os.listdir(package.__path__[0]):
if child.startswith(".") or child == "__pycache__":
return local_classes_and_functions


def traverse_package(package: ModuleType, parent: str) -> List[ModuleType]:
"""Traverse the package and return all subpackages and modules defined along the way.

Parameters
----------
package : ModuleType
The package, subpackage, or module to traverse.
parent : str
The parent package name.

Returns
-------
local_packages_and_modules : List[ModuleType]
A list of all subpackages and modules defined in the given package.
"""
local_packages_and_modules = []

for name in dir(package):
if name.startswith("__") and name.endswith("__"): # skip all magic methods
continue
elif child.endswith(".py"):
module_name = child[:-3]
module = importlib.import_module(f"{package.__name__}.{module_name}")
traverse_module(module, objs)
else: # subpackage
subpackage = importlib.import_module(f"{package.__name__}.{child}")
traverse_package(subpackage, objs)

object_ = getattr(package, name)

if (
isinstance(object_, ModuleType)
and object_.__file__[-11:] == "__init__.py"
and object_.__package__.startswith(parent)
):
subpackage = object_
subpackage_members = traverse_package(package=subpackage, parent=parent)
local_packages_and_modules.append(subpackage)
local_packages_and_modules.extend(subpackage_members)

elif isinstance(object_, ModuleType) and object_.__package__.startswith(parent):
module = object_
module_members = traverse_module(module=module, parent=parent)
local_packages_and_modules.append(module)
local_packages_and_modules.extend(module_members)

return local_packages_and_modules


objs = []
traverse_package(roiextractors, objs)
objs = traverse_package(roiextractors, "roiextractors")


@pytest.mark.parametrize("obj", objs)
Expand All @@ -51,8 +120,6 @@ def test_has_docstring(obj):
msg = f"{obj.__name__} has no docstring."
else:
msg = f"{obj.__module__}.{obj.__qualname__} has no docstring."
if "__create_fn__" in msg:
return # skip dataclass functions created by __create_fn__
assert doc is not None, msg


Expand Down