66start servers based on configuration files or strings.
77"""
88
9+ from importlib import import_module
10+ import re
911from pydantic import (
1012 BaseModel ,
1113 Field ,
1214 ImportString ,
1315 AliasChoices ,
1416 field_validator ,
15- ValidationError ,
1617 ValidatorFunctionWrapHandler ,
1718 WrapValidator ,
1819)
19- from pydantic_core import PydanticCustomError
2020from typing import Any , Annotated , TypeAlias
2121from collections .abc import Mapping , Sequence , Iterable
2222
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+
28+
29+ class ThingImportFailure (BaseException ):
30+ """Failed to import Thing. Raise with import traceback."""
31+
2332
2433def contain_import_errors (value : Any , handler : ValidatorFunctionWrapHandler ) -> Any :
2534 """Prevent errors during import from causing odd validation errors.
@@ -32,24 +41,41 @@ def contain_import_errors(value: Any, handler: ValidatorFunctionWrapHandler) ->
3241
3342 :return: The validated value.
3443
35- :raises PydanticCustomError: if an import error occurs.
36- :raises Exception: if the ImportString logic raises a ValidationError
37- containing only import errors, this will be re-raised. All other
38- exceptions are wrapped in a PydanticCustomError.
44+ :raises ThingImportFailure: if an import error occurs, with the stack trace from
45+ retrying the import.
46+ :raises Exception: In the unlikely event that the import error cannot be reproduced
3947 """
4048 try :
4149 return handler (value )
42- except Exception as e :
43- # ImportErrors get wrapped as ValidationErrors, and that's fine:
44- # it results in a sensible error message.
45- if isinstance (e , ValidationError ):
46- if all (err ["type" ] == "import_error" for err in e .errors ()):
47- raise
48- raise PydanticCustomError (
49- "import_error" ,
50- "An exception was raised when importing '{name}'." ,
51- {"name" : str (value ), "error" : e },
52- ) from e
50+ except Exception :
51+ # In the case where this is a matching import rule.
52+ if isinstance (value , str ) and IMPORT_REGEX .match (value ):
53+ # Try to import the module again
54+ module_name = value .split (":" )[0 ]
55+ thing_name = value .split (":" )[1 ]
56+ try :
57+ module = import_module (module_name )
58+ except Exception as import_err : # noqa: BLE001
59+ # Capture the import exception and raise as a ThingImportFailure which
60+ # is a subclass of BaseException.
61+ msg = f"[{ type (import_err ).__name__ } ] { import_err } "
62+ exc = ThingImportFailure (msg )
63+ # Raise from None so the traceback is just the clear import traceback.
64+ raise exc .with_traceback (import_err .__traceback__ ) from None
65+
66+ # If check the Thing is there and if not raise the ThingImportFailure
67+ # wrapping an ImportError.
68+ if not hasattr (module , thing_name ):
69+ msg = (
70+ f"[ImportError] cannot import name '{ thing_name } ' from "
71+ f"'{ module_name } '"
72+ )
73+ # Raise from None so the traceback is just the clear import traceback.
74+ raise ThingImportFailure (msg ) from None
75+
76+ # If this was the wrong type, didn't match the regex, or somehow imported fine
77+ # then re-raise the original error.
78+ raise
5379
5480
5581ThingImportString = Annotated [
0 commit comments