Skip to content

Commit ce8a586

Browse files
use tempfile for state in json
1 parent 1c52a41 commit ce8a586

File tree

1 file changed

+63
-63
lines changed

1 file changed

+63
-63
lines changed

tests/utils.py

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from __future__ import annotations
22

33
import datetime
4+
import json
5+
import tempfile
46
import warnings
57
from collections.abc import Iterator
6-
from threading import Lock
8+
from pathlib import Path
79
from typing import Any
810

11+
from filelock import FileLock
12+
913
EPOCH = datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
1014

1115

@@ -19,64 +23,13 @@ class seq:
1923
The class maintains separate sequences for different parameter combinations using
2024
class-level state, protected by locks for thread safety. It supports numbers,
2125
strings, dates, times, and datetimes.
22-
23-
Examples:
24-
Simple number sequence:
25-
>>> seq(1000)
26-
1001
27-
>>> seq(1000)
28-
1002
29-
>>> seq(1000)
30-
1003
31-
32-
String sequence with suffix:
33-
>>> seq("User-", suffix="-test")
34-
'User-1-test'
35-
>>> seq("User-", suffix="-test")
36-
'User-2-test'
37-
38-
String sequence with custom start:
39-
>>> seq("User-", start=10)
40-
'User-10'
41-
>>> seq("User-", start=10)
42-
'User-11'
43-
44-
Number sequence with custom increment:
45-
>>> seq(1000, increment_by=10)
46-
1010
47-
>>> seq(1000, increment_by=10)
48-
1020
49-
50-
DateTime sequence:
51-
>>> start_date = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
52-
>>> first = seq(start_date, increment_by=datetime.timedelta(days=1))
53-
>>> first.isoformat()
54-
'2024-01-02T00:00:00+00:00'
55-
>>> second = seq(start_date, increment_by=datetime.timedelta(days=1))
56-
>>> second.isoformat()
57-
'2024-01-03T00:00:00+00:00'
58-
59-
Date sequence:
60-
>>> start = datetime.date(2024, 1, 1)
61-
>>> first = seq(start, increment_by=datetime.timedelta(days=1))
62-
>>> str(first)
63-
'2024-01-02'
64-
>>> second = seq(start, increment_by=datetime.timedelta(days=1))
65-
>>> str(second)
66-
'2024-01-03'
67-
68-
Time sequence:
69-
>>> start = datetime.time(12, 0)
70-
>>> first = seq(start, increment_by=datetime.timedelta(hours=1))
71-
>>> str(first)
72-
'13:00:00'
73-
>>> second = seq(start, increment_by=datetime.timedelta(hours=1))
74-
>>> str(second)
75-
'14:00:00'
7626
"""
7727

7828
_instances = {}
7929
_locks = {}
30+
_process_lock_file = Path(tempfile.gettempdir(), "seq_process_lock")
31+
_process_lock = FileLock(_process_lock_file)
32+
_state_file = Path(tempfile.gettempdir(), "seq_state.json")
8033

8134
def __init__(
8235
self,
@@ -128,13 +81,23 @@ def __new__(
12881
):
12982
key = (value, increment_by, start, suffix)
13083

131-
if key not in cls._locks:
132-
# Use a temporary lock to protect lock creation
133-
with Lock():
134-
if key not in cls._locks:
135-
cls._locks[key] = Lock()
84+
# Use process-safe lock for instance creation
85+
with cls._process_lock:
86+
if key not in cls._locks:
87+
cls._locks[key] = cls._get_process_safe_lock(key)
13688

137-
with cls._locks[key]:
89+
# Load saved state if instance doesn't exist
90+
if key not in cls._instances:
91+
saved_state = cls._load_state()
92+
if key in saved_state:
93+
instance = super().__new__(cls)
94+
instance.__init__(value, increment_by, start, suffix)
95+
instance._current = saved_state[key]
96+
instance._initialized = True
97+
cls._instances[key] = instance
98+
99+
lock = cls._locks[key]
100+
with lock:
138101
if key not in cls._instances:
139102
instance = super().__new__(cls)
140103
instance.__init__(value, increment_by, start, suffix)
@@ -148,6 +111,9 @@ def __new__(
148111
instance = cls._instances[key]
149112
instance._current += instance._increment
150113

114+
# Save state after updating
115+
cls._save_state()
116+
151117
if isinstance(value, (datetime.datetime, datetime.date)):
152118
return instance._generate_datetime_value()
153119
elif isinstance(value, datetime.time):
@@ -212,8 +178,42 @@ def _generate_text_value(self) -> str:
212178
@classmethod
213179
def _reset(cls):
214180
"""Reset all sequence state. Used for testing purposes."""
215-
cls._instances.clear()
216-
cls._locks.clear()
181+
with cls._process_lock:
182+
cls._instances.clear()
183+
cls._locks.clear()
184+
if cls._state_file.exists():
185+
cls._state_file.unlink()
186+
187+
@classmethod
188+
def _load_state(cls):
189+
"""Load sequence state from file."""
190+
try:
191+
with cls._process_lock:
192+
if cls._state_file.exists():
193+
state = json.loads(cls._state_file.read_text())
194+
return {
195+
tuple(json.loads(k)): v.get("_current", 0)
196+
for k, v in state.items()
197+
}
198+
except Exception:
199+
return {}
200+
return {}
201+
202+
@classmethod
203+
def _save_state(cls):
204+
"""Save sequence state to file."""
205+
with cls._process_lock:
206+
state = {
207+
json.dumps([str(k) for k in key]): {"_current": instance._current}
208+
for key, instance in cls._instances.items()
209+
}
210+
cls._state_file.write_text(json.dumps(state))
211+
212+
@classmethod
213+
def _get_process_safe_lock(cls, key):
214+
"""Get or create a process-safe lock for the given key."""
215+
lock_file = Path(tempfile.gettempdir(), f"seq_lock_{hash(key)}")
216+
return FileLock(lock_file)
217217

218218
@classmethod
219219
def iter(

0 commit comments

Comments
 (0)