Skip to content

Commit 738b536

Browse files
authored
Merge pull request slgobinath#747 from deltragon/typing-context
typing: add Context class for gradual typing
2 parents e1ddb74 + d545223 commit 738b536

File tree

8 files changed

+310
-183
lines changed

8 files changed

+310
-183
lines changed

safeeyes/context.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Safe Eyes is a utility to remind you to take break frequently
2+
# to protect your eyes from eye strain.
3+
4+
# Copyright (C) 2025 Mel Dafert <[email protected]>
5+
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from collections.abc import MutableMapping
20+
import datetime
21+
import typing
22+
23+
from safeeyes import utility
24+
from safeeyes.model import State
25+
26+
if typing.TYPE_CHECKING:
27+
from safeeyes.safeeyes import SafeEyes
28+
29+
30+
class API:
31+
_application: "SafeEyes"
32+
33+
def __init__(
34+
self,
35+
application: "SafeEyes",
36+
) -> None:
37+
self._application = application
38+
39+
def __getitem__(self, key: str) -> typing.Callable:
40+
"""This is soft-deprecated - it is preferred to access the property."""
41+
return getattr(self, key)
42+
43+
def show_settings(self) -> None:
44+
utility.execute_main_thread(self._application.show_settings)
45+
46+
def show_about(self) -> None:
47+
utility.execute_main_thread(self._application.show_about)
48+
49+
def enable_safeeyes(self, next_break_time=-1) -> None:
50+
utility.execute_main_thread(self._application.enable_safeeyes, next_break_time)
51+
52+
def disable_safeeyes(self, status=None, is_resting=False) -> None:
53+
utility.execute_main_thread(
54+
self._application.disable_safeeyes, status, is_resting
55+
)
56+
57+
def status(self) -> str:
58+
return self._application.status()
59+
60+
def quit(self) -> None:
61+
utility.execute_main_thread(self._application.quit)
62+
63+
def take_break(self, break_type=None) -> None:
64+
self._application.take_break(break_type)
65+
66+
def has_breaks(self, break_type=None) -> bool:
67+
return self._application.safe_eyes_core.has_breaks(break_type)
68+
69+
def postpone(self, duration=-1) -> None:
70+
self._application.safe_eyes_core.postpone(duration)
71+
72+
def get_break_time(self, break_type=None) -> typing.Optional[datetime.datetime]:
73+
return self._application.safe_eyes_core.get_break_time(break_type)
74+
75+
76+
class Context(MutableMapping):
77+
version: str
78+
api: API
79+
desktop: str
80+
is_wayland: bool
81+
locale: str
82+
session: dict[str, typing.Any]
83+
state: State
84+
85+
skipped: bool = False
86+
postponed: bool = False
87+
skip_button_disabled: bool = False
88+
postpone_button_disabled: bool = False
89+
90+
ext: dict
91+
92+
def __init__(
93+
self,
94+
api: API,
95+
locale: str,
96+
version: str,
97+
session: dict[str, typing.Any],
98+
) -> None:
99+
self.version = version
100+
self.desktop = utility.desktop_environment()
101+
self.is_wayland = utility.is_wayland()
102+
self.locale = locale
103+
self.session = session
104+
self.state = State.START
105+
self.api = api
106+
107+
self.ext = {}
108+
109+
def __setitem__(self, key: str, value: typing.Any) -> None:
110+
"""This is soft-deprecated - it is preferred to access the property."""
111+
if hasattr(self, key):
112+
setattr(self, key, value)
113+
return
114+
115+
self.ext[key] = value
116+
117+
def __getitem__(self, key: str) -> typing.Any:
118+
"""This is soft-deprecated - it is preferred to access the property."""
119+
if hasattr(self, key):
120+
return getattr(self, key)
121+
122+
return self.ext[key]
123+
124+
def __delitem__(self, key: str) -> None:
125+
"""This is soft-deprecated - it is preferred to access the property."""
126+
if hasattr(self, key):
127+
raise Exception("cannot delete property")
128+
129+
del self.ext[key]
130+
131+
def __len__(self) -> int:
132+
"""This is soft-deprecated."""
133+
return len(self.ext)
134+
135+
def __iter__(self) -> typing.Iterator[typing.Any]:
136+
"""This is soft-deprecated."""
137+
return iter(self.ext)

safeeyes/core.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from safeeyes.model import State
3030
from safeeyes.model import Config
3131

32+
from safeeyes.context import Context
33+
3234
import gi
3335

3436
gi.require_version("GLib", "2.0")
@@ -45,6 +47,7 @@ class SafeEyesCore:
4547
postpone_duration: int = 0
4648
default_postpone_duration: int = 0
4749
pre_break_warning_time: int = 0
50+
context: Context
4851

4952
_break_queue: typing.Optional[BreakQueue] = None
5053

@@ -62,7 +65,7 @@ class SafeEyesCore:
6265
# set to true when a break was requested
6366
_take_break_now: bool = False
6467

