diff --git a/src/neuroconv/datainterfaces/ophys/miniscope/miniscopeimagingdatainterface.py b/src/neuroconv/datainterfaces/ophys/miniscope/miniscopeimagingdatainterface.py index 9c524078b..04f563dd9 100644 --- a/src/neuroconv/datainterfaces/ophys/miniscope/miniscopeimagingdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/miniscope/miniscopeimagingdatainterface.py @@ -433,6 +433,7 @@ def add_to_nwbfile( self, nwbfile: NWBFile, metadata: dict | None = None, + *args, # TODO: change to * (keyword only) on or after August 2026 photon_series_type: Literal["TwoPhotonSeries", "OnePhotonSeries"] = "OnePhotonSeries", **kwargs, ): @@ -441,6 +442,33 @@ def add_to_nwbfile( This method adds the Miniscope device and then delegates to the parent class. """ + # Handle deprecated positional arguments + if args: + import warnings + + parameter_names = [ + "photon_series_type", + ] + num_positional_args_before_args = 2 # nwbfile, metadata + if len(args) > len(parameter_names): + raise TypeError( + f"add_to_nwbfile() takes at most {len(parameter_names) + num_positional_args_before_args} positional arguments but " + f"{len(args) + num_positional_args_before_args} were given. " + "Note: Positional arguments are deprecated and will be removed on or after August 2026. " + "Please use keyword arguments." + ) + positional_values = dict(zip(parameter_names, args)) + passed_as_positional = list(positional_values.keys()) + warnings.warn( + "Passing arguments positionally to MiniscopeImagingInterface.add_to_nwbfile() is deprecated " + "and will be removed on or after August 2026. " + f"The following arguments were passed positionally: {passed_as_positional}. " + "Please use keyword arguments instead.", + FutureWarning, + stacklevel=2, + ) + photon_series_type = positional_values.get("photon_series_type", photon_series_type) + from ndx_miniscope.utils import add_miniscope_device # Add Miniscope device - required for proper ndx_miniscope.Miniscope device type diff --git a/tests/test_minimal/test_keyword_only_arguments.py b/tests/test_minimal/test_keyword_only_arguments.py new file mode 100644 index 000000000..47531478d --- /dev/null +++ b/tests/test_minimal/test_keyword_only_arguments.py @@ -0,0 +1,41 @@ +"""Tests to enforce keyword-only argument conventions for add_to_nwbfile methods. + +These tests ensure that all interface add_to_nwbfile methods enforce keyword-only arguments +after nwbfile and metadata. Only nwbfile and metadata should be positional. + +During the deprecation period (before August 2026), methods use *args with FutureWarning. +After the deprecation period, methods should use bare * for keyword-only enforcement. + +See the developer style guide for details on the convention. +""" + +import inspect + +import pytest + +from neuroconv.datainterfaces import interface_list + + +@pytest.mark.parametrize( + "interface_class", + interface_list, + ids=lambda cls: cls.__name__, +) +def test_add_to_nwbfile_only_nwbfile_metadata_positional(interface_class): + """Only nwbfile and metadata should be positional in add_to_nwbfile.""" + if "add_to_nwbfile" not in interface_class.__dict__: + pytest.skip(f"{interface_class.__name__} does not override add_to_nwbfile") + + add_to_nwbfile_method = getattr(interface_class, "add_to_nwbfile") + sig = inspect.signature(add_to_nwbfile_method) + + # No add_to_nwbfile uses POSITIONAL_ONLY (/), so POSITIONAL_OR_KEYWORD means "can be passed positionally" + can_be_passed_positionally = lambda param: param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + positional_params = {name for name, param in sig.parameters.items() if can_be_passed_positionally(param)} + + allowed_positional_params = {"self", "nwbfile", "metadata"} + assert positional_params == allowed_positional_params, ( + f"{interface_class.__name__}.add_to_nwbfile() positional parameters are {positional_params}, " + f"expected {allowed_positional_params}. " + f"All other parameters should be keyword-only (use * or *args)." + )