-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a00acc3
Showing
10 changed files
with
751 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the | ||
// README at: https://github.com/devcontainers/templates/tree/main/src/python | ||
{ | ||
"name": "Python 3", | ||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile | ||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", | ||
"customizations": { | ||
"vscode": { | ||
"extensions": [ | ||
"ms-python.python", | ||
"github.vscode-github-actions" | ||
] | ||
} | ||
} | ||
|
||
// Features to add to the dev container. More info: https://containers.dev/features. | ||
// "features": {}, | ||
|
||
// Use 'forwardPorts' to make a list of ports inside the container available locally. | ||
// "forwardPorts": [], | ||
|
||
// Use 'postCreateCommand' to run commands after the container is created. | ||
// "postCreateCommand": "pip3 install --user -r requirements.txt", | ||
|
||
// Configure tool-specific properties. | ||
// "customizations": {}, | ||
|
||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. | ||
// "remoteUser": "root" | ||
} | ||
|
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,30 @@ | ||
name: Annotate Pull Request | ||
|
||
# In a separate workflow because of | ||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ | ||
on: | ||
workflow_run: | ||
workflows: | ||
- Python test | ||
types: | ||
- completed | ||
|
||
jobs: | ||
python_test_reporter: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
checks: write | ||
if: github.event.workflow_run.name == 'Python test' | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
os: [ubuntu-20.04, windows-2019, macos-11] | ||
python-version: ["3.10", "3.11", "3.12-dev"] | ||
steps: | ||
- name: Test Report | ||
uses: dorny/test-reporter@v1 | ||
with: | ||
name: test-py${{ matrix.python-version}}-on-${{ matrix.os }} | ||
reporter: java-junit | ||
artifact: test-py${{ matrix.python-version}}-on-${{ matrix.os }} | ||
path: test_py${{ matrix.python-version }}_on_${{ matrix.os }}.xml |
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,55 @@ | ||
name: Release Wheels | ||
|
||
on: | ||
push: | ||
release: | ||
types: | ||
- published | ||
|
||
jobs: | ||
build_wheel: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.11 | ||
cache: pip | ||
- run: pip wheel . | ||
- uses: actions/upload-artifact@v3 | ||
with: | ||
name: wheel | ||
path: ./*.whl | ||
|
||
upload_wheel_test: | ||
needs: [build_wheel] | ||
runs-on: ubuntu-latest | ||
environment: | ||
name: pypi | ||
url: https://pypi.org/p/once-py | ||
permissions: | ||
id-token: write | ||
steps: | ||
- uses: actions/download-artifact@v3 | ||
with: | ||
name: wheel | ||
path: dist/ | ||
- uses: pypa/gh-action-pypi-publish@release/v1 | ||
with: | ||
repository-url: https://test.pypi.org/legacy/ | ||
|
||
upload_wheel: | ||
needs: [build_wheel] | ||
runs-on: ubuntu-latest | ||
if: github.event_name == 'release' && github.event.action == 'published' | ||
environment: | ||
name: pypi | ||
url: https://pypi.org/p/once-py | ||
permissions: | ||
id-token: write | ||
steps: | ||
- uses: actions/download-artifact@v3 | ||
with: | ||
name: wheel | ||
path: dist/ | ||
- uses: pypa/gh-action-pypi-publish@release/v1 |
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,46 @@ | ||
name: Python test | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
test: | ||
name: test-py${{ matrix.python-version}}-on-${{ matrix.os }} | ||
runs-on: ${{ matrix.os }} | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
os: [ubuntu-20.04, windows-2019, macos-11] | ||
python-version: ["3.10", "3.11", "3.12-dev", "pypy3.10"] | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
cache: pip | ||
- run: pip install pytest | ||
- run: pytest . --junitxml=junit/test_py${{ matrix.python-version }}_on_${{ matrix.os }}.xml | ||
- name: Upload pytest test results | ||
uses: actions/upload-artifact@v3 | ||
if: success() || failure() | ||
with: | ||
name: test-py${{ matrix.python-version}}-on-${{ matrix.os }} | ||
path: junit/test_py${{ matrix.python-version }}_on_${{ matrix.os }}.xml | ||
|
||
lint: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: psf/black@stable | ||
|
||
mypy: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.11 | ||
cache: pip | ||
- run: | | ||
pip install mypy | ||
mypy . |
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,8 @@ | ||
build/ | ||
dist/ | ||
*.egg-info | ||
__pycache__ | ||
.DS_Store | ||
*.whl | ||
_version.py | ||
.pytest_cache/ |
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,9 @@ | ||
MIT License | ||
|
||
Copyright (c) 2023 Delfina Care Inc. | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
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,38 @@ | ||
# Once | ||
|
||
This library provides functionality to ensure a function is called exactly | ||
once in Python, heavily inspired by `std::call_once`. | ||
|
||
During initialization, we often want to ensure code is run **exactly once**. | ||
But thinking about all the different ways this constraint can be violated can | ||
be time-consuming and complex. We don't want to have to reason about what other | ||
callers are doing and from which thread. | ||
|
||
Introducing a simple solution - the `once.once` decorator! Simply decorate a | ||
function with this decorator, and this library will handle all the edge cases | ||
to ensure it is called exactly once! The first call will invoke the function, | ||
and all subsequent calls will return the same result. Enough talking, let's | ||
cut to an example: | ||
|
||
```python | ||
import once | ||
|
||
@once.once | ||
def my_expensive_object(): | ||
load_expensive_resource() | ||
load_more_expensive_resources() | ||
return ObjectSingletonUsingLotsOfMemory() | ||
|
||
def caller_one(): | ||
my_expensive_object().use_it() | ||
|
||
def caller_two_from_a_separate_thread(): | ||
my_expensive_object().use_it() | ||
|
||
def optional_init_function_to_prewarm(): | ||
my_expensive_object() | ||
|
||
``` | ||
|
||
This module is extremely simple, with no external dependencies, and heavily | ||
tested for races. |
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,157 @@ | ||
"""Utility for initialization ensuring functions are called only once.""" | ||
import abc | ||
import inspect | ||
import functools | ||
import threading | ||
import weakref | ||
|
||
|
||
def _new_lock() -> threading.Lock: | ||
return threading.Lock() | ||
|
||
|
||
def _is_method(func): | ||
"""Determine if a function is a method on a class.""" | ||
if isinstance(func, (classmethod, staticmethod)): | ||
return True | ||
sig = inspect.signature(func) | ||
return "self" in sig.parameters | ||
|
||
|
||
class _OnceBase(abc.ABC): | ||
"""Abstract Base Class for once function decorators.""" | ||
|
||
def __init__(self, func): | ||
self._inspect_function(func) | ||
functools.update_wrapper(self, func) | ||
self.lock = _new_lock() | ||
self.called = False | ||
self.return_value = None | ||
self.func = func | ||
|
||
@abc.abstractmethod | ||
def _inspect_function(self, func): | ||
"""Inspect the passed-in function to ensure it can be wrapped. | ||
This function should raise a SyntaxError if the passed-in function is | ||
not suitable.""" | ||
|
||
def _execute_call_once(self, func, *args, **kwargs): | ||
if self.called: | ||
return self.return_value | ||
with self.lock: | ||
if self.called: | ||
return self.return_value | ||
self.return_value = func(*args, **kwargs) | ||
self.called = True | ||
return self.return_value | ||
|
||
|
||
class once(_OnceBase): # pylint: disable=invalid-name | ||
"""Decorator to ensure a function is only called once. | ||
The restriction of only one call also holds across threads. However, this | ||
restriction does not apply to unsuccessful function calls. If the function | ||
raises an exception, the next call will invoke a new call to the function. | ||
If the function is called with multiple arguments, it will still only be | ||
called only once. | ||
This decorator will fail for methods defined on a class. Use | ||
once_per_class or once_per_instance for methods on a class instead. | ||
Please note that because the value returned by the decorated function is | ||
stored to return for subsequent calls, it will not be eligible for garbage | ||
collection until after the decorated function itself has been deleted. For | ||
module and class level functions (i.e. non-closures), this means the return | ||
value will never be deleted. | ||
""" | ||
|
||
def _inspect_function(self, func): | ||
if _is_method(func): | ||
raise SyntaxError( | ||
"Attempting to use @once.once decorator on method " | ||
"instead of @once.once_per_class or @once.once_per_instance" | ||
) | ||
|
||
def __call__(self, *args, **kwargs): | ||
return self._execute_call_once(self.func, *args, **kwargs) | ||
|
||
|
||
class once_per_class(_OnceBase): # pylint: disable=invalid-name | ||
"""A version of once for class methods which runs once across all instances.""" | ||
|
||
def _inspect_function(self, func): | ||
if not _is_method(func): | ||
raise SyntaxError( | ||
"Attempting to use @once.once_per_class method-only decorator " | ||
"instead of @once.once" | ||
) | ||
|
||
# This is needed for a decorator on a class method to return a | ||
# bound version of the function to the object or class. | ||
def __get__(self, obj, cls): | ||
if isinstance(self.func, classmethod): | ||
func = functools.partial(self.func.__func__, cls) | ||
return functools.partial(self._execute_call_once, func) | ||
if isinstance(self.func, staticmethod): | ||
return functools.partial(self._execute_call_once, self.func) | ||
return functools.partial(self._execute_call_once, self.func, obj) | ||
|
||
|
||
class once_per_instance(_OnceBase): # pylint: disable=invalid-name | ||
"""A version of once for class methods which runs once per instance.""" | ||
|
||
def __init__(self, func): | ||
super().__init__(func) | ||
self.return_value = weakref.WeakKeyDictionary() | ||
self.inflight_lock = {} | ||
|
||
def _inspect_function(self, func): | ||
if isinstance(func, (classmethod, staticmethod)): | ||
raise SyntaxError("Must use @once.once_per_class on classmethod and staticmethod") | ||
if not _is_method(func): | ||
raise SyntaxError( | ||
"Attempting to use @once.once_per_instance method-only decorator " | ||
"instead of @once.once" | ||
) | ||
|
||
# This is needed for a decorator on a class method to return a | ||
# bound version of the function to the object. | ||
def __get__(self, obj, cls): | ||
del cls | ||
return functools.partial(self._execute_call_once_per_instance, obj) | ||
|
||
def _execute_call_once_per_instance(self, obj, *args, **kwargs): | ||
# We only append to the call history, and do not overwrite or remove keys. | ||
# Therefore, we can check the call history without a lock for an early | ||
# exit. | ||
# Another concern might be the weakref dictionary for return_value | ||
# getting garbage collected without a lock. However, because | ||
# user_function references whichever key it matches, it cannot be | ||
# garbage collected during this call. | ||
if obj in self.return_value: | ||
return self.return_value[obj] | ||
with self.lock: | ||
if obj in self.return_value: | ||
return self.return_value[obj] | ||
if obj in self.inflight_lock: | ||
inflight_lock = self.inflight_lock[obj] | ||
else: | ||
inflight_lock = _new_lock() | ||
self.inflight_lock[obj] = inflight_lock | ||
# Now we have a per-object lock. This means that we will not block | ||
# other instances. In addition to better performance, this reduces the | ||
# potential for deadlocks. | ||
with inflight_lock: | ||
if obj in self.return_value: | ||
return self.return_value[obj] | ||
result = self.func(obj, *args, **kwargs) | ||
self.return_value[obj] = result | ||
# At this point, any new call will find a cache hit before | ||
# even grabbing a lock. It is now safe to clean up the inflight | ||
# lock entry from the dictionary, as all subsequent will not need | ||
# it. Any other previously called inflight requests already have | ||
# their reference to the lock object, and do not need it present | ||
# in this dict either. | ||
self.inflight_lock.pop(obj) | ||
return result |
Oops, something went wrong.