-
Notifications
You must be signed in to change notification settings - Fork 135
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
Replace Rollback with Scope #444
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
9b1c687
Replace rollback with scope
0f823cf
Update tests
655505a
Replace rollback uses
73d7b3f
Fix imports
e75c7ab
update changelog
afc658a
Fix add() type anns in Scope
75e80c5
Remove current()
1a21d48
Refactor scope
350308d
Update tests
4582adc
Rename add to scope_add
4932951
fix imports
c6d7dbf
Code cleanings
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# Copyright (C) 2021 Intel Corporation | ||
# | ||
# SPDX-License-Identifier: MIT | ||
|
||
from contextlib import ExitStack, contextmanager | ||
from functools import partial, wraps | ||
from typing import Any, Callable, ContextManager, Dict, Optional, Tuple, TypeVar | ||
import threading | ||
|
||
from attr import attrs | ||
|
||
from datumaro.util import optional_arg_decorator | ||
|
||
T = TypeVar('T') | ||
|
||
class Scope: | ||
""" | ||
A context manager that allows to register error and exit callbacks. | ||
""" | ||
|
||
_thread_locals = threading.local() | ||
|
||
@attrs(auto_attribs=True) | ||
class ExitHandler: | ||
callback: Callable[[], Any] | ||
ignore_errors: bool = True | ||
|
||
def __exit__(self, exc_type, exc_value, exc_traceback): | ||
try: | ||
self.callback() | ||
except Exception: | ||
if not self.ignore_errors: | ||
raise | ||
|
||
@attrs | ||
class ErrorHandler(ExitHandler): | ||
def __exit__(self, exc_type, exc_value, exc_traceback): | ||
if exc_type: | ||
return super().__exit__(exc_type=exc_type, exc_value=exc_value, | ||
exc_traceback=exc_traceback) | ||
|
||
|
||
def __init__(self): | ||
self._stack = ExitStack() | ||
self.enabled = True | ||
|
||
def on_error_do(self, callback: Callable, | ||
*args, kwargs: Optional[Dict[str, Any]] = None, | ||
ignore_errors: bool = False): | ||
""" | ||
Registers a function to be called on scope exit because of an error. | ||
|
||
If ignore_errors is True, the errors from this function call | ||
will be ignored. | ||
""" | ||
|
||
self._register_callback(self.ErrorHandler, | ||
ignore_errors=ignore_errors, | ||
callback=callback, args=args, kwargs=kwargs) | ||
|
||
def on_exit_do(self, callback: Callable, | ||
*args, kwargs: Optional[Dict[str, Any]] = None, | ||
ignore_errors: bool = False): | ||
""" | ||
Registers a function to be called on scope exit. | ||
""" | ||
|
||
self._register_callback(self.ExitHandler, | ||
ignore_errors=ignore_errors, | ||
callback=callback, args=args, kwargs=kwargs) | ||
|
||
def _register_callback(self, handler_type, callback: Callable, | ||
args: Tuple[Any] = None, kwargs: Dict[str, Any] = None, | ||
ignore_errors: bool = False): | ||
if args or kwargs: | ||
callback = partial(callback, *args, **(kwargs or {})) | ||
|
||
self._stack.push(handler_type(callback, ignore_errors=ignore_errors)) | ||
|
||
def add(self, cm: ContextManager[T]) -> T: | ||
""" | ||
Enters a context manager and adds it to the exit stack. | ||
|
||
Returns: cm.__enter__() result | ||
""" | ||
|
||
return self._stack.enter_context(cm) | ||
|
||
def enable(self): | ||
self.enabled = True | ||
|
||
def disable(self): | ||
self.enabled = False | ||
|
||
def close(self): | ||
self.__exit__(None, None, None) | ||
|
||
def __enter__(self) -> 'Scope': | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_value, exc_traceback): | ||
if not self.enabled: | ||
return | ||
|
||
self._stack.__exit__(exc_type, exc_value, exc_traceback) | ||
self._stack.pop_all() # prevent issues on repetitive calls | ||
|
||
@classmethod | ||
def current(cls) -> 'Scope': | ||
return cls._thread_locals.current | ||
|
||
@contextmanager | ||
def as_current(self): | ||
previous = getattr(self._thread_locals, 'current', None) | ||
self._thread_locals.current = self | ||
try: | ||
yield | ||
finally: | ||
self._thread_locals.current = previous | ||
|
||
@optional_arg_decorator | ||
def scoped(func, arg_name=None): | ||
""" | ||
A function decorator, which allows to do actions with the current scope, | ||
such as registering error and exit callbacks and context managers. | ||
""" | ||
|
||
@wraps(func) | ||
def wrapped_func(*args, **kwargs): | ||
with Scope() as scope: | ||
if arg_name is None: | ||
with scope.as_current(): | ||
ret_val = func(*args, **kwargs) | ||
else: | ||
kwargs[arg_name] = scope | ||
ret_val = func(*args, **kwargs) | ||
return ret_val | ||
|
||
return wrapped_func | ||
|
||
# Shorthands for common cases | ||
def on_error_do(callback, *args, ignore_errors=False, kwargs=None): | ||
return Scope.current().on_error_do(callback, *args, | ||
ignore_errors=ignore_errors, kwargs=kwargs) | ||
|
||
def on_exit_do(callback, *args, ignore_errors=False, kwargs=None): | ||
return Scope.current().on_exit_do(callback, *args, | ||
ignore_errors=ignore_errors, kwargs=kwargs) | ||
|
||
def scope_add(cm: ContextManager[T]) -> T: | ||
return Scope.current().add(cm) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
(and ditto for the other
None
-returning functions)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.
What is the point in this?
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.
To help typecheckers find errors.
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.
So you suggest to add it to all functions without return value?
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 would say yes (if there are annotations on the function's arguments). Otherwise, the return type defaults to
Any
.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'm not convinced it is worthy investment of efforts in most cases. I'm open for a PR on this, but I don't think it is needed for the functions, which names don't not suppose any return value.