66start servers based on configuration files or strings.
77"""
88
9- from importlib import import_module
10- import re
119from pydantic import (
1210 BaseModel ,
1311 Field ,
1614 field_validator ,
1715 ValidatorFunctionWrapHandler ,
1816 WrapValidator ,
17+ ValidationError ,
1918)
2019from typing import Any , Annotated , TypeAlias
2120from collections .abc import Mapping , Sequence , Iterable
2221
23- PYTHON_EL_RE_STR = r"[a-zA-Z_][a-zA-Z0-9_]*"
24- IMPORT_REGEX = re .compile (
25- rf"^{ PYTHON_EL_RE_STR } (?:\.{ PYTHON_EL_RE_STR } )*:{ PYTHON_EL_RE_STR } $"
26- )
27-
2822
2923class ThingImportFailure (BaseException ):
3024 """Failed to import Thing. Raise with import traceback."""
@@ -51,35 +45,30 @@ def contain_import_errors(value: Any, handler: ValidatorFunctionWrapHandler) ->
5145 """
5246 try :
5347 return handler (value )
54- except Exception :
55- # In the case where this is a matching import rule.
56- if isinstance (value , str ) and IMPORT_REGEX .match (value ):
57- # Try to import the module again
58- module_name = value .split (":" )[0 ]
59- thing_name = value .split (":" )[1 ]
60- try :
61- module = import_module (module_name )
62- except Exception as import_err : # noqa: BLE001
63- # Capture the import exception and raise as a ThingImportFailure which
64- # is a subclass of BaseException.
65- msg = f"[{ type (import_err ).__name__ } ] { import_err } "
66- exc = ThingImportFailure (msg )
67- # Raise from None so the traceback is just the clear import traceback.
68- raise exc .with_traceback (import_err .__traceback__ ) from None
69-
70- # If check the Thing is there and if not raise the ThingImportFailure
71- # wrapping an ImportError.
72- if not hasattr (module , thing_name ):
73- msg = (
74- f"[ImportError] cannot import name '{ thing_name } ' from "
75- f"'{ module_name } '"
76- )
77- # Raise from None so the traceback is just the clear import traceback.
78- raise ThingImportFailure (msg ) from None
79-
80- # If this was the wrong type, didn't match the regex, or somehow imported fine
81- # then re-raise the original error.
82- raise
48+ except Exception as validation_err :
49+ # TypeError and ValueErrors are turned into validation errors. Other errors
50+ # are passed through.
51+ # First reraise other errors directly from None to improve the trace.
52+ if not isinstance (validation_err , ValidationError ):
53+ exc = ThingImportFailure (
54+ f"[{ type (validation_err ).__name__ } ] { validation_err } "
55+ )
56+ raise exc .with_traceback (validation_err .__traceback__ ) from None
57+ # If it is a validation errror but we we can't find the source then just raise.
58+ errors = validation_err .errors ()
59+ if not (len (errors ) > 0 and "ctx" in errors [0 ] and "error" in errors [0 ]["ctx" ]):
60+ raise
61+
62+ # Finally raise the original error during import if it triggered a validation
63+ # error
64+ orig_err = errors [0 ]["ctx" ]["error" ]
65+ if isinstance (orig_err , str ):
66+ # If the import failed due to the module/class not being found then it is
67+ # a string so raise it.
68+ raise ThingImportFailure (f"{ orig_err } " ) from None
69+ exc = ThingImportFailure (f"[{ type (orig_err ).__name__ } ] { orig_err } " )
70+ # Raise from None so the traceback is just the clear import traceback.
71+ raise exc .with_traceback (orig_err .__traceback__ ) from None
8372
8473
8574ThingImportString = Annotated [
0 commit comments