-
Notifications
You must be signed in to change notification settings - Fork 0
ci(gha): switch to custom gha which allows 0.36 of kubo installation #76
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
005856f
869766b
c14d66d
1dd5764
d5d20d9
9ca8287
d958d2a
eec4372
a276069
43c6dc7
6d48c19
b0ede54
850cee8
7094b5f
6171905
0f56989
8086d50
f400438
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 | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||||||||
| from abc import ABC, abstractmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, Literal, Tuple, cast | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Literal, Tuple, cast | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||||||||||||||||||||
| from dag_cbor.ipld import IPLDKind | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -210,27 +210,43 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||
| self.gateway_base_url: str = gateway_base_url | ||||||||||||||||||||||||||||||||||||||||||||||
| """@private""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop: Dict[asyncio.AbstractEventLoop, httpx.AsyncClient] = {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if client is not None: | ||||||||||||||||||||||||||||||||||||||||||||||
| # user supplied → bind it to *their* current loop | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop[asyncio.get_running_loop()] = client | ||||||||||||||||||||||||||||||||||||||||||||||
| self._owns_client: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||
| # A client was supplied by the user. We don't own it. | ||||||||||||||||||||||||||||||||||||||||||||||
| self._owns_client = False | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop = {asyncio.get_running_loop(): client} | ||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||
| self._owns_client = True # we'll create clients lazily | ||||||||||||||||||||||||||||||||||||||||||||||
| # No client supplied. We will own any clients we create. | ||||||||||||||||||||||||||||||||||||||||||||||
| self._owns_client = True | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop = {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
213
to
+221
Contributor
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. 🛠️ Refactor suggestion Don’t call asyncio.get_running_loop() in init when a user supplies the client Constructors run in both sync and async contexts. Calling asyncio.get_running_loop() here can raise RuntimeError if KuboCAS is instantiated outside a running loop (valid when a pre-built AsyncClient is passed). Defer binding to the first use instead, and remember the user client for that loop. Proposed minimal change: keep ownership flag, store the user client for deferred binding, and only bind once a loop exists. - if client is not None:
- # A client was supplied by the user. We don't own it.
- self._owns_client = False
- self._client_per_loop = {asyncio.get_running_loop(): client}
+ if client is not None:
+ # A client was supplied by the user. We don't own it.
+ self._owns_client = False
+ # Defer per-loop binding to _loop_client() to avoid requiring a running loop here.
+ self._client_per_loop = {}
+ self._deferred_user_client = client # bound on first useThis pairs with the _loop_client() fix below. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| # The instance is never closed on initialization. | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed = False | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # store for later use by _loop_client() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._default_headers = headers | ||||||||||||||||||||||||||||||||||||||||||||||
| self._default_auth = auth | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| self._sem: asyncio.Semaphore = asyncio.Semaphore(concurrency) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------- # | ||||||||||||||||||||||||||||||||||||||||||||||
| # helper: get or create the client bound to the current running loop # | ||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------- # | ||||||||||||||||||||||||||||||||||||||||||||||
| def _loop_client(self) -> httpx.AsyncClient: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Get or create a client for the current event loop.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| """Get or create a client for the current event loop. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| If the instance was previously closed but owns its clients, a fresh | ||||||||||||||||||||||||||||||||||||||||||||||
| client mapping is lazily created on demand. Users that supplied their | ||||||||||||||||||||||||||||||||||||||||||||||
| own ``httpx.AsyncClient`` still receive an error when the instance has | ||||||||||||||||||||||||||||||||||||||||||||||
| been closed, as we cannot safely recreate their client. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| if self._closed: | ||||||||||||||||||||||||||||||||||||||||||||||
| if not self._owns_client: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError("KuboCAS is closed; create a new instance") | ||||||||||||||||||||||||||||||||||||||||||||||
| # We previously closed all internally-owned clients. Reset the | ||||||||||||||||||||||||||||||||||||||||||||||
| # state so that new clients can be created lazily. | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed = False | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop = {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+242
to
+249
Contributor
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. Critical: leaked AsyncClients when a user-supplied client is used across loops When self._owns_client is False and _loop_client() is called from a different loop, the KeyError branch creates a new AsyncClient but aclose() is a no-op for non-owned clients. Result: clients that will never be closed. Instead, either bind the deferred user client to the first loop only, or raise if callers try to use the instance in other loops. Suggested fix (works with the deferred-binding change above): def _loop_client(self) -> httpx.AsyncClient:
@@
if self._closed:
if not self._owns_client:
raise RuntimeError("KuboCAS is closed; create a new instance")
# We previously closed all internally-owned clients. Reset the
# state so that new clients can be created lazily.
self._closed = False
self._client_per_loop = {}
@@
try:
return self._client_per_loop[loop]
except KeyError:
- # Create a new client
- client = httpx.AsyncClient(
- timeout=60.0,
- headers=self._default_headers,
- auth=self._default_auth,
- limits=httpx.Limits(max_connections=64, max_keepalive_connections=32),
- # Uncomment when they finally support Robust HTTP/2 GOAWAY responses
- # http2=True,
- )
- self._client_per_loop[loop] = client
- return client
+ # If a user-supplied client was provided, bind it to the first loop only.
+ if not self._owns_client:
+ if hasattr(self, "_deferred_user_client"):
+ self._client_per_loop[loop] = self._deferred_user_client # type: ignore[attr-defined]
+ del self._deferred_user_client
+ return self._client_per_loop[loop]
+ # Using the instance from a different loop is unsafe — refuse.
+ raise RuntimeError(
+ "User-supplied AsyncClient is bound to a different event loop; "
+ "create a per-loop KuboCAS or omit `client=` to let KuboCAS manage clients per loop."
+ )
+ # Create an internally-owned client for this loop.
+ client = httpx.AsyncClient(
+ timeout=60.0,
+ headers=self._default_headers,
+ auth=self._default_auth,
+ limits=httpx.Limits(max_connections=64, max_keepalive_connections=32),
+ # http2=True, # enable when httpx supports robust GOAWAY handling
+ )
+ self._client_per_loop[loop] = client
+ return clientThis prevents unowned clients from being created (and leaked) and still supports the per-loop model. Also applies to: 254-263 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| return self._client_per_loop[loop] | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -241,7 +257,7 @@ def _loop_client(self) -> httpx.AsyncClient: | |||||||||||||||||||||||||||||||||||||||||||||
| headers=self._default_headers, | ||||||||||||||||||||||||||||||||||||||||||||||
| auth=self._default_auth, | ||||||||||||||||||||||||||||||||||||||||||||||
| limits=httpx.Limits(max_connections=64, max_keepalive_connections=32), | ||||||||||||||||||||||||||||||||||||||||||||||
| # Uncomment when they finally support Robost HTTP/2 GOAWAY responses | ||||||||||||||||||||||||||||||||||||||||||||||
| # Uncomment when they finally support Robust HTTP/2 GOAWAY responses | ||||||||||||||||||||||||||||||||||||||||||||||
| # http2=True, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop[loop] = client | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -251,18 +267,20 @@ def _loop_client(self) -> httpx.AsyncClient: | |||||||||||||||||||||||||||||||||||||||||||||
| # graceful shutdown: close **all** clients we own # | ||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------- # | ||||||||||||||||||||||||||||||||||||||||||||||
| async def aclose(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Close all internally-created clients.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| if not self._owns_client: | ||||||||||||||||||||||||||||||||||||||||||||||
| # User supplied the client; they are responsible for closing it. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| Closes all internally-created clients. Must be called from an async context. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| if self._owns_client is False: # external client → caller closes | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # This method is async, so we can reliably await the async close method. | ||||||||||||||||||||||||||||||||||||||||||||||
| # The complex sync/async logic is handled by __del__. | ||||||||||||||||||||||||||||||||||||||||||||||
| for client in list(self._client_per_loop.values()): | ||||||||||||||||||||||||||||||||||||||||||||||
| if not client.is_closed: | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| await client.aclose() | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Best-effort cleanup; ignore errors during shutdown | ||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||
| pass # best-effort cleanup | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed = True | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -277,23 +295,44 @@ async def __aexit__(self, *exc: Any) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def __del__(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Best-effort close for internally-created clients.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| if not hasattr(self, "_owns_client") or not hasattr(self, "_closed"): | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if not self._owns_client or self._closed: | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Attempt proper cleanup if possible | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||||||||||||||||||||||||
| except RuntimeError: | ||||||||||||||||||||||||||||||||||||||||||||||
| loop = None | ||||||||||||||||||||||||||||||||||||||||||||||
| # No running loop - can't do async cleanup | ||||||||||||||||||||||||||||||||||||||||||||||
| # Just clear the client references synchronously | ||||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(self, "_client_per_loop"): | ||||||||||||||||||||||||||||||||||||||||||||||
| # We can't await client.aclose() without a loop, | ||||||||||||||||||||||||||||||||||||||||||||||
| # so just clear the references | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed = True | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # If we get here, we have a running loop | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| if loop is None or not loop.is_running(): | ||||||||||||||||||||||||||||||||||||||||||||||
| asyncio.run(self.aclose()) | ||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||
| if loop.is_running(): | ||||||||||||||||||||||||||||||||||||||||||||||
| # Schedule cleanup in the existing loop | ||||||||||||||||||||||||||||||||||||||||||||||
| loop.create_task(self.aclose()) | ||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Loop exists but not running - try asyncio.run | ||||||||||||||||||||||||||||||||||||||||||||||
| coro = self.aclose() # Create the coroutine | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| asyncio.run(coro) | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| # If asyncio.run fails, we need to close the coroutine properly | ||||||||||||||||||||||||||||||||||||||||||||||
| coro.close() # This prevents the RuntimeWarning | ||||||||||||||||||||||||||||||||||||||||||||||
| raise # Re-raise to hit the outer except block | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Suppress all errors during interpreter shutdown or loop teardown | ||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||
| # If all else fails, just clear references | ||||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(self, "_client_per_loop"): | ||||||||||||||||||||||||||||||||||||||||||||||
| self._client_per_loop.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._closed = True | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------- # | ||||||||||||||||||||||||||||||||||||||||||||||
| # save() – now uses the per-loop client # | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,8 @@ class ZarrHAMTStore(zarr.abc.store.Store): | |
| ``` | ||
| """ | ||
|
|
||
| _forced_read_only: bool | None = None # sentinel for wrapper clones | ||
|
|
||
| def __init__(self, hamt: HAMT, read_only: bool = False) -> None: | ||
| """ | ||
| ### `hamt` and `read_only` | ||
|
|
@@ -79,10 +81,36 @@ def __init__(self, hamt: HAMT, read_only: bool = False) -> None: | |
| """@private""" | ||
|
|
||
| @property | ||
| def read_only(self) -> bool: | ||
| """@private""" | ||
| def read_only(self) -> bool: # type: ignore[override] | ||
| if self._forced_read_only is not None: # instance attr overrides | ||
| return self._forced_read_only | ||
| return self.hamt.read_only | ||
|
|
||
| def with_read_only(self, read_only: bool = False) -> "ZarrHAMTStore": | ||
| """ | ||
| Return this store (if the flag already matches) or a *shallow* | ||
| clone that presents the requested read‑only status. | ||
|
|
||
| The clone **shares** the same :class:`~py_hamt.hamt.HAMT` | ||
| instance; no flushing, network traffic or async work is done. | ||
| """ | ||
| # Fast path | ||
| if read_only == self.read_only: | ||
| return self # Same mode, return same instance | ||
|
|
||
| # Create new instance with different read_only flag | ||
| # Creates a *bare* instance without running its __init__ | ||
| clone = type(self).__new__(type(self)) | ||
|
|
||
| # Copy attributes that matter | ||
| clone.hamt = self.hamt # Share the HAMT | ||
| clone._forced_read_only = read_only | ||
| clone.metadata_read_cache = self.metadata_read_cache.copy() | ||
|
|
||
| # Re‑initialise the zarr base class so that Zarr sees the flag | ||
| zarr.abc.store.Store.__init__(clone, read_only=read_only) | ||
| return clone | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| """@private""" | ||
| if not isinstance(other, ZarrHAMTStore): | ||
|
|
@@ -145,6 +173,9 @@ def supports_partial_writes(self) -> bool: | |
|
|
||
| async def set(self, key: str, value: zarr.core.buffer.Buffer) -> None: | ||
| """@private""" | ||
| if self.read_only: | ||
| raise Exception("Cannot write to a read-only store.") | ||
|
|
||
|
Comment on lines
+176
to
+178
Contributor
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. 🛠️ Refactor suggestion supports_ currently diverge from wrapper state; fix to reflect presented mode* supports_writes and supports_deletes derive from hamt.read_only. In a read‑only wrapper over a writable HAMT, these will incorrectly return True, encouraging callers to attempt writes and only fail at runtime. They should reflect self.read_only. Apply this diff: @@
@property
def supports_writes(self) -> bool:
"""@private"""
- return not self.hamt.read_only
+ return not self.read_only
@@
@property
def supports_deletes(self) -> bool:
"""@private"""
- return not self.hamt.read_only
+ return not self.read_onlyTo strengthen the contract, also consider raising a more specific exception (PermissionError) below. See next comment. Also applies to: 201-203 🤖 Prompt for AI Agents |
||
| if key in self.metadata_read_cache: | ||
| self.metadata_read_cache[key] = value.to_bytes() | ||
| await self.hamt.set(key, value.to_bytes()) | ||
|
|
@@ -167,6 +198,8 @@ def supports_deletes(self) -> bool: | |
|
|
||
| async def delete(self, key: str) -> None: | ||
| """@private""" | ||
| if self.read_only: | ||
| raise Exception("Cannot write to a read-only store.") | ||
| try: | ||
| await self.hamt.delete(key) | ||
| # In practice these lines never seem to be needed, creating and appending data are the only operations most zarrs actually undergo | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,26 +104,30 @@ async def test_kubocas_no_running_loop_in_aclose(): | |
| # Create a client in the current loop | ||
| _ = cas._loop_client() | ||
|
|
||
| # Simulate calling aclose when there's no event loop | ||
| # We'll mock this by calling the method directly | ||
| import unittest.mock | ||
|
|
||
| # Test the __del__ method with no running loop scenario | ||
| # Test __del__ behavior when there's no running loop | ||
| with unittest.mock.patch( | ||
| "asyncio.get_running_loop", side_effect=RuntimeError("No running loop") | ||
| ): | ||
| # This will trigger the exception path in __del__ | ||
| # where it gets a RuntimeError and sets loop = None | ||
| # This should handle the no-loop case gracefully | ||
| cas.__del__() | ||
|
|
||
| # Now test the normal aclose path with no running loop | ||
| # Also test aclose directly with no loop | ||
| # First close it normally | ||
| await cas.aclose() | ||
|
|
||
| # Create a new instance | ||
| cas2 = KuboCAS() | ||
| _ = cas2._loop_client() | ||
|
|
||
| # Now mock no running loop for aclose | ||
| with unittest.mock.patch( | ||
| "asyncio.get_running_loop", side_effect=RuntimeError("No running loop") | ||
| ): | ||
| await cas.aclose() | ||
| # The aclose method should handle this gracefully | ||
| await cas2.aclose() | ||
|
Comment on lines
+122
to
+127
Contributor
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. Test logic issue: Cannot await inside no-loop mock context. The test attempts to Consider restructuring the test to properly simulate the no-loop scenario: - # Now mock no running loop for aclose
- with unittest.mock.patch(
- "asyncio.get_running_loop", side_effect=RuntimeError("No running loop")
- ):
- # The aclose method should handle this gracefully
- await cas2.aclose()
+ # Test aclose in a synchronous context (simulating no event loop)
+ def test_sync_aclose():
+ cas3 = KuboCAS()
+ # Don't create any clients in this sync context
+ # Now patch get_running_loop only inside aclose
+ with unittest.mock.patch(
+ "asyncio.get_running_loop", side_effect=RuntimeError("No running loop")
+ ):
+ # Can't await in sync context, but we can call the coroutine
+ coro = cas3.aclose()
+ # The coroutine should complete without error when stepped through
+ try:
+ coro.send(None)
+ except StopIteration:
+ pass # Expected - coroutine completed
+
+ # Run the sync test
+ test_sync_aclose()
🤖 Prompt for AI Agents |
||
|
|
||
| # The client references should be cleared | ||
| assert len(cas._client_per_loop) == 0 | ||
| # Verify cleanup happened | ||
| assert len(cas2._client_per_loop) == 0 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
|
|
||
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.
💡 Verification agent
🧩 Analysis chain
Pin the
setup-ipfsaction to a commit SHA to mitigate supply-chain riskUsing a moving Git tag (
v0.7.0) means the referenced code can change without notice.For reproducible, tamper-resistant builds, pin to the exact commit hash published for that tag and optionally add the tag as an inline comment:
🏁 Script executed:
Length of output: 170
Pin
setup-ipfsto a specific commit SHATo ensure tamper-resistant and reproducible workflows, replace the moving tag with the exact commit SHA for v0.7.0:
Files to update:
Suggested diff:
📝 Committable suggestion
🤖 Prompt for AI Agents