Skip to content

Commit 513cc59

Browse files
committed
typing: add Context to gradually type
1 parent e1ddb74 commit 513cc59

File tree

4 files changed

+181
-60
lines changed

4 files changed

+181
-60
lines changed

safeeyes/context.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
ext: dict
86+
87+
def __init__(
88+
self,
89+
api: API,
90+
locale: str,
91+
version: str,
92+
session: dict[str, typing.Any],
93+
) -> None:
94+
self.version = version
95+
self.desktop = utility.desktop_environment()
96+
self.is_wayland = utility.is_wayland()
97+
self.locale = locale
98+
self.session = session
99+
self.state = State.START
100+
self.api = api
101+
102+
self.ext = {}
103+
104+
def __setitem__(self, key: str, value: typing.Any) -> None:
105+
"""This is soft-deprecated - it is preferred to access the property."""
106+
if hasattr(self, key):
107+
setattr(self, key, value)
108+
return
109+
110+
self.ext[key] = value
111+
112+
def __getitem__(self, key: str) -> typing.Any:
113+
"""This is soft-deprecated - it is preferred to access the property."""
114+
if hasattr(self, key):
115+
return getattr(self, key)
116+
117+
return self.ext[key]
118+
119+
def __delitem__(self, key: str) -> None:
120+
"""This is soft-deprecated - it is preferred to access the property."""
121+
if hasattr(self, key):
122+
raise Exception("cannot delete property")
123+
124+
del self.ext[key]
125+
126+
def __len__(self) -> int:
127+
"""This is soft-deprecated."""
128+
return len(self.ext)
129+
130+
def __iter__(self) -> typing.Iterator[typing.Any]:
131+
"""This is soft-deprecated."""
132+
return iter(self.ext)

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)

safeeyes/safeeyes.py

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222

2323
import atexit
2424
import logging
25-
import typing
2625
from importlib import metadata
2726

2827
import gi
29-
from safeeyes import utility
28+
from safeeyes import context, utility
3029
from safeeyes.ui.about_dialog import AboutDialog
3130
from safeeyes.ui.break_screen import BreakScreen
3231
from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog
@@ -47,19 +46,20 @@ class SafeEyes(Gtk.Application):
4746

4847
required_plugin_dialog_active = False
4948
retry_errored_plugins_count = 0
49+
context: context.Context
50+
break_screen: BreakScreen
51+
safe_eyes_core: SafeEyesCore
52+
plugins_manager: PluginManager
53+
system_locale: str
5054

51-
def __init__(self, system_locale, config) -> None:
55+
def __init__(self, system_locale: str, config) -> None:
5256
super().__init__(
5357
application_id="io.github.slgobinath.SafeEyes",
5458
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
5559
)
5660

5761
self.active = False
58-
self.break_screen = None
59-
self.safe_eyes_core = None
6062
self.config = config
61-
self.context: typing.Any = {}
62-
self.plugins_manager = None
6363
self.settings_dialog_active = False
6464
self._status = ""
6565
self.system_locale = system_locale
@@ -219,39 +219,23 @@ def do_command_line(self, command_line):
219219

220220
return 0
221221

222-
def do_startup(self):
222+
def do_startup(self) -> None:
223223
Gtk.Application.do_startup(self)
224224

225225
logging.info("Starting up Application")
226226

227227
# Initialize the Safe Eyes Context
228-
self.context["version"] = SAFE_EYES_VERSION
229-
self.context["desktop"] = utility.desktop_environment()
230-
self.context["is_wayland"] = utility.is_wayland()
231-
self.context["locale"] = self.system_locale
232-
self.context["api"] = {}
233-
self.context["api"]["show_settings"] = lambda: utility.execute_main_thread(
234-
self.show_settings
235-
)
236-
self.context["api"]["show_about"] = lambda: utility.execute_main_thread(
237-
self.show_about
238-
)
239-
self.context["api"]["enable_safeeyes"] = (
240-
lambda next_break_time=-1: utility.execute_main_thread(
241-
self.enable_safeeyes, next_break_time
242-
)
243-
)
244-
self.context["api"]["disable_safeeyes"] = (
245-
lambda status=None, is_resting=False: utility.execute_main_thread(
246-
self.disable_safeeyes, status, is_resting
247-
)
248-
)
249-
self.context["api"]["status"] = self.status
250-
self.context["api"]["quit"] = lambda: utility.execute_main_thread(self.quit)
251228
if self.config.get("persist_state"):
252-
self.context["session"] = utility.open_session()
229+
session = utility.open_session()
253230
else:
254-
self.context["session"] = {"plugin": {}}
231+
session = {"plugin": {}}
232+
233+
self.context = context.Context(
234+
api=context.API(self),
235+
locale=self.system_locale,
236+
version=SAFE_EYES_VERSION,
237+
session=session,
238+
)
255239

256240
# Initialize the theme
257241
self._initialize_styles()
@@ -269,10 +253,6 @@ def do_startup(self):
269253
self.safe_eyes_core.on_stop_break += self.stop_break
270254
self.safe_eyes_core.on_update_next_break += self.update_next_break
271255
self.safe_eyes_core.initialize(self.config)
272-
self.context["api"]["take_break"] = self.take_break
273-
self.context["api"]["has_breaks"] = self.safe_eyes_core.has_breaks
274-
self.context["api"]["postpone"] = self.safe_eyes_core.postpone
275-
self.context["api"]["get_break_time"] = self.safe_eyes_core.get_break_time
276256

277257
try:
278258
self.plugins_manager.init(self.context, self.config)
@@ -289,7 +269,7 @@ def do_startup(self):
289269
and self.safe_eyes_core.has_breaks()
290270
):
291271
self.active = True
292-
self.context["state"] = State.START
272+
self.context.state = State.START
293273
self.plugins_manager.start() # Call the start method of all plugins
294274
self.safe_eyes_core.start()
295275
self.handle_system_suspend()

safeeyes/tests/test_model.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
import pytest
2020
import random
2121
import typing
22-
from safeeyes import model
22+
from unittest import mock
23+
from safeeyes import context, model
2324

2425

2526
class TestBreak:
@@ -65,9 +66,11 @@ def test_create_empty(self) -> None:
6566
system_config={},
6667
)
6768

68-
context: dict[str, typing.Any] = {}
69+
ctx = context.Context(
70+
api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={}
71+
)
6972

70-
bq = model.BreakQueue.create(config, context)
73+
bq = model.BreakQueue.create(config, ctx)
7174

7275
assert bq is None
7376

@@ -98,11 +101,11 @@ def get_bq_only_short(
98101
system_config={},
99102
)
100103

101-
context: dict[str, typing.Any] = {
102-
"session": {},
103-
}
104+
ctx = context.Context(
105+
api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={}
106+
)
104107

105-
bq = model.BreakQueue.create(config, context)
108+
bq = model.BreakQueue.create(config, ctx)
106109

107110
assert bq is not None
108111

@@ -135,11 +138,11 @@ def get_bq_only_long(
135138
system_config={},
136139
)
137140

138-
context: dict[str, typing.Any] = {
139-
"session": {},
140-
}
141+
ctx = context.Context(
142+
api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={}
143+
)
141144

142-
bq = model.BreakQueue.create(config, context)
145+
bq = model.BreakQueue.create(config, ctx)
143146

144147
assert bq is not None
145148

@@ -177,11 +180,11 @@ def get_bq_full(
177180
system_config={},
178181
)
179182

180-
context: dict[str, typing.Any] = {
181-
"session": {},
182-
}
183+
ctx = context.Context(
184+
api=mock.Mock(spec=context.API), locale="en_US", version="0.0.0", session={}
185+
)
183186

184-
bq = model.BreakQueue.create(config, context)
187+
bq = model.BreakQueue.create(config, ctx)
185188

186189
assert bq is not None
187190

0 commit comments

Comments
 (0)