From 3c621a5c55809c4a6e0371716aa795ac2ce96def Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Mon, 3 Aug 2026 21:38:09 +0200 Subject: [PATCH] Defer heavy imports to speed up import time traitlets and its core utils imported inspect (which pulls in ast, dis, tokenize, linecache), pathlib, and ast at module top level, and traitlets.config additionally imported logging.config (pulling logging.handlers, socket, pickle, dataclasses), pprint and json eagerly. All of these are only needed on cold paths (string parsing, filesystem Path traits, help/config-dump output, logging configuration at Application startup). Because the modules use `from __future__ import annotations`, annotations referring to these names are never evaluated at runtime, so the imports can be deferred to their actual (rare) use sites. inspect.isclass(x) is replaced with isinstance(x, type) (which is strictly identical since python 3) and inspect.currentframe() with sys._getframe() (which is the same on Cpython) to avoid needing inspect at all on the class-definition path. --- tests/test_traitlets.py | 14 ++++++++++++++ traitlets/config/application.py | 11 ++++++++--- traitlets/config/loader.py | 5 ++++- traitlets/traitlets.py | 26 +++++++++++++++++++------- traitlets/utils/__init__.py | 3 ++- traitlets/utils/decorators.py | 5 ++++- traitlets/utils/descriptions.py | 11 ++++++----- traitlets/utils/getargspec.py | 10 +++++++--- traitlets/utils/warnings.py | 3 ++- 9 files changed, 66 insertions(+), 22 deletions(-) diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py index d2c755a45..3e9377fa8 100644 --- a/tests/test_traitlets.py +++ b/tests/test_traitlets.py @@ -1657,6 +1657,11 @@ def coerce(self, value): return pathlib.Path(value) +def test_path_from_string(): + assert Path().from_string("foo/bar") == pathlib.Path("foo/bar") + assert Path(allow_none=True).from_string("None") is None + + class ListTrait(HasTraits): value = List(Int()) @@ -3040,6 +3045,15 @@ def test_list_from_string(s, expected): _from_string_test(List, s, expected) +def test_container_from_string_list_deprecated_literal(): + # Passing a whole container literal as a single --opt="[...]" element is + # deprecated (traitlets 5.0) but still supported. + trait = List(Integer()) + trait.name = "x" + with pytest.warns(DeprecationWarning, match="is deprecated in traitlets 5.0"): + assert trait.from_string_list(["[1, 2, 3]"]) == [1, 2, 3] + + @pytest.mark.parametrize( "s, expected, value_trait", [ diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 5b1f0e6aa..dc80ab123 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -5,17 +5,14 @@ from __future__ import annotations import functools -import json import logging import os -import pprint import re import sys import typing as t from collections import OrderedDict, defaultdict from contextlib import suppress from copy import deepcopy -from logging.config import dictConfig from textwrap import dedent from traitlets.config.configurable import Configurable, SingletonConfigurable @@ -286,6 +283,8 @@ def _observe_logging_default(self, change: Bunch) -> None: self._configure_logging() def _configure_logging(self) -> None: + from logging.config import dictConfig + config = self.get_default_logging_config() nested_update(config, self.logging_config or {}) dictConfig(config) @@ -483,6 +482,8 @@ def start_show_config(self) -> None: cls_config.pop("show_config_json", None) if self.show_config_json: + import json + json.dump(config, sys.stdout, indent=1, sort_keys=True, default=repr) # add trailing newline sys.stdout.write("\n") @@ -498,6 +499,8 @@ def start_show_config(self) -> None: class_config = config[classname] if not class_config: continue + import pprint + print(classname) pformat_kwargs: StrDict = dict(indent=4, compact=True) # noqa: C408 @@ -929,6 +932,8 @@ def _load_config_files( if log: log.debug("Loaded config file: %s", loader.full_filename) if config: + import json + for filename, earlier_config in zip(filenames, loaded, strict=True): collisions = earlier_config.collisions(config) if collisions and log: diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index baaa4eaff..876f1186c 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -7,7 +7,6 @@ import argparse import copy import functools -import json import os import re import sys @@ -577,6 +576,8 @@ def load_config(self) -> Config: return self.config def _read_file_as_dict(self) -> dict[str, t.Any]: + import json + with open(self.full_filename) as f: return t.cast("dict[str, t.Any]", json.load(f)) @@ -603,6 +604,8 @@ def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> No configuration to disk. """ self.config.version = 1 + import json + json_config = json.dumps(self.config, indent=2) with open(self.full_filename, "w") as f: f.write(json_config) diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index d149f13c3..5e066aee4 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -43,15 +43,12 @@ import contextlib import enum -import inspect import numbers import os -import pathlib 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 @@ -63,6 +60,8 @@ SequenceTypes = (list, tuple, set, frozenset) if t.TYPE_CHECKING: + import pathlib + from typing_extensions import TypeVar else: from typing import TypeVar @@ -185,6 +184,8 @@ def _safe_literal_eval(s: str) -> t.Any: Use only where types are ambiguous. """ + from ast import literal_eval + try: return literal_eval(s) except (NameError, SyntaxError, ValueError): @@ -559,7 +560,7 @@ def __init__( if len(kwargs) > 0: stacklevel = 1 - f = inspect.currentframe() + f: types.FrameType | None = sys._getframe() # count supers to determine stacklevel for warning assert f is not None while f.f_code.co_name == "__init__": @@ -974,7 +975,7 @@ def __new__( # ---------------------------------------------------------------- # Support of deprecated behavior allowing for TraitType types # to be used instead of TraitType instances. - if inspect.isclass(v) and issubclass(v, TraitType): + if isinstance(v, type) 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.", @@ -2114,7 +2115,7 @@ def __init__( else: klass = default_value - if not (inspect.isclass(klass) or isinstance(klass, str)): + if not isinstance(klass, (type, str)): raise TraitError("A Type trait must specify a class.") self.klass = klass @@ -2278,7 +2279,7 @@ class or its subclasses. Our implementation is quite different if klass is None: klass = self.klass - if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, str)): + if (klass is not None) and isinstance(klass, (type, str)): self.klass = klass else: raise TraitError(f"The klass attribute must be a class not: {klass!r}") @@ -3510,6 +3511,8 @@ 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}") + from ast import literal_eval + try: test = literal_eval(s) except Exception: @@ -3542,7 +3545,11 @@ def from_string_list(self, s_list: list[str]) -> T | None: DeprecationWarning, stacklevel=2, ) + from ast import literal_eval + return self.klass(literal_eval(r)) # type:ignore[call-arg] + import inspect + sig = inspect.signature(self.item_from_string) if "index" in sig.parameters: item_from_string = self.item_from_string @@ -4069,6 +4076,7 @@ def from_string_list(self, s_list: list[str]) -> t.Any: DeprecationWarning, stacklevel=2, ) + from ast import literal_eval return literal_eval(s_list[0]) @@ -4192,11 +4200,15 @@ class Path(TraitType["pathlib.Path", t.Union["pathlib.Path", str, "os.PathLike[s info_text = "a filesystem path" def validate(self, obj: t.Any, value: t.Any) -> pathlib.Path | None: + import pathlib + if isinstance(value, (str, os.PathLike)): return pathlib.Path(value) self.error(obj, value) def from_string(self, s: str) -> pathlib.Path | None: + import pathlib + if self.allow_none and s == "None": return None return pathlib.Path(s) diff --git a/traitlets/utils/__init__.py b/traitlets/utils/__init__.py index 10d6a80fa..ef5530560 100644 --- a/traitlets/utils/__init__.py +++ b/traitlets/utils/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import pathlib from collections.abc import Sequence @@ -51,6 +50,8 @@ def filefind(filename: str, path_dirs: Sequence[str] | None = None) -> str: if os.path.isabs(filename) and os.path.isfile(filename): return filename + import pathlib + if path_dirs is None: path_dirs = ("",) elif isinstance(path_dirs, str): diff --git a/traitlets/utils/decorators.py b/traitlets/utils/decorators.py index e661e3f63..9ae3bdff2 100644 --- a/traitlets/utils/decorators.py +++ b/traitlets/utils/decorators.py @@ -3,7 +3,6 @@ from __future__ import annotations import copy -from inspect import Parameter, Signature, signature from typing import Any, TypeVar from ..traitlets import HasTraits, Undefined @@ -11,6 +10,8 @@ def _get_default(value: Any) -> Any: """Get default argument value, given the trait default value.""" + from inspect import Parameter + return Parameter.empty if value == Undefined else value @@ -19,6 +20,8 @@ def _get_default(value: Any) -> Any: def signature_has_traits(cls: type[T]) -> type[T]: """Return a decorated class with a constructor signature that contain Trait names as kwargs.""" + from inspect import Parameter, Signature, signature + traits = [ (name, _get_default(value.default_value)) for name, value in cls.class_traits().items() diff --git a/traitlets/utils/descriptions.py b/traitlets/utils/descriptions.py index eefbafb8e..82c258260 100644 --- a/traitlets/utils/descriptions.py +++ b/traitlets/utils/descriptions.py @@ -1,6 +1,5 @@ from __future__ import annotations -import inspect import re import types from typing import Any @@ -71,14 +70,14 @@ class name where an object was defined. if isinstance(article, str): article = article.lower() - if not inspect.isclass(value): + if not isinstance(value, type): typename = type(value).__name__ else: typename = value.__name__ if verbose: typename = _prefix(value) + typename - if article == "the" or (article is None and not inspect.isclass(value)): + if article == "the" or (article is None and not isinstance(value, type)): if name is not None: result = f"{typename} {name}" if article is not None: @@ -87,7 +86,7 @@ class name where an object was defined. return result else: tick_wrap = False - if inspect.isclass(value): + if isinstance(value, type): name = value.__name__ elif isinstance(value, types.FunctionType): name = value.__name__ @@ -123,6 +122,8 @@ def _prefix(value: Any) -> str: if isinstance(value, types.MethodType): name = describe(None, value.__self__, verbose=True) + "." else: + import inspect + module = inspect.getmodule(value) if module is not None and module.__name__ != "builtins": name = module.__name__ + "." @@ -136,7 +137,7 @@ def class_of(value: Any) -> Any: For example 'an Image' or 'a PlotValue'. """ - if inspect.isclass(value): + if isinstance(value, type): return add_article(value.__name__) else: return class_of(type(value)) diff --git a/traitlets/utils/getargspec.py b/traitlets/utils/getargspec.py index 742191be9..f8f86a728 100644 --- a/traitlets/utils/getargspec.py +++ b/traitlets/utils/getargspec.py @@ -10,15 +10,19 @@ from __future__ import annotations -import inspect -from functools import partial -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import inspect # Unmodified from sphinx below this line def getargspec(func: Any) -> inspect.FullArgSpec: """Like inspect.getargspec but supports functools.partial as well.""" + import inspect + from functools import partial + if inspect.ismethod(func): func = func.__func__ if type(func) is partial: diff --git a/traitlets/utils/warnings.py b/traitlets/utils/warnings.py index e4d2c1260..aa99b7dcf 100644 --- a/traitlets/utils/warnings.py +++ b/traitlets/utils/warnings.py @@ -1,6 +1,5 @@ from __future__ import annotations -import inspect import os import typing as t import warnings @@ -20,6 +19,8 @@ def deprecated_method(method: t.Any, cls: t.Any, method_name: str, msg: str) -> Uses warn_explicit to bind warning to method definition instead of triggering code, which isn't relevant. """ + import inspect + warn_msg = f"{cls.__name__}.{method_name} is deprecated in traitlets 4.1: {msg}" for parent in inspect.getmro(cls):