Skip to content

Commit

Permalink
Add deprecated Field parameter
Browse files Browse the repository at this point in the history
The deprecation message gets appended to the docstring, so if the Config
is saved it rides along "for free". It also emits a warning if that field is
set later.
  • Loading branch information
parejkoj committed Jul 23, 2019
1 parent 54f2ebc commit 9dba923
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 18 deletions.
22 changes: 19 additions & 3 deletions python/lsst/pex/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import copy
import tempfile
import shutil
import warnings

from .comparison import getComparisonName, compareScalars, compareConfigs
from .callStack import getStackFrame, getCallStack
Expand Down Expand Up @@ -208,6 +209,9 @@ class Field:
This sets whether the field is considered optional, and therefore
doesn't need to be set by the user. When `False`,
`lsst.pex.config.Config.validate` fails if the field's value is `None`.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
Raises
------
Expand Down Expand Up @@ -263,24 +267,32 @@ class Field:
"""Supported data types for field values (`set` of types).
"""

def __init__(self, doc, dtype, default=None, check=None, optional=False):
def __init__(self, doc, dtype, default=None, check=None, optional=False, deprecated=None):
if dtype not in self.supportedTypes:
raise ValueError("Unsupported Field dtype %s" % _typeStr(dtype))

source = getStackFrame()
self._setup(doc=doc, dtype=dtype, default=default, check=check, optional=optional, source=source)
self._setup(doc=doc, dtype=dtype, default=default, check=check, optional=optional, source=source,
deprecated=deprecated)

def _setup(self, doc, dtype, default, check, optional, source):
def _setup(self, doc, dtype, default, check, optional, source, deprecated):
"""Set attributes, usually during initialization.
"""
self.dtype = dtype
"""Data type for the field.
"""

# append the deprecation message to the docstring.
if deprecated is not None:
doc = f"{doc} Deprecated: {deprecated}"
self.doc = doc
"""A description of the field (`str`).
"""

self.deprecated = deprecated
"""If not None, a description of why this field is deprecated (`str`).
"""

self.__doc__ = f"{doc} (`{dtype.__name__}`"
if optional or default is not None:
self.__doc__ += f", default ``{default!r}``"
Expand Down Expand Up @@ -1258,6 +1270,10 @@ def __setattr__(self, attr, value, at=None, label="assignment"):
non-existent field.
"""
if attr in self._fields:
if self._fields[attr].deprecated is not None:
fullname = _joinNamePath(self._name, self._fields[attr].name)
warnings.warn(f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}",
FutureWarning)
if at is None:
at = getCallStack()
# This allows Field descriptors to work.
Expand Down
7 changes: 5 additions & 2 deletions python/lsst/pex/config/configChoiceField.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ class ConfigChoiceField(Field):
If `False`, the field allows only a single selection. In this case,
set the active config by assigning the config's key from the
``typemap`` to the field's ``name`` attribute (see *Examples*).
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand Down Expand Up @@ -406,10 +409,10 @@ class ConfigChoiceField(Field):

instanceDictClass = ConfigInstanceDict

def __init__(self, doc, typemap, default=None, optional=False, multi=False):
def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None):
source = getStackFrame()
self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional,
source=source)
source=source, deprecated=deprecated)
self.typemap = typemap
self.multi = multi

Expand Down
8 changes: 6 additions & 2 deletions python/lsst/pex/config/configDictField.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class ConfigDictField(DictField):
optional : `bool`, optional
If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
Default is `True`.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
Raises
------
Expand Down Expand Up @@ -139,10 +142,11 @@ class ConfigDictField(DictField):

DictClass = ConfigDict

def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None):
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
deprecated=None):
source = getStackFrame()
self._setup(doc=doc, dtype=ConfigDict, default=default, check=None,
optional=optional, source=source)
optional=optional, source=source, deprecated=deprecated)
if keytype not in self.supportedTypes:
raise ValueError("'keytype' %s is not a supported type" %
_typeStr(keytype))
Expand Down
7 changes: 5 additions & 2 deletions python/lsst/pex/config/configField.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class ConfigField(Field):
check : callable, optional
A callback function that validates the field's value, returning `True`
if the value is valid, and `False` otherwise.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand All @@ -70,15 +73,15 @@ class ConfigField(Field):
configuration.
"""

def __init__(self, doc, dtype, default=None, check=None):
def __init__(self, doc, dtype, default=None, check=None, deprecated=None):
if not issubclass(dtype, Config):
raise ValueError("dtype=%s is not a subclass of Config" %
_typeStr(dtype))
if default is None:
default = dtype
source = getStackFrame()
self._setup(doc=doc, dtype=dtype, default=default, check=check,
optional=False, source=source)
optional=False, source=source, deprecated=deprecated)

def __get__(self, instance, owner=None):
if instance is None or not isinstance(instance, Config):
Expand Down
7 changes: 5 additions & 2 deletions python/lsst/pex/config/configurableField.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ class of the ``target``. If ``ConfigClass`` is unset then
Callable that takes the field's value (the ``target``) as its only
positional argument, and returns `True` if the ``target`` is valid (and
`False` otherwise).
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand Down Expand Up @@ -247,7 +250,7 @@ def validateTarget(self, target, ConfigClass):
"(must have '__module__' and '__name__' attributes)")
return ConfigClass

def __init__(self, doc, target, ConfigClass=None, default=None, check=None):
def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None):
ConfigClass = self.validateTarget(target, ConfigClass)

if default is None:
Expand All @@ -258,7 +261,7 @@ def __init__(self, doc, target, ConfigClass=None, default=None, check=None):

source = getStackFrame()
self._setup(doc=doc, dtype=ConfigurableInstance, default=default,
check=check, optional=False, source=source)
check=check, optional=False, source=source, deprecated=deprecated)
self.target = target
self.ConfigClass = ConfigClass

Expand Down
8 changes: 6 additions & 2 deletions python/lsst/pex/config/dictField.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ class DictField(Field):
A function that validates the dictionary as a whole.
itemCheck : callable
A function that validates individual mapping values.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand Down Expand Up @@ -193,10 +196,11 @@ class DictField(Field):

DictClass = Dict

def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None):
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
deprecated=None):
source = getStackFrame()
self._setup(doc=doc, dtype=Dict, default=default, check=None,
optional=optional, source=source)
optional=optional, source=source, deprecated=deprecated)
if keytype not in self.supportedTypes:
raise ValueError("'keytype' %s is not a supported type" %
_typeStr(keytype))
Expand Down
9 changes: 7 additions & 2 deletions python/lsst/pex/config/listField.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ class ListField(Field):
maxLength : `int`, optional
If set, this field must contain *no more than* ``maxLength`` number of
items.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand All @@ -251,7 +254,8 @@ class ListField(Field):
"""
def __init__(self, doc, dtype, default=None, optional=False,
listCheck=None, itemCheck=None,
length=None, minLength=None, maxLength=None):
length=None, minLength=None, maxLength=None,
deprecated=None):
if dtype not in Field.supportedTypes:
raise ValueError("Unsupported dtype %s" % _typeStr(dtype))
if length is not None:
Expand All @@ -273,7 +277,8 @@ def __init__(self, doc, dtype, default=None, optional=False,
raise ValueError("'itemCheck' must be callable")

source = getStackFrame()
self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source)
self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source,
deprecated=deprecated)

self.listCheck = listCheck
"""Callable used to check the list as a whole.
Expand Down
9 changes: 7 additions & 2 deletions python/lsst/pex/config/rangeField.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class RangeField(Field):
inclusiveMax : `bool`, optional
If `True` (default), the ``max`` value is included in the allowed
range.
deprecated : None or `str`, optional
A description of why this Field is deprecated, including removal date.
If not None, the string is appended to the docstring for this Field.
See also
--------
Expand All @@ -73,7 +76,8 @@ class RangeField(Field):
"""

def __init__(self, doc, dtype, default=None, optional=False,
min=None, max=None, inclusiveMin=True, inclusiveMax=False):
min=None, max=None, inclusiveMin=True, inclusiveMax=False,
deprecated=None):
if dtype not in self.supportedTypes:
raise ValueError("Unsupported RangeField dtype %s" % (_typeStr(dtype)))
source = getStackFrame()
Expand Down Expand Up @@ -104,7 +108,8 @@ def __init__(self, doc, dtype, default=None, optional=False,
self.minCheck = lambda x, y: True if y is None else x >= y
else:
self.minCheck = lambda x, y: True if y is None else x > y
self._setup(doc, dtype=dtype, default=default, check=None, optional=optional, source=source)
self._setup(doc, dtype=dtype, default=default, check=None, optional=optional, source=source,
deprecated=deprecated)
self.rangeString = "%s%s,%s%s" % \
(("[" if inclusiveMin else "("),
("-inf" if self.min is None else self.min),
Expand Down
20 changes: 19 additions & 1 deletion tests/test_Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
import itertools
import re
import os
import pickle
import unittest

import lsst.utils.tests
import lsst.pex.config as pexConfig
import pickle

GLOBAL_REGISTRY = {}

Expand Down Expand Up @@ -80,12 +81,17 @@ class Complex(pexConfig.Config):
default="BBB", optional=True)


class Deprecation(pexConfig.Config):
old = pexConfig.Field("Something.", int, default=10, deprecated="not used!")


class ConfigTest(unittest.TestCase):
def setUp(self):
self.simple = Simple()
self.inner = InnerConfig()
self.outer = OuterConfig()
self.comp = Complex()
self.deprecation = Deprecation()

def tearDown(self):
del self.simple
Expand All @@ -101,6 +107,9 @@ def testInit(self):
self.assertEqual(list(self.simple.ll), [1, 2, 3])
self.assertEqual(self.simple.d["key"], "value")
self.assertEqual(self.inner.f, 0.0)
self.assertEqual(self.deprecation.old, 10)

self.assertEqual(self.deprecation._fields['old'].doc, "Something. Deprecated: not used!")

self.assertEqual(self.outer.i.f, 5.0)
self.assertEqual(self.outer.f, 0.0)
Expand All @@ -110,6 +119,15 @@ def testInit(self):
self.assertEqual(self.comp.r.active.f, 3.0)
self.assertEqual(self.comp.r["BBB"].f, 0.0)

def testDeprecationWarning(self):
"""Test that a deprecated field emits a warning when it is set.
"""
with self.assertWarns(FutureWarning) as w:
self.deprecation.old = 5
self.assertEqual(self.deprecation.old, 5)

self.assertIn(self.deprecation._fields['old'].deprecated, str(w.warnings[-1].message))

def testValidate(self):
self.simple.validate()

Expand Down

0 comments on commit 9dba923

Please sign in to comment.