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):