Skip to content

PyKwalify uses deprecated load_module function in core._load_extensions() #202

@JoostJM

Description

@JoostJM

Environment

  • Python version: 3.10
  • PyKwalify version: 1.8.0

Steps to Reproduce

  • Validate any schema with additional extensions. Easiest to reproduce using PyRadiomics repo (which uses pykwalify). From the pyradiomics repo run python bin/testParams.py examples/exampleSettings/exampleCT.yaml. This won't fail, because the function is deprecated, but not removed.
  • Alternative, run pyradiomcis tests of example settings: from repo tests folder, run pytest test_exampleSettings.py. This fails because of the deprecation warning.

Schema

See Steps to reproduce. Schema: PyRadiomics Schema, with extensions

Schema:

# Parameters schema
name: Parameter schema
desc: This schema defines what arguments may be present in the parameters file that can be passed to the pyradiomics package.
type: map
mapping:
  setting: &settings
    type: map
    mapping:
      minimumROIDimensions:
        type: int
        range:
          min: 1
          max: 3
      minimumROISize:
        type: int
        range:
          min-ex: 0
      geometryTolerance:
        type: float
        range:
          min-ex: 0
      correctMask:
        type: bool
      additionalInfo:
        type: bool
      label:
        type: int
        range:
          min-ex: 0
      label_channel:
        type: int
        range:
          min: 0
      binWidth:
        type: float
        range:
          min-ex: 0
      binCount:
        type: int
        range:
          min-ex: 0
      normalize:
        type: bool
      normalizeScale:
        type: float
        range:
          min-ex: 0
      removeOutliers:
        type: float
        range:
          min-ex: 0
      resampledPixelSpacing:
        seq:
          - type: float
            range:
              min: 0
      interpolator:
        type: any
        func: checkInterpolator
      padDistance:
        type: int
        range:
          min: 0
      distances:
        seq:
          - type: int
            range:
              min-ex: 0
      force2D:
        type: bool
      force2Ddimension:
        type: int
        range:
          min: 0
          max: 2
      resegmentRange:
        seq:
          - type: float
      resegmentMode:
        type: str
        enum: ['absolute', 'relative', 'sigma']
      resegmentShape:
        type: bool
      preCrop:
        type: bool
      sigma:
        seq:
          - type: float
            range:
              min-ex: 0
      start_level:
        type: int
        range:
          min: 0
      level:
        type: int
        range:
          min-ex: 0
      wavelet:
        type: str
        func: checkWavelet
      gradientUseSpacing:
        type: bool
      lbp2DRadius:
        type: float
        range:
          min-ex: 0
      lbp2DSamples:
        type: int
        range:
          min: 1
      lbp2DMethod:
        type: str
        enum: ['default', 'ror', 'uniform', 'var']
      lbp3DLevels:
        type: int
        range:
          min: 1
      lbp3DIcosphereRadius:
        type: float
        range:
          min-ex: 0
      lbp3DIcosphereSubdivision:
        type: int
        range:
          min: 0
      voxelArrayShift:
        type: int
      symmetricalGLCM:
        type: bool
      weightingNorm:
        type: any
        func: checkWeighting
      gldm_a:
        type: int
        range:
          min: 0

  voxelSetting:
    type: map
    mapping:
      kernelRadius:
        type: int
        range:
          min-ex: 0
      maskedKernel:
        type: bool
      initValue:
        type: float
      voxelBatch:
        type: int
        range:
          min-ex: 0

  featureClass:
    type: map
    func: checkFeatureClass
    matching-rule: 'any'
    mapping:
      regex;(.+):
        type: any

  imageType:
    type: map
    func: checkImageType
    matching-rule: 'any'
    mapping:
       regex;(.+): *settings

Schema extensions:

import pywt
import six

from radiomics import getFeatureClasses, getImageTypes

featureClasses = getFeatureClasses()
imageTypes = getImageTypes()

def checkWavelet(value, rule_obj, path):
  if not isinstance(value, six.string_types):
    raise TypeError('Wavelet not expected type (str)')
  wavelist = pywt.wavelist()
  if value not in wavelist:
    raise ValueError('Wavelet "%s" not available in pyWavelets %s' % (value, wavelist))
  return True


def checkInterpolator(value, rule_obj, path):
  if value is None:
    return True
  if isinstance(value, six.string_types):
    enum = {'sitkNearestNeighbor',
            'sitkLinear',
            'sitkBSpline',
            'sitkGaussian',
            'sitkLabelGaussian',
            'sitkHammingWindowedSinc',
            'sitkCosineWindowedSinc',
            'sitkWelchWindowedSinc',
            'sitkLanczosWindowedSinc',
            'sitkBlackmanWindowedSinc'}
    if value not in enum:
      raise ValueError('Interpolator value "%s" not valid, possible values: %s' % (value, enum))
  elif isinstance(value, int):
    if value < 1 or value > 10:
      raise ValueError('Intepolator value %i, must be in range of [1-10]' % (value))
  else:
    raise TypeError('Interpolator not expected type (str or int)')
  return True


