-
Notifications
You must be signed in to change notification settings - Fork 183
[v8.0] Annotate ReturnValues.py #6309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bdef771
6e60a38
ecd12f7
d458ae0
0d61a46
4c171ce
d1d07c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,14 +5,43 @@ | |
|
||
keys are converted to string | ||
""" | ||
from __future__ import annotations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It fixes a handful of annoying quirks when adding types. It was planned to be the default in 3.10 and then 3.11 but it has now been delayed twice due to having a different set of issues. PEP-649 has some more details but for now I think Technically what it does it change the type hints to being evaluated on demand rather than at function definition time. For example: class C:
@classmethod
def factory_func(cls) -> C:
return cls() By default this fails with class C:
@classmethod
def factory_func(cls) -> "C":
return cls() But this is quite ugly and annoying and makes evaluating the type hints harder. Using (Note this example isn't quite correct if you want correct typing for subclasses of |
||
|
||
import functools | ||
import sys | ||
import traceback | ||
from types import TracebackType | ||
from typing import Any, Callable, cast, Generic, Literal, overload, Type, TypeVar, Union | ||
from typing_extensions import TypedDict, ParamSpec, NotRequired | ||
|
||
from DIRAC.Core.Utilities.DErrno import strerror | ||
|
||
|
||
def S_ERROR(*args, **kwargs): | ||
T = TypeVar("T") | ||
P = ParamSpec("P") | ||
|
||
|
||
class DOKReturnType(TypedDict, Generic[T]): | ||
"""used for typing the DIRAC return structure""" | ||
|
||
OK: Literal[True] | ||
Value: T | ||
|
||
|
||
class DErrorReturnType(TypedDict): | ||
"""used for typing the DIRAC return structure""" | ||
|
||
OK: Literal[False] | ||
Message: str | ||
Errno: int | ||
ExecInfo: NotRequired[tuple[Type[BaseException], BaseException, TracebackType]] | ||
CallStack: NotRequired[list[str]] | ||
|
||
|
||
DReturnType = Union[DOKReturnType[T], DErrorReturnType] | ||
|
||
|
||
def S_ERROR(*args: Any, **kwargs: Any) -> DErrorReturnType: | ||
"""return value on error condition | ||
|
||
Arguments are either Errno and ErrorMessage or just ErrorMessage fro backward compatibility | ||
|
@@ -23,7 +52,7 @@ def S_ERROR(*args, **kwargs): | |
""" | ||
callStack = kwargs.pop("callStack", None) | ||
|
||
result = {"OK": False, "Errno": 0, "Message": ""} | ||
result: DErrorReturnType = {"OK": False, "Errno": 0, "Message": ""} | ||
|
||
message = "" | ||
if args: | ||
|
@@ -47,14 +76,21 @@ def S_ERROR(*args, **kwargs): | |
|
||
result["CallStack"] = callStack | ||
|
||
# print "AT >>> S_ERROR", result['OK'], result['Errno'], result['Message'] | ||
# for item in result['CallStack']: | ||
# print item | ||
|
||
return result | ||
|
||
|
||
def S_OK(value=None): | ||
# mypy doesn't understand default parameter values with generics so use overloads (python/mypy#3737) | ||
@overload | ||
def S_OK() -> DOKReturnType[None]: | ||
... | ||
|
||
|
||
@overload | ||
def S_OK(value: T) -> DOKReturnType[T]: | ||
... | ||
|
||
|
||
def S_OK(value=None): # type: ignore | ||
"""return value on success | ||
|
||
:param value: value of the 'Value' | ||
|
@@ -63,7 +99,7 @@ def S_OK(value=None): | |
return {"OK": True, "Value": value} | ||
|
||
|
||
def isReturnStructure(unk): | ||
def isReturnStructure(unk: Any) -> bool: | ||
"""Check if value is an `S_OK`/`S_ERROR` object""" | ||
if not isinstance(unk, dict): | ||
return False | ||
|
@@ -75,7 +111,7 @@ def isReturnStructure(unk): | |
return "Message" in unk | ||
|
||
|
||
def isSError(value): | ||
def isSError(value: Any) -> bool: | ||
"""Check if value is an `S_ERROR` object""" | ||
if not isinstance(value, dict): | ||
return False | ||
|
@@ -84,7 +120,7 @@ def isSError(value): | |
return "Message" in value | ||
|
||
|
||
def reprReturnErrorStructure(struct, full=False): | ||
def reprReturnErrorStructure(struct: DErrorReturnType, full: bool = False) -> str: | ||
errorNumber = struct.get("Errno", 0) | ||
message = struct.get("Message", "") | ||
if errorNumber: | ||
|
@@ -100,7 +136,7 @@ def reprReturnErrorStructure(struct, full=False): | |
return reprStr | ||
|
||
|
||
def returnSingleResult(dictRes): | ||
def returnSingleResult(dictRes: DReturnType[Any]) -> DReturnType[Any]: | ||
"""Transform the S_OK{Successful/Failed} dictionary convention into | ||
an S_OK/S_ERROR return. To be used when a single returned entity | ||
is expected from a generally bulk call. | ||
|
@@ -136,7 +172,7 @@ def returnSingleResult(dictRes): | |
errorMessage = list(dictRes["Value"]["Failed"].values())[0] | ||
if isinstance(errorMessage, dict): | ||
if isReturnStructure(errorMessage): | ||
return errorMessage | ||
return cast(DErrorReturnType, errorMessage) | ||
else: | ||
return S_ERROR(str(errorMessage)) | ||
return S_ERROR(errorMessage) | ||
|
@@ -150,7 +186,7 @@ def returnSingleResult(dictRes): | |
class SErrorException(Exception): | ||
"""Exception class for use with `convertToReturnValue`""" | ||
|
||
def __init__(self, result): | ||
def __init__(self, result: Union[DErrorReturnType, str]): | ||
"""Create a new exception return value | ||
|
||
If `result` is a `S_ERROR` return it directly else convert it to an | ||
|
@@ -160,10 +196,10 @@ def __init__(self, result): | |
""" | ||
if not isSError(result): | ||
result = S_ERROR(result) | ||
self.result = result | ||
self.result = cast(DErrorReturnType, result) | ||
|
||
|
||
def returnValueOrRaise(result): | ||
def returnValueOrRaise(result: DReturnType[T]) -> T: | ||
"""Unwrap an S_OK/S_ERROR response into a value or Exception | ||
|
||
This method assists with using exceptions in DIRAC code by raising | ||
|
@@ -184,7 +220,7 @@ def returnValueOrRaise(result): | |
return result["Value"] | ||
|
||
|
||
def convertToReturnValue(func): | ||
def convertToReturnValue(func: Callable[P, T]) -> Callable[P, DReturnType[T]]: | ||
"""Decorate a function to convert return values to `S_OK`/`S_ERROR` | ||
|
||
If `func` returns, wrap the return value in `S_OK`. | ||
|
@@ -196,17 +232,23 @@ def convertToReturnValue(func): | |
""" | ||
|
||
@functools.wraps(func) | ||
def wrapped(*args, **kwargs): | ||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> DReturnType[T]: | ||
try: | ||
return S_OK(func(*args, **kwargs)) | ||
value = func(*args, **kwargs) | ||
except SErrorException as e: | ||
return e.result | ||
except Exception as e: | ||
retval = S_ERROR(repr(e)) | ||
# Replace CallStack with the one from the exception | ||
exc_type, exc_value, exc_tb = sys.exc_info() | ||
retval["ExecInfo"] = exc_type, exc_value, exc_tb | ||
# Use cast as mypy doesn't understand that sys.exc_info can't return None in an exception block | ||
retval["ExecInfo"] = cast(tuple[Type[BaseException], BaseException, TracebackType], sys.exc_info()) | ||
exc_type, exc_value, exc_tb = retval["ExecInfo"] | ||
retval["CallStack"] = traceback.format_tb(exc_tb) | ||
return retval | ||
else: | ||
return S_OK(value) | ||
|
||
# functools will copy the annotations. Since we change the return type | ||
# we have to update it | ||
wrapped.__annotations__["return"] = DReturnType | ||
return wrapped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed now? Are you just adding to not forget later on?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I defer to @chaen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if
pylint
isn't complaining anymore, then we can remove it :-)