catalystneuro / neuroconv

Create NWB files by converting and combining neural data in proprietary formats and adding essential metadata.
https://neuroconv.readthedocs.io
BSD 3-Clause "New" or "Revised" License
50 stars 21 forks source link

Using ruff rule to enforce the existence of docstrings in public methods #1063

Open h-mayorquin opened 3 weeks ago

h-mayorquin commented 3 weeks ago

Should come after #1062

This was the most annoying one.

h-mayorquin commented 1 week ago

ne point of discussion is should we include Returns sections for standard methods like get_metadata, get_metadata_schema, etc.? Or should we just have them be 1-liners.

I am fine with adding it, but this is just a minimal effort to get the ruff passing and then we can improve piecewise and unify.

bendichter commented 5 days ago

When a method in a child class is overriding a parent class and does not have a docstring, it takes on the docstring of the parent. I think this works fine and in many cases makes it unnecessary to create a new docstring. However, it looks like this ruff inspector does not respect this and requires docstrings to be defined even on child classes where the parent has an appropriate docstring defined. Is that correct? If that's the case, I think we should be able to write a custom check that will work better for us.

bendichter commented 5 days ago

e.g.

import importlib
import inspect
import pkgutil

def check_package_docstrings(package_name):
    """
    Check if all public methods of public classes in public modules of a package have docstrings.

    Args:
    package_name (str): The name of the package to check.

    Returns:
    dict: A dictionary containing information about missing docstrings.
    """
    missing_docstrings = {}
    package = importlib.import_module(package_name)

    for _, module_name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
        try:
            module = importlib.import_module(module_name)

            for name, obj in inspect.getmembers(module):
                if inspect.isclass(obj) and not name.startswith('_'):
                    class_missing = check_class_docstrings(obj)
                    if class_missing:
                        missing_docstrings[f"{module_name}.{name}"] = class_missing

        except ImportError as e:
            print(f"Error importing module {module_name}: {e}")

    return missing_docstrings

def check_class_docstrings(cls):
    """
    Check if all public methods of a class have docstrings.

    Args:
    cls (type): The class to check.

    Returns:
    list: A list of method names without docstrings.
    """
    missing = []
    for name, method in inspect.getmembers(cls, inspect.isfunction):
        if not name.startswith('_') and not method.__doc__:
            missing.append(name)
    return missing

# Example usage
if __name__ == "__main__":
    package_name = "your_package_name"
    result = check_package_docstrings(package_name)

    if result:
        print("Missing docstrings found:")
        for class_name, methods in result.items():
            print(f"  {class_name}: {', '.join(methods)}")
    else:
        print("All public methods have docstrings.")
h-mayorquin commented 5 days ago

When a method in a child class is overriding a parent class and does not have a docstring, it takes on the docstring of the parent. I think this works fine and in many cases makes it unnecessary to create a new docstring. However, it looks like this ruff inspector does not respect this and requires docstrings to be defined even on child classes where the parent has an appropriate docstring defined. Is that correct? If that's the case, I think we should be able to write a custom check that will work better for us.

Yeah, that's a downside. The approach that I am taking here is just annotating it like this so it gets ignored:

image

This is good because it forces one to be explicit about it (you add it when you know the docstrings should be inherited) and is less complex than implementing something on our own. Although, @pauladkisson already implemented an action that I think accomplishes this if you prefer to avoid having to add #noqa annotations

codecov[bot] commented 5 days ago

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 90.58%. Comparing base (36464df) to head (716d400). Report is 16 commits behind head on main.

Additional details and impacted files [![Impacted file tree graph](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063/graphs/tree.svg?width=650&height=150&src=pr&token=h63OQoWehP&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro)](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro) ```diff @@ Coverage Diff @@ ## main #1063 +/- ## ========================================== + Coverage 90.44% 90.58% +0.13% ========================================== Files 129 129 Lines 8055 8164 +109 ========================================== + Hits 7285 7395 +110 + Misses 770 769 -1 ``` | [Flag](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro) | Coverage Δ | | |---|---|---| | [unittests](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro) | `90.58% <100.00%> (+0.13%)` | :arrow_up: | Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#carryforward-flags-in-the-pull-request-comment) to find out more. | [Files with missing lines](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro) | Coverage Δ | | |---|---|---| | [src/neuroconv/basedatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fbasedatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9iYXNlZGF0YWludGVyZmFjZS5weQ==) | `95.34% <ø> (+0.82%)` | :arrow_up: | | [src/neuroconv/baseextractorinterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fbaseextractorinterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9iYXNlZXh0cmFjdG9yaW50ZXJmYWNlLnB5) | `100.00% <100.00%> (ø)` | | | [...nv/datainterfaces/behavior/audio/audiointerface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Faudio%2Faudiointerface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9hdWRpby9hdWRpb2ludGVyZmFjZS5weQ==) | `87.95% <100.00%> (ø)` | | | [...ces/behavior/deeplabcut/deeplabcutdatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Fdeeplabcut%2Fdeeplabcutdatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9kZWVwbGFiY3V0L2RlZXBsYWJjdXRkYXRhaW50ZXJmYWNlLnB5) | `93.18% <100.00%> (ø)` | | | [...nterfaces/behavior/fictrac/fictracdatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Ffictrac%2Ffictracdatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9maWN0cmFjL2ZpY3RyYWNkYXRhaW50ZXJmYWNlLnB5) | `91.78% <100.00%> (ø)` | | | [...s/behavior/lightningpose/lightningposeconverter.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Flightningpose%2Flightningposeconverter.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9saWdodG5pbmdwb3NlL2xpZ2h0bmluZ3Bvc2Vjb252ZXJ0ZXIucHk=) | `96.82% <100.00%> (ø)` | | | [...havior/lightningpose/lightningposedatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Flightningpose%2Flightningposedatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9saWdodG5pbmdwb3NlL2xpZ2h0bmluZ3Bvc2VkYXRhaW50ZXJmYWNlLnB5) | `95.32% <100.00%> (ø)` | | | [...atainterfaces/behavior/medpc/medpcdatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Fmedpc%2Fmedpcdatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9tZWRwYy9tZWRwY2RhdGFpbnRlcmZhY2UucHk=) | `94.31% <100.00%> (ø)` | | | [...faces/behavior/miniscope/miniscopedatainterface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Fminiscope%2Fminiscopedatainterface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9taW5pc2NvcGUvbWluaXNjb3BlZGF0YWludGVyZmFjZS5weQ==) | `100.00% <100.00%> (ø)` | | | [...aces/behavior/neuralynx/neuralynx\_nvt\_interface.py](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree&filepath=src%2Fneuroconv%2Fdatainterfaces%2Fbehavior%2Fneuralynx%2Fneuralynx_nvt_interface.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro#diff-c3JjL25ldXJvY29udi9kYXRhaW50ZXJmYWNlcy9iZWhhdmlvci9uZXVyYWx5bngvbmV1cmFseW54X252dF9pbnRlcmZhY2UucHk=) | `98.36% <100.00%> (ø)` | | | ... and [50 more](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro) | | ... and [1 file with indirect coverage changes](https://app.codecov.io/gh/catalystneuro/neuroconv/pull/1063/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=catalystneuro)
bendichter commented 5 days ago

I tend to side more on not adding chores for devs. They tend to pile up in a way that creates a lot of busy work and makes it difficult to onboard people and is unwelcoming to new contributors. In this particular case I suppose I could go either way