def checkWeighting(value, rule_obj, path):
  if value is None:
    return True
  elif isinstance(value, six.string_types):
    enum = ['euclidean', 'manhattan', 'infinity', 'no_weighting']
    if value not in enum:
      raise ValueError('WeightingNorm value "%s" not valid, possible values: %s' % (value, enum))
  else:
    raise TypeError('WeightingNorm not expected type (str or None)')
  return True


def checkFeatureClass(value, rule_obj, path):
  global featureClasses
  if value is None:
    raise TypeError('featureClass dictionary cannot be None value')
  for className, features in six.iteritems(value):
    if className not in featureClasses.keys():
      raise ValueError(
        'Feature Class %s is not recognized. Available feature classes are %s' % (className, list(featureClasses.keys())))
    if features is not None:
      if not isinstance(features, list):
        raise TypeError('Value of feature class %s not expected type (list)' % (className))
      unrecognizedFeatures = set(features) - set(featureClasses[className].getFeatureNames())
      if len(unrecognizedFeatures) > 0:
        raise ValueError('Feature Class %s contains unrecognized features: %s' % (className, str(unrecognizedFeatures)))

  return True


def checkImageType(value, rule_obj, path):
  global imageTypes
  if value is None:
    raise TypeError('imageType dictionary cannot be None value')

  for im_type in value:
    if im_type not in imageTypes:
      raise ValueError('Image Type %s is not recognized. Available image types are %s' %
                       (im_type, imageTypes))

  return True

Data

PyRadiomics Example Schema

imageType:
  Original: {}
  LoG:
    sigma: [1.0, 2.0, 3.0, 4.0, 5.0]  # If you include sigma values >5, remember to also increase the padDistance.
  Wavelet: {}

featureClass:
  # redundant Compactness 1, Compactness 2 an Spherical Disproportion features are disabled by default, they can be
  # enabled by specifying individual feature names (as is done for glcm) and including them in the list.
  shape:
  firstorder:
  glcm:  # Disable SumAverage by specifying all other GLCM features available
    - 'Autocorrelation'
    - 'JointAverage'
    - 'ClusterProminence'
    - 'ClusterShade'
    - 'ClusterTendency'
    - 'Contrast'
    - 'Correlation'
    - 'DifferenceAverage'
    - 'DifferenceEntropy'
    - 'DifferenceVariance'
    - 'JointEnergy'
    - 'JointEntropy'
    - 'Imc1'
    - 'Imc2'
    - 'Idm'
    - 'Idmn'
    - 'Id'
    - 'Idn'
    - 'InverseVariance'
    - 'MaximumProbability'
    - 'SumEntropy'
    - 'SumSquares'
  glrlm:
  glszm:
  gldm:

setting:
  # Normalization:
  # most likely not needed, CT gray values reflect absolute world values (HU) and should be comparable between scanners.
  # If analyzing using different scanners / vendors, check if the extracted features are correlated to the scanner used.
  # If so, consider enabling normalization by uncommenting settings below:
  #normalize: true
  #normalizeScale: 500  # This allows you to use more or less the same bin width.

  # Resampling:
  # Usual spacing for CT is often close to 1 or 2 mm, if very large slice thickness is used,
  # increase the resampled spacing.
  # On a side note: increasing the resampled spacing forces PyRadiomics to look at more coarse textures, which may or
  # may not increase accuracy and stability of your extracted features.
  interpolator: 'sitkBSpline'
  resampledPixelSpacing: [1, 1, 1]
  padDistance: 10  # Extra padding for large sigma valued LoG filtered images

  # Mask validation:
  # correctMask and geometryTolerance are not needed, as both image and mask are resampled, if you expect very small
  # masks, consider to enable a size constraint by uncommenting settings below:
  #minimumROIDimensions: 2
  #minimumROISize: 50

  # Image discretization:
  # The ideal number of bins is somewhere in the order of 16-128 bins. A possible way to define a good binwidt is to
  # extract firstorder:Range from the dataset to analyze, and choose a binwidth so, that range/binwidth remains approximately
  # in this range of bins.
  binWidth: 25

  # first order specific settings:
  voxelArrayShift: 1000  # Minimum value in HU is -1000, shift +1000 to prevent negative values from being squared.

  # Misc:
  # default label value. Labels can also be defined in the call to featureextractor.execute, as a commandline argument,
  # or in a column "Label" in the input csv (batchprocessing)
  label: 1

Expected Behavior

To run pykwalify without deprecation warnings. This is needed to allow PyTest to complete without errors.

Observed Behavior

Got a deprecation warning from the function in pykwalify core: core._load_extensions(), uses SourceFileLoader.load_module(), it suggests to use exec_module() instead.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions