From 71e3cccda997837ca6820d4edb90060eafe670a8 Mon Sep 17 00:00:00 2001 From: Ross Markello Date: Fri, 6 Dec 2019 10:03:17 -0500 Subject: [PATCH 1/3] [REF] Removes keywordonly methods + functions Uses built-in Python kw-only syntax (*) instead. --- nibabel/analyze.py | 4 +-- nibabel/arrayproxy.py | 4 +-- nibabel/brikhead.py | 7 ++--- nibabel/cifti2/cifti2.py | 4 +-- nibabel/dataobj_images.py | 7 ++--- nibabel/ecat.py | 4 +-- nibabel/freesurfer/mghformat.py | 4 +-- nibabel/keywordonly.py | 28 -------------------- nibabel/minc1.py | 4 +-- nibabel/minc2.py | 4 +-- nibabel/parrec.py | 10 +++---- nibabel/spm99analyze.py | 4 +-- nibabel/tests/test_keywordonly.py | 43 ------------------------------- 13 files changed, 15 insertions(+), 112 deletions(-) delete mode 100644 nibabel/keywordonly.py delete mode 100644 nibabel/tests/test_keywordonly.py diff --git a/nibabel/analyze.py b/nibabel/analyze.py index dc352505c6..a5ff79d83b 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -95,7 +95,6 @@ from .fileholders import copy_file_map from .batteryrunners import Report from .arrayproxy import ArrayProxy -from .keywordonly import kw_only_meth # Sub-parts of standard analyze header from # Mayo dbh.h file @@ -933,8 +932,7 @@ def set_data_dtype(self, dtype): self._header.set_data_dtype(dtype) @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): ''' Class method to create image from mapping in ``file_map`` .. deprecated:: 2.4.1 diff --git a/nibabel/arrayproxy.py b/nibabel/arrayproxy.py index b8313e8ae4..5852191968 100644 --- a/nibabel/arrayproxy.py +++ b/nibabel/arrayproxy.py @@ -34,7 +34,6 @@ from .deprecated import deprecate_with_version from .volumeutils import array_from_file, apply_read_scaling from .fileslice import fileslice, canonical_slicers -from .keywordonly import kw_only_meth from . import openers @@ -96,8 +95,7 @@ class ArrayProxy(object): order = 'F' _header = None - @kw_only_meth(2) - def __init__(self, file_like, spec, mmap=True, keep_file_open=None): + def __init__(self, file_like, spec, *, mmap=True, keep_file_open=None): """Initialize array proxy instance .. deprecated:: 2.4.1 diff --git a/nibabel/brikhead.py b/nibabel/brikhead.py index 7ef1386872..7e34c36b36 100644 --- a/nibabel/brikhead.py +++ b/nibabel/brikhead.py @@ -36,7 +36,6 @@ from .arrayproxy import ArrayProxy from .fileslice import strided_scalar -from .keywordonly import kw_only_meth from .spatialimages import ( SpatialImage, SpatialHeader, @@ -220,8 +219,7 @@ class AFNIArrayProxy(ArrayProxy): None """ - @kw_only_meth(2) - def __init__(self, file_like, header, mmap=True, keep_file_open=None): + def __init__(self, file_like, header, *, mmap=True, keep_file_open=None): """ Initialize AFNI array proxy @@ -504,8 +502,7 @@ class AFNIImage(SpatialImage): ImageArrayProxy = AFNIArrayProxy @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): """ Creates an AFNIImage instance from `file_map` diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 1a5307eba5..a19a04a02f 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -24,7 +24,6 @@ from ..dataobj_images import DataobjImage from ..nifti2 import Nifti2Image, Nifti2Header from ..arrayproxy import reshape_dataobj -from ..keywordonly import kw_only_meth from warnings import warn @@ -1389,8 +1388,7 @@ def nifti_header(self): return self._nifti_header @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): """ Load a CIFTI-2 image from a file_map Parameters diff --git a/nibabel/dataobj_images.py b/nibabel/dataobj_images.py index dd4c853537..bc8ef07f03 100644 --- a/nibabel/dataobj_images.py +++ b/nibabel/dataobj_images.py @@ -12,7 +12,6 @@ from .arrayproxy import is_proxy from .filebasedimages import FileBasedImage -from .keywordonly import kw_only_meth from .deprecated import deprecate_with_version @@ -420,8 +419,7 @@ def get_shape(self): return self.shape @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): ''' Class method to create image from mapping in ``file_map`` .. deprecated:: 2.4.1 @@ -458,8 +456,7 @@ def from_file_map(klass, file_map, mmap=True, keep_file_open=None): raise NotImplementedError @classmethod - @kw_only_meth(1) - def from_filename(klass, filename, mmap=True, keep_file_open=None): + def from_filename(klass, filename, *, mmap=True, keep_file_open=None): '''Class method to create image from filename `filename` .. deprecated:: 2.4.1 diff --git a/nibabel/ecat.py b/nibabel/ecat.py index a0923f0753..4a2b55551f 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -54,7 +54,6 @@ from .arraywriters import make_array_writer from .wrapstruct import WrapStruct from .fileslice import canonical_slicers, predict_shape, slice2outax -from .keywordonly import kw_only_meth from .deprecated import deprecate_with_version BLOCK_SIZE = 512 @@ -901,8 +900,7 @@ def _get_fileholders(file_map): return file_map['header'], file_map['image'] @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): """class method to create image from mapping specified in file_map """ diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 6eb0f156e9..8324c112e0 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -21,7 +21,6 @@ from ..spatialimages import HeaderDataError, SpatialImage from ..fileholders import FileHolder from ..arrayproxy import ArrayProxy, reshape_dataobj -from ..keywordonly import kw_only_meth from ..openers import ImageOpener from ..batteryrunners import BatteryRunner, Report from ..wrapstruct import LabeledWrapStruct @@ -537,8 +536,7 @@ def filespec_to_file_map(klass, filespec): return super(MGHImage, klass).filespec_to_file_map(filespec) @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): ''' Class method to create image from mapping in ``file_map`` .. deprecated:: 2.4.1 diff --git a/nibabel/keywordonly.py b/nibabel/keywordonly.py deleted file mode 100644 index 8cb4908c1e..0000000000 --- a/nibabel/keywordonly.py +++ /dev/null @@ -1,28 +0,0 @@ -""" Decorator for labeling keyword arguments as keyword only -""" - -from functools import wraps - - -def kw_only_func(n): - """ Return function decorator enforcing maximum of `n` positional arguments - """ - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - if len(args) > n: - raise TypeError( - '{0} takes at most {1} positional argument{2}'.format( - func.__name__, n, 's' if n > 1 else '')) - return func(*args, **kwargs) - return wrapper - return decorator - - -def kw_only_meth(n): - """ Return method decorator enforcing maximum of `n` positional arguments - - The method has at least one positional argument ``self`` or ``cls``; allow - for that. - """ - return kw_only_func(n + 1) diff --git a/nibabel/minc1.py b/nibabel/minc1.py index c41af5c454..a4c1f3c832 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -17,7 +17,6 @@ from .spatialimages import SpatialHeader, SpatialImage from .fileslice import canonical_slicers -from .keywordonly import kw_only_meth from .deprecated import deprecate_with_version _dt_dict = { @@ -340,8 +339,7 @@ class Minc1Image(SpatialImage): ImageArrayProxy = MincImageArrayProxy @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): # Note that mmap and keep_file_open are included for proper with file_map['image'].get_prepare_fileobj() as fobj: minc_file = Minc1File(netcdf_file(fobj)) diff --git a/nibabel/minc2.py b/nibabel/minc2.py index b27d43f77f..a73114081c 100644 --- a/nibabel/minc2.py +++ b/nibabel/minc2.py @@ -27,7 +27,6 @@ """ import numpy as np -from .keywordonly import kw_only_meth from ._h5py_compat import h5py from .minc1 import Minc1File, MincHeader, Minc1Image, MincError @@ -158,8 +157,7 @@ class Minc2Image(Minc1Image): header_class = Minc2Header @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): holder = file_map['image'] if holder.filename is None: raise MincError('MINC2 needs filename for load') diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 59eac79809..98ae00587f 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -130,7 +130,6 @@ from locale import getpreferredencoding from collections import OrderedDict -from .keywordonly import kw_only_meth from .spatialimages import SpatialHeader, SpatialImage from .eulerangles import euler2mat from .volumeutils import Recoder, array_from_file @@ -584,8 +583,7 @@ def exts2pars(exts_source): class PARRECArrayProxy(object): - @kw_only_meth(2) - def __init__(self, file_like, header, mmap=True, scaling='dv'): + def __init__(self, file_like, header, *, mmap=True, scaling='dv'): """ Initialize PARREC array proxy Parameters @@ -1277,8 +1275,7 @@ class PARRECImage(SpatialImage): ImageArrayProxy = PARRECArrayProxy @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, permit_truncated=False, + def from_file_map(klass, file_map, *, mmap=True, permit_truncated=False, scaling='dv', strict_sort=False): """ Create PARREC image from file map `file_map` @@ -1318,8 +1315,7 @@ def from_file_map(klass, file_map, mmap=True, permit_truncated=False, file_map=file_map) @classmethod - @kw_only_meth(1) - def from_filename(klass, filename, mmap=True, permit_truncated=False, + def from_filename(klass, filename, *, mmap=True, permit_truncated=False, scaling='dv', strict_sort=False): """ Create PARREC image from filename `filename` diff --git a/nibabel/spm99analyze.py b/nibabel/spm99analyze.py index d420bb0c2e..09b857ba32 100644 --- a/nibabel/spm99analyze.py +++ b/nibabel/spm99analyze.py @@ -16,7 +16,6 @@ from .batteryrunners import Report from . import analyze # module import -from .keywordonly import kw_only_meth from .optpkg import optional_package have_scipy = optional_package('scipy')[1] @@ -244,8 +243,7 @@ class Spm99AnalyzeImage(analyze.AnalyzeImage): rw = have_scipy @classmethod - @kw_only_meth(1) - def from_file_map(klass, file_map, mmap=True, keep_file_open=None): + def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): ''' Class method to create image from mapping in ``file_map`` .. deprecated:: 2.4.1 diff --git a/nibabel/tests/test_keywordonly.py b/nibabel/tests/test_keywordonly.py deleted file mode 100644 index 0ef63d9b13..0000000000 --- a/nibabel/tests/test_keywordonly.py +++ /dev/null @@ -1,43 +0,0 @@ -""" Test kw_only decorators """ - -from ..keywordonly import kw_only_func, kw_only_meth - -from nose.tools import assert_equal -from nose.tools import assert_raises - - -def test_kw_only_func(): - # Test decorator - def func(an_arg): - "My docstring" - return an_arg - assert_equal(func(1), 1) - assert_raises(TypeError, func, 1, 2) - dec_func = kw_only_func(1)(func) - assert_equal(dec_func(1), 1) - assert_raises(TypeError, dec_func, 1, 2) - assert_raises(TypeError, dec_func, 1, akeyarg=3) - assert_equal(dec_func.__doc__, 'My docstring') - - @kw_only_func(1) - def kw_func(an_arg, a_kwarg='thing'): - "Another docstring" - return an_arg, a_kwarg - assert_equal(kw_func(1), (1, 'thing')) - assert_raises(TypeError, kw_func, 1, 2) - assert_equal(kw_func(1, a_kwarg=2), (1, 2)) - assert_raises(TypeError, kw_func, 1, akeyarg=3) - assert_equal(kw_func.__doc__, 'Another docstring') - - class C(object): - - @kw_only_meth(1) - def kw_meth(self, an_arg, a_kwarg='thing'): - "Method docstring" - return an_arg, a_kwarg - c = C() - assert_equal(c.kw_meth(1), (1, 'thing')) - assert_raises(TypeError, c.kw_meth, 1, 2) - assert_equal(c.kw_meth(1, a_kwarg=2), (1, 2)) - assert_raises(TypeError, c.kw_meth, 1, akeyarg=3) - assert_equal(c.kw_meth.__doc__, 'Method docstring') From 540e85e8d84df540436ed6854b4064b31e0e1786 Mon Sep 17 00:00:00 2001 From: Ross Markello Date: Fri, 6 Dec 2019 11:44:36 -0500 Subject: [PATCH 2/3] REF: Undelete keywordonly.py module --- nibabel/keywordonly.py | 28 ++++++++++++++++++++ nibabel/tests/test_keywordonly.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 nibabel/keywordonly.py create mode 100644 nibabel/tests/test_keywordonly.py diff --git a/nibabel/keywordonly.py b/nibabel/keywordonly.py new file mode 100644 index 0000000000..8cb4908c1e --- /dev/null +++ b/nibabel/keywordonly.py @@ -0,0 +1,28 @@ +""" Decorator for labeling keyword arguments as keyword only +""" + +from functools import wraps + + +def kw_only_func(n): + """ Return function decorator enforcing maximum of `n` positional arguments + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if len(args) > n: + raise TypeError( + '{0} takes at most {1} positional argument{2}'.format( + func.__name__, n, 's' if n > 1 else '')) + return func(*args, **kwargs) + return wrapper + return decorator + + +def kw_only_meth(n): + """ Return method decorator enforcing maximum of `n` positional arguments + + The method has at least one positional argument ``self`` or ``cls``; allow + for that. + """ + return kw_only_func(n + 1) diff --git a/nibabel/tests/test_keywordonly.py b/nibabel/tests/test_keywordonly.py new file mode 100644 index 0000000000..0ef63d9b13 --- /dev/null +++ b/nibabel/tests/test_keywordonly.py @@ -0,0 +1,43 @@ +""" Test kw_only decorators """ + +from ..keywordonly import kw_only_func, kw_only_meth + +from nose.tools import assert_equal +from nose.tools import assert_raises + + +def test_kw_only_func(): + # Test decorator + def func(an_arg): + "My docstring" + return an_arg + assert_equal(func(1), 1) + assert_raises(TypeError, func, 1, 2) + dec_func = kw_only_func(1)(func) + assert_equal(dec_func(1), 1) + assert_raises(TypeError, dec_func, 1, 2) + assert_raises(TypeError, dec_func, 1, akeyarg=3) + assert_equal(dec_func.__doc__, 'My docstring') + + @kw_only_func(1) + def kw_func(an_arg, a_kwarg='thing'): + "Another docstring" + return an_arg, a_kwarg + assert_equal(kw_func(1), (1, 'thing')) + assert_raises(TypeError, kw_func, 1, 2) + assert_equal(kw_func(1, a_kwarg=2), (1, 2)) + assert_raises(TypeError, kw_func, 1, akeyarg=3) + assert_equal(kw_func.__doc__, 'Another docstring') + + class C(object): + + @kw_only_meth(1) + def kw_meth(self, an_arg, a_kwarg='thing'): + "Method docstring" + return an_arg, a_kwarg + c = C() + assert_equal(c.kw_meth(1), (1, 'thing')) + assert_raises(TypeError, c.kw_meth, 1, 2) + assert_equal(c.kw_meth(1, a_kwarg=2), (1, 2)) + assert_raises(TypeError, c.kw_meth, 1, akeyarg=3) + assert_equal(c.kw_meth.__doc__, 'Method docstring') From e69ec32d45556d79deb503d4ebc1446fef93c979 Mon Sep 17 00:00:00 2001 From: Ross Markello Date: Fri, 6 Dec 2019 11:59:19 -0500 Subject: [PATCH 3/3] REF: Add keywordonly module to deprecation cycle Marked for removal in version 5.0.0 --- nibabel/keywordonly.py | 7 +++++++ nibabel/tests/test_removalschedule.py | 1 + 2 files changed, 8 insertions(+) diff --git a/nibabel/keywordonly.py b/nibabel/keywordonly.py index 8cb4908c1e..198e70f2c9 100644 --- a/nibabel/keywordonly.py +++ b/nibabel/keywordonly.py @@ -2,6 +2,13 @@ """ from functools import wraps +import warnings + +warnings.warn("We will remove this module from nibabel 5.0. " + "Please use the built-in Python `*` argument to ensure " + "keyword-only parameters (see PEP 3102).", + DeprecationWarning, + stacklevel=2) def kw_only_func(n): diff --git a/nibabel/tests/test_removalschedule.py b/nibabel/tests/test_removalschedule.py index ce1ba668b2..a0c3484a3a 100644 --- a/nibabel/tests/test_removalschedule.py +++ b/nibabel/tests/test_removalschedule.py @@ -2,6 +2,7 @@ from ..testing import assert_raises, assert_false MODULE_SCHEDULE = [ + ('5.0.0', ['nibabel.keywordonly']), ('4.0.0', ['nibabel.trackvis']), ('3.0.0', ['nibabel.minc', 'nibabel.checkwarns']), # Verify that the test will be quiet if the schedule outlives the modules