1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
# Upgrade guide
Welcome to the Upgrade Guide for Param! When we make backward-incompatible changes, we will provide a detailed guide here on how you can update your code to be compatible with the latest changes.
## Version 2.0
### Breaking changes
#### Parameter attributes inheritance
`Parameter`s have attributes like `default`, `doc`, `bounds`, etc. When redefining a `Parameter` in a subclass, attributes not specified in the subclass are meant to be inherited from the superclass.
Inheritance is not a new feature; it's a core design of Param. You can see in the example below that the `doc` attribute of the `x` Parameter on the class `B` has been inherited from `A`:
```python
class A(param.Parameterized):
x = param.Number(default=5, doc='The value of x')
class B(A):
x = param.Number(default=10)
print(B().param['x'].doc)
# The value of x
```
However, throughout the Param 1.x series, inheritance was broken in some subtle ways! The issues traced back to using `None` as the specific sentinel value for allowing inheritance, when `None` is also often a valid `Parameter` value. Let's have a look at two examples that have been fixed in Param 2.0.
In this first example we want to re-set the default value of `x` to `None` in a subclass. As you can see, the explicit `None` value we provided in `B` was discarded entirely in versions before Param 2.0, while the new version now correctly re-assigns the value to `None`:
```python
class A(param.Parameterized):
x = param.Number(default=5.0)
class B(A):
x = param.Number(default=None)
print(B().x)
# Param < 2: 5.0 :(
# Param 2.0: None :)
```
In this second example we want to narrow the bounds `x` in the subclass `B`. Because no default value is specified for `x` in `B`, you might reasonably have expected the default value of `5.0` to be inherited from `A`. However, prior to Param 2.0, instead the default value of the `Number` `Parameter` (`0.0`) is used for `B.x` instead of what was declared in `A`:
```python
class A(param.Parameterized):
x = param.Number(default=5.0, bounds=(-10, 10))
class B(A):
x = param.Number(bounds=(0, 10))
print(B().x)
# Param < 2: 0.0 :(
# Param 2.0: 5.0 :)
```
These are just two of the most common cases where inheritance changes can affect your code, but there are many other cases, affecting any attribute where `None` is a legal value. The new behavior should be much more predictable and intuitive, avoiding subtle cases where your code would previously have behaved inappropriately without necessarily having any obvious error. Fixing this was already enough to be worth the major bump to Param 2.0!
Because of all our work on making sure that Parameter attributes are properly inherited, we've realized that you can now more easily end up with a Parameter whose state, i.e. the combination of all of its attribute values, is invalid according to the Parameter declarations. Therefore, once class inheritance has completed as your classes get defined, we have added validation that the Parameter's *default* value is an allowed value. For now Param 2.0 is only emitting a warning when this validation fails, to avoid breaking your code on imports! If you do see these warnings, you should address them immediately by adding a legal default value or by relaxing one of the declared constraints on the value, because the warnings indicate a Parameter is in an invalid state. In the following example, the `bounds` of `x` are narrowed down in `B` without updating the `default` value that is inherited from `A.x` of `-1`, making the Parameter `x` on `B` in an invalid state:
```python
class A(param.Parameterized):
x = param.Number(default=-1, bounds=(-5, 5))
class B(A):
x = param.Number(bounds=(0, 5))
# Param < 2: No warning and B().x == 0.0
# Param 2.0: Warning and B().x == -1 / ParamFutureWarning: Number parameter 'B.x' failed to validate its default value on class creation, this is going to raise an error in the future. The Parameter is defined with attributes which when combined with attributes inherited from its parent classes (A) make it invalid. Please fix the Parameter attributes.
```
Param doesn't do any more validation that what was just explained when creating subclasses that override `Parameter`s. For instance, it doesn't programmatically enforce that a subclass should define a `Parameter` that is of the same type or a subtype of the `Parameter` defined in a super class. However, the best approach for you to follow is clearly to make sure that `Parameter`s defined in subclasses satisfy the ["is-a"](https://en.wikipedia.org/wiki/Is-a) relationship. I.e., for an instance `b` of subclass `B` and an instance `a` of parent class `A`, `b` should always be usable where `a` is. That's the general rule for inheritance, and for `Parameter`s it's specifically that any parameter value accepted by `B` should be a valid setting for `A`. That way any code written for `A`, including code that handles the various values of any attribute of `A`, can be trusted to work properly with any instance of `B`. In more concrete terms, it means a `Parameter` of a subclass should not increase the range of accepted values, it should instead narrow it down, i.e. by specifying a subtype (e.g. `param.Number` to `param.Integer`) or restricting the range of accepted values (e.g. smaller `bounds`).
#### Defining custom `Parameter`s
To implement `Parameter` attribute inheritance correctly as described above, we have had to make some changes to how new `Parameter` classes are declared. While the custom `Parameter`s you previously wrote for Param before 2.0 should continue to work as they always have, unless you update their definitions, they could continue to suffer from the attribute inheritance issues that have been fixed within Param itself. We now recommend custom `Parameter`s to be written following the pattern we have adopted internally. In particular, we have:
- introduced the `param.parameterized.Undefined` sentinel object that is used as the default value of the `Parameter` parameters in its `__init__()` method
- introduced the `_slot_defaults` class attribute to declare the actual default values of the `Parameter` parameters, e.g. in the example below the default value of `some_attribute` is declared to be `10`
- leveraged `@typing.overload` to expose the real default values to static type checkers and IDEs like VSCode
All put together, this is now how `Parameter`s are structured internally and how we recommend writing custom `Parameter` classes:
```python
class CustomParameter(Parameter):
__slots__ = ['some_attribute']
_slot_defaults = _dict_update(Parameter._slot_defaults,
default=None, some_attribute=10
)
@typing.overload
def __init__(
self,
default=None, *, some_attribute=10,
doc=None, label=None, precedence=None, instantiate=False, constant=False,
readonly=False, pickle_default_value=True, allow_None=False, per_instance=True,
allow_refs=False, nested_refs=False
):
...
def __init__(self, default=Undefined, *, some_attribute=Undefined, **params):
super().__init__(default=default, **params)
self.some_attribute = some_attribute
...
```
To make Parameter attributes work as expected, we have overridden `Parameter.__getattribute__` to detect whether the attribute value has been set to something that is not `Undefined`, and if not, to fall back to returning the default value declared in `_slot_defaults`:
```python
print(CustomParameter(some_attribute=0).some_attribute)
# 0
print(CustomParameter().some_attribute) # Fallback case
# 10
```
#### Default value of `name` accounted for
Every `Parameterized` class has always been equipped with a `name` `Parameter`, which was originally to support using a Parameterized object in a GUI context that needs a text label, but is not always needed in other contexts. At the class level the `name` value is the class name, equivalent to `<class>.__name__`, and at the instance level the `name` value is automatically generated as a unique string, unless you set it in the constructor:
```python
class A(param.Parameterized):
x = param.Number()
print(A.name)
# A
print(A().name)
# A00002
print(A(name='some name').name)
# some name
```
However, sometimes you want to define your own default value for the `name` parameter, as in the following example. Before Param 2.0, any such default value was discarded at both the class and instance levels, but since 2.0 the provided `name` is now respected:
```python
class Person(param.Parameterized):
name = param.String(default='Eva')
print(Person.name)
# Param < 2: Person :(
# Param 2.0: Eva :)
print(Person().name)
# Param < 2: Person0000 :(
# Param 2.0: Eva :)
```
#### Setting a non-Parameter attribute via the constructor
Before Param 2.0, you could set an instance attribute that was not a `Parameter` via the constructor, though you would get a warning if you did so. This behavior was prone to let typos slip through your code, setting the wrong instance attribute but continuing to function. Starting from Param 2.0, providing an attribute value that is not a Parameter now raises a `TypeError`, similarly to when you call a Python callable with an incorrect parameter name:
```python
class A(param.Parameterized):
number = param.Number()
A(numbre=10) # oops typo!
# Param < 2: WARNING:param.A00002: Setting non-parameter attribute numbre=10 using a mechanism intended only for parameters
# Param 2.0: TypeError: A.__init__() got an unexpected keyword argument 'numbre'
```
#### `instance.param.watchers` value changed
`instance.param.watchers` no longer returns the transient watchers state, which was never very useful, and now instead returns what you would expect, i.e. a dictionary of the watchers set up on this instance. To access that dictionary previously, you would have had to access the private attribute `instance._param_watchers`, which is now deprecated and was never intended to be public API:
```python
class A(param.Parameterized):
x = param.Number()
@param.depends('x', watch=True)
def cb(self):
pass
print(A().param.watchers)
# Param < 2: []
# Param 2.0: {'x': {'value': [Watcher(...)]}}
```
#### Clean-up of the `Parameterized` namespace
To avoid having your Parameterized object namespaces polluted with internal Param details, the methods listed below have been removed from the `Parameterized` namespace (members available to any class that inherits from `param.Parameterized`). Most of the time, a replacement method is now available from the `.param` namespace instead:
- `_add_parameter`: use instead `param.add_parameter`
- `params`: use instead `.param.values()` or `.param['param']`
- `set_default`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `print_param_defaults`: `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `set_param`: use instead `.param.update`
- `set_dynamic_time_fn`: use instead `.param.set_dynamic_time_fn`
- `get_param_values`: use instead `.param.values().items()` (or `.param.values()` for the common case of `dict(....param.get_param_values())`)
- `force_new_dynamic_value`: use instead `.param.force_new_dynamic_value`
- `get_value_generator`: use instead `.param.get_value_generator`
- `inspect_value`: use instead `.param.inspect_value`
- `_set_name`: no replacement
- `__db_print`: no replacement
- `warning`: use instead `.param.warning`
- `message`: use instead `.param.log(param.MESSAGE, ...)`
- `verbose`: use instead `.param.log(param.VERBOSE, ...)`
- `debug`: use instead `.param.log(param.DEBUG, ...)`
- `print_param_values`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `defaults`: use instead `{k:v.default for k,v in p.param.objects().items()}`
- `pprint` and `_pprint`: use instead `.param.pprint`
- `script_repr`: use instead `param.parameterized.script_repr`
- `state_pop`: moved to `.param._state_pop`, it might be removed in a future version, let us know if you need it!
- `state_push`: moved to `.param._state_push`, it might be removed in a future version, let us know if you need it!
#### `Parameter` clean-up
- Removed unused `bounds` slot from `Boolean` and `Event`
- Removed private Parameter `_internal_name` slot
#### Additional removals
- Removed `Time.next` method that was only needed for Python 2
### Deprecations
Param 2.0 was originally meant to clean up even more Param's API, except that we noticed that deprecating API in the release notes of previous versions was not enough to warn Param's users (including ourselves!). Thus for 2.0 we decided to postpone removing some APIs, and this time properly warn Param's users that the code is about to be deleted. We've also spent a lot of time looking at the code base while working on this major release, and noticed additional clean up that could be made in the future. Therefore when you upgrade to 2.0, you are likely to encounter quite a large number of deprecation warnings. Make sure you're adapting your code as soon as you can whenever you notice one of the newly emitted warnings, because that code will not continue to be supported in later releases in the 2.x series. (Or you can pin your code to Param 1.x if you want to avoid the warnings and do not need any of the features or bugfixes from 2.0.)
Here is the complete list of deprecation warnings added in Param 2.0:
- Parameter signature:
- Instantiating parameters other than `default` with positional arguments is deprecated:
- `String('prefix-test', '^prefix')`: deprecated!
- `String('prefix-test', regex='^prefix')`: OK
- `String(default='prefix-test', regex='^prefix')`: OK
- For `Selector` parameters that accept `objects` as first positional argument, and `ClassSelector` parameters that accept `class_` as first positional argument, passing *any* argument by position is deprecated:
- `Selector([1, 2])`: deprecated!
- `Selector(objects=[1, 2])`: OK
- `ClassSelector((str, int))`: deprecated!
- `ClassSelector(class_=(str, int))`: OK
- It's possible that in the future the signature of these two parameters will be aligned with the other parameters to accept `default` as first and only positional argument, but for now please use an explicit keyword so that your code will be compatible with all versions.
- Parameter slots:
- `List._class`: use instead `item_type`.
- `Number.set_hook`: no replacement
- `param.__init__` module:
- `param.produce_value`: no replacement
- `param.as_unicode`: no replacement
- `param.is_ordered_dict`: no replacement
- `param.is_ordered_dict`: no replacement
- `param.hashable`: no replacement
- `param.named_objs`: no replacement
- `param.normalize_path`: no replacement
- `param.abbreviate_paths`: no replacement
- `param.parameterized` module:
- `param.parameterized.all_equal`: no replacement
- `param.parameterized.add_metaclass`: no replacement
- `param.parameterized.batch_watch`: use instead `batch_call_watchers`
- `param.parameterized.recursive_repr`: no replacement
- `param.parameterized.overridable_property`: no replacement
- Parameterized `.param` namespace; many of these methods have been deprecated since version 1.12.0, but because the deprecation was announced only in the release notes and not with runtime warnings, meany users have failed to realize the methods were deprecated, sometimes even us included! All these methods now emit deprecation warnings when executed and are now clearly marked as deprecated in the API reference:
- `.param.set_default`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `.param._add_parameter`: use instead `.param.add_parameter`
- `.param.params`: use instead `.param.values()` or `.param['param']`
- `.param.set_param`: use instead `.param.update`
- `.param.get_param_values`: use instead `.param.values().items()` (or `.param.values()` for the common case of `dict(....param.get_param_values())`)
- `.param.params_depended_on`: use instead `.param.method_dependencies`
- `.param.defaults`: use instead `{k:v.default for k,v in p.param.objects().items()}`
- `.param.print_param_defaults`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `.param.print_param_values`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`
- `.param.message`: use instead `.param.log(param.MESSAGE, ...)`
- `.param.verbose`: use instead `.param.log(param.VERBOSE, ...)`
- `.param.debug`: use instead `.param.log(param.DEBUG, ...)`
- Some methods now (correctly) warn that they are not safe to run in a `Parameterized` subclass constructor until you have called `super().__init__(**params)`:
- `instance.param.objects(instance=True)`
- `instance.param.trigger("<param_name>")`
- `instance.param.watch(callback, "<param_name>")`
- Parameterized namespace:
- `instance._param_watchers` (getter and setter): use instead the property `inst.param.watchers`
|