65-
def __init__(self, context) -> None:
68+
def __init__(self, context: Context) -> None:
6669
"""Create an instance of SafeEyesCore and initialize the variables."""
6770
# This event is fired before <time-to-prepare> for a break
6871
self.on_pre_break = EventHook()
@@ -77,11 +80,7 @@ def __init__(self, context) -> None:
7780
# This event is fired when deciding the next break time
7881
self.on_update_next_break = EventHook()
7982
self.context = context
80-
self.context["skipped"] = False
81-
self.context["postponed"] = False
82-
self.context["skip_button_disabled"] = False
83-
self.context["postpone_button_disabled"] = False
84-
self.context["state"] = State.WAITING
83+
self.context.state = State.WAITING
8584

8685
def initialize(self, config: Config):
8786
"""Initialize the internal properties from configuration."""
@@ -116,14 +115,14 @@ def stop(self, is_resting=False) -> None:
116115
self.paused_time = datetime.datetime.now().timestamp()
117116
# Stop the break thread
118117
self.running = False
119-
if self.context["state"] != State.QUIT:
120-
self.context["state"] = State.RESTING if (is_resting) else State.STOPPED
118+
if self.context.state != State.QUIT:
119+
self.context.state = State.RESTING if (is_resting) else State.STOPPED
121120

122121
self.__wakeup_scheduler()
123122

124123
def skip(self) -> None:
125124
"""User skipped the break using Skip button."""
126-
self.context["skipped"] = True
125+
self.context.skipped = True
127126

128127
def postpone(self, duration=-1) -> None:
129128
"""User postponed the break using Postpone button."""
@@ -132,7 +131,7 @@ def postpone(self, duration=-1) -> None:
132131
else:
133132
self.postpone_duration = self.default_postpone_duration
134133
logging.debug("Postpone the break for %d seconds", self.postpone_duration)
135-
self.context["postponed"] = True
134+
self.context.postponed = True
136135

