4313 lines
148 KiB
Python
4313 lines
148 KiB
Python
"""
|
|
A lightweight Traits like module.
|
|
|
|
This is designed to provide a lightweight, simple, pure Python version of
|
|
many of the capabilities of enthought.traits. This includes:
|
|
|
|
* Validation
|
|
* Type specification with defaults
|
|
* Static and dynamic notification
|
|
* Basic predefined types
|
|
* An API that is similar to enthought.traits
|
|
|
|
We don't support:
|
|
|
|
* Delegation
|
|
* Automatic GUI generation
|
|
* A full set of trait types. Most importantly, we don't provide container
|
|
traits (list, dict, tuple) that can trigger notifications if their
|
|
contents change.
|
|
* API compatibility with enthought.traits
|
|
|
|
There are also some important difference in our design:
|
|
|
|
* enthought.traits does not validate default values. We do.
|
|
|
|
We choose to create this module because we need these capabilities, but
|
|
we need them to be pure Python so they work in all Python implementations,
|
|
including Jython and IronPython.
|
|
|
|
Inheritance diagram:
|
|
|
|
.. inheritance-diagram:: traitlets.traitlets
|
|
:parts: 3
|
|
"""
|
|
|
|
# Copyright (c) IPython Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
#
|
|
# Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
|
|
# also under the terms of the Modified BSD License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import enum
|
|
import inspect
|
|
import os
|
|
import re
|
|
import sys
|
|
import types
|
|
import typing as t
|
|
from ast import literal_eval
|
|
|
|
from .utils.bunch import Bunch
|
|
from .utils.descriptions import add_article, class_of, describe, repr_type
|
|
from .utils.getargspec import getargspec
|
|
from .utils.importstring import import_item
|
|
from .utils.sentinel import Sentinel
|
|
from .utils.warnings import deprecated_method, should_warn, warn
|
|
|
|
SequenceTypes = (list, tuple, set, frozenset)
|
|
|
|
# backward compatibility, use to differ between Python 2 and 3.
|
|
ClassTypes = (type,)
|
|
|
|
if t.TYPE_CHECKING:
|
|
from typing_extensions import TypeVar
|
|
else:
|
|
from typing import TypeVar
|
|
|
|
# exports:
|
|
|
|
__all__ = [
|
|
"All",
|
|
"Any",
|
|
"BaseDescriptor",
|
|
"Bool",
|
|
"Bytes",
|
|
"CBool",
|
|
"CBytes",
|
|
"CComplex",
|
|
"CFloat",
|
|
"CInt",
|
|
"CLong",
|
|
"CRegExp",
|
|
"CUnicode",
|
|
"Callable",
|
|
"CaselessStrEnum",
|
|
"ClassBasedTraitType",
|
|
"Complex",
|
|
"Container",
|
|
"DefaultHandler",
|
|
"Dict",
|
|
"DottedObjectName",
|
|
"Enum",
|
|
"EventHandler",
|
|
"Float",
|
|
"ForwardDeclaredInstance",
|
|
"ForwardDeclaredMixin",
|
|
"ForwardDeclaredType",
|
|
"FuzzyEnum",
|
|
"HasDescriptors",
|
|
"HasTraits",
|
|
"Instance",
|
|
"Int",
|
|
"Integer",
|
|
"List",
|
|
"Long",
|
|
"MetaHasDescriptors",
|
|
"MetaHasTraits",
|
|
"ObjectName",
|
|
"ObserveHandler",
|
|
"Set",
|
|
"TCPAddress",
|
|
"This",
|
|
"TraitError",
|
|
"TraitType",
|
|
"Tuple",
|
|
"Type",
|
|
"Unicode",
|
|
"Undefined",
|
|
"Union",
|
|
"UseEnum",
|
|
"ValidateHandler",
|
|
"default",
|
|
"directional_link",
|
|
"dlink",
|
|
"link",
|
|
"observe",
|
|
"observe_compat",
|
|
"parse_notifier_name",
|
|
"validate",
|
|
]
|
|
|
|
# any TraitType subclass (that doesn't start with _) will be added automatically
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Basic classes
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
Undefined = Sentinel(
|
|
"Undefined",
|
|
"traitlets",
|
|
"""
|
|
Used in Traitlets to specify that no defaults are set in kwargs
|
|
""",
|
|
)
|
|
|
|
All = Sentinel(
|
|
"All",
|
|
"traitlets",
|
|
"""
|
|
Used in Traitlets to listen to all types of notification or to notifications
|
|
from all trait attributes.
|
|
""",
|
|
)
|
|
|
|
# Deprecated alias
|
|
NoDefaultSpecified = Undefined
|
|
|
|
|
|
class TraitError(Exception):
|
|
pass
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Utilities
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def isidentifier(s: t.Any) -> bool:
|
|
return t.cast(bool, s.isidentifier())
|
|
|
|
|
|
def _safe_literal_eval(s: str) -> t.Any:
|
|
"""Safely evaluate an expression
|
|
|
|
Returns original string if eval fails.
|
|
|
|
Use only where types are ambiguous.
|
|
"""
|
|
try:
|
|
return literal_eval(s)
|
|
except (NameError, SyntaxError, ValueError):
|
|
return s
|
|
|
|
|
|
def is_trait(t: t.Any) -> bool:
|
|
"""Returns whether the given value is an instance or subclass of TraitType."""
|
|
return isinstance(t, TraitType) or (isinstance(t, type) and issubclass(t, TraitType))
|
|
|
|
|
|
def parse_notifier_name(names: Sentinel | str | t.Iterable[Sentinel | str]) -> t.Iterable[t.Any]:
|
|
"""Convert the name argument to a list of names.
|
|
|
|
Examples
|
|
--------
|
|
>>> parse_notifier_name([])
|
|
[traitlets.All]
|
|
>>> parse_notifier_name("a")
|
|
['a']
|
|
>>> parse_notifier_name(["a", "b"])
|
|
['a', 'b']
|
|
>>> parse_notifier_name(All)
|
|
[traitlets.All]
|
|
"""
|
|
if names is All or isinstance(names, str):
|
|
return [names]
|
|
elif isinstance(names, Sentinel):
|
|
raise TypeError("`names` must be either `All`, a str, or a list of strs.")
|
|
else:
|
|
if not names or All in names:
|
|
return [All]
|
|
for n in names:
|
|
if not isinstance(n, str):
|
|
raise TypeError(f"names must be strings, not {type(n).__name__}({n!r})")
|
|
return names
|
|
|
|
|
|
class _SimpleTest:
|
|
def __init__(self, value: t.Any) -> None:
|
|
self.value = value
|
|
|
|
def __call__(self, test: t.Any) -> bool:
|
|
return bool(test == self.value)
|
|
|
|
def __repr__(self) -> str:
|
|
return "<SimpleTest(%r)" % self.value
|
|
|
|
def __str__(self) -> str:
|
|
return self.__repr__()
|
|
|
|
|
|
def getmembers(object: t.Any, predicate: t.Any = None) -> list[tuple[str, t.Any]]:
|
|
"""A safe version of inspect.getmembers that handles missing attributes.
|
|
|
|
This is useful when there are descriptor based attributes that for
|
|
some reason raise AttributeError even though they exist. This happens
|
|
in zope.interface with the __provides__ attribute.
|
|
"""
|
|
results = []
|
|
for key in dir(object):
|
|
try:
|
|
value = getattr(object, key)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if not predicate or predicate(value):
|
|
results.append((key, value))
|
|
results.sort()
|
|
return results
|
|
|
|
|
|
def _validate_link(*tuples: t.Any) -> None:
|
|
"""Validate arguments for traitlet link functions"""
|
|
for tup in tuples:
|
|
if not len(tup) == 2:
|
|
raise TypeError(
|
|
"Each linked traitlet must be specified as (HasTraits, 'trait_name'), not %r" % t
|
|
)
|
|
obj, trait_name = tup
|
|
if not isinstance(obj, HasTraits):
|
|
raise TypeError("Each object must be HasTraits, not %r" % type(obj))
|
|
if trait_name not in obj.traits():
|
|
raise TypeError(f"{obj!r} has no trait {trait_name!r}")
|
|
|
|
|
|
class link:
|
|
"""Link traits from different objects together so they remain in sync.
|
|
|
|
Parameters
|
|
----------
|
|
source : (object / attribute name) pair
|
|
target : (object / attribute name) pair
|
|
transform: iterable with two callables (optional)
|
|
Data transformation between source and target and target and source.
|
|
|
|
Examples
|
|
--------
|
|
>>> class X(HasTraits):
|
|
... value = Int()
|
|
|
|
>>> src = X(value=1)
|
|
>>> tgt = X(value=42)
|
|
>>> c = link((src, "value"), (tgt, "value"))
|
|
|
|
Setting source updates target objects:
|
|
>>> src.value = 5
|
|
>>> tgt.value
|
|
5
|
|
"""
|
|
|
|
updating = False
|
|
|
|
def __init__(self, source: t.Any, target: t.Any, transform: t.Any = None) -> None:
|
|
_validate_link(source, target)
|
|
self.source, self.target = source, target
|
|
self._transform, self._transform_inv = transform if transform else (lambda x: x,) * 2
|
|
|
|
self.link()
|
|
|
|
def link(self) -> None:
|
|
try:
|
|
setattr(
|
|
self.target[0],
|
|
self.target[1],
|
|
self._transform(getattr(self.source[0], self.source[1])),
|
|
)
|
|
|
|
finally:
|
|
self.source[0].observe(self._update_target, names=self.source[1])
|
|
self.target[0].observe(self._update_source, names=self.target[1])
|
|
|
|
@contextlib.contextmanager
|
|
def _busy_updating(self) -> t.Any:
|
|
self.updating = True
|
|
try:
|
|
yield
|
|
finally:
|
|
self.updating = False
|
|
|
|
def _update_target(self, change: t.Any) -> None:
|
|
if self.updating:
|
|
return
|
|
with self._busy_updating():
|
|
setattr(self.target[0], self.target[1], self._transform(change.new))
|
|
if getattr(self.source[0], self.source[1]) != change.new:
|
|
raise TraitError(
|
|
f"Broken link {self}: the source value changed while updating " "the target."
|
|
)
|
|
|
|
def _update_source(self, change: t.Any) -> None:
|
|
if self.updating:
|
|
return
|
|
with self._busy_updating():
|
|
setattr(self.source[0], self.source[1], self._transform_inv(change.new))
|
|
if getattr(self.target[0], self.target[1]) != change.new:
|
|
raise TraitError(
|
|
f"Broken link {self}: the target value changed while updating " "the source."
|
|
)
|
|
|
|
def unlink(self) -> None:
|
|
self.source[0].unobserve(self._update_target, names=self.source[1])
|
|
self.target[0].unobserve(self._update_source, names=self.target[1])
|
|
|
|
|
|
class directional_link:
|
|
"""Link the trait of a source object with traits of target objects.
|
|
|
|
Parameters
|
|
----------
|
|
source : (object, attribute name) pair
|
|
target : (object, attribute name) pair
|
|
transform: callable (optional)
|
|
Data transformation between source and target.
|
|
|
|
Examples
|
|
--------
|
|
>>> class X(HasTraits):
|
|
... value = Int()
|
|
|
|
>>> src = X(value=1)
|
|
>>> tgt = X(value=42)
|
|
>>> c = directional_link((src, "value"), (tgt, "value"))
|
|
|
|
Setting source updates target objects:
|
|
>>> src.value = 5
|
|
>>> tgt.value
|
|
5
|
|
|
|
Setting target does not update source object:
|
|
>>> tgt.value = 6
|
|
>>> src.value
|
|
5
|
|
|
|
"""
|
|
|
|
updating = False
|
|
|
|
def __init__(self, source: t.Any, target: t.Any, transform: t.Any = None) -> None:
|
|
self._transform = transform if transform else lambda x: x
|
|
_validate_link(source, target)
|
|
self.source, self.target = source, target
|
|
self.link()
|
|
|
|
def link(self) -> None:
|
|
try:
|
|
setattr(
|
|
self.target[0],
|
|
self.target[1],
|
|
self._transform(getattr(self.source[0], self.source[1])),
|
|
)
|
|
finally:
|
|
self.source[0].observe(self._update, names=self.source[1])
|
|
|
|
@contextlib.contextmanager
|
|
def _busy_updating(self) -> t.Any:
|
|
self.updating = True
|
|
try:
|
|
yield
|
|
finally:
|
|
self.updating = False
|
|
|
|
def _update(self, change: t.Any) -> None:
|
|
if self.updating:
|
|
return
|
|
with self._busy_updating():
|
|
setattr(self.target[0], self.target[1], self._transform(change.new))
|
|
|
|
def unlink(self) -> None:
|
|
self.source[0].unobserve(self._update, names=self.source[1])
|
|
|
|
|
|
dlink = directional_link
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Base Descriptor Class
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class BaseDescriptor:
|
|
"""Base descriptor class
|
|
|
|
Notes
|
|
-----
|
|
This implements Python's descriptor protocol.
|
|
|
|
This class is the base class for all such descriptors. The
|
|
only magic we use is a custom metaclass for the main :class:`HasTraits`
|
|
class that does the following:
|
|
|
|
1. Sets the :attr:`name` attribute of every :class:`BaseDescriptor`
|
|
instance in the class dict to the name of the attribute.
|
|
2. Sets the :attr:`this_class` attribute of every :class:`BaseDescriptor`
|
|
instance in the class dict to the *class* that declared the trait.
|
|
This is used by the :class:`This` trait to allow subclasses to
|
|
accept superclasses for :class:`This` values.
|
|
"""
|
|
|
|
name: str | None = None
|
|
this_class: type[HasTraits] | None = None
|
|
|
|
def class_init(self, cls: type[HasTraits], name: str | None) -> None:
|
|
"""Part of the initialization which may depend on the underlying
|
|
HasDescriptors class.
|
|
|
|
It is typically overloaded for specific types.
|
|
|
|
This method is called by :meth:`MetaHasDescriptors.__init__`
|
|
passing the class (`cls`) and `name` under which the descriptor
|
|
has been assigned.
|
|
"""
|
|
self.this_class = cls
|
|
self.name = name
|
|
|
|
def subclass_init(self, cls: type[HasTraits]) -> None:
|
|
# Instead of HasDescriptors.setup_instance calling
|
|
# every instance_init, we opt in by default.
|
|
# This gives descriptors a change to opt out for
|
|
# performance reasons.
|
|
# Because most traits do not need instance_init,
|
|
# and it will otherwise be called for every HasTrait instance
|
|
# being created, this otherwise gives a significant performance
|
|
# pentalty. Most TypeTraits in traitlets opt out.
|
|
cls._instance_inits.append(self.instance_init)
|
|
|
|
def instance_init(self, obj: t.Any) -> None:
|
|
"""Part of the initialization which may depend on the underlying
|
|
HasDescriptors instance.
|
|
|
|
It is typically overloaded for specific types.
|
|
|
|
This method is called by :meth:`HasTraits.__new__` and in the
|
|
:meth:`BaseDescriptor.instance_init` method of descriptors holding
|
|
other descriptors.
|
|
"""
|
|
|
|
|
|
G = TypeVar("G")
|
|
S = TypeVar("S")
|
|
T = TypeVar("T")
|
|
|
|
|
|
# Self from typing extension doesn't work well with mypy https://github.com/python/mypy/pull/14041
|
|
# see https://peps.python.org/pep-0673/#use-in-generic-classes
|
|
# Self = t.TypeVar("Self", bound="TraitType[Any, Any]")
|
|
if t.TYPE_CHECKING:
|
|
from typing_extensions import Literal, Self
|
|
|
|
K = TypeVar("K", default=str)
|
|
V = TypeVar("V", default=t.Any)
|
|
|
|
|
|
# We use a type for the getter (G) and setter (G) because we allow
|
|
# for traits to cast (for instance CInt will use G=int, S=t.Any)
|
|
class TraitType(BaseDescriptor, t.Generic[G, S]):
|
|
"""A base class for all trait types."""
|
|
|
|
metadata: dict[str, t.Any] = {}
|
|
allow_none: bool = False
|
|
read_only: bool = False
|
|
info_text: str = "any value"
|
|
default_value: t.Any = Undefined
|
|
|
|
def __init__(
|
|
self: TraitType[G, S],
|
|
default_value: t.Any = Undefined,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
config: t.Any = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Declare a traitlet.
|
|
|
|
If *allow_none* is True, None is a valid value in addition to any
|
|
values that are normally valid. The default is up to the subclass.
|
|
For most trait types, the default value for ``allow_none`` is False.
|
|
|
|
If *read_only* is True, attempts to directly modify a trait attribute raises a TraitError.
|
|
|
|
If *help* is a string, it documents the attribute's purpose.
|
|
|
|
Extra metadata can be associated with the traitlet using the .tag() convenience method
|
|
or by using the traitlet instance's .metadata dictionary.
|
|
"""
|
|
if default_value is not Undefined:
|
|
self.default_value = default_value
|
|
if allow_none:
|
|
self.allow_none = allow_none
|
|
if read_only is not None:
|
|
self.read_only = read_only
|
|
self.help = help if help is not None else ""
|
|
if self.help:
|
|
# define __doc__ so that inspectors like autodoc find traits
|
|
self.__doc__ = self.help
|
|
|
|
if len(kwargs) > 0:
|
|
stacklevel = 1
|
|
f = inspect.currentframe()
|
|
# count supers to determine stacklevel for warning
|
|
assert f is not None
|
|
while f.f_code.co_name == "__init__":
|
|
stacklevel += 1
|
|
f = f.f_back
|
|
assert f is not None
|
|
mod = f.f_globals.get("__name__") or ""
|
|
pkg = mod.split(".", 1)[0]
|
|
key = ("metadata-tag", pkg, *sorted(kwargs))
|
|
if should_warn(key):
|
|
warn(
|
|
f"metadata {kwargs} was set from the constructor. "
|
|
"With traitlets 4.1, metadata should be set using the .tag() method, "
|
|
"e.g., Int().tag(key1='value1', key2='value2')",
|
|
DeprecationWarning,
|
|
stacklevel=stacklevel,
|
|
)
|
|
if len(self.metadata) > 0:
|
|
self.metadata = self.metadata.copy()
|
|
self.metadata.update(kwargs)
|
|
else:
|
|
self.metadata = kwargs
|
|
else:
|
|
self.metadata = self.metadata.copy()
|
|
if config is not None:
|
|
self.metadata["config"] = config
|
|
|
|
# We add help to the metadata during a deprecation period so that
|
|
# code that looks for the help string there can find it.
|
|
if help is not None:
|
|
self.metadata["help"] = help
|
|
|
|
def from_string(self, s: str) -> G | None:
|
|
"""Get a value from a config string
|
|
|
|
such as an environment variable or CLI arguments.
|
|
|
|
Traits can override this method to define their own
|
|
parsing of config strings.
|
|
|
|
.. seealso:: item_from_string
|
|
|
|
.. versionadded:: 5.0
|
|
"""
|
|
if self.allow_none and s == "None":
|
|
return None
|
|
return s # type:ignore[return-value]
|
|
|
|
def default(self, obj: t.Any = None) -> G | None:
|
|
"""The default generator for this trait
|
|
|
|
Notes
|
|
-----
|
|
This method is registered to HasTraits classes during ``class_init``
|
|
in the same way that dynamic defaults defined by ``@default`` are.
|
|
"""
|
|
if self.default_value is not Undefined:
|
|
return t.cast(G, self.default_value)
|
|
elif hasattr(self, "make_dynamic_default"):
|
|
return t.cast(G, self.make_dynamic_default())
|
|
else:
|
|
# Undefined will raise in TraitType.get
|
|
return t.cast(G, self.default_value)
|
|
|
|
def get_default_value(self) -> G | None:
|
|
"""DEPRECATED: Retrieve the static default value for this trait.
|
|
Use self.default_value instead
|
|
"""
|
|
warn(
|
|
"get_default_value is deprecated in traitlets 4.0: use the .default_value attribute",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return t.cast(G, self.default_value)
|
|
|
|
def init_default_value(self, obj: t.Any) -> G | None:
|
|
"""DEPRECATED: Set the static default value for the trait type."""
|
|
warn(
|
|
"init_default_value is deprecated in traitlets 4.0, and may be removed in the future",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
value = self._validate(obj, self.default_value)
|
|
obj._trait_values[self.name] = value
|
|
return value
|
|
|
|
def get(self, obj: HasTraits, cls: type[t.Any] | None = None) -> G | None:
|
|
assert self.name is not None
|
|
try:
|
|
value = obj._trait_values[self.name]
|
|
except KeyError:
|
|
# Check for a dynamic initializer.
|
|
default = obj.trait_defaults(self.name)
|
|
if default is Undefined:
|
|
warn(
|
|
"Explicit using of Undefined as the default value "
|
|
"is deprecated in traitlets 5.0, and may cause "
|
|
"exceptions in the future.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
# Using a context manager has a large runtime overhead, so we
|
|
# write out the obj.cross_validation_lock call here.
|
|
_cross_validation_lock = obj._cross_validation_lock
|
|
try:
|
|
obj._cross_validation_lock = True
|
|
value = self._validate(obj, default)
|
|
finally:
|
|
obj._cross_validation_lock = _cross_validation_lock
|
|
obj._trait_values[self.name] = value
|
|
obj._notify_observers(
|
|
Bunch(
|
|
name=self.name,
|
|
value=value,
|
|
owner=obj,
|
|
type="default",
|
|
)
|
|
)
|
|
return t.cast(G, value)
|
|
except Exception as e:
|
|
# This should never be reached.
|
|
raise TraitError("Unexpected error in TraitType: default value not set properly") from e
|
|
else:
|
|
return t.cast(G, value)
|
|
|
|
@t.overload
|
|
def __get__(self, obj: None, cls: type[t.Any]) -> Self:
|
|
...
|
|
|
|
@t.overload
|
|
def __get__(self, obj: t.Any, cls: type[t.Any]) -> G:
|
|
...
|
|
|
|
def __get__(self, obj: HasTraits | None, cls: type[t.Any]) -> Self | G:
|
|
"""Get the value of the trait by self.name for the instance.
|
|
|
|
Default values are instantiated when :meth:`HasTraits.__new__`
|
|
is called. Thus by the time this method gets called either the
|
|
default value or a user defined value (they called :meth:`__set__`)
|
|
is in the :class:`HasTraits` instance.
|
|
"""
|
|
if obj is None:
|
|
return self
|
|
else:
|
|
return t.cast(G, self.get(obj, cls)) # the G should encode the Optional
|
|
|
|
def set(self, obj: HasTraits, value: S) -> None:
|
|
new_value = self._validate(obj, value)
|
|
assert self.name is not None
|
|
try:
|
|
old_value = obj._trait_values[self.name]
|
|
except KeyError:
|
|
old_value = self.default_value
|
|
|
|
obj._trait_values[self.name] = new_value
|
|
try:
|
|
silent = bool(old_value == new_value)
|
|
except Exception:
|
|
# if there is an error in comparing, default to notify
|
|
silent = False
|
|
if silent is not True:
|
|
# we explicitly compare silent to True just in case the equality
|
|
# comparison above returns something other than True/False
|
|
obj._notify_trait(self.name, old_value, new_value)
|
|
|
|
def __set__(self, obj: HasTraits, value: S) -> None:
|
|
"""Set the value of the trait by self.name for the instance.
|
|
|
|
Values pass through a validation stage where errors are raised when
|
|
impropper types, or types that cannot be coerced, are encountered.
|
|
"""
|
|
if self.read_only:
|
|
raise TraitError('The "%s" trait is read-only.' % self.name)
|
|
self.set(obj, value)
|
|
|
|
def _validate(self, obj: t.Any, value: t.Any) -> G | None:
|
|
if value is None and self.allow_none:
|
|
return value
|
|
if hasattr(self, "validate"):
|
|
value = self.validate(obj, value)
|
|
if obj._cross_validation_lock is False:
|
|
value = self._cross_validate(obj, value)
|
|
return t.cast(G, value)
|
|
|
|
def _cross_validate(self, obj: t.Any, value: t.Any) -> G | None:
|
|
if self.name in obj._trait_validators:
|
|
proposal = Bunch({"trait": self, "value": value, "owner": obj})
|
|
value = obj._trait_validators[self.name](obj, proposal)
|
|
elif hasattr(obj, "_%s_validate" % self.name):
|
|
meth_name = "_%s_validate" % self.name
|
|
cross_validate = getattr(obj, meth_name)
|
|
deprecated_method(
|
|
cross_validate,
|
|
obj.__class__,
|
|
meth_name,
|
|
"use @validate decorator instead.",
|
|
)
|
|
value = cross_validate(value, self)
|
|
return t.cast(G, value)
|
|
|
|
def __or__(self, other: TraitType[t.Any, t.Any]) -> Union:
|
|
if isinstance(other, Union):
|
|
return Union([self, *other.trait_types])
|
|
else:
|
|
return Union([self, other])
|
|
|
|
def info(self) -> str:
|
|
return self.info_text
|
|
|
|
def error(
|
|
self,
|
|
obj: HasTraits | None,
|
|
value: t.Any,
|
|
error: Exception | None = None,
|
|
info: str | None = None,
|
|
) -> t.NoReturn:
|
|
"""Raise a TraitError
|
|
|
|
Parameters
|
|
----------
|
|
obj : HasTraits or None
|
|
The instance which owns the trait. If not
|
|
object is given, then an object agnostic
|
|
error will be raised.
|
|
value : any
|
|
The value that caused the error.
|
|
error : Exception (default: None)
|
|
An error that was raised by a child trait.
|
|
The arguments of this exception should be
|
|
of the form ``(value, info, *traits)``.
|
|
Where the ``value`` and ``info`` are the
|
|
problem value, and string describing the
|
|
expected value. The ``traits`` are a series
|
|
of :class:`TraitType` instances that are
|
|
"children" of this one (the first being
|
|
the deepest).
|
|
info : str (default: None)
|
|
A description of the expected value. By
|
|
default this is inferred from this trait's
|
|
``info`` method.
|
|
"""
|
|
if error is not None:
|
|
# handle nested error
|
|
error.args += (self,)
|
|
if self.name is not None:
|
|
# this is the root trait that must format the final message
|
|
chain = " of ".join(describe("a", t) for t in error.args[2:])
|
|
if obj is not None:
|
|
error.args = (
|
|
"The '{}' trait of {} instance contains {} which "
|
|
"expected {}, not {}.".format(
|
|
self.name,
|
|
describe("an", obj),
|
|
chain,
|
|
error.args[1],
|
|
describe("the", error.args[0]),
|
|
),
|
|
)
|
|
else:
|
|
error.args = (
|
|
"The '{}' trait contains {} which " "expected {}, not {}.".format(
|
|
self.name,
|
|
chain,
|
|
error.args[1],
|
|
describe("the", error.args[0]),
|
|
),
|
|
)
|
|
raise error
|
|
|
|
# this trait caused an error
|
|
if self.name is None:
|
|
# this is not the root trait
|
|
raise TraitError(value, info or self.info(), self)
|
|
|
|
# this is the root trait
|
|
if obj is not None:
|
|
e = "The '{}' trait of {} instance expected {}, not {}.".format(
|
|
self.name,
|
|
class_of(obj),
|
|
info or self.info(),
|
|
describe("the", value),
|
|
)
|
|
else:
|
|
e = "The '{}' trait expected {}, not {}.".format(
|
|
self.name,
|
|
info or self.info(),
|
|
describe("the", value),
|
|
)
|
|
raise TraitError(e)
|
|
|
|
def get_metadata(self, key: str, default: t.Any = None) -> t.Any:
|
|
"""DEPRECATED: Get a metadata value.
|
|
|
|
Use .metadata[key] or .metadata.get(key, default) instead.
|
|
"""
|
|
if key == "help":
|
|
msg = "use the instance .help string directly, like x.help"
|
|
else:
|
|
msg = "use the instance .metadata dictionary directly, like x.metadata[key] or x.metadata.get(key, default)"
|
|
warn("Deprecated in traitlets 4.1, " + msg, DeprecationWarning, stacklevel=2)
|
|
return self.metadata.get(key, default)
|
|
|
|
def set_metadata(self, key: str, value: t.Any) -> None:
|
|
"""DEPRECATED: Set a metadata key/value.
|
|
|
|
Use .metadata[key] = value instead.
|
|
"""
|
|
if key == "help":
|
|
msg = "use the instance .help string directly, like x.help = value"
|
|
else:
|
|
msg = "use the instance .metadata dictionary directly, like x.metadata[key] = value"
|
|
warn("Deprecated in traitlets 4.1, " + msg, DeprecationWarning, stacklevel=2)
|
|
self.metadata[key] = value
|
|
|
|
def tag(self, **metadata: t.Any) -> Self:
|
|
"""Sets metadata and returns self.
|
|
|
|
This allows convenient metadata tagging when initializing the trait, such as:
|
|
|
|
Examples
|
|
--------
|
|
>>> Int(0).tag(config=True, sync=True)
|
|
<traitlets.traitlets.Int object at ...>
|
|
|
|
"""
|
|
maybe_constructor_keywords = set(metadata.keys()).intersection(
|
|
{"help", "allow_none", "read_only", "default_value"}
|
|
)
|
|
if maybe_constructor_keywords:
|
|
warn(
|
|
"The following attributes are set in using `tag`, but seem to be constructor keywords arguments: %s "
|
|
% maybe_constructor_keywords,
|
|
UserWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
self.metadata.update(metadata)
|
|
return self
|
|
|
|
def default_value_repr(self) -> str:
|
|
return repr(self.default_value)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# The HasTraits implementation
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class _CallbackWrapper:
|
|
"""An object adapting a on_trait_change callback into an observe callback.
|
|
|
|
The comparison operator __eq__ is implemented to enable removal of wrapped
|
|
callbacks.
|
|
"""
|
|
|
|
def __init__(self, cb: t.Any) -> None:
|
|
self.cb = cb
|
|
# Bound methods have an additional 'self' argument.
|
|
offset = -1 if isinstance(self.cb, types.MethodType) else 0
|
|
self.nargs = len(getargspec(cb)[0]) + offset
|
|
if self.nargs > 4:
|
|
raise TraitError("a trait changed callback must have 0-4 arguments.")
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
# The wrapper is equal to the wrapped element
|
|
if isinstance(other, _CallbackWrapper):
|
|
return bool(self.cb == other.cb)
|
|
else:
|
|
return bool(self.cb == other)
|
|
|
|
def __call__(self, change: Bunch) -> None:
|
|
# The wrapper is callable
|
|
if self.nargs == 0:
|
|
self.cb()
|
|
elif self.nargs == 1:
|
|
self.cb(change.name)
|
|
elif self.nargs == 2:
|
|
self.cb(change.name, change.new)
|
|
elif self.nargs == 3:
|
|
self.cb(change.name, change.old, change.new)
|
|
elif self.nargs == 4:
|
|
self.cb(change.name, change.old, change.new, change.owner)
|
|
|
|
|
|
def _callback_wrapper(cb: t.Any) -> _CallbackWrapper:
|
|
if isinstance(cb, _CallbackWrapper):
|
|
return cb
|
|
else:
|
|
return _CallbackWrapper(cb)
|
|
|
|
|
|
class MetaHasDescriptors(type):
|
|
"""A metaclass for HasDescriptors.
|
|
|
|
This metaclass makes sure that any TraitType class attributes are
|
|
instantiated and sets their name attribute.
|
|
"""
|
|
|
|
def __new__(
|
|
mcls: type[MetaHasDescriptors],
|
|
name: str,
|
|
bases: tuple[type, ...],
|
|
classdict: dict[str, t.Any],
|
|
**kwds: t.Any,
|
|
) -> MetaHasDescriptors:
|
|
"""Create the HasDescriptors class."""
|
|
for k, v in classdict.items():
|
|
# ----------------------------------------------------------------
|
|
# Support of deprecated behavior allowing for TraitType types
|
|
# to be used instead of TraitType instances.
|
|
if inspect.isclass(v) and issubclass(v, TraitType):
|
|
warn(
|
|
"Traits should be given as instances, not types (for example, `Int()`, not `Int`)."
|
|
" Passing types is deprecated in traitlets 4.1.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
classdict[k] = v()
|
|
# ----------------------------------------------------------------
|
|
|
|
return super().__new__(mcls, name, bases, classdict, **kwds)
|
|
|
|
def __init__(
|
|
cls, name: str, bases: tuple[type, ...], classdict: dict[str, t.Any], **kwds: t.Any
|
|
) -> None:
|
|
"""Finish initializing the HasDescriptors class."""
|
|
super().__init__(name, bases, classdict, **kwds)
|
|
cls.setup_class(classdict)
|
|
|
|
def setup_class(cls: MetaHasDescriptors, classdict: dict[str, t.Any]) -> None:
|
|
"""Setup descriptor instance on the class
|
|
|
|
This sets the :attr:`this_class` and :attr:`name` attributes of each
|
|
BaseDescriptor in the class dict of the newly created ``cls`` before
|
|
calling their :attr:`class_init` method.
|
|
"""
|
|
cls._descriptors = []
|
|
cls._instance_inits: list[t.Any] = []
|
|
for k, v in classdict.items():
|
|
if isinstance(v, BaseDescriptor):
|
|
v.class_init(cls, k) # type:ignore[arg-type]
|
|
|
|
for _, v in getmembers(cls):
|
|
if isinstance(v, BaseDescriptor):
|
|
v.subclass_init(cls) # type:ignore[arg-type]
|
|
cls._descriptors.append(v)
|
|
|
|
|
|
class MetaHasTraits(MetaHasDescriptors):
|
|
"""A metaclass for HasTraits."""
|
|
|
|
def setup_class(cls: MetaHasTraits, classdict: dict[str, t.Any]) -> None:
|
|
# for only the current class
|
|
cls._trait_default_generators: dict[str, t.Any] = {}
|
|
# also looking at base classes
|
|
cls._all_trait_default_generators = {}
|
|
cls._traits = {}
|
|
cls._static_immutable_initial_values = {}
|
|
|
|
super().setup_class(classdict)
|
|
|
|
mro = cls.mro()
|
|
|
|
for name in dir(cls):
|
|
# Some descriptors raise AttributeError like zope.interface's
|
|
# __provides__ attributes even though they exist. This causes
|
|
# AttributeErrors even though they are listed in dir(cls).
|
|
try:
|
|
value = getattr(cls, name)
|
|
except AttributeError:
|
|
continue
|
|
if isinstance(value, TraitType):
|
|
cls._traits[name] = value
|
|
trait = value
|
|
default_method_name = "_%s_default" % name
|
|
mro_trait = mro
|
|
try:
|
|
mro_trait = mro[: mro.index(trait.this_class) + 1] # type:ignore[arg-type]
|
|
except ValueError:
|
|
# this_class not in mro
|
|
pass
|
|
for c in mro_trait:
|
|
if default_method_name in c.__dict__:
|
|
cls._all_trait_default_generators[name] = c.__dict__[default_method_name]
|
|
break
|
|
if name in c.__dict__.get("_trait_default_generators", {}):
|
|
cls._all_trait_default_generators[name] = c._trait_default_generators[name] # type: ignore[attr-defined]
|
|
break
|
|
else:
|
|
# We don't have a dynamic default generator using @default etc.
|
|
# Now if the default value is not dynamic and immutable (string, number)
|
|
# and does not require any validation, we keep them in a dict
|
|
# of initial values to speed up instance creation.
|
|
# This is a very specific optimization, but a very common scenario in
|
|
# for instance ipywidgets.
|
|
none_ok = trait.default_value is None and trait.allow_none
|
|
if (
|
|
type(trait) in [CInt, Int]
|
|
and trait.min is None # type: ignore[attr-defined]
|
|
and trait.max is None # type: ignore[attr-defined]
|
|
and (isinstance(trait.default_value, int) or none_ok)
|
|
):
|
|
cls._static_immutable_initial_values[name] = trait.default_value
|
|
elif (
|
|
type(trait) in [CFloat, Float]
|
|
and trait.min is None # type: ignore[attr-defined]
|
|
and trait.max is None # type: ignore[attr-defined]
|
|
and (isinstance(trait.default_value, float) or none_ok)
|
|
):
|
|
cls._static_immutable_initial_values[name] = trait.default_value
|
|
elif type(trait) in [CBool, Bool] and (
|
|
isinstance(trait.default_value, bool) or none_ok
|
|
):
|
|
cls._static_immutable_initial_values[name] = trait.default_value
|
|
elif type(trait) in [CUnicode, Unicode] and (
|
|
isinstance(trait.default_value, str) or none_ok
|
|
):
|
|
cls._static_immutable_initial_values[name] = trait.default_value
|
|
elif type(trait) == Any and (
|
|
isinstance(trait.default_value, (str, int, float, bool)) or none_ok
|
|
):
|
|
cls._static_immutable_initial_values[name] = trait.default_value
|
|
elif type(trait) == Union and trait.default_value is None:
|
|
cls._static_immutable_initial_values[name] = None
|
|
elif (
|
|
isinstance(trait, Instance)
|
|
and trait.default_args is None
|
|
and trait.default_kwargs is None
|
|
and trait.allow_none
|
|
):
|
|
cls._static_immutable_initial_values[name] = None
|
|
|
|
# we always add it, because a class may change when we call add_trait
|
|
# and then the instance may not have all the _static_immutable_initial_values
|
|
cls._all_trait_default_generators[name] = trait.default
|
|
|
|
|
|
def observe(*names: Sentinel | str, type: str = "change") -> ObserveHandler:
|
|
"""A decorator which can be used to observe Traits on a class.
|
|
|
|
The handler passed to the decorator will be called with one ``change``
|
|
dict argument. The change dictionary at least holds a 'type' key and a
|
|
'name' key, corresponding respectively to the type of notification and the
|
|
name of the attribute that triggered the notification.
|
|
|
|
Other keys may be passed depending on the value of 'type'. In the case
|
|
where type is 'change', we also have the following keys:
|
|
* ``owner`` : the HasTraits instance
|
|
* ``old`` : the old value of the modified trait attribute
|
|
* ``new`` : the new value of the modified trait attribute
|
|
* ``name`` : the name of the modified trait attribute.
|
|
|
|
Parameters
|
|
----------
|
|
*names
|
|
The str names of the Traits to observe on the object.
|
|
type : str, kwarg-only
|
|
The type of event to observe (e.g. 'change')
|
|
"""
|
|
if not names:
|
|
raise TypeError("Please specify at least one trait name to observe.")
|
|
for name in names:
|
|
if name is not All and not isinstance(name, str):
|
|
raise TypeError("trait names to observe must be strings or All, not %r" % name)
|
|
return ObserveHandler(names, type=type)
|
|
|
|
|
|
def observe_compat(func: FuncT) -> FuncT:
|
|
"""Backward-compatibility shim decorator for observers
|
|
|
|
Use with:
|
|
|
|
@observe('name')
|
|
@observe_compat
|
|
def _foo_changed(self, change):
|
|
...
|
|
|
|
With this, `super()._foo_changed(self, name, old, new)` in subclasses will still work.
|
|
Allows adoption of new observer API without breaking subclasses that override and super.
|
|
"""
|
|
|
|
def compatible_observer(
|
|
self: t.Any, change_or_name: str, old: t.Any = Undefined, new: t.Any = Undefined
|
|
) -> t.Any:
|
|
if isinstance(change_or_name, dict): # type:ignore[unreachable]
|
|
change = Bunch(change_or_name) # type:ignore[unreachable]
|
|
else:
|
|
clsname = self.__class__.__name__
|
|
warn(
|
|
f"A parent of {clsname}._{change_or_name}_changed has adopted the new (traitlets 4.1) @observe(change) API",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
change = Bunch(
|
|
type="change",
|
|
old=old,
|
|
new=new,
|
|
name=change_or_name,
|
|
owner=self,
|
|
)
|
|
return func(self, change)
|
|
|
|
return t.cast(FuncT, compatible_observer)
|
|
|
|
|
|
def validate(*names: Sentinel | str) -> ValidateHandler:
|
|
"""A decorator to register cross validator of HasTraits object's state
|
|
when a Trait is set.
|
|
|
|
The handler passed to the decorator must have one ``proposal`` dict argument.
|
|
The proposal dictionary must hold the following keys:
|
|
|
|
* ``owner`` : the HasTraits instance
|
|
* ``value`` : the proposed value for the modified trait attribute
|
|
* ``trait`` : the TraitType instance associated with the attribute
|
|
|
|
Parameters
|
|
----------
|
|
*names
|
|
The str names of the Traits to validate.
|
|
|
|
Notes
|
|
-----
|
|
Since the owner has access to the ``HasTraits`` instance via the 'owner' key,
|
|
the registered cross validator could potentially make changes to attributes
|
|
of the ``HasTraits`` instance. However, we recommend not to do so. The reason
|
|
is that the cross-validation of attributes may run in arbitrary order when
|
|
exiting the ``hold_trait_notifications`` context, and such changes may not
|
|
commute.
|
|
"""
|
|
if not names:
|
|
raise TypeError("Please specify at least one trait name to validate.")
|
|
for name in names:
|
|
if name is not All and not isinstance(name, str):
|
|
raise TypeError("trait names to validate must be strings or All, not %r" % name)
|
|
return ValidateHandler(names)
|
|
|
|
|
|
def default(name: str) -> DefaultHandler:
|
|
"""A decorator which assigns a dynamic default for a Trait on a HasTraits object.
|
|
|
|
Parameters
|
|
----------
|
|
name
|
|
The str name of the Trait on the object whose default should be generated.
|
|
|
|
Notes
|
|
-----
|
|
Unlike observers and validators which are properties of the HasTraits
|
|
instance, default value generators are class-level properties.
|
|
|
|
Besides, default generators are only invoked if they are registered in
|
|
subclasses of `this_type`.
|
|
|
|
::
|
|
|
|
class A(HasTraits):
|
|
bar = Int()
|
|
|
|
@default('bar')
|
|
def get_bar_default(self):
|
|
return 11
|
|
|
|
class B(A):
|
|
bar = Float() # This trait ignores the default generator defined in
|
|
# the base class A
|
|
|
|
class C(B):
|
|
|
|
@default('bar')
|
|
def some_other_default(self): # This default generator should not be
|
|
return 3.0 # ignored since it is defined in a
|
|
# class derived from B.a.this_class.
|
|
"""
|
|
if not isinstance(name, str):
|
|
raise TypeError("Trait name must be a string or All, not %r" % name)
|
|
return DefaultHandler(name)
|
|
|
|
|
|
FuncT = t.TypeVar("FuncT", bound=t.Callable[..., t.Any])
|
|
|
|
|
|
class EventHandler(BaseDescriptor):
|
|
def _init_call(self, func: FuncT) -> EventHandler:
|
|
self.func = func
|
|
return self
|
|
|
|
@t.overload
|
|
def __call__(self, func: FuncT, *args: t.Any, **kwargs: t.Any) -> FuncT:
|
|
...
|
|
|
|
@t.overload
|
|
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
...
|
|
|
|
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
"""Pass `*args` and `**kwargs` to the handler's function if it exists."""
|
|
if hasattr(self, "func"):
|
|
return self.func(*args, **kwargs)
|
|
else:
|
|
return self._init_call(*args, **kwargs)
|
|
|
|
def __get__(self, inst: t.Any, cls: t.Any = None) -> types.MethodType | EventHandler:
|
|
if inst is None:
|
|
return self
|
|
return types.MethodType(self.func, inst)
|
|
|
|
|
|
class ObserveHandler(EventHandler):
|
|
def __init__(self, names: tuple[Sentinel | str, ...], type: str = "") -> None:
|
|
self.trait_names = names
|
|
self.type = type
|
|
|
|
def instance_init(self, inst: HasTraits) -> None:
|
|
inst.observe(self, self.trait_names, type=self.type)
|
|
|
|
|
|
class ValidateHandler(EventHandler):
|
|
def __init__(self, names: tuple[Sentinel | str, ...]) -> None:
|
|
self.trait_names = names
|
|
|
|
def instance_init(self, inst: HasTraits) -> None:
|
|
inst._register_validator(self, self.trait_names)
|
|
|
|
|
|
class DefaultHandler(EventHandler):
|
|
def __init__(self, name: str) -> None:
|
|
self.trait_name = name
|
|
|
|
def class_init(self, cls: type[HasTraits], name: str | None) -> None:
|
|
super().class_init(cls, name)
|
|
cls._trait_default_generators[self.trait_name] = self
|
|
|
|
|
|
class HasDescriptors(metaclass=MetaHasDescriptors):
|
|
"""The base class for all classes that have descriptors."""
|
|
|
|
def __new__(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
# Pass cls as args[0] to allow "cls" as keyword argument
|
|
cls = args[0]
|
|
args = args[1:]
|
|
|
|
# This is needed because object.__new__ only accepts
|
|
# the cls argument.
|
|
new_meth = super(HasDescriptors, cls).__new__
|
|
if new_meth is object.__new__:
|
|
inst = new_meth(cls)
|
|
else:
|
|
inst = new_meth(cls, *args, **kwargs)
|
|
inst.setup_instance(*args, **kwargs)
|
|
return inst
|
|
|
|
def setup_instance(*args: t.Any, **kwargs: t.Any) -> None:
|
|
"""
|
|
This is called **before** self.__init__ is called.
|
|
"""
|
|
# Pass self as args[0] to allow "self" as keyword argument
|
|
self = args[0]
|
|
args = args[1:]
|
|
|
|
self._cross_validation_lock = False
|
|
cls = self.__class__
|
|
# Let descriptors performance initialization when a HasDescriptor
|
|
# instance is created. This allows registration of observers and
|
|
# default creations or other bookkeepings.
|
|
# Note that descriptors can opt-out of this behavior by overriding
|
|
# subclass_init.
|
|
for init in cls._instance_inits:
|
|
init(self)
|
|
|
|
|
|
class HasTraits(HasDescriptors, metaclass=MetaHasTraits):
|
|
_trait_values: dict[str, t.Any]
|
|
_static_immutable_initial_values: dict[str, t.Any]
|
|
_trait_notifiers: dict[str | Sentinel, t.Any]
|
|
_trait_validators: dict[str | Sentinel, t.Any]
|
|
_cross_validation_lock: bool
|
|
_traits: dict[str, t.Any]
|
|
_all_trait_default_generators: dict[str, t.Any]
|
|
|
|
def setup_instance(*args: t.Any, **kwargs: t.Any) -> None:
|
|
# Pass self as args[0] to allow "self" as keyword argument
|
|
self = args[0]
|
|
args = args[1:]
|
|
|
|
# although we'd prefer to set only the initial values not present
|
|
# in kwargs, we will overwrite them in `__init__`, and simply making
|
|
# a copy of a dict is faster than checking for each key.
|
|
self._trait_values = self._static_immutable_initial_values.copy()
|
|
self._trait_notifiers = {}
|
|
self._trait_validators = {}
|
|
self._cross_validation_lock = False
|
|
super(HasTraits, self).setup_instance(*args, **kwargs)
|
|
|
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
# Allow trait values to be set using keyword arguments.
|
|
# We need to use setattr for this to trigger validation and
|
|
# notifications.
|
|
super_args = args
|
|
super_kwargs = {}
|
|
|
|
if kwargs:
|
|
# this is a simplified (and faster) version of
|
|
# the hold_trait_notifications(self) context manager
|
|
def ignore(change: Bunch) -> None:
|
|
pass
|
|
|
|
self.notify_change = ignore # type:ignore[method-assign]
|
|
self._cross_validation_lock = True
|
|
changes = {}
|
|
for key, value in kwargs.items():
|
|
if self.has_trait(key):
|
|
setattr(self, key, value)
|
|
changes[key] = Bunch(
|
|
name=key,
|
|
old=None,
|
|
new=value,
|
|
owner=self,
|
|
type="change",
|
|
)
|
|
else:
|
|
# passthrough args that don't set traits to super
|
|
super_kwargs[key] = value
|
|
# notify and cross validate all trait changes that were set in kwargs
|
|
changed = set(kwargs) & set(self._traits)
|
|
for key in changed:
|
|
value = self._traits[key]._cross_validate(self, getattr(self, key))
|
|
self.set_trait(key, value)
|
|
changes[key]["new"] = value
|
|
self._cross_validation_lock = False
|
|
# Restore method retrieval from class
|
|
del self.notify_change
|
|
for key in changed:
|
|
self.notify_change(changes[key])
|
|
|
|
try:
|
|
super().__init__(*super_args, **super_kwargs)
|
|
except TypeError as e:
|
|
arg_s_list = [repr(arg) for arg in super_args]
|
|
for k, v in super_kwargs.items():
|
|
arg_s_list.append(f"{k}={v!r}")
|
|
arg_s = ", ".join(arg_s_list)
|
|
warn(
|
|
"Passing unrecognized arguments to super({classname}).__init__({arg_s}).\n"
|
|
"{error}\n"
|
|
"This is deprecated in traitlets 4.2."
|
|
"This error will be raised in a future release of traitlets.".format(
|
|
arg_s=arg_s,
|
|
classname=self.__class__.__name__,
|
|
error=e,
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
def __getstate__(self) -> dict[str, t.Any]:
|
|
d = self.__dict__.copy()
|
|
# event handlers stored on an instance are
|
|
# expected to be reinstantiated during a
|
|
# recall of instance_init during __setstate__
|
|
d["_trait_notifiers"] = {}
|
|
d["_trait_validators"] = {}
|
|
d["_trait_values"] = self._trait_values.copy()
|
|
d["_cross_validation_lock"] = False # FIXME: raise if cloning locked!
|
|
|
|
return d
|
|
|
|
def __setstate__(self, state: dict[str, t.Any]) -> None:
|
|
self.__dict__ = state.copy()
|
|
|
|
# event handlers are reassigned to self
|
|
cls = self.__class__
|
|
for key in dir(cls):
|
|
# Some descriptors raise AttributeError like zope.interface's
|
|
# __provides__ attributes even though they exist. This causes
|
|
# AttributeErrors even though they are listed in dir(cls).
|
|
try:
|
|
value = getattr(cls, key)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if isinstance(value, EventHandler):
|
|
value.instance_init(self)
|
|
|
|
@property
|
|
@contextlib.contextmanager
|
|
def cross_validation_lock(self) -> t.Any:
|
|
"""
|
|
A contextmanager for running a block with our cross validation lock set
|
|
to True.
|
|
|
|
At the end of the block, the lock's value is restored to its value
|
|
prior to entering the block.
|
|
"""
|
|
if self._cross_validation_lock:
|
|
yield
|
|
return
|
|
else:
|
|
try:
|
|
self._cross_validation_lock = True
|
|
yield
|
|
finally:
|
|
self._cross_validation_lock = False
|
|
|
|
@contextlib.contextmanager
|
|
def hold_trait_notifications(self) -> t.Any:
|
|
"""Context manager for bundling trait change notifications and cross
|
|
validation.
|
|
|
|
Use this when doing multiple trait assignments (init, config), to avoid
|
|
race conditions in trait notifiers requesting other trait values.
|
|
All trait notifications will fire after all values have been assigned.
|
|
"""
|
|
if self._cross_validation_lock:
|
|
yield
|
|
return
|
|
else:
|
|
cache: dict[str, list[Bunch]] = {}
|
|
|
|
def compress(past_changes: list[Bunch] | None, change: Bunch) -> list[Bunch]:
|
|
"""Merges the provided change with the last if possible."""
|
|
if past_changes is None:
|
|
return [change]
|
|
else:
|
|
if past_changes[-1]["type"] == "change" and change.type == "change":
|
|
past_changes[-1]["new"] = change.new
|
|
else:
|
|
# In case of changes other than 'change', append the notification.
|
|
past_changes.append(change)
|
|
return past_changes
|
|
|
|
def hold(change: Bunch) -> None:
|
|
name = change.name
|
|
cache[name] = compress(cache.get(name), change)
|
|
|
|
try:
|
|
# Replace notify_change with `hold`, caching and compressing
|
|
# notifications, disable cross validation and yield.
|
|
self.notify_change = hold # type:ignore[method-assign]
|
|
self._cross_validation_lock = True
|
|
yield
|
|
# Cross validate final values when context is released.
|
|
for name in list(cache.keys()):
|
|
trait = getattr(self.__class__, name)
|
|
value = trait._cross_validate(self, getattr(self, name))
|
|
self.set_trait(name, value)
|
|
except TraitError as e:
|
|
# Roll back in case of TraitError during final cross validation.
|
|
self.notify_change = lambda x: None # type:ignore[method-assign, assignment] # noqa: ARG005
|
|
for name, changes in cache.items():
|
|
for change in changes[::-1]:
|
|
# TODO: Separate in a rollback function per notification type.
|
|
if change.type == "change":
|
|
if change.old is not Undefined:
|
|
self.set_trait(name, change.old)
|
|
else:
|
|
self._trait_values.pop(name)
|
|
cache = {}
|
|
raise e
|
|
finally:
|
|
self._cross_validation_lock = False
|
|
# Restore method retrieval from class
|
|
del self.notify_change
|
|
|
|
# trigger delayed notifications
|
|
for changes in cache.values():
|
|
for change in changes:
|
|
self.notify_change(change)
|
|
|
|
def _notify_trait(self, name: str, old_value: t.Any, new_value: t.Any) -> None:
|
|
self.notify_change(
|
|
Bunch(
|
|
name=name,
|
|
old=old_value,
|
|
new=new_value,
|
|
owner=self,
|
|
type="change",
|
|
)
|
|
)
|
|
|
|
def notify_change(self, change: Bunch) -> None:
|
|
"""Notify observers of a change event"""
|
|
return self._notify_observers(change)
|
|
|
|
def _notify_observers(self, event: Bunch) -> None:
|
|
"""Notify observers of any event"""
|
|
if not isinstance(event, Bunch):
|
|
# cast to bunch if given a dict
|
|
event = Bunch(event) # type:ignore[unreachable]
|
|
name, type = event["name"], event["type"]
|
|
|
|
callables = []
|
|
if name in self._trait_notifiers:
|
|
callables.extend(self._trait_notifiers.get(name, {}).get(type, []))
|
|
callables.extend(self._trait_notifiers.get(name, {}).get(All, []))
|
|
if All in self._trait_notifiers:
|
|
callables.extend(self._trait_notifiers.get(All, {}).get(type, []))
|
|
callables.extend(self._trait_notifiers.get(All, {}).get(All, []))
|
|
|
|
# Now static ones
|
|
magic_name = "_%s_changed" % name
|
|
if event["type"] == "change" and hasattr(self, magic_name):
|
|
class_value = getattr(self.__class__, magic_name)
|
|
if not isinstance(class_value, ObserveHandler):
|
|
deprecated_method(
|
|
class_value,
|
|
self.__class__,
|
|
magic_name,
|
|
"use @observe and @unobserve instead.",
|
|
)
|
|
cb = getattr(self, magic_name)
|
|
# Only append the magic method if it was not manually registered
|
|
if cb not in callables:
|
|
callables.append(_callback_wrapper(cb))
|
|
|
|
# Call them all now
|
|
# Traits catches and logs errors here. I allow them to raise
|
|
for c in callables:
|
|
# Bound methods have an additional 'self' argument.
|
|
|
|
if isinstance(c, _CallbackWrapper):
|
|
c = c.__call__
|
|
elif isinstance(c, EventHandler) and c.name is not None:
|
|
c = getattr(self, c.name)
|
|
|
|
c(event)
|
|
|
|
def _add_notifiers(
|
|
self, handler: t.Callable[..., t.Any], name: Sentinel | str, type: str | Sentinel
|
|
) -> None:
|
|
if name not in self._trait_notifiers:
|
|
nlist: list[t.Any] = []
|
|
self._trait_notifiers[name] = {type: nlist}
|
|
else:
|
|
if type not in self._trait_notifiers[name]:
|
|
nlist = []
|
|
self._trait_notifiers[name][type] = nlist
|
|
else:
|
|
nlist = self._trait_notifiers[name][type]
|
|
if handler not in nlist:
|
|
nlist.append(handler)
|
|
|
|
def _remove_notifiers(
|
|
self, handler: t.Callable[..., t.Any] | None, name: Sentinel | str, type: str | Sentinel
|
|
) -> None:
|
|
try:
|
|
if handler is None:
|
|
del self._trait_notifiers[name][type]
|
|
else:
|
|
self._trait_notifiers[name][type].remove(handler)
|
|
except KeyError:
|
|
pass
|
|
|
|
def on_trait_change(
|
|
self,
|
|
handler: EventHandler | None = None,
|
|
name: Sentinel | str | None = None,
|
|
remove: bool = False,
|
|
) -> None:
|
|
"""DEPRECATED: Setup a handler to be called when a trait changes.
|
|
|
|
This is used to setup dynamic notifications of trait changes.
|
|
|
|
Static handlers can be created by creating methods on a HasTraits
|
|
subclass with the naming convention '_[traitname]_changed'. Thus,
|
|
to create static handler for the trait 'a', create the method
|
|
_a_changed(self, name, old, new) (fewer arguments can be used, see
|
|
below).
|
|
|
|
If `remove` is True and `handler` is not specified, all change
|
|
handlers for the specified name are uninstalled.
|
|
|
|
Parameters
|
|
----------
|
|
handler : callable, None
|
|
A callable that is called when a trait changes. Its
|
|
signature can be handler(), handler(name), handler(name, new),
|
|
handler(name, old, new), or handler(name, old, new, self).
|
|
name : list, str, None
|
|
If None, the handler will apply to all traits. If a list
|
|
of str, handler will apply to all names in the list. If a
|
|
str, the handler will apply just to that name.
|
|
remove : bool
|
|
If False (the default), then install the handler. If True
|
|
then unintall it.
|
|
"""
|
|
warn(
|
|
"on_trait_change is deprecated in traitlets 4.1: use observe instead",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
if name is None:
|
|
name = All
|
|
if remove:
|
|
self.unobserve(_callback_wrapper(handler), names=name)
|
|
else:
|
|
self.observe(_callback_wrapper(handler), names=name)
|
|
|
|
def observe(
|
|
self,
|
|
handler: t.Callable[..., t.Any],
|
|
names: Sentinel | str | t.Iterable[Sentinel | str] = All,
|
|
type: Sentinel | str = "change",
|
|
) -> None:
|
|
"""Setup a handler to be called when a trait changes.
|
|
|
|
This is used to setup dynamic notifications of trait changes.
|
|
|
|
Parameters
|
|
----------
|
|
handler : callable
|
|
A callable that is called when a trait changes. Its
|
|
signature should be ``handler(change)``, where ``change`` is a
|
|
dictionary. The change dictionary at least holds a 'type' key.
|
|
* ``type``: the type of notification.
|
|
Other keys may be passed depending on the value of 'type'. In the
|
|
case where type is 'change', we also have the following keys:
|
|
* ``owner`` : the HasTraits instance
|
|
* ``old`` : the old value of the modified trait attribute
|
|
* ``new`` : the new value of the modified trait attribute
|
|
* ``name`` : the name of the modified trait attribute.
|
|
names : list, str, All
|
|
If names is All, the handler will apply to all traits. If a list
|
|
of str, handler will apply to all names in the list. If a
|
|
str, the handler will apply just to that name.
|
|
type : str, All (default: 'change')
|
|
The type of notification to filter by. If equal to All, then all
|
|
notifications are passed to the observe handler.
|
|
"""
|
|
for name in parse_notifier_name(names):
|
|
self._add_notifiers(handler, name, type)
|
|
|
|
def unobserve(
|
|
self,
|
|
handler: t.Callable[..., t.Any],
|
|
names: Sentinel | str | t.Iterable[Sentinel | str] = All,
|
|
type: Sentinel | str = "change",
|
|
) -> None:
|
|
"""Remove a trait change handler.
|
|
|
|
This is used to unregister handlers to trait change notifications.
|
|
|
|
Parameters
|
|
----------
|
|
handler : callable
|
|
The callable called when a trait attribute changes.
|
|
names : list, str, All (default: All)
|
|
The names of the traits for which the specified handler should be
|
|
uninstalled. If names is All, the specified handler is uninstalled
|
|
from the list of notifiers corresponding to all changes.
|
|
type : str or All (default: 'change')
|
|
The type of notification to filter by. If All, the specified handler
|
|
is uninstalled from the list of notifiers corresponding to all types.
|
|
"""
|
|
for name in parse_notifier_name(names):
|
|
self._remove_notifiers(handler, name, type)
|
|
|
|
def unobserve_all(self, name: str | t.Any = All) -> None:
|
|
"""Remove trait change handlers of any type for the specified name.
|
|
If name is not specified, removes all trait notifiers."""
|
|
if name is All:
|
|
self._trait_notifiers = {}
|
|
else:
|
|
try:
|
|
del self._trait_notifiers[name]
|
|
except KeyError:
|
|
pass
|
|
|
|
def _register_validator(
|
|
self, handler: t.Callable[..., None], names: tuple[str | Sentinel, ...]
|
|
) -> None:
|
|
"""Setup a handler to be called when a trait should be cross validated.
|
|
|
|
This is used to setup dynamic notifications for cross-validation.
|
|
|
|
If a validator is already registered for any of the provided names, a
|
|
TraitError is raised and no new validator is registered.
|
|
|
|
Parameters
|
|
----------
|
|
handler : callable
|
|
A callable that is called when the given trait is cross-validated.
|
|
Its signature is handler(proposal), where proposal is a Bunch (dictionary with attribute access)
|
|
with the following attributes/keys:
|
|
* ``owner`` : the HasTraits instance
|
|
* ``value`` : the proposed value for the modified trait attribute
|
|
* ``trait`` : the TraitType instance associated with the attribute
|
|
names : List of strings
|
|
The names of the traits that should be cross-validated
|
|
"""
|
|
for name in names:
|
|
magic_name = "_%s_validate" % name
|
|
if hasattr(self, magic_name):
|
|
class_value = getattr(self.__class__, magic_name)
|
|
if not isinstance(class_value, ValidateHandler):
|
|
deprecated_method(
|
|
class_value,
|
|
self.__class__,
|
|
magic_name,
|
|
"use @validate decorator instead.",
|
|
)
|
|
for name in names:
|
|
self._trait_validators[name] = handler
|
|
|
|
def add_traits(self, **traits: t.Any) -> None:
|
|
"""Dynamically add trait attributes to the HasTraits instance."""
|
|
cls = self.__class__
|
|
attrs = {"__module__": cls.__module__}
|
|
if hasattr(cls, "__qualname__"):
|
|
# __qualname__ introduced in Python 3.3 (see PEP 3155)
|
|
attrs["__qualname__"] = cls.__qualname__
|
|
attrs.update(traits)
|
|
self.__class__ = type(cls.__name__, (cls,), attrs)
|
|
for trait in traits.values():
|
|
trait.instance_init(self)
|
|
|
|
def set_trait(self, name: str, value: t.Any) -> None:
|
|
"""Forcibly sets trait attribute, including read-only attributes."""
|
|
cls = self.__class__
|
|
if not self.has_trait(name):
|
|
raise TraitError(f"Class {cls.__name__} does not have a trait named {name}")
|
|
getattr(cls, name).set(self, value)
|
|
|
|
@classmethod
|
|
def class_trait_names(cls: type[HasTraits], **metadata: t.Any) -> list[str]:
|
|
"""Get a list of all the names of this class' traits.
|
|
|
|
This method is just like the :meth:`trait_names` method,
|
|
but is unbound.
|
|
"""
|
|
return list(cls.class_traits(**metadata))
|
|
|
|
@classmethod
|
|
def class_traits(cls: type[HasTraits], **metadata: t.Any) -> dict[str, TraitType[t.Any, t.Any]]:
|
|
"""Get a ``dict`` of all the traits of this class. The dictionary
|
|
is keyed on the name and the values are the TraitType objects.
|
|
|
|
This method is just like the :meth:`traits` method, but is unbound.
|
|
|
|
The TraitTypes returned don't know anything about the values
|
|
that the various HasTrait's instances are holding.
|
|
|
|
The metadata kwargs allow functions to be passed in which
|
|
filter traits based on metadata values. The functions should
|
|
take a single value as an argument and return a boolean. If
|
|
any function returns False, then the trait is not included in
|
|
the output. If a metadata key doesn't exist, None will be passed
|
|
to the function.
|
|
"""
|
|
traits = cls._traits.copy()
|
|
|
|
if len(metadata) == 0:
|
|
return traits
|
|
|
|
result = {}
|
|
for name, trait in traits.items():
|
|
for meta_name, meta_eval in metadata.items():
|
|
if not callable(meta_eval):
|
|
meta_eval = _SimpleTest(meta_eval)
|
|
if not meta_eval(trait.metadata.get(meta_name, None)):
|
|
break
|
|
else:
|
|
result[name] = trait
|
|
|
|
return result
|
|
|
|
@classmethod
|
|
def class_own_traits(
|
|
cls: type[HasTraits], **metadata: t.Any
|
|
) -> dict[str, TraitType[t.Any, t.Any]]:
|
|
"""Get a dict of all the traitlets defined on this class, not a parent.
|
|
|
|
Works like `class_traits`, except for excluding traits from parents.
|
|
"""
|
|
sup = super(cls, cls)
|
|
return {
|
|
n: t
|
|
for (n, t) in cls.class_traits(**metadata).items()
|
|
if getattr(sup, n, None) is not t
|
|
}
|
|
|
|
def has_trait(self, name: str) -> bool:
|
|
"""Returns True if the object has a trait with the specified name."""
|
|
return name in self._traits
|
|
|
|
def trait_has_value(self, name: str) -> bool:
|
|
"""Returns True if the specified trait has a value.
|
|
|
|
This will return false even if ``getattr`` would return a
|
|
dynamically generated default value. These default values
|
|
will be recognized as existing only after they have been
|
|
generated.
|
|
|
|
Example
|
|
|
|
.. code-block:: python
|
|
|
|
class MyClass(HasTraits):
|
|
i = Int()
|
|
|
|
|
|
mc = MyClass()
|
|
assert not mc.trait_has_value("i")
|
|
mc.i # generates a default value
|
|
assert mc.trait_has_value("i")
|
|
"""
|
|
return name in self._trait_values
|
|
|
|
def trait_values(self, **metadata: t.Any) -> dict[str, t.Any]:
|
|
"""A ``dict`` of trait names and their values.
|
|
|
|
The metadata kwargs allow functions to be passed in which
|
|
filter traits based on metadata values. The functions should
|
|
take a single value as an argument and return a boolean. If
|
|
any function returns False, then the trait is not included in
|
|
the output. If a metadata key doesn't exist, None will be passed
|
|
to the function.
|
|
|
|
Returns
|
|
-------
|
|
A ``dict`` of trait names and their values.
|
|
|
|
Notes
|
|
-----
|
|
Trait values are retrieved via ``getattr``, any exceptions raised
|
|
by traits or the operations they may trigger will result in the
|
|
absence of a trait value in the result ``dict``.
|
|
"""
|
|
return {name: getattr(self, name) for name in self.trait_names(**metadata)}
|
|
|
|
def _get_trait_default_generator(self, name: str) -> t.Any:
|
|
"""Return default generator for a given trait
|
|
|
|
Walk the MRO to resolve the correct default generator according to inheritance.
|
|
"""
|
|
method_name = "_%s_default" % name
|
|
if method_name in self.__dict__:
|
|
return getattr(self, method_name)
|
|
if method_name in self.__class__.__dict__:
|
|
return getattr(self.__class__, method_name)
|
|
return self._all_trait_default_generators[name]
|
|
|
|
def trait_defaults(self, *names: str, **metadata: t.Any) -> dict[str, t.Any] | Sentinel:
|
|
"""Return a trait's default value or a dictionary of them
|
|
|
|
Notes
|
|
-----
|
|
Dynamically generated default values may
|
|
depend on the current state of the object."""
|
|
for n in names:
|
|
if not self.has_trait(n):
|
|
raise TraitError(f"'{n}' is not a trait of '{type(self).__name__}' instances")
|
|
|
|
if len(names) == 1 and len(metadata) == 0:
|
|
return t.cast(Sentinel, self._get_trait_default_generator(names[0])(self))
|
|
|
|
trait_names = self.trait_names(**metadata)
|
|
trait_names.extend(names)
|
|
|
|
defaults = {}
|
|
for n in trait_names:
|
|
defaults[n] = self._get_trait_default_generator(n)(self)
|
|
return defaults
|
|
|
|
def trait_names(self, **metadata: t.Any) -> list[str]:
|
|
"""Get a list of all the names of this class' traits."""
|
|
return list(self.traits(**metadata))
|
|
|
|
def traits(self, **metadata: t.Any) -> dict[str, TraitType[t.Any, t.Any]]:
|
|
"""Get a ``dict`` of all the traits of this class. The dictionary
|
|
is keyed on the name and the values are the TraitType objects.
|
|
|
|
The TraitTypes returned don't know anything about the values
|
|
that the various HasTrait's instances are holding.
|
|
|
|
The metadata kwargs allow functions to be passed in which
|
|
filter traits based on metadata values. The functions should
|
|
take a single value as an argument and return a boolean. If
|
|
any function returns False, then the trait is not included in
|
|
the output. If a metadata key doesn't exist, None will be passed
|
|
to the function.
|
|
"""
|
|
traits = self._traits.copy()
|
|
|
|
if len(metadata) == 0:
|
|
return traits
|
|
|
|
result = {}
|
|
for name, trait in traits.items():
|
|
for meta_name, meta_eval in metadata.items():
|
|
if not callable(meta_eval):
|
|
meta_eval = _SimpleTest(meta_eval)
|
|
if not meta_eval(trait.metadata.get(meta_name, None)):
|
|
break
|
|
else:
|
|
result[name] = trait
|
|
|
|
return result
|
|
|
|
def trait_metadata(self, traitname: str, key: str, default: t.Any = None) -> t.Any:
|
|
"""Get metadata values for trait by key."""
|
|
try:
|
|
trait = getattr(self.__class__, traitname)
|
|
except AttributeError as e:
|
|
raise TraitError(
|
|
f"Class {self.__class__.__name__} does not have a trait named {traitname}"
|
|
) from e
|
|
metadata_name = "_" + traitname + "_metadata"
|
|
if hasattr(self, metadata_name) and key in getattr(self, metadata_name):
|
|
return getattr(self, metadata_name).get(key, default)
|
|
else:
|
|
return trait.metadata.get(key, default)
|
|
|
|
@classmethod
|
|
def class_own_trait_events(cls: type[HasTraits], name: str) -> dict[str, EventHandler]:
|
|
"""Get a dict of all event handlers defined on this class, not a parent.
|
|
|
|
Works like ``event_handlers``, except for excluding traits from parents.
|
|
"""
|
|
sup = super(cls, cls)
|
|
return {
|
|
n: e
|
|
for (n, e) in cls.events(name).items() # type:ignore[attr-defined]
|
|
if getattr(sup, n, None) is not e
|
|
}
|
|
|
|
@classmethod
|
|
def trait_events(cls: type[HasTraits], name: str | None = None) -> dict[str, EventHandler]:
|
|
"""Get a ``dict`` of all the event handlers of this class.
|
|
|
|
Parameters
|
|
----------
|
|
name : str (default: None)
|
|
The name of a trait of this class. If name is ``None`` then all
|
|
the event handlers of this class will be returned instead.
|
|
|
|
Returns
|
|
-------
|
|
The event handlers associated with a trait name, or all event handlers.
|
|
"""
|
|
events = {}
|
|
for k, v in getmembers(cls):
|
|
if isinstance(v, EventHandler):
|
|
if name is None:
|
|
events[k] = v
|
|
elif name in v.trait_names: # type:ignore[attr-defined]
|
|
events[k] = v
|
|
elif hasattr(v, "tags"):
|
|
if cls.trait_names(**v.tags):
|
|
events[k] = v
|
|
return events
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Actual TraitTypes implementations/subclasses
|
|
# -----------------------------------------------------------------------------
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# TraitTypes subclasses for handling classes and instances of classes
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ClassBasedTraitType(TraitType[G, S]):
|
|
"""
|
|
A trait with error reporting and string -> type resolution for Type,
|
|
Instance and This.
|
|
"""
|
|
|
|
def _resolve_string(self, string: str) -> t.Any:
|
|
"""
|
|
Resolve a string supplied for a type into an actual object.
|
|
"""
|
|
return import_item(string)
|
|
|
|
|
|
class Type(ClassBasedTraitType[G, S]):
|
|
"""A trait whose value must be a subclass of a specified class."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Type[type, type],
|
|
default_value: Sentinel | None | str = ...,
|
|
klass: None | str = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Type[type | None, type | None],
|
|
default_value: Sentinel | None | str = ...,
|
|
klass: None | str = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Type[S, S],
|
|
default_value: S = ...,
|
|
klass: S = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Type[S | None, S | None],
|
|
default_value: S | None = ...,
|
|
klass: S = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self,
|
|
default_value: t.Any = Undefined,
|
|
klass: t.Any = None,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
config: t.Any | None = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Construct a Type trait
|
|
|
|
A Type trait specifies that its values must be subclasses of
|
|
a particular class.
|
|
|
|
If only ``default_value`` is given, it is used for the ``klass`` as
|
|
well. If neither are given, both default to ``object``.
|
|
|
|
Parameters
|
|
----------
|
|
default_value : class, str or None
|
|
The default value must be a subclass of klass. If an str,
|
|
the str must be a fully specified class name, like 'foo.bar.Bah'.
|
|
The string is resolved into real class, when the parent
|
|
:class:`HasTraits` class is instantiated.
|
|
klass : class, str [ default object ]
|
|
Values of this trait must be a subclass of klass. The klass
|
|
may be specified in a string like: 'foo.bar.MyClass'.
|
|
The string is resolved into real class, when the parent
|
|
:class:`HasTraits` class is instantiated.
|
|
allow_none : bool [ default False ]
|
|
Indicates whether None is allowed as an assignable value.
|
|
**kwargs
|
|
extra kwargs passed to `ClassBasedTraitType`
|
|
"""
|
|
if default_value is Undefined:
|
|
new_default_value = object if (klass is None) else klass
|
|
else:
|
|
new_default_value = default_value
|
|
|
|
if klass is None:
|
|
if (default_value is None) or (default_value is Undefined):
|
|
klass = object
|
|
else:
|
|
klass = default_value
|
|
|
|
if not (inspect.isclass(klass) or isinstance(klass, str)):
|
|
raise TraitError("A Type trait must specify a class.")
|
|
|
|
self.klass = klass
|
|
|
|
super().__init__(
|
|
new_default_value,
|
|
allow_none=allow_none,
|
|
read_only=read_only,
|
|
help=help,
|
|
config=config,
|
|
**kwargs,
|
|
)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
"""Validates that the value is a valid object instance."""
|
|
if isinstance(value, str):
|
|
try:
|
|
value = self._resolve_string(value)
|
|
except ImportError as e:
|
|
raise TraitError(
|
|
f"The '{self.name}' trait of {obj} instance must be a type, but "
|
|
f"{value!r} could not be imported"
|
|
) from e
|
|
try:
|
|
if issubclass(value, self.klass): # type:ignore[arg-type]
|
|
return t.cast(G, value)
|
|
except Exception:
|
|
pass
|
|
|
|
self.error(obj, value)
|
|
|
|
def info(self) -> str:
|
|
"""Returns a description of the trait."""
|
|
if isinstance(self.klass, str):
|
|
klass = self.klass
|
|
else:
|
|
klass = self.klass.__module__ + "." + self.klass.__name__
|
|
result = "a subclass of '%s'" % klass
|
|
if self.allow_none:
|
|
return result + " or None"
|
|
return result
|
|
|
|
def instance_init(self, obj: t.Any) -> None:
|
|
# we can't do this in subclass_init because that
|
|
# might be called before all imports are done.
|
|
self._resolve_classes()
|
|
|
|
def _resolve_classes(self) -> None:
|
|
if isinstance(self.klass, str):
|
|
self.klass = self._resolve_string(self.klass)
|
|
if isinstance(self.default_value, str):
|
|
self.default_value = self._resolve_string(self.default_value)
|
|
|
|
def default_value_repr(self) -> str:
|
|
value = self.default_value
|
|
assert value is not None
|
|
if isinstance(value, str):
|
|
return repr(value)
|
|
else:
|
|
return repr(f"{value.__module__}.{value.__name__}")
|
|
|
|
|
|
class Instance(ClassBasedTraitType[T, T]):
|
|
"""A trait whose value must be an instance of a specified class.
|
|
|
|
The value can also be an instance of a subclass of the specified class.
|
|
|
|
Subclasses can declare default classes by overriding the klass attribute
|
|
"""
|
|
|
|
klass: str | type[T] | None = None
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Instance[T],
|
|
klass: type[T] = ...,
|
|
args: tuple[t.Any, ...] | None = ...,
|
|
kw: dict[str, t.Any] | None = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Instance[T | None],
|
|
klass: type[T] = ...,
|
|
args: tuple[t.Any, ...] | None = ...,
|
|
kw: dict[str, t.Any] | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Instance[t.Any],
|
|
klass: str | None = ...,
|
|
args: tuple[t.Any, ...] | None = ...,
|
|
kw: dict[str, t.Any] | None = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Instance[t.Any | None],
|
|
klass: str | None = ...,
|
|
args: tuple[t.Any, ...] | None = ...,
|
|
kw: dict[str, t.Any] | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self,
|
|
klass: str | type[T] | None = None,
|
|
args: tuple[t.Any, ...] | None = None,
|
|
kw: dict[str, t.Any] | None = None,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Construct an Instance trait.
|
|
|
|
This trait allows values that are instances of a particular
|
|
class or its subclasses. Our implementation is quite different
|
|
from that of enthough.traits as we don't allow instances to be used
|
|
for klass and we handle the ``args`` and ``kw`` arguments differently.
|
|
|
|
Parameters
|
|
----------
|
|
klass : class, str
|
|
The class that forms the basis for the trait. Class names
|
|
can also be specified as strings, like 'foo.bar.Bar'.
|
|
args : tuple
|
|
Positional arguments for generating the default value.
|
|
kw : dict
|
|
Keyword arguments for generating the default value.
|
|
allow_none : bool [ default False ]
|
|
Indicates whether None is allowed as a value.
|
|
**kwargs
|
|
Extra kwargs passed to `ClassBasedTraitType`
|
|
|
|
Notes
|
|
-----
|
|
If both ``args`` and ``kw`` are None, then the default value is None.
|
|
If ``args`` is a tuple and ``kw`` is a dict, then the default is
|
|
created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is
|
|
None, the None is replaced by ``()`` or ``{}``, respectively.
|
|
"""
|
|
if klass is None:
|
|
klass = self.klass
|
|
|
|
if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, str)):
|
|
self.klass = klass
|
|
else:
|
|
raise TraitError("The klass attribute must be a class not: %r" % klass)
|
|
|
|
if (kw is not None) and not isinstance(kw, dict):
|
|
raise TraitError("The 'kw' argument must be a dict or None.")
|
|
if (args is not None) and not isinstance(args, tuple):
|
|
raise TraitError("The 'args' argument must be a tuple or None.")
|
|
|
|
self.default_args = args
|
|
self.default_kwargs = kw
|
|
|
|
super().__init__(allow_none=allow_none, read_only=read_only, help=help, **kwargs)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> T | None:
|
|
assert self.klass is not None
|
|
if self.allow_none and value is None:
|
|
return value
|
|
if isinstance(value, self.klass): # type:ignore[arg-type]
|
|
return t.cast(T, value)
|
|
else:
|
|
self.error(obj, value)
|
|
|
|
def info(self) -> str:
|
|
if isinstance(self.klass, str):
|
|
result = add_article(self.klass)
|
|
else:
|
|
result = describe("a", self.klass)
|
|
if self.allow_none:
|
|
result += " or None"
|
|
return result
|
|
|
|
def instance_init(self, obj: t.Any) -> None:
|
|
# we can't do this in subclass_init because that
|
|
# might be called before all imports are done.
|
|
self._resolve_classes()
|
|
|
|
def _resolve_classes(self) -> None:
|
|
if isinstance(self.klass, str):
|
|
self.klass = self._resolve_string(self.klass)
|
|
|
|
def make_dynamic_default(self) -> T | None:
|
|
if (self.default_args is None) and (self.default_kwargs is None):
|
|
return None
|
|
assert self.klass is not None
|
|
return self.klass(*(self.default_args or ()), **(self.default_kwargs or {})) # type:ignore[operator]
|
|
|
|
def default_value_repr(self) -> str:
|
|
return repr(self.make_dynamic_default())
|
|
|
|
def from_string(self, s: str) -> T | None:
|
|
return t.cast(T, _safe_literal_eval(s))
|
|
|
|
|
|
class ForwardDeclaredMixin:
|
|
"""
|
|
Mixin for forward-declared versions of Instance and Type.
|
|
"""
|
|
|
|
def _resolve_string(self, string: str) -> t.Any:
|
|
"""
|
|
Find the specified class name by looking for it in the module in which
|
|
our this_class attribute was defined.
|
|
"""
|
|
modname = self.this_class.__module__ # type:ignore[attr-defined]
|
|
return import_item(".".join([modname, string]))
|
|
|
|
|
|
class ForwardDeclaredType(ForwardDeclaredMixin, Type[G, S]):
|
|
"""
|
|
Forward-declared version of Type.
|
|
"""
|
|
|
|
|
|
class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance[T]):
|
|
"""
|
|
Forward-declared version of Instance.
|
|
"""
|
|
|
|
|
|
class This(ClassBasedTraitType[t.Optional[T], t.Optional[T]]):
|
|
"""A trait for instances of the class containing this trait.
|
|
|
|
Because how how and when class bodies are executed, the ``This``
|
|
trait can only have a default value of None. This, and because we
|
|
always validate default values, ``allow_none`` is *always* true.
|
|
"""
|
|
|
|
info_text = "an instance of the same type as the receiver or None"
|
|
|
|
def __init__(self, **kwargs: t.Any) -> None:
|
|
super().__init__(None, **kwargs)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> HasTraits | None:
|
|
# What if value is a superclass of obj.__class__? This is
|
|
# complicated if it was the superclass that defined the This
|
|
# trait.
|
|
assert self.this_class is not None
|
|
if isinstance(value, self.this_class) or (value is None):
|
|
return value
|
|
else:
|
|
self.error(obj, value)
|
|
|
|
|
|
class Union(TraitType[t.Any, t.Any]):
|
|
"""A trait type representing a Union type."""
|
|
|
|
def __init__(self, trait_types: t.Any, **kwargs: t.Any) -> None:
|
|
"""Construct a Union trait.
|
|
|
|
This trait allows values that are allowed by at least one of the
|
|
specified trait types. A Union traitlet cannot have metadata on
|
|
its own, besides the metadata of the listed types.
|
|
|
|
Parameters
|
|
----------
|
|
trait_types : sequence
|
|
The list of trait types of length at least 1.
|
|
**kwargs
|
|
Extra kwargs passed to `TraitType`
|
|
|
|
Notes
|
|
-----
|
|
Union([Float(), Bool(), Int()]) attempts to validate the provided values
|
|
with the validation function of Float, then Bool, and finally Int.
|
|
|
|
Parsing from string is ambiguous for container types which accept other
|
|
collection-like literals (e.g. List accepting both `[]` and `()`
|
|
precludes Union from ever parsing ``Union([List(), Tuple()])`` as a tuple;
|
|
you can modify behaviour of too permissive container traits by overriding
|
|
``_literal_from_string_pairs`` in subclasses.
|
|
Similarly, parsing unions of numeric types is only unambiguous if
|
|
types are provided in order of increasing permissiveness, e.g.
|
|
``Union([Int(), Float()])`` (since floats accept integer-looking values).
|
|
"""
|
|
self.trait_types = list(trait_types)
|
|
self.info_text = " or ".join([tt.info() for tt in self.trait_types])
|
|
super().__init__(**kwargs)
|
|
|
|
def default(self, obj: t.Any = None) -> t.Any:
|
|
default = super().default(obj)
|
|
for trait in self.trait_types:
|
|
if default is Undefined:
|
|
default = trait.default(obj)
|
|
else:
|
|
break
|
|
return default
|
|
|
|
def class_init(self, cls: type[HasTraits], name: str | None) -> None:
|
|
for trait_type in reversed(self.trait_types):
|
|
trait_type.class_init(cls, None)
|
|
super().class_init(cls, name)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
for trait_type in reversed(self.trait_types):
|
|
trait_type.subclass_init(cls)
|
|
# explicitly not calling super().subclass_init(cls)
|
|
# to opt out of instance_init
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
with obj.cross_validation_lock:
|
|
for trait_type in self.trait_types:
|
|
try:
|
|
v = trait_type._validate(obj, value)
|
|
# In the case of an element trait, the name is None
|
|
if self.name is not None:
|
|
setattr(obj, "_" + self.name + "_metadata", trait_type.metadata)
|
|
return v
|
|
except TraitError:
|
|
continue
|
|
self.error(obj, value)
|
|
|
|
def __or__(self, other: t.Any) -> Union:
|
|
if isinstance(other, Union):
|
|
return Union(self.trait_types + other.trait_types)
|
|
else:
|
|
return Union([*self.trait_types, other])
|
|
|
|
def from_string(self, s: str) -> t.Any:
|
|
for trait_type in self.trait_types:
|
|
try:
|
|
v = trait_type.from_string(s)
|
|
return trait_type.validate(None, v)
|
|
except (TraitError, ValueError):
|
|
continue
|
|
return super().from_string(s)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Basic TraitTypes implementations/subclasses
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class Any(TraitType[t.Optional[t.Any], t.Optional[t.Any]]):
|
|
"""A trait which allows any value."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Any,
|
|
default_value: t.Any = ...,
|
|
*,
|
|
allow_none: Literal[False],
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Any,
|
|
default_value: t.Any = ...,
|
|
*,
|
|
allow_none: Literal[True],
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Any,
|
|
default_value: t.Any = ...,
|
|
*,
|
|
allow_none: Literal[True, False] = ...,
|
|
help: str | None = ...,
|
|
read_only: bool | None = False,
|
|
config: t.Any = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: Any,
|
|
default_value: t.Any = ...,
|
|
*,
|
|
allow_none: bool = False,
|
|
help: str | None = "",
|
|
read_only: bool | None = False,
|
|
config: t.Any = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __get__(self, obj: None, cls: type[t.Any]) -> Any:
|
|
...
|
|
|
|
@t.overload
|
|
def __get__(self, obj: t.Any, cls: type[t.Any]) -> t.Any:
|
|
...
|
|
|
|
def __get__(self, obj: t.Any | None, cls: type[t.Any]) -> t.Any | Any:
|
|
...
|
|
|
|
default_value: t.Any | None = None
|
|
allow_none = True
|
|
info_text = "any value"
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
def _validate_bounds(
|
|
trait: Int[t.Any, t.Any] | Float[t.Any, t.Any], obj: t.Any, value: t.Any
|
|
) -> t.Any:
|
|
"""
|
|
Validate that a number to be applied to a trait is between bounds.
|
|
|
|
If value is not between min_bound and max_bound, this raises a
|
|
TraitError with an error message appropriate for this trait.
|
|
"""
|
|
if trait.min is not None and value < trait.min:
|
|
raise TraitError(
|
|
f"The value of the '{trait.name}' trait of {class_of(obj)} instance should "
|
|
f"not be less than {trait.min}, but a value of {value} was "
|
|
"specified"
|
|
)
|
|
if trait.max is not None and value > trait.max:
|
|
raise TraitError(
|
|
f"The value of the '{trait.name}' trait of {class_of(obj)} instance should "
|
|
f"not be greater than {trait.max}, but a value of {value} was "
|
|
"specified"
|
|
)
|
|
return value
|
|
|
|
|
|
# I = t.TypeVar('I', t.Optional[int], int)
|
|
|
|
|
|
class Int(TraitType[G, S]):
|
|
"""An int trait."""
|
|
|
|
default_value = 0
|
|
info_text = "an int"
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Int[int, int],
|
|
default_value: int | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Int[int | None, int | None],
|
|
default_value: int | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self,
|
|
default_value: t.Any = Undefined,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
config: t.Any | None = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
self.min = kwargs.pop("min", None)
|
|
self.max = kwargs.pop("max", None)
|
|
super().__init__(
|
|
default_value=default_value,
|
|
allow_none=allow_none,
|
|
read_only=read_only,
|
|
help=help,
|
|
config=config,
|
|
**kwargs,
|
|
)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if not isinstance(value, int):
|
|
self.error(obj, value)
|
|
return t.cast(G, _validate_bounds(self, obj, value))
|
|
|
|
def from_string(self, s: str) -> G:
|
|
if self.allow_none and s == "None":
|
|
return t.cast(G, None)
|
|
return t.cast(G, int(s))
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
class CInt(Int[G, S]):
|
|
"""A casting version of the int trait."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CInt[int, t.Any],
|
|
default_value: t.Any | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CInt[int | None, t.Any],
|
|
default_value: t.Any | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: CInt[int | None, t.Any],
|
|
default_value: t.Any | Sentinel | None = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
try:
|
|
value = int(value)
|
|
except Exception:
|
|
self.error(obj, value)
|
|
return t.cast(G, _validate_bounds(self, obj, value))
|
|
|
|
|
|
Long, CLong = Int, CInt
|
|
Integer = Int
|
|
|
|
|
|
class Float(TraitType[G, S]):
|
|
"""A float trait."""
|
|
|
|
default_value = 0.0
|
|
info_text = "a float"
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Float[float, int | float],
|
|
default_value: float | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Float[int | None, int | float | None],
|
|
default_value: float | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: Float[int | None, int | float | None],
|
|
default_value: float | Sentinel | None = Undefined,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = False,
|
|
help: str | None = None,
|
|
config: t.Any | None = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
self.min = kwargs.pop("min", -float("inf"))
|
|
self.max = kwargs.pop("max", float("inf"))
|
|
super().__init__(
|
|
default_value=default_value,
|
|
allow_none=allow_none,
|
|
read_only=read_only,
|
|
help=help,
|
|
config=config,
|
|
**kwargs,
|
|
)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if isinstance(value, int):
|
|
value = float(value)
|
|
if not isinstance(value, float):
|
|
self.error(obj, value)
|
|
return t.cast(G, _validate_bounds(self, obj, value))
|
|
|
|
def from_string(self, s: str) -> G:
|
|
if self.allow_none and s == "None":
|
|
return t.cast(G, None)
|
|
return t.cast(G, float(s))
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
class CFloat(Float[G, S]):
|
|
"""A casting version of the float trait."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CFloat[float, t.Any],
|
|
default_value: t.Any = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CFloat[float | None, t.Any],
|
|
default_value: t.Any = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: CFloat[float | None, t.Any],
|
|
default_value: t.Any = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
try:
|
|
value = float(value)
|
|
except Exception:
|
|
self.error(obj, value)
|
|
return t.cast(G, _validate_bounds(self, obj, value))
|
|
|
|
|
|
class Complex(TraitType[complex, t.Union[complex, float, int]]):
|
|
"""A trait for complex numbers."""
|
|
|
|
default_value = 0.0 + 0.0j
|
|
info_text = "a complex number"
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> complex | None:
|
|
if isinstance(value, complex):
|
|
return value
|
|
if isinstance(value, (float, int)):
|
|
return complex(value)
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> complex | None:
|
|
if self.allow_none and s == "None":
|
|
return None
|
|
return complex(s)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
class CComplex(Complex, TraitType[complex, t.Any]):
|
|
"""A casting version of the complex number trait."""
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> complex | None:
|
|
try:
|
|
return complex(value)
|
|
except Exception:
|
|
self.error(obj, value)
|
|
|
|
|
|
# We should always be explicit about whether we're using bytes or unicode, both
|
|
# for Python 3 conversion and for reliable unicode behaviour on Python 2. So
|
|
# we don't have a Str type.
|
|
class Bytes(TraitType[bytes, bytes]):
|
|
"""A trait for byte strings."""
|
|
|
|
default_value = b""
|
|
info_text = "a bytes object"
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> bytes | None:
|
|
if isinstance(value, bytes):
|
|
return value
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> bytes | None:
|
|
if self.allow_none and s == "None":
|
|
return None
|
|
if len(s) >= 3:
|
|
# handle deprecated b"string"
|
|
for quote in ('"', "'"):
|
|
if s[:2] == f"b{quote}" and s[-1] == quote:
|
|
old_s = s
|
|
s = s[2:-1]
|
|
warn(
|
|
"Supporting extra quotes around Bytes is deprecated in traitlets 5.0. "
|
|
f"Use {s!r} instead of {old_s!r}.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
break
|
|
return s.encode("utf8")
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
class CBytes(Bytes, TraitType[bytes, t.Any]):
|
|
"""A casting version of the byte string trait."""
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> bytes | None:
|
|
try:
|
|
return bytes(value)
|
|
except Exception:
|
|
self.error(obj, value)
|
|
|
|
|
|
class Unicode(TraitType[G, S]):
|
|
"""A trait for unicode strings."""
|
|
|
|
default_value = ""
|
|
info_text = "a unicode string"
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Unicode[str, str | bytes],
|
|
default_value: str | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Unicode[str | None, str | bytes | None],
|
|
default_value: str | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: Unicode[str | None, str | bytes | None],
|
|
default_value: str | Sentinel | None = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if isinstance(value, str):
|
|
return t.cast(G, value)
|
|
if isinstance(value, bytes):
|
|
try:
|
|
return t.cast(G, value.decode("ascii", "strict"))
|
|
except UnicodeDecodeError as e:
|
|
msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
|
|
raise TraitError(msg.format(value, self.name, class_of(obj))) from e
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> G:
|
|
if self.allow_none and s == "None":
|
|
return t.cast(G, None)
|
|
s = os.path.expanduser(s)
|
|
if len(s) >= 2:
|
|
# handle deprecated "1"
|
|
for c in ('"', "'"):
|
|
if s[0] == s[-1] == c:
|
|
old_s = s
|
|
s = s[1:-1]
|
|
warn(
|
|
"Supporting extra quotes around strings is deprecated in traitlets 5.0. "
|
|
f"You can use {s!r} instead of {old_s!r} if you require traitlets >=5.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return t.cast(G, s)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
|
|
class CUnicode(Unicode[G, S], TraitType[str, t.Any]):
|
|
"""A casting version of the unicode trait."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CUnicode[str, t.Any],
|
|
default_value: str | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CUnicode[str | None, t.Any],
|
|
default_value: str | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: CUnicode[str | None, t.Any],
|
|
default_value: str | Sentinel | None = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
try:
|
|
return t.cast(G, str(value))
|
|
except Exception:
|
|
self.error(obj, value)
|
|
|
|
|
|
class ObjectName(TraitType[str, str]):
|
|
"""A string holding a valid object name in this version of Python.
|
|
|
|
This does not check that the name exists in any scope."""
|
|
|
|
info_text = "a valid object identifier in Python"
|
|
|
|
coerce_str = staticmethod(lambda _, s: s)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> str:
|
|
value = self.coerce_str(obj, value)
|
|
|
|
if isinstance(value, str) and isidentifier(value):
|
|
return value
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> str | None:
|
|
if self.allow_none and s == "None":
|
|
return None
|
|
return s
|
|
|
|
|
|
class DottedObjectName(ObjectName):
|
|
"""A string holding a valid dotted object name in Python, such as A.b3._c"""
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> str:
|
|
value = self.coerce_str(obj, value)
|
|
|
|
if isinstance(value, str) and all(isidentifier(a) for a in value.split(".")):
|
|
return value
|
|
self.error(obj, value)
|
|
|
|
|
|
class Bool(TraitType[G, S]):
|
|
"""A boolean (True, False) trait."""
|
|
|
|
default_value = False
|
|
info_text = "a boolean"
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Bool[bool, bool | int],
|
|
default_value: bool | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Bool[bool | None, bool | int | None],
|
|
default_value: bool | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: Bool[bool | None, bool | int | None],
|
|
default_value: bool | Sentinel | None = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if isinstance(value, bool):
|
|
return t.cast(G, value)
|
|
elif isinstance(value, int):
|
|
if value == 1:
|
|
return t.cast(G, True)
|
|
elif value == 0:
|
|
return t.cast(G, False)
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> G:
|
|
if self.allow_none and s == "None":
|
|
return t.cast(G, None)
|
|
s = s.lower()
|
|
if s in {"true", "1"}:
|
|
return t.cast(G, True)
|
|
elif s in {"false", "0"}:
|
|
return t.cast(G, False)
|
|
else:
|
|
raise ValueError("%r is not 1, 0, true, or false")
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
def argcompleter(self, **kwargs: t.Any) -> list[str]:
|
|
"""Completion hints for argcomplete"""
|
|
completions = ["true", "1", "false", "0"]
|
|
if self.allow_none:
|
|
completions.append("None")
|
|
return completions
|
|
|
|
|
|
class CBool(Bool[G, S]):
|
|
"""A casting version of the boolean trait."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CBool[bool, t.Any],
|
|
default_value: bool | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: CBool[bool | None, t.Any],
|
|
default_value: bool | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: CBool[bool | None, t.Any],
|
|
default_value: bool | Sentinel | None = ...,
|
|
allow_none: bool = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
try:
|
|
return t.cast(G, bool(value))
|
|
except Exception:
|
|
self.error(obj, value)
|
|
|
|
|
|
class Enum(TraitType[G, G]):
|
|
"""An enum whose value must be in a given sequence."""
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Enum[G],
|
|
values: t.Sequence[G],
|
|
default_value: G | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Enum[G | None],
|
|
values: t.Sequence[G] | None,
|
|
default_value: G | Sentinel | None = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: Enum[G],
|
|
values: t.Sequence[G] | None,
|
|
default_value: G | Sentinel | None = Undefined,
|
|
allow_none: bool = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
config: t.Any = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
self.values = values
|
|
if allow_none is True and default_value is Undefined:
|
|
default_value = None
|
|
kwargs["allow_none"] = allow_none
|
|
kwargs["read_only"] = read_only
|
|
kwargs["help"] = help
|
|
kwargs["config"] = config
|
|
super().__init__(default_value, **kwargs)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if self.values and value in self.values:
|
|
return t.cast(G, value)
|
|
self.error(obj, value)
|
|
|
|
def _choices_str(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait choices (not none)."""
|
|
choices = self.values or []
|
|
if as_rst:
|
|
choice_str = "|".join("``%r``" % x for x in choices)
|
|
else:
|
|
choice_str = repr(list(choices))
|
|
return choice_str
|
|
|
|
def _info(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait."""
|
|
none = " or %s" % ("`None`" if as_rst else "None") if self.allow_none else ""
|
|
return f"any of {self._choices_str(as_rst)}{none}"
|
|
|
|
def info(self) -> str:
|
|
return self._info(as_rst=False)
|
|
|
|
def info_rst(self) -> str:
|
|
return self._info(as_rst=True)
|
|
|
|
def from_string(self, s: str) -> G:
|
|
try:
|
|
return self.validate(None, s)
|
|
except TraitError:
|
|
return t.cast(G, _safe_literal_eval(s))
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
pass # fully opt out of instance_init
|
|
|
|
def argcompleter(self, **kwargs: t.Any) -> list[str]:
|
|
"""Completion hints for argcomplete"""
|
|
return [str(v) for v in self.values or []]
|
|
|
|
|
|
class CaselessStrEnum(Enum[G]):
|
|
"""An enum of strings where the case should be ignored."""
|
|
|
|
def __init__(
|
|
self: CaselessStrEnum[t.Any],
|
|
values: t.Any,
|
|
default_value: t.Any = Undefined,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
super().__init__(values, default_value=default_value, **kwargs)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if not isinstance(value, str):
|
|
self.error(obj, value)
|
|
|
|
for v in self.values or []:
|
|
assert isinstance(v, str)
|
|
if v.lower() == value.lower():
|
|
return t.cast(G, v)
|
|
self.error(obj, value)
|
|
|
|
def _info(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait."""
|
|
none = " or %s" % ("`None`" if as_rst else "None") if self.allow_none else ""
|
|
return f"any of {self._choices_str(as_rst)} (case-insensitive){none}"
|
|
|
|
def info(self) -> str:
|
|
return self._info(as_rst=False)
|
|
|
|
def info_rst(self) -> str:
|
|
return self._info(as_rst=True)
|
|
|
|
|
|
class FuzzyEnum(Enum[G]):
|
|
"""An case-ignoring enum matching choices by unique prefixes/substrings."""
|
|
|
|
case_sensitive = False
|
|
#: If True, choices match anywhere in the string, otherwise match prefixes.
|
|
substring_matching = False
|
|
|
|
def __init__(
|
|
self: FuzzyEnum[t.Any],
|
|
values: t.Any,
|
|
default_value: t.Any = Undefined,
|
|
case_sensitive: bool = False,
|
|
substring_matching: bool = False,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
self.case_sensitive = case_sensitive
|
|
self.substring_matching = substring_matching
|
|
super().__init__(values, default_value=default_value, **kwargs)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if not isinstance(value, str):
|
|
self.error(obj, value)
|
|
|
|
conv_func = (lambda c: c) if self.case_sensitive else lambda c: c.lower()
|
|
substring_matching = self.substring_matching
|
|
match_func = (lambda v, c: v in c) if substring_matching else (lambda v, c: c.startswith(v))
|
|
value = conv_func(value) # type:ignore[no-untyped-call]
|
|
choices = self.values or []
|
|
matches = [match_func(value, conv_func(c)) for c in choices] # type:ignore[no-untyped-call]
|
|
if sum(matches) == 1:
|
|
for v, m in zip(choices, matches):
|
|
if m:
|
|
return v
|
|
|
|
self.error(obj, value)
|
|
|
|
def _info(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait."""
|
|
none = " or %s" % ("`None`" if as_rst else "None") if self.allow_none else ""
|
|
case = "sensitive" if self.case_sensitive else "insensitive"
|
|
substr = "substring" if self.substring_matching else "prefix"
|
|
return f"any case-{case} {substr} of {self._choices_str(as_rst)}{none}"
|
|
|
|
def info(self) -> str:
|
|
return self._info(as_rst=False)
|
|
|
|
def info_rst(self) -> str:
|
|
return self._info(as_rst=True)
|
|
|
|
|
|
class Container(Instance[T]):
|
|
"""An instance of a container (list, set, etc.)
|
|
|
|
To be subclassed by overriding klass.
|
|
"""
|
|
|
|
klass: type[T] | None = None
|
|
_cast_types: t.Any = ()
|
|
_valid_defaults = SequenceTypes
|
|
_trait: t.Any = None
|
|
_literal_from_string_pairs: t.Any = ("[]", "()")
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Container[T],
|
|
*,
|
|
allow_none: Literal[False],
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Container[T | None],
|
|
*,
|
|
allow_none: Literal[True],
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any | None = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: Container[T],
|
|
*,
|
|
trait: t.Any = ...,
|
|
default_value: t.Any = ...,
|
|
help: str = ...,
|
|
read_only: bool = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self,
|
|
trait: t.Any | None = None,
|
|
default_value: t.Any = Undefined,
|
|
help: str | None = None,
|
|
read_only: bool | None = None,
|
|
config: t.Any | None = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Create a container trait type from a list, set, or tuple.
|
|
|
|
The default value is created by doing ``List(default_value)``,
|
|
which creates a copy of the ``default_value``.
|
|
|
|
``trait`` can be specified, which restricts the type of elements
|
|
in the container to that TraitType.
|
|
|
|
If only one arg is given and it is not a Trait, it is taken as
|
|
``default_value``:
|
|
|
|
``c = List([1, 2, 3])``
|
|
|
|
Parameters
|
|
----------
|
|
trait : TraitType [ optional ]
|
|
the type for restricting the contents of the Container. If unspecified,
|
|
types are not checked.
|
|
default_value : SequenceType [ optional ]
|
|
The default value for the Trait. Must be list/tuple/set, and
|
|
will be cast to the container type.
|
|
allow_none : bool [ default False ]
|
|
Whether to allow the value to be None
|
|
**kwargs : any
|
|
further keys for extensions to the Trait (e.g. config)
|
|
|
|
"""
|
|
|
|
# allow List([values]):
|
|
if trait is not None and default_value is Undefined and not is_trait(trait):
|
|
default_value = trait
|
|
trait = None
|
|
|
|
if default_value is None and not kwargs.get("allow_none", False):
|
|
# improve backward-compatibility for possible subclasses
|
|
# specifying default_value=None as default,
|
|
# keeping 'unspecified' behavior (i.e. empty container)
|
|
warn(
|
|
f"Specifying {self.__class__.__name__}(default_value=None)"
|
|
" for no default is deprecated in traitlets 5.0.5."
|
|
" Use default_value=Undefined",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
default_value = Undefined
|
|
|
|
if default_value is Undefined:
|
|
args: t.Any = ()
|
|
elif default_value is None:
|
|
# default_value back on kwargs for super() to handle
|
|
args = ()
|
|
kwargs["default_value"] = None
|
|
elif isinstance(default_value, self._valid_defaults):
|
|
args = (default_value,)
|
|
else:
|
|
raise TypeError(f"default value of {self.__class__.__name__} was {default_value}")
|
|
|
|
if is_trait(trait):
|
|
if isinstance(trait, type):
|
|
warn(
|
|
"Traits should be given as instances, not types (for example, `Int()`, not `Int`)."
|
|
" Passing types is deprecated in traitlets 4.1.",
|
|
DeprecationWarning,
|
|
stacklevel=3,
|
|
)
|
|
self._trait = trait() if isinstance(trait, type) else trait
|
|
elif trait is not None:
|
|
raise TypeError("`trait` must be a Trait or None, got %s" % repr_type(trait))
|
|
|
|
super().__init__(
|
|
klass=self.klass, args=args, help=help, read_only=read_only, config=config, **kwargs
|
|
)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> T | None:
|
|
if isinstance(value, self._cast_types):
|
|
assert self.klass is not None
|
|
value = self.klass(value) # type:ignore[call-arg]
|
|
value = super().validate(obj, value)
|
|
if value is None:
|
|
return value
|
|
|
|
value = self.validate_elements(obj, value)
|
|
|
|
return t.cast(T, value)
|
|
|
|
def validate_elements(self, obj: t.Any, value: t.Any) -> T | None:
|
|
validated = []
|
|
if self._trait is None or isinstance(self._trait, Any):
|
|
return t.cast(T, value)
|
|
for v in value:
|
|
try:
|
|
v = self._trait._validate(obj, v)
|
|
except TraitError as error:
|
|
self.error(obj, v, error)
|
|
else:
|
|
validated.append(v)
|
|
assert self.klass is not None
|
|
return self.klass(validated) # type:ignore[call-arg]
|
|
|
|
def class_init(self, cls: type[t.Any], name: str | None) -> None:
|
|
if isinstance(self._trait, TraitType):
|
|
self._trait.class_init(cls, None)
|
|
super().class_init(cls, name)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
if isinstance(self._trait, TraitType):
|
|
self._trait.subclass_init(cls)
|
|
# explicitly not calling super().subclass_init(cls)
|
|
# to opt out of instance_init
|
|
|
|
def from_string(self, s: str) -> T | None:
|
|
"""Load value from a single string"""
|
|
if not isinstance(s, str):
|
|
raise TraitError(f"Expected string, got {s!r}")
|
|
try:
|
|
test = literal_eval(s)
|
|
except Exception:
|
|
test = None
|
|
return self.validate(None, test)
|
|
|
|
def from_string_list(self, s_list: list[str]) -> T | None:
|
|
"""Return the value from a list of config strings
|
|
|
|
This is where we parse CLI configuration
|
|
"""
|
|
assert self.klass is not None
|
|
if len(s_list) == 1:
|
|
# check for deprecated --Class.trait="['a', 'b', 'c']"
|
|
r = s_list[0]
|
|
if r == "None" and self.allow_none:
|
|
return None
|
|
if len(r) >= 2 and any(
|
|
r.startswith(start) and r.endswith(end)
|
|
for start, end in self._literal_from_string_pairs
|
|
):
|
|
if self.this_class:
|
|
clsname = self.this_class.__name__ + "."
|
|
else:
|
|
clsname = ""
|
|
assert self.name is not None
|
|
warn(
|
|
"--{0}={1} for containers is deprecated in traitlets 5.0. "
|
|
"You can pass `--{0} item` ... multiple times to add items to a list.".format(
|
|
clsname + self.name, r
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self.klass(literal_eval(r)) # type:ignore[call-arg]
|
|
sig = inspect.signature(self.item_from_string)
|
|
if "index" in sig.parameters:
|
|
item_from_string = self.item_from_string
|
|
else:
|
|
# backward-compat: allow item_from_string to ignore index arg
|
|
def item_from_string(s: str, index: int | None = None) -> T | str:
|
|
return t.cast(T, self.item_from_string(s))
|
|
|
|
return self.klass( # type:ignore[call-arg]
|
|
[item_from_string(s, index=idx) for idx, s in enumerate(s_list)]
|
|
)
|
|
|
|
def item_from_string(self, s: str, index: int | None = None) -> T | str:
|
|
"""Cast a single item from a string
|
|
|
|
Evaluated when parsing CLI configuration from a string
|
|
"""
|
|
if self._trait:
|
|
return t.cast(T, self._trait.from_string(s))
|
|
else:
|
|
return s
|
|
|
|
|
|
class List(Container[t.List[T]]):
|
|
"""An instance of a Python list."""
|
|
|
|
klass = list # type:ignore[assignment]
|
|
_cast_types: t.Any = (tuple,)
|
|
|
|
def __init__(
|
|
self,
|
|
trait: t.List[T] | t.Tuple[T] | t.Set[T] | Sentinel | TraitType[T, t.Any] | None = None,
|
|
default_value: t.List[T] | t.Tuple[T] | t.Set[T] | Sentinel | None = Undefined,
|
|
minlen: int = 0,
|
|
maxlen: int = sys.maxsize,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Create a List trait type from a list, set, or tuple.
|
|
|
|
The default value is created by doing ``list(default_value)``,
|
|
which creates a copy of the ``default_value``.
|
|
|
|
``trait`` can be specified, which restricts the type of elements
|
|
in the container to that TraitType.
|
|
|
|
If only one arg is given and it is not a Trait, it is taken as
|
|
``default_value``:
|
|
|
|
``c = List([1, 2, 3])``
|
|
|
|
Parameters
|
|
----------
|
|
trait : TraitType [ optional ]
|
|
the type for restricting the contents of the Container.
|
|
If unspecified, types are not checked.
|
|
default_value : SequenceType [ optional ]
|
|
The default value for the Trait. Must be list/tuple/set, and
|
|
will be cast to the container type.
|
|
minlen : Int [ default 0 ]
|
|
The minimum length of the input list
|
|
maxlen : Int [ default sys.maxsize ]
|
|
The maximum length of the input list
|
|
"""
|
|
self._maxlen = maxlen
|
|
self._minlen = minlen
|
|
super().__init__(trait=trait, default_value=default_value, **kwargs)
|
|
|
|
def length_error(self, obj: t.Any, value: t.Any) -> None:
|
|
e = (
|
|
"The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified."
|
|
% (self.name, class_of(obj), self._minlen, self._maxlen, value)
|
|
)
|
|
raise TraitError(e)
|
|
|
|
def validate_elements(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
length = len(value)
|
|
if length < self._minlen or length > self._maxlen:
|
|
self.length_error(obj, value)
|
|
|
|
return super().validate_elements(obj, value)
|
|
|
|
def set(self, obj: t.Any, value: t.Any) -> None:
|
|
if isinstance(value, str):
|
|
return super().set(obj, [value]) # type:ignore[list-item]
|
|
else:
|
|
return super().set(obj, value)
|
|
|
|
|
|
class Set(Container[t.Set[t.Any]]):
|
|
"""An instance of a Python set."""
|
|
|
|
klass = set
|
|
_cast_types = (tuple, list)
|
|
|
|
_literal_from_string_pairs = ("[]", "()", "{}")
|
|
|
|
# Redefine __init__ just to make the docstring more accurate.
|
|
def __init__(
|
|
self,
|
|
trait: t.Any = None,
|
|
default_value: t.Any = Undefined,
|
|
minlen: int = 0,
|
|
maxlen: int = sys.maxsize,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Create a Set trait type from a list, set, or tuple.
|
|
|
|
The default value is created by doing ``set(default_value)``,
|
|
which creates a copy of the ``default_value``.
|
|
|
|
``trait`` can be specified, which restricts the type of elements
|
|
in the container to that TraitType.
|
|
|
|
If only one arg is given and it is not a Trait, it is taken as
|
|
``default_value``:
|
|
|
|
``c = Set({1, 2, 3})``
|
|
|
|
Parameters
|
|
----------
|
|
trait : TraitType [ optional ]
|
|
the type for restricting the contents of the Container.
|
|
If unspecified, types are not checked.
|
|
default_value : SequenceType [ optional ]
|
|
The default value for the Trait. Must be list/tuple/set, and
|
|
will be cast to the container type.
|
|
minlen : Int [ default 0 ]
|
|
The minimum length of the input list
|
|
maxlen : Int [ default sys.maxsize ]
|
|
The maximum length of the input list
|
|
"""
|
|
self._maxlen = maxlen
|
|
self._minlen = minlen
|
|
super().__init__(trait=trait, default_value=default_value, **kwargs)
|
|
|
|
def length_error(self, obj: t.Any, value: t.Any) -> None:
|
|
e = (
|
|
"The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified."
|
|
% (self.name, class_of(obj), self._minlen, self._maxlen, value)
|
|
)
|
|
raise TraitError(e)
|
|
|
|
def validate_elements(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
length = len(value)
|
|
if length < self._minlen or length > self._maxlen:
|
|
self.length_error(obj, value)
|
|
|
|
return super().validate_elements(obj, value)
|
|
|
|
def set(self, obj: t.Any, value: t.Any) -> None:
|
|
if isinstance(value, str):
|
|
return super().set(obj, {value})
|
|
else:
|
|
return super().set(obj, value)
|
|
|
|
def default_value_repr(self) -> str:
|
|
# Ensure default value is sorted for a reproducible build
|
|
list_repr = repr(sorted(self.make_dynamic_default() or []))
|
|
if list_repr == "[]":
|
|
return "set()"
|
|
return "{" + list_repr[1:-1] + "}"
|
|
|
|
|
|
class Tuple(Container[t.Tuple[t.Any, ...]]):
|
|
"""An instance of a Python tuple."""
|
|
|
|
klass = tuple
|
|
_cast_types = (list,)
|
|
|
|
def __init__(self, *traits: t.Any, **kwargs: t.Any) -> None:
|
|
"""Create a tuple from a list, set, or tuple.
|
|
|
|
Create a fixed-type tuple with Traits:
|
|
|
|
``t = Tuple(Int(), Str(), CStr())``
|
|
|
|
would be length 3, with Int,Str,CStr for each element.
|
|
|
|
If only one arg is given and it is not a Trait, it is taken as
|
|
default_value:
|
|
|
|
``t = Tuple((1, 2, 3))``
|
|
|
|
Otherwise, ``default_value`` *must* be specified by keyword.
|
|
|
|
Parameters
|
|
----------
|
|
*traits : TraitTypes [ optional ]
|
|
the types for restricting the contents of the Tuple. If unspecified,
|
|
types are not checked. If specified, then each positional argument
|
|
corresponds to an element of the tuple. Tuples defined with traits
|
|
are of fixed length.
|
|
default_value : SequenceType [ optional ]
|
|
The default value for the Tuple. Must be list/tuple/set, and
|
|
will be cast to a tuple. If ``traits`` are specified,
|
|
``default_value`` must conform to the shape and type they specify.
|
|
**kwargs
|
|
Other kwargs passed to `Container`
|
|
"""
|
|
default_value = kwargs.pop("default_value", Undefined)
|
|
# allow Tuple((values,)):
|
|
if len(traits) == 1 and default_value is Undefined and not is_trait(traits[0]):
|
|
default_value = traits[0]
|
|
traits = ()
|
|
|
|
if default_value is None and not kwargs.get("allow_none", False):
|
|
# improve backward-compatibility for possible subclasses
|
|
# specifying default_value=None as default,
|
|
# keeping 'unspecified' behavior (i.e. empty container)
|
|
warn(
|
|
f"Specifying {self.__class__.__name__}(default_value=None)"
|
|
" for no default is deprecated in traitlets 5.0.5."
|
|
" Use default_value=Undefined",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
default_value = Undefined
|
|
|
|
if default_value is Undefined:
|
|
args: t.Any = ()
|
|
elif default_value is None:
|
|
# default_value back on kwargs for super() to handle
|
|
args = ()
|
|
kwargs["default_value"] = None
|
|
elif isinstance(default_value, self._valid_defaults):
|
|
args = (default_value,)
|
|
else:
|
|
raise TypeError(f"default value of {self.__class__.__name__} was {default_value}")
|
|
|
|
self._traits = []
|
|
for trait in traits:
|
|
if isinstance(trait, type):
|
|
warn(
|
|
"Traits should be given as instances, not types (for example, `Int()`, not `Int`)"
|
|
" Passing types is deprecated in traitlets 4.1.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
trait = trait()
|
|
self._traits.append(trait)
|
|
|
|
if self._traits and (default_value is None or default_value is Undefined):
|
|
# don't allow default to be an empty container if length is specified
|
|
args = None
|
|
super(Container, self).__init__(klass=self.klass, args=args, **kwargs)
|
|
|
|
def item_from_string(self, s: str, index: int) -> t.Any: # type:ignore[override]
|
|
"""Cast a single item from a string
|
|
|
|
Evaluated when parsing CLI configuration from a string
|
|
"""
|
|
if not self._traits or index >= len(self._traits):
|
|
# return s instead of raising index error
|
|
# length errors will be raised later on validation
|
|
return s
|
|
return self._traits[index].from_string(s)
|
|
|
|
def validate_elements(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
if not self._traits:
|
|
# nothing to validate
|
|
return value
|
|
if len(value) != len(self._traits):
|
|
e = (
|
|
"The '%s' trait of %s instance requires %i elements, but a value of %s was specified."
|
|
% (self.name, class_of(obj), len(self._traits), repr_type(value))
|
|
)
|
|
raise TraitError(e)
|
|
|
|
validated = []
|
|
for trait, v in zip(self._traits, value):
|
|
try:
|
|
v = trait._validate(obj, v)
|
|
except TraitError as error:
|
|
self.error(obj, v, error)
|
|
else:
|
|
validated.append(v)
|
|
return tuple(validated)
|
|
|
|
def class_init(self, cls: type[t.Any], name: str | None) -> None:
|
|
for trait in self._traits:
|
|
if isinstance(trait, TraitType):
|
|
trait.class_init(cls, None)
|
|
super(Container, self).class_init(cls, name)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
for trait in self._traits:
|
|
if isinstance(trait, TraitType):
|
|
trait.subclass_init(cls)
|
|
# explicitly not calling super().subclass_init(cls)
|
|
# to opt out of instance_init
|
|
|
|
|
|
class Dict(Instance["dict[K, V]"]):
|
|
"""An instance of a Python dict.
|
|
|
|
One or more traits can be passed to the constructor
|
|
to validate the keys and/or values of the dict.
|
|
If you need more detailed validation,
|
|
you may use a custom validator method.
|
|
|
|
.. versionchanged:: 5.0
|
|
Added key_trait for validating dict keys.
|
|
|
|
.. versionchanged:: 5.0
|
|
Deprecated ambiguous ``trait``, ``traits`` args in favor of ``value_trait``, ``per_key_traits``.
|
|
"""
|
|
|
|
_value_trait = None
|
|
_key_trait = None
|
|
|
|
def __init__(
|
|
self,
|
|
value_trait: TraitType[t.Any, t.Any] | dict[K, V] | Sentinel | None = None,
|
|
per_key_traits: t.Any = None,
|
|
key_trait: TraitType[t.Any, t.Any] | None = None,
|
|
default_value: dict[K, V] | Sentinel | None = Undefined,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
"""Create a dict trait type from a Python dict.
|
|
|
|
The default value is created by doing ``dict(default_value)``,
|
|
which creates a copy of the ``default_value``.
|
|
|
|
Parameters
|
|
----------
|
|
value_trait : TraitType [ optional ]
|
|
The specified trait type to check and use to restrict the values of
|
|
the dict. If unspecified, values are not checked.
|
|
per_key_traits : Dictionary of {keys:trait types} [ optional, keyword-only ]
|
|
A Python dictionary containing the types that are valid for
|
|
restricting the values of the dict on a per-key basis.
|
|
Each value in this dict should be a Trait for validating
|
|
key_trait : TraitType [ optional, keyword-only ]
|
|
The type for restricting the keys of the dict. If
|
|
unspecified, the types of the keys are not checked.
|
|
default_value : SequenceType [ optional, keyword-only ]
|
|
The default value for the Dict. Must be dict, tuple, or None, and
|
|
will be cast to a dict if not None. If any key or value traits are specified,
|
|
the `default_value` must conform to the constraints.
|
|
|
|
Examples
|
|
--------
|
|
a dict whose values must be text
|
|
>>> d = Dict(Unicode())
|
|
|
|
d2['n'] must be an integer
|
|
d2['s'] must be text
|
|
>>> d2 = Dict(per_key_traits={"n": Integer(), "s": Unicode()})
|
|
|
|
d3's keys must be text
|
|
d3's values must be integers
|
|
>>> d3 = Dict(value_trait=Integer(), key_trait=Unicode())
|
|
|
|
"""
|
|
|
|
# handle deprecated keywords
|
|
trait = kwargs.pop("trait", None)
|
|
if trait is not None:
|
|
if value_trait is not None:
|
|
raise TypeError(
|
|
"Found a value for both `value_trait` and its deprecated alias `trait`."
|
|
)
|
|
value_trait = trait
|
|
warn(
|
|
"Keyword `trait` is deprecated in traitlets 5.0, use `value_trait` instead",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
traits = kwargs.pop("traits", None)
|
|
if traits is not None:
|
|
if per_key_traits is not None:
|
|
raise TypeError(
|
|
"Found a value for both `per_key_traits` and its deprecated alias `traits`."
|
|
)
|
|
per_key_traits = traits
|
|
warn(
|
|
"Keyword `traits` is deprecated in traitlets 5.0, use `per_key_traits` instead",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
# Handling positional arguments
|
|
if default_value is Undefined and value_trait is not None:
|
|
if not is_trait(value_trait):
|
|
assert not isinstance(value_trait, TraitType)
|
|
default_value = value_trait
|
|
value_trait = None
|
|
|
|
if key_trait is None and per_key_traits is not None:
|
|
if is_trait(per_key_traits):
|
|
key_trait = per_key_traits
|
|
per_key_traits = None
|
|
|
|
# Handling default value
|
|
if default_value is Undefined:
|
|
default_value = {}
|
|
if default_value is None:
|
|
args: t.Any = None
|
|
elif isinstance(default_value, dict):
|
|
args = (default_value,)
|
|
elif isinstance(default_value, SequenceTypes):
|
|
args = (default_value,)
|
|
else:
|
|
raise TypeError("default value of Dict was %s" % default_value)
|
|
|
|
# Case where a type of TraitType is provided rather than an instance
|
|
if is_trait(value_trait):
|
|
if isinstance(value_trait, type):
|
|
warn( # type:ignore[unreachable]
|
|
"Traits should be given as instances, not types (for example, `Int()`, not `Int`)"
|
|
" Passing types is deprecated in traitlets 4.1.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
value_trait = value_trait()
|
|
self._value_trait = value_trait
|
|
elif value_trait is not None:
|
|
raise TypeError(
|
|
"`value_trait` must be a Trait or None, got %s" % repr_type(value_trait)
|
|
)
|
|
|
|
if is_trait(key_trait):
|
|
if isinstance(key_trait, type):
|
|
warn( # type:ignore[unreachable]
|
|
"Traits should be given as instances, not types (for example, `Int()`, not `Int`)"
|
|
" Passing types is deprecated in traitlets 4.1.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
key_trait = key_trait()
|
|
self._key_trait = key_trait
|
|
elif key_trait is not None:
|
|
raise TypeError("`key_trait` must be a Trait or None, got %s" % repr_type(key_trait))
|
|
|
|
self._per_key_traits = per_key_traits
|
|
|
|
super().__init__(klass=dict, args=args, **kwargs)
|
|
|
|
def element_error(
|
|
self, obj: t.Any, element: t.Any, validator: t.Any, side: str = "Values"
|
|
) -> None:
|
|
e = (
|
|
side
|
|
+ f" of the '{self.name}' trait of {class_of(obj)} instance must be {validator.info()}, but a value of {repr_type(element)} was specified."
|
|
)
|
|
raise TraitError(e)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> dict[K, V] | None:
|
|
value = super().validate(obj, value)
|
|
if value is None:
|
|
return value
|
|
return self.validate_elements(obj, value)
|
|
|
|
def validate_elements(self, obj: t.Any, value: dict[t.Any, t.Any]) -> dict[K, V] | None:
|
|
per_key_override = self._per_key_traits or {}
|
|
key_trait = self._key_trait
|
|
value_trait = self._value_trait
|
|
if not (key_trait or value_trait or per_key_override):
|
|
return value
|
|
|
|
validated = {}
|
|
for key in value:
|
|
v = value[key]
|
|
if key_trait:
|
|
try:
|
|
key = key_trait._validate(obj, key)
|
|
except TraitError:
|
|
self.element_error(obj, key, key_trait, "Keys")
|
|
active_value_trait = per_key_override.get(key, value_trait)
|
|
if active_value_trait:
|
|
try:
|
|
v = active_value_trait._validate(obj, v)
|
|
except TraitError:
|
|
self.element_error(obj, v, active_value_trait, "Values")
|
|
validated[key] = v
|
|
|
|
return self.klass(validated) # type:ignore[misc,operator]
|
|
|
|
def class_init(self, cls: type[t.Any], name: str | None) -> None:
|
|
if isinstance(self._value_trait, TraitType):
|
|
self._value_trait.class_init(cls, None)
|
|
if isinstance(self._key_trait, TraitType):
|
|
self._key_trait.class_init(cls, None)
|
|
if self._per_key_traits is not None:
|
|
for trait in self._per_key_traits.values():
|
|
trait.class_init(cls, None)
|
|
super().class_init(cls, name)
|
|
|
|
def subclass_init(self, cls: type[t.Any]) -> None:
|
|
if isinstance(self._value_trait, TraitType):
|
|
self._value_trait.subclass_init(cls)
|
|
if isinstance(self._key_trait, TraitType):
|
|
self._key_trait.subclass_init(cls)
|
|
if self._per_key_traits is not None:
|
|
for trait in self._per_key_traits.values():
|
|
trait.subclass_init(cls)
|
|
# explicitly not calling super().subclass_init(cls)
|
|
# to opt out of instance_init
|
|
|
|
def from_string(self, s: str) -> dict[K, V] | None:
|
|
"""Load value from a single string"""
|
|
if not isinstance(s, str):
|
|
raise TypeError(f"from_string expects a string, got {s!r} of type {type(s)}")
|
|
try:
|
|
return t.cast("dict[K, V]", self.from_string_list([s]))
|
|
except Exception:
|
|
test = _safe_literal_eval(s)
|
|
if isinstance(test, dict):
|
|
return test
|
|
raise
|
|
|
|
def from_string_list(self, s_list: list[str]) -> t.Any:
|
|
"""Return a dict from a list of config strings.
|
|
|
|
This is where we parse CLI configuration.
|
|
|
|
Each item should have the form ``"key=value"``.
|
|
|
|
item parsing is done in :meth:`.item_from_string`.
|
|
"""
|
|
if len(s_list) == 1 and s_list[0] == "None" and self.allow_none:
|
|
return None
|
|
if len(s_list) == 1 and s_list[0].startswith("{") and s_list[0].endswith("}"):
|
|
warn(
|
|
f"--{self.name}={s_list[0]} for dict-traits is deprecated in traitlets 5.0. "
|
|
f"You can pass --{self.name} <key=value> ... multiple times to add items to a dict.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
return literal_eval(s_list[0])
|
|
|
|
combined = {}
|
|
for d in [self.item_from_string(s) for s in s_list]:
|
|
combined.update(d)
|
|
return combined
|
|
|
|
def item_from_string(self, s: str) -> dict[K, V]:
|
|
"""Cast a single-key dict from a string.
|
|
|
|
Evaluated when parsing CLI configuration from a string.
|
|
|
|
Dicts expect strings of the form key=value.
|
|
|
|
Returns a one-key dictionary,
|
|
which will be merged in :meth:`.from_string_list`.
|
|
"""
|
|
|
|
if "=" not in s:
|
|
raise TraitError(
|
|
f"'{self.__class__.__name__}' options must have the form 'key=value', got {s!r}"
|
|
)
|
|
key, value = s.split("=", 1)
|
|
|
|
# cast key with key trait, if defined
|
|
if self._key_trait:
|
|
key = self._key_trait.from_string(key)
|
|
|
|
# cast value with value trait, if defined (per-key or global)
|
|
value_trait = (self._per_key_traits or {}).get(key, self._value_trait)
|
|
if value_trait:
|
|
value = value_trait.from_string(value)
|
|
return t.cast("dict[K, V]", {key: value})
|
|
|
|
|
|
class TCPAddress(TraitType[G, S]):
|
|
"""A trait for an (ip, port) tuple.
|
|
|
|
This allows for both IPv4 IP addresses as well as hostnames.
|
|
"""
|
|
|
|
default_value = ("127.0.0.1", 0)
|
|
info_text = "an (ip, port) tuple"
|
|
|
|
if t.TYPE_CHECKING:
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: TCPAddress[tuple[str, int], tuple[str, int]],
|
|
default_value: bool | Sentinel = ...,
|
|
allow_none: Literal[False] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
@t.overload
|
|
def __init__(
|
|
self: TCPAddress[tuple[str, int] | None, tuple[str, int] | None],
|
|
default_value: bool | None | Sentinel = ...,
|
|
allow_none: Literal[True] = ...,
|
|
read_only: bool | None = ...,
|
|
help: str | None = ...,
|
|
config: t.Any = ...,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def __init__(
|
|
self: TCPAddress[tuple[str, int] | None, tuple[str, int] | None]
|
|
| TCPAddress[tuple[str, int], tuple[str, int]],
|
|
default_value: bool | None | Sentinel = Undefined,
|
|
allow_none: Literal[True, False] = False,
|
|
read_only: bool | None = None,
|
|
help: str | None = None,
|
|
config: t.Any = None,
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
...
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> G:
|
|
if isinstance(value, tuple):
|
|
if len(value) == 2:
|
|
if isinstance(value[0], str) and isinstance(value[1], int):
|
|
port = value[1]
|
|
if port >= 0 and port <= 65535:
|
|
return t.cast(G, value)
|
|
self.error(obj, value)
|
|
|
|
def from_string(self, s: str) -> G:
|
|
if self.allow_none and s == "None":
|
|
return t.cast(G, None)
|
|
if ":" not in s:
|
|
raise ValueError("Require `ip:port`, got %r" % s)
|
|
ip, port_str = s.split(":", 1)
|
|
port = int(port_str)
|
|
return t.cast(G, (ip, port))
|
|
|
|
|
|
class CRegExp(TraitType["re.Pattern[t.Any]", t.Union["re.Pattern[t.Any]", str]]):
|
|
"""A casting compiled regular expression trait.
|
|
|
|
Accepts both strings and compiled regular expressions. The resulting
|
|
attribute will be a compiled regular expression."""
|
|
|
|
info_text = "a regular expression"
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> re.Pattern[t.Any] | None:
|
|
try:
|
|
return re.compile(value)
|
|
except Exception:
|
|
self.error(obj, value)
|
|
|
|
|
|
class UseEnum(TraitType[t.Any, t.Any]):
|
|
"""Use a Enum class as model for the data type description.
|
|
Note that if no default-value is provided, the first enum-value is used
|
|
as default-value.
|
|
|
|
.. sourcecode:: python
|
|
|
|
# -- SINCE: Python 3.4 (or install backport: pip install enum34)
|
|
import enum
|
|
from traitlets import HasTraits, UseEnum
|
|
|
|
|
|
class Color(enum.Enum):
|
|
red = 1 # -- IMPLICIT: default_value
|
|
blue = 2
|
|
green = 3
|
|
|
|
|
|
class MyEntity(HasTraits):
|
|
color = UseEnum(Color, default_value=Color.blue)
|
|
|
|
|
|
entity = MyEntity(color=Color.red)
|
|
entity.color = Color.green # USE: Enum-value (preferred)
|
|
entity.color = "green" # USE: name (as string)
|
|
entity.color = "Color.green" # USE: scoped-name (as string)
|
|
entity.color = 3 # USE: number (as int)
|
|
assert entity.color is Color.green
|
|
"""
|
|
|
|
default_value: enum.Enum | None = None
|
|
info_text = "Trait type adapter to a Enum class"
|
|
|
|
def __init__(
|
|
self, enum_class: type[t.Any], default_value: t.Any = None, **kwargs: t.Any
|
|
) -> None:
|
|
assert issubclass(enum_class, enum.Enum), "REQUIRE: enum.Enum, but was: %r" % enum_class
|
|
allow_none = kwargs.get("allow_none", False)
|
|
if default_value is None and not allow_none:
|
|
default_value = next(iter(enum_class.__members__.values()))
|
|
super().__init__(default_value=default_value, **kwargs)
|
|
self.enum_class = enum_class
|
|
self.name_prefix = enum_class.__name__ + "."
|
|
|
|
def select_by_number(self, value: int, default: t.Any = Undefined) -> t.Any:
|
|
"""Selects enum-value by using its number-constant."""
|
|
assert isinstance(value, int)
|
|
enum_members = self.enum_class.__members__
|
|
for enum_item in enum_members.values():
|
|
if enum_item.value == value:
|
|
return enum_item
|
|
# -- NOT FOUND:
|
|
return default
|
|
|
|
def select_by_name(self, value: str, default: t.Any = Undefined) -> t.Any:
|
|
"""Selects enum-value by using its name or scoped-name."""
|
|
assert isinstance(value, str)
|
|
if value.startswith(self.name_prefix):
|
|
# -- SUPPORT SCOPED-NAMES, like: "Color.red" => "red"
|
|
value = value.replace(self.name_prefix, "", 1)
|
|
return self.enum_class.__members__.get(value, default)
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
if isinstance(value, self.enum_class):
|
|
return value
|
|
elif isinstance(value, int):
|
|
# -- CONVERT: number => enum_value (item)
|
|
value2 = self.select_by_number(value)
|
|
if value2 is not Undefined:
|
|
return value2
|
|
elif isinstance(value, str):
|
|
# -- CONVERT: name or scoped_name (as string) => enum_value (item)
|
|
value2 = self.select_by_name(value)
|
|
if value2 is not Undefined:
|
|
return value2
|
|
elif value is None:
|
|
if self.allow_none:
|
|
return None
|
|
else:
|
|
return self.default_value
|
|
self.error(obj, value)
|
|
|
|
def _choices_str(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait choices (not none)."""
|
|
choices = self.enum_class.__members__.keys()
|
|
if as_rst:
|
|
return "|".join("``%r``" % x for x in choices)
|
|
else:
|
|
return repr(list(choices)) # Listify because py3.4- prints odict-class
|
|
|
|
def _info(self, as_rst: bool = False) -> str:
|
|
"""Returns a description of the trait."""
|
|
none = " or %s" % ("`None`" if as_rst else "None") if self.allow_none else ""
|
|
return f"any of {self._choices_str(as_rst)}{none}"
|
|
|
|
def info(self) -> str:
|
|
return self._info(as_rst=False)
|
|
|
|
def info_rst(self) -> str:
|
|
return self._info(as_rst=True)
|
|
|
|
|
|
class Callable(TraitType[t.Callable[..., t.Any], t.Callable[..., t.Any]]):
|
|
"""A trait which is callable.
|
|
|
|
Notes
|
|
-----
|
|
Classes are callable, as are instances
|
|
with a __call__() method."""
|
|
|
|
info_text = "a callable"
|
|
|
|
def validate(self, obj: t.Any, value: t.Any) -> t.Any:
|
|
if callable(value):
|
|
return value
|
|
else:
|
|
self.error(obj, value)
|