|
""" |
|
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 |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
ClassTypes = (type,) |
|
|
|
if t.TYPE_CHECKING: |
|
from typing_extensions import TypeVar |
|
else: |
|
from typing import TypeVar |
|
|
|
|
|
|
|
__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", |
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
""", |
|
) |
|
|
|
|
|
NoDefaultSpecified = Undefined |
|
|
|
|
|
class TraitError(Exception): |
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
if t.TYPE_CHECKING: |
|
from typing_extensions import Literal, Self |
|
|
|
K = TypeVar("K", default=str) |
|
V = TypeVar("V", default=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: |
|
|
|
self.__doc__ = self.help |
|
|
|
if len(kwargs) > 0: |
|
stacklevel = 1 |
|
f = inspect.currentframe() |
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
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: |
|
|
|
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: |
|
|
|
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, |
|
) |
|
|
|
|
|
_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: |
|
|
|
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)) |
|
|
|
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: |
|
|
|
silent = False |
|
if silent is not True: |
|
|
|
|
|
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: |
|
|
|
error.args += (self,) |
|
if self.name is not None: |
|
|
|
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 |
|
|
|
|
|
if self.name is None: |
|
|
|
raise TraitError(value, info or self.info(), self) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
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: |
|
|
|
if isinstance(other, _CallbackWrapper): |
|
return bool(self.cb == other.cb) |
|
else: |
|
return bool(self.cb == other) |
|
|
|
def __call__(self, change: Bunch) -> None: |
|
|
|
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(): |
|
|
|
|
|
|
|
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) |
|
|
|
for _, v in getmembers(cls): |
|
if isinstance(v, BaseDescriptor): |
|
v.subclass_init(cls) |
|
cls._descriptors.append(v) |
|
|
|
|
|
class MetaHasTraits(MetaHasDescriptors): |
|
"""A metaclass for HasTraits.""" |
|
|
|
def setup_class(cls: MetaHasTraits, classdict: dict[str, t.Any]) -> None: |
|
|
|
cls._trait_default_generators: dict[str, t.Any] = {} |
|
|
|
cls._all_trait_default_generators = {} |
|
cls._traits = {} |
|
cls._static_immutable_initial_values = {} |
|
|
|
super().setup_class(classdict) |
|
|
|
mro = cls.mro() |
|
|
|
for name 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] |
|
except ValueError: |
|
|
|
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] |
|
break |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
|
|
none_ok = trait.default_value is None and trait.allow_none |
|
if ( |
|
type(trait) in [CInt, Int] |
|
and trait.min is None |
|
and trait.max is None |
|
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 |
|
and trait.max is None |
|
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 |
|
|
|
|
|
|
|
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): |
|
change = Bunch(change_or_name) |
|
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: |
|
|
|
cls = args[0] |
|
args = args[1:] |
|
|
|
|
|
|
|
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. |
|
""" |
|
|
|
self = args[0] |
|
args = args[1:] |
|
|
|
self._cross_validation_lock = False |
|
cls = self.__class__ |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
self = args[0] |
|
args = args[1:] |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
super_args = args |
|
super_kwargs = {} |
|
|
|
if kwargs: |
|
|
|
|
|
def ignore(change: Bunch) -> None: |
|
pass |
|
|
|
self.notify_change = ignore |
|
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: |
|
|
|
super_kwargs[key] = value |
|
|
|
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 |
|
|
|
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() |
|
|
|
|
|
|
|
d["_trait_notifiers"] = {} |
|
d["_trait_validators"] = {} |
|
d["_trait_values"] = self._trait_values.copy() |
|
d["_cross_validation_lock"] = False |
|
|
|
return d |
|
|
|
def __setstate__(self, state: dict[str, t.Any]) -> None: |
|
self.__dict__ = state.copy() |
|
|
|
|
|
cls = self.__class__ |
|
for key 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: |
|
|
|
past_changes.append(change) |
|
return past_changes |
|
|
|
def hold(change: Bunch) -> None: |
|
name = change.name |
|
cache[name] = compress(cache.get(name), change) |
|
|
|
try: |
|
|
|
|
|
self.notify_change = hold |
|
self._cross_validation_lock = True |
|
yield |
|
|
|
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: |
|
|
|
self.notify_change = lambda x: None |
|
for name, changes in cache.items(): |
|
for change in changes[::-1]: |
|
|
|
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 |
|
|
|
del self.notify_change |
|
|
|
|
|
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): |
|
|
|
event = Bunch(event) |
|
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, [])) |
|
|
|
|
|
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) |
|
|
|
if cb not in callables: |
|
callables.append(_callback_wrapper(cb)) |
|
|
|
|
|
|
|
for c in callables: |
|
|
|
|
|
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__"): |
|
|
|
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() |
|
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: |
|
events[k] = v |
|
elif hasattr(v, "tags"): |
|
if cls.trait_names(**v.tags): |
|
events[k] = v |
|
return events |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
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: |
|
|
|
|
|
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): |
|
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: |
|
|
|
|
|
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 {})) |
|
|
|
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__ |
|
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: |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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) |
|
choices = self.values or [] |
|
matches = [match_func(value, conv_func(c)) for c in choices] |
|
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) |
|
|
|
""" |
|
|
|
|
|
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): |
|
|
|
|
|
|
|
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: |
|
|
|
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) |
|
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) |
|
|
|
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) |
|
|
|
|
|
|
|
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: |
|
|
|
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)) |
|
sig = inspect.signature(self.item_from_string) |
|
if "index" in sig.parameters: |
|
item_from_string = self.item_from_string |
|
else: |
|
|
|
def item_from_string(s: str, index: int | None = None) -> T | str: |
|
return t.cast(T, self.item_from_string(s)) |
|
|
|
return self.klass( |
|
[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 |
|
_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]) |
|
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 = ("[]", "()", "{}") |
|
|
|
|
|
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: |
|
|
|
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) |
|
|
|
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): |
|
|
|
|
|
|
|
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: |
|
|
|
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): |
|
|
|
args = None |
|
super(Container, self).__init__(klass=self.klass, args=args, **kwargs) |
|
|
|
def item_from_string(self, s: str, index: int) -> t.Any: |
|
"""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 |
|
return self._traits[index].from_string(s) |
|
|
|
def validate_elements(self, obj: t.Any, value: t.Any) -> t.Any: |
|
if not self._traits: |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
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()) |
|
|
|
""" |
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
if is_trait(value_trait): |
|
if isinstance(value_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, |
|
) |
|
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( |
|
"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) |
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
if self._key_trait: |
|
key = self._key_trait.from_string(key) |
|
|
|
|
|
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 |
|
|
|
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): |
|
|
|
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): |
|
|
|
value2 = self.select_by_number(value) |
|
if value2 is not Undefined: |
|
return value2 |
|
elif isinstance(value, str): |
|
|
|
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)) |
|
|
|
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) |
|
|