137136
def get_break_time(
138137
self, break_type: typing.Optional[BreakType] = None
@@ -154,7 +153,7 @@ def take_break(self, break_type: typing.Optional[BreakType] = None) -> None:
154153
"""
155154
if self._break_queue is None:
156155
return
157-
if not self.context["state"] == State.WAITING:
156+
if not self.context.state == State.WAITING:
158157
return
159158

160159
if break_type is not None and self._break_queue.get_break().type != break_type:
@@ -189,7 +188,7 @@ def __scheduler_job(self) -> None:
189188
current_time = datetime.datetime.now()
190189
current_timestamp = current_time.timestamp()
191190

192-
if self.context["state"] == State.RESTING and self.paused_time > -1:
191+
if self.context.state == State.RESTING and self.paused_time > -1:
193192
# Safe Eyes was resting
194193
paused_duration = int(current_timestamp - self.paused_time)
195194
self.paused_time = -1
@@ -203,11 +202,11 @@ def __scheduler_job(self) -> None:
203202
# Skip the next long break
204203
self._break_queue.skip_long_break()
205204

206-
if self.context["postponed"]:
205+
if self.context.postponed:
207206
# Previous break was postponed
208207
logging.info("Prepare for postponed break")
209208
time_to_wait = self.postpone_duration
210-
self.context["postponed"] = False
209+
self.context.postponed = False
211210
elif current_timestamp < self.scheduled_next_break_timestamp:
212211
# Non-standard break was set.
213212
time_to_wait = round(
@@ -221,7 +220,7 @@ def __scheduler_job(self) -> None:
221220
self.scheduled_next_break_time = current_time + datetime.timedelta(
222221
seconds=time_to_wait
223222
)
224-
self.context["state"] = State.WAITING
223+
self.context.state = State.WAITING
225224
self.__fire_on_update_next_break(self.scheduled_next_break_time)
226225

227226
# Wait for the pre break warning period
@@ -262,7 +261,7 @@ def __fire_pre_break(self) -> None:
262261
if self._break_queue is None:
263262
# This will only be called by methods which check this
264263
return
265-
self.context["state"] = State.PRE_BREAK
264+
self.context.state = State.PRE_BREAK
266265
proceed = self.__fire_hook(self.on_pre_break, self._break_queue.get_break())
267266
if not proceed:
268267
# Plugins wanted to ignore this break
@@ -298,9 +297,9 @@ def __do_start_break(self) -> None:
298297
# Plugins want to ignore this break
299298
self.__start_next_break()
300299
return
301-
if self.context["postponed"]:
300+
if self.context.postponed:
302301
# Plugins want to postpone this break
303-
self.context["postponed"] = False
302+
self.context.postponed = False
304303

305304
if self.scheduled_next_break_time is None:
306305
raise Exception("this should never happen")
@@ -322,7 +321,7 @@ def __start_break(self) -> None:
322321
if self._break_queue is None:
323322
# This will only be called by methods which check this
324323
return
325-
self.context["state"] = State.BREAK
324+
self.context.state = State.BREAK
326325
break_obj = self._break_queue.get_break()
327326
self._taking_break = break_obj
328327
self._countdown = break_obj.duration
@@ -340,8 +339,8 @@ def __cycle_break_countdown(self) -> None:
340339
if (
341340
self._countdown > 0
342341
and self.running
343-
and not self.context["skipped"]
344-
and not self.context["postponed"]
342+
and not self.context.skipped
343+
and not self.context.postponed
345344
):
346345
countdown = self._countdown
347346
self._countdown -= 1
@@ -359,14 +358,14 @@ def __cycle_break_countdown(self) -> None:
359358

360359
def __fire_stop_break(self) -> None:
361360
# Loop terminated because of timeout (not skipped) -> Close the break alert
362-
if not self.context["skipped"] and not self.context["postponed"]:
361+
if not self.context.skipped and not self.context.postponed:
363362
logging.info("Break is terminated automatically")
364363
self.__fire_hook(self.on_stop_break)
365364

366365
# Reset the skipped flag
367-
self.context["skipped"] = False
368-
self.context["skip_button_disabled"] = False
369-
self.context["postpone_button_disabled"] = False
366+
self.context.skipped = False
367+
self.context.skip_button_disabled = False
368+
self.context.postpone_button_disabled = False
370369
self.__start_next_break()
371370

372371
def __wait_for(
@@ -440,7 +439,7 @@ def __start_next_break(self) -> None:
440439
if self._break_queue is None:
441440
# This will only be called by methods which check this
442441
return
443-
if not self.context["postponed"]:
442+
if not self.context.postponed:
444443
self._break_queue.next()
445444

446445
if self.running:

safeeyes/model.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
from safeeyes import utility
3939
from safeeyes.translations import translate as _
4040

41+
if typing.TYPE_CHECKING:
42+
from safeeyes.context import Context
43+
4144

4245
class BreakType(Enum):
4346
"""Type of Safe Eyes breaks."""
@@ -105,9 +108,12 @@ class BreakQueue:
105108
__is_random_order: bool
106109
__long_queue: typing.Optional[list[Break]]
107110
__short_queue: typing.Optional[list[Break]]
111+
context: "Context"
108112

109113
@classmethod
110-
def create(cls, config: "Config", context) -> typing.Optional["BreakQueue"]:
114+
def create(
115+
cls, config: "Config", context: "Context"
116+
) -> typing.Optional["BreakQueue"]:
111117
short_break_time = config.get("short_break_interval")
112118
long_break_time = config.get("long_break_interval")
113119
is_random_order = config.get("random_order")
@@ -142,7 +148,7 @@ def create(cls, config: "Config", context) -> typing.Optional["BreakQueue"]:
142148

143149
def __init__(
144150
self,
145-
context,
151+
context: "Context",
146152
short_break_time: int,
147153
long_break_time: int,
148154
is_random_order: bool,
@@ -166,7 +172,7 @@ def __init__(
166172
self.__set_next_break()
167173

168174
# Restore the last break from session
169-
last_break = context["session"].get("break")
175+
last_break = context.session.get("break")
170176
if last_break is not None:
171177
current_break = self.get_break()
172178
if last_break != current_break.name:
@@ -247,7 +253,7 @@ def __set_next_break(self, break_type: typing.Optional[BreakType] = None) -> Non
247253
break_obj = self.__next_short()
248254

249255
self.__current_break = break_obj
250-
self.context["session"]["break"] = self.__current_break.name
256+
self.context.session["break"] = self.__current_break.name
251257

252258
def skip_long_break(self) -> None:
253259
if not (self.__short_queue and self.__long_queue):
@@ -265,7 +271,7 @@ def skip_long_break(self) -> None:
265271
# we could decrement the __current_long counter, but then we'd need to
266272
# handle wraparound and possibly randomizing, which seems complicated
267273
self.__current_break = self.__next_short()
268-
self.context["session"]["break"] = self.__current_break.name
274+
self.context.session["break"] = self.__current_break.name
269275

270276
def is_empty(self, break_type: BreakType) -> bool:
271277
"""Check if the given break type is empty or not."""
@@ -283,7 +289,7 @@ def __next_short(self) -> Break:
283289
raise Exception("this may only be called when there are short breaks")
284290

285291
break_obj = shorts[self.__current_short]
286-
self.context["break_type"] = "short"
292+
self.context.ext["break_type"] = "short"
287293

288294
# Update the index to next
289295
self.__current_short = (self.__current_short + 1) % len(shorts)
@@ -297,7 +303,7 @@ def __next_long(self) -> Break:
297303
raise Exception("this may only be called when there are long breaks")
298304

299305
break_obj = longs[self.__current_long]
300-
self.context["break_type"] = "long"
306+
self.context.ext["break_type"] = "long"
301307

302308
# Update the index to next
303309
self.__current_long = (self.__current_long + 1) % len(longs)

0 commit comments

Comments
 (0)