Skip to content

Commit ceb79fa

Browse files
committed
fix: keep WtSite deep-copyable after adding the session lock
- threading.RLock is not copyable; the WtPage deepcopy in OSW._apply_overwrite_policy reached it through page.wtSite - broke every store_entity() call with cannot pickle '_thread.RLock' - __getstate__ drops the lock; _get_session_lock() recreates it lazily - regression test deep-copies a WtPage carrying a locked WtSite - drop unused noqa: BLE001 directives that failed ruff in CI
1 parent 71a31cf commit ceb79fa

4 files changed

Lines changed: 41 additions & 7 deletions

File tree

‎src/osw/core.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,7 +1610,7 @@ class StoreEntityPartialError(Exception):
16101610
(``failed`` / ``result.failed``) without a separate existence query.
16111611
"""
16121612

1613-
def __init__(self, result: "OSW.StoreEntityResult"):
1613+
def __init__(self, result: OSW.StoreEntityResult):
16141614
self.result = result
16151615
self.stored = list(result.pages.keys())
16161616
self.failed = result.failed
@@ -1852,7 +1852,7 @@ def failure_title_(upload_object: UploadObject) -> str:
18521852
try:
18531853
handle_upload_object_(upload_object)
18541854
results.append(None)
1855-
except Exception as e: # noqa: BLE001 - collected below
1855+
except Exception as e:
18561856
results.append(e)
18571857

18581858
failed: Dict[str, Exception] = {}

‎src/osw/utils/util.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ async def _run_tasks():
338338
async def _capture(task):
339339
try:
340340
return await task
341-
except Exception as exc: # noqa: BLE001 - returned to caller
341+
except Exception as exc:
342342
return exc
343343

344344
tasks = [_capture(task) for task in tasks]

‎src/osw/wtsite.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@ def _get_session_lock(self) -> threading.RLock:
175175
self._session_lock = lock
176176
return lock
177177

178+
def __getstate__(self):
179+
"""Drop the session lock when copying or pickling.
180+
181+
A WtSite is deep-copied as a side effect of copying a WtPage, which
182+
happens on every store_entity() call (OSW._apply_overwrite_policy).
183+
threading.RLock cannot be copied, so exclude it here; the copy gets a
184+
fresh lock from _get_session_lock() on first use.
185+
"""
186+
state = self.__dict__.copy()
187+
state.pop("_session_lock", None)
188+
return state
189+
178190
def _relogin(self):
179191
"""Re-login to the wiki site using stored credentials.
180192
@@ -1759,7 +1771,7 @@ def edit(self, comment: str = None, mode="action-multislot", bot_edit: bool = Tr
17591771
for attempt in range(max_retry):
17601772
try:
17611773
return self._edit(comment, mode, bot_edit)
1762-
except Exception as e: # noqa: BLE001 - re-raised below after retries
1774+
except Exception as e:
17631775
last_exc = e
17641776
print(f"Page edit failed: {e}. Retry ({attempt + 1}/{max_retry})")
17651777
if attempt + 1 < max_retry:

‎tests/test_wtsite_session_lock.py‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
import threading
99
import types
10+
from copy import deepcopy
1011

1112
import requests.cookies as rc
1213

13-
from osw.wtsite import WtSite
14+
from osw.wtsite import WtPage, WtSite
1415

1516

1617
def _make_wtsite_with_jar():
@@ -61,14 +62,14 @@ def clearer():
6162
path="/",
6263
)
6364
ws._clear_cookies()
64-
except Exception as exc: # noqa: BLE001 - collected and asserted below
65+
except Exception as exc:
6566
errors.append(exc)
6667

6768
def reader():
6869
try:
6970
while not stop.is_set():
7071
_ = [c.name for c in list(jar)]
71-
except Exception as exc: # noqa: BLE001 - collected and asserted below
72+
except Exception as exc:
7273
errors.append(exc)
7374

7475
clearers = [threading.Thread(target=clearer) for _ in range(4)]
@@ -86,3 +87,24 @@ def reader():
8687
assert errors == []
8788
# the non-PostEditRevision cookie must survive every clear
8889
assert "sessionToken" in {c.name for c in jar}
90+
91+
92+
def test_wtsite_survives_deepcopy_of_a_page():
93+
"""store_entity deep-copies the page, reaching WtSite through page.wtSite.
94+
95+
Without WtSite.__getstate__ dropping the lock this raises
96+
TypeError: cannot pickle '_thread.RLock' object.
97+
"""
98+
ws, jar = _make_wtsite_with_jar()
99+
_populate(jar)
100+
page = WtPage.__new__(WtPage)
101+
page.wtSite = ws
102+
page._slots = {"jsondata": {"uuid": "abc"}}
103+
104+
copied = deepcopy(page)
105+
106+
assert copied._slots == page._slots
107+
assert copied._slots is not page._slots
108+
# the copy has no lock until something asks for one
109+
assert "_session_lock" not in copied.wtSite.__dict__
110+
assert copied.wtSite._get_session_lock() is not ws._session_lock

0 commit comments

Comments
 (0)