Skip to content

Commit f5ecc99

Browse files
committed
[0.8.0] refactor app_state using dataclass
1 parent 32445c3 commit f5ecc99

File tree

4 files changed

+185
-156
lines changed

4 files changed

+185
-156
lines changed

app_state/__init__.py

Lines changed: 82 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from dataclasses import dataclass, field
12
from functools import partial
23
from json import dumps, loads
34
from queue import Queue
4-
from typing import Optional
5+
from typing import Optional, Any, List
56

67
from keyring import get_password
78
from obsws_python import ReqClient
@@ -10,74 +11,91 @@
1011

1112
import constant
1213
from constant import *
13-
from models.classes import ThreadSafeDict
14+
from .app_state_base import StateBase
1415

1516
dumps = partial(dumps, ensure_ascii=False,
1617
separators=(",", ":"))
17-
_APP_CONFIG_INITIALIZATION = {
18-
"proxy_mode": ProxyMode.NONE,
19-
"custom_proxy_url": "",
20-
"custom_tray_icon": "",
21-
"custom_tray_hint": "",
22-
"custom_font": "",
23-
"prefer_proto": PreferProto.RTMP,
24-
}
25-
_OBS_SETTINGS_INITIALIZATION = {
26-
"ip_addr": "localhost",
27-
"port": "4455",
28-
"password": "",
29-
"auto_live": False,
30-
"auto_connect": False
31-
}
32-
_SCAN_STATUS_INITIALIZATION = {
33-
"scanned": False, "qr_key": None, "qr_url": None,
34-
"expired": False, "is_new": False,
35-
"cred_loaded": False,
36-
"timeout": False, "wait_for_confirm": False,
37-
"area_updated": False, "room_updated": False,
38-
"const_updated": False, "announce_updated": False
39-
}
40-
_STREAM_STATUS_INITIALIZATION = {
41-
"live_status": False,
42-
"required_face": False,
43-
"identified_face": False,
44-
"face_url": None,
45-
"stream_addr": None,
46-
"stream_key": None
47-
}
48-
_ROOM_INFO_INITIALIZATION = {
49-
"cover_audit_reason": "",
50-
"cover_url": "",
51-
"cover_status": CoverStatus.AUDIT_IN_PROGRESS,
52-
"cover_data": None,
53-
"room_id": "",
54-
"title": "",
55-
"parent_area": "",
56-
"area": "",
57-
"announcement": "",
58-
"recent_areas": [],
59-
"recent_title": [],
60-
}
18+
19+
20+
@dataclass
21+
class AppSettings(StateBase):
22+
proxy_mode: ProxyMode = ProxyMode.NONE
23+
custom_proxy_url: str = ""
24+
custom_tray_icon: str = ""
25+
custom_tray_hint: str = ""
26+
custom_font: str = ""
27+
prefer_proto: PreferProto = PreferProto.RTMP
28+
29+
30+
@dataclass
31+
class ObsSettings(StateBase):
32+
ip_addr: str = "localhost"
33+
port: str = "4455"
34+
password: str = ""
35+
auto_live: bool = False
36+
auto_connect: bool = False
37+
38+
39+
@dataclass
40+
class ScanStatus(StateBase):
41+
scanned: bool = False
42+
qr_key: Optional[str] = None
43+
qr_url: Optional[str] = None
44+
expired: bool = False
45+
is_new: bool = False
46+
cred_loaded: bool = False
47+
timeout: bool = False
48+
wait_for_confirm: bool = False
49+
area_updated: bool = False
50+
room_updated: bool = False
51+
const_updated: bool = False
52+
announce_updated: bool = False
53+
54+
55+
@dataclass
56+
class StreamStatus(StateBase):
57+
live_status: bool = False
58+
required_face: bool = False
59+
identified_face: bool = False
60+
face_url: Optional[str] = None
61+
stream_addr: Optional[str] = None
62+
stream_key: Optional[str] = None
63+
64+
65+
@dataclass
66+
class RoomInfo(StateBase):
67+
cover_audit_reason: str = ""
68+
cover_url: str = ""
69+
cover_status: CoverStatus = CoverStatus.AUDIT_IN_PROGRESS
70+
cover_data: Any = None
71+
room_id: str = ""
72+
title: str = ""
73+
parent_area: str = ""
74+
area: str = ""
75+
announcement: str = ""
76+
recent_areas: List[str] = field(default_factory=list)
77+
recent_title: List[str] = field(default_factory=list)
78+
6179

6280
# Queue to communicate with OBS in a separate thread
6381
obs_req_queue = Queue()
6482

6583
# Scan status flags for login
66-
scan_status = ThreadSafeDict.new(_SCAN_STATUS_INITIALIZATION)
84+
scan_status = ScanStatus()
6785

6886
# Stream status stores fetched RTMP info and verification state
69-
stream_status = ThreadSafeDict.new(_STREAM_STATUS_INITIALIZATION)
87+
stream_status = StreamStatus()
7088

71-
app_settings = ThreadSafeDict.new(_APP_CONFIG_INITIALIZATION)
89+
app_settings = AppSettings()
7290

7391
if (app := get_password(KEYRING_SERVICE_NAME,
7492
KEYRING_APP_SETTINGS)) is not None:
7593
app_settings.update(loads(app))
7694

7795
# Managed by models.workers.credential_manager
78-
room_info = ThreadSafeDict({})
79-
obs_settings = ThreadSafeDict({})
80-
usernames = ThreadSafeDict({})
96+
room_info = RoomInfo()
97+
obs_settings = ObsSettings()
98+
usernames = {}
8199
# A cache of cookie indices
82100
cookie_indices = []
83101

@@ -125,24 +143,24 @@ def create_session() -> Session:
125143
return session
126144

127145

128-
def app_settings_default():
129-
app_settings.update(_APP_CONFIG_INITIALIZATION)
146+
def app_settings_default() -> None:
147+
app_settings.reset()
130148

131149

132-
def scan_settings_default():
133-
scan_status.update(_SCAN_STATUS_INITIALIZATION)
150+
def scan_settings_default() -> None:
151+
scan_status.reset()
134152
scan_status["const_updated"] = True
135153

136154

137-
def room_info_default():
138-
_ROOM_INFO_INITIALIZATION["recent_areas"].clear()
139-
_ROOM_INFO_INITIALIZATION["recent_title"].clear()
140-
room_info.update(_ROOM_INFO_INITIALIZATION)
155+
def room_info_default() -> None:
156+
room_info.recent_areas.clear()
157+
room_info.recent_title.clear()
158+
room_info.reset()
141159

142160

143-
def stream_status_default():
144-
stream_status.update(_STREAM_STATUS_INITIALIZATION)
161+
def stream_status_default() -> None:
162+
stream_status.reset()
145163

146164

147-
def obs_settings_default():
148-
obs_settings.update(_OBS_SETTINGS_INITIALIZATION)
165+
def obs_settings_default() -> None:
166+
obs_settings.reset()

app_state/app_state_base.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from dataclasses import dataclass, field, fields, MISSING
2+
from typing import Any, Mapping, Iterator, Tuple
3+
from copy import deepcopy
4+
5+
from PySide6.QtCore import QMutex, QMutexLocker
6+
7+
8+
@dataclass
9+
class StateBase:
10+
_lock: QMutex = field(default_factory=QMutex, init=False, repr=False)
11+
12+
# obj["field"]
13+
def __getitem__(self, key: str) -> Any:
14+
with QMutexLocker(self._lock):
15+
if not hasattr(self, key):
16+
raise KeyError(key)
17+
return getattr(self, key)
18+
19+
# obj["field"] = value
20+
def __setitem__(self, key: str, value: Any) -> None:
21+
with QMutexLocker(self._lock):
22+
if not hasattr(self, key):
23+
raise KeyError(key)
24+
setattr(self, key, value)
25+
26+
# obj.get("field", default)
27+
def get(self, key: str, default: Any = None) -> Any:
28+
with QMutexLocker(self._lock):
29+
if not hasattr(self, key):
30+
return default
31+
return getattr(self, key)
32+
33+
# obj.update({...})
34+
def update(self, mapping: Mapping[str, Any] | None = None,
35+
**kwargs: Any) -> None:
36+
with QMutexLocker(self._lock):
37+
if mapping:
38+
for k, v in mapping.items():
39+
if hasattr(self, k):
40+
setattr(self, k, v)
41+
for k, v in kwargs.items():
42+
if hasattr(self, k):
43+
setattr(self, k, v)
44+
45+
def as_dict(self) -> dict[str, Any]:
46+
with QMutexLocker(self._lock):
47+
return {
48+
f.name: getattr(self, f.name)
49+
for f in fields(self)
50+
if not f.name.startswith("_")
51+
}
52+
53+
@classmethod
54+
def default_dict(cls) -> dict[str, Any]:
55+
result: dict[str, Any] = {}
56+
for f in fields(cls):
57+
# 跳过内部字段(比如 _lock)
58+
if f.name.startswith("_"):
59+
continue
60+
61+
if f.default_factory is not MISSING:
62+
value = f.default_factory()
63+
elif f.default is not MISSING:
64+
# 防止用到可变对象,做一层 deepcopy 稳妥
65+
value = deepcopy(f.default)
66+
else:
67+
value = None
68+
69+
result[f.name] = value
70+
return result
71+
72+
def reset(self) -> None:
73+
defaults = type(self).default_dict()
74+
with QMutexLocker(self._lock):
75+
for name, value in defaults.items():
76+
setattr(self, name, value)
77+
78+
@property
79+
def internal(self) -> dict[str, Any]:
80+
return self.as_dict()
81+
82+
def items(self) -> Iterator[Tuple[str, Any]]:
83+
return iter(self.as_dict().items())
84+
85+
def values(self) -> Iterator[Any]:
86+
return iter(self.as_dict().values())
87+
88+
def keys(self) -> Iterator[str]:
89+
return iter(self.as_dict().keys())
90+
91+
def __iter__(self) -> Iterator[str]:
92+
return self.keys()
93+
94+
def __contains__(self, key: str) -> bool:
95+
with QMutexLocker(self._lock):
96+
return hasattr(self, key)
97+
98+
def __len__(self) -> int:
99+
with QMutexLocker(self._lock):
100+
return len([f for f in fields(self) if not f.name.startswith("_")])
101+
102+
def __bool__(self) -> bool:
103+
return bool(len(self))

models/classes/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@
33
from .focus_placeholder_line_edit import FocusPlaceholderLineEdit
44
from .completion_combo import CompletionComboBox
55
from .single_instance_window import SingleInstanceWindow
6-
from .thread_safe_dict import ThreadSafeDict

models/classes/thread_safe_dict.py

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)