diff --git a/.github/workflows/call_check_docstrings.yaml b/.github/workflows/call_check_docstrings.yaml new file mode 100644 index 0000000..e18392f --- /dev/null +++ b/.github/workflows/call_check_docstrings.yaml @@ -0,0 +1,11 @@ +name: Call Check Docstrings +on: + workflow_dispatch: + +jobs: + check-docstrings: + uses: catalystneuro/.github/.github/workflows/check_docstrings.yaml@docstring_tests + with: + python-version: '3.10' + repository: 'catalystneuro/roiextractors' + package-name: 'roiextractors' \ No newline at end of file diff --git a/.github/workflows/check_docstrings.yaml b/.github/workflows/check_docstrings.yaml new file mode 100644 index 0000000..e15ca74 --- /dev/null +++ b/.github/workflows/check_docstrings.yaml @@ -0,0 +1,53 @@ +name: Check Docstrings +on: + workflow_call: + inputs: + python-version: + description: 'The version of Python to use for the workflow.' + default: '3.10' + required: false + type: string + repository: + description: 'The repository to check the docstrings for.' + required: True + type: string + package-name: + description: 'The name of the package to check the docstrings for.' + required: True + type: string + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Setup Conda + uses: s-weigand/setup-conda@v1 + + - name: Setup Python ${{ inputs.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ inputs.python-version }} + + - name: Global Setup + run: | + pip install -U pip + pip install pytest-xdist + git config --global user.email "CI@example.com" + git config --global user.name "CI Almighty" + pip install wheel==0.41.2 # needed for scanimage + + - name: Checkout Repository to be tested + uses: actions/checkout@v2 + with: + repository: ${{ inputs.repository }} + + - name: Install package + run: pip install . + + - name: Checkout Home Repository + uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + + - name: Run docstring check + run: pytest tests/test_docstrings.py --package=${{ inputs.package-name }} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f1f4570 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import pytest +from .traverse import traverse_package +from importlib import import_module + +def pytest_addoption(parser): + parser.addoption("--package", action="store") + +def pytest_generate_tests(metafunc): + package_name = metafunc.config.getoption("--package") + package = import_module(package_name) + objs = traverse_package(package, package_name) + if "obj" in metafunc.fixturenames: + metafunc.parametrize("obj", objs) \ No newline at end of file diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py new file mode 100644 index 0000000..6c47424 --- /dev/null +++ b/tests/test_docstrings.py @@ -0,0 +1,11 @@ +import inspect +import pytest + +def test_has_docstring(obj): + """Check if an object has a docstring.""" + doc = inspect.getdoc(obj) + if inspect.ismodule(obj): + msg = f"{obj.__name__} has no docstring." + else: + msg = f"{obj.__module__}.{obj.__qualname__} has no docstring." + assert doc is not None, msg diff --git a/tests/traverse.py b/tests/traverse.py new file mode 100644 index 0000000..792894a --- /dev/null +++ b/tests/traverse.py @@ -0,0 +1,103 @@ +from types import ModuleType, FunctionType +from typing import List + +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_module(module: ModuleType, parent: str) -> List: + """Traverse the module and return all classes and functions defined along the way. + + 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 + + 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) + + 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 + + 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 \ No newline at end of file