Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-20378: System for deprecating Config fields #39

Merged
merged 1 commit into from
Jul 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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