This commit is contained in:
deltragon 2024-11-04 11:35:39 +01:00 committed by GitHub
commit 7a7d580d28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1075 additions and 105 deletions

View File

@ -24,15 +24,16 @@ jobs:
pip install build wheel
- name: Get Current Version
run: |
project_version=$(python3 setup.py --version)
echo "project_version=$project_version" >> $GITHUB_OUTPUT
uses: SebRollen/toml-action@v1.2.0
with:
file: "pyproject.toml"
field: project.version
id: get_current_version
- name: Create Tag
uses: mathieudutour/github-tag-action@v6.1
with:
custom_tag: "${{steps.get_current_version.outputs.project_version}}"
custom_tag: "${{steps.get_current_version.outputs.value}}"
github_token: ${{ secrets.GH_API_SECRET }}
- name: Build Changelog
@ -44,7 +45,7 @@ jobs:
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: 'v${{steps.get_current_version.outputs.project_version}}'
tag_name: 'v${{steps.get_current_version.outputs.value}}'
body: ${{steps.build_changelog.outputs.changelog}}
token: ${{ secrets.GH_API_SECRET }}

View File

@ -1,2 +1,8 @@
include LICENSES/GPL-3.0-or-later.txt
include README.md
graft safeeyes
prune safeeyes/tests
global-exclude *.py[cod]

View File

@ -200,9 +200,8 @@ To ensure the new strings are well-formed, you can use `python validate_po.py --
1. Checkout the latest commits from the `master` branch
2. Run `python3 -m safeeyes` to make sure nothing is broken
3. Update the Safe Eyes version in the following places (Open the project in VSCode and search for the current version):
- [setup.py](https://github.com/slgobinath/SafeEyes/blob/master/setup.py#L83)
- [setup.py](https://github.com/slgobinath/SafeEyes/blob/master/setup.py#L90)
- [safeeyes.py](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/safeeyes.py#L42)
- [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L4)
- [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L35)
- [io.github.slgobinath.SafeEyes.metainfo.xml](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml#L56)
- [about_dialog.glade](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/glade/about_dialog.glade#L74)
4. Update the [changelog](https://github.com/slgobinath/SafeEyes/blob/master/debian/changelog) (for Ubuntu PPA release)

54
pyproject.toml Normal file
View File

@ -0,0 +1,54 @@
[project]
name = "safeeyes"
version = "2.2.2"
description = "Protect your eyes from eye strain using this continuous breaks reminder."
keywords = ["linux utility health eye-strain safe-eyes"]
readme = "README.md"
authors = [
{name = "Gobinath Loganathan", email = "slgobinath@gmail.com"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: X11 Applications :: GTK",
"Environment :: Other Environment",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
]
dependencies = [
"PyGObject",
"babel",
"psutil",
"packaging",
"python-xlib",
]
requires-python = ">=3.10"
[project.urls]
Homepage = "https://github.com/slgobinath/SafeEyes"
Downloads = "https://github.com/slgobinath/SafeEyes/archive/v2.2.2.tar.gz"
[project.scripts]
safeeyes = "safeeyes.__main__:main"
[project.optional-dependencies]
healthstats = ["croniter"]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include=["safeeyes*"]
exclude=["safeeyes.tests*"]
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]

View File

@ -24,6 +24,7 @@ import atexit
import logging
import os
from threading import Timer
from importlib import metadata
import gi
from safeeyes import utility
@ -36,10 +37,11 @@ from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib
SAFE_EYES_VERSION = "2.2.2"
SAFE_EYES_VERSION = metadata.version("safeeyes")
class SafeEyes(Gtk.Application):

View File

584
safeeyes/tests/test_core.py Normal file
View File

@ -0,0 +1,584 @@
from collections import deque
import datetime
import logging
import pytest
from safeeyes import core
from safeeyes import model
import threading
from time import sleep
from unittest import mock
class TestSafeEyesCore:
@pytest.fixture(autouse=True)
def set_time(self, time_machine):
time_machine.move_to(datetime.datetime.fromisoformat("2024-08-25T13:00:00+00:00"), tick=False)
@pytest.fixture(autouse=True)
def monkeypatch_translations(self, monkeypatch):
monkeypatch.setattr(core, "_", lambda message: "translated!: " + message, raising=False)
monkeypatch.setattr(model, "_", lambda message: "translated!: " + message, raising=False)
@pytest.fixture
def sequential_threading(self, monkeypatch, time_machine):
"""This fixture allows stopping threads at any point.
It is hard-coded for SafeEyesCore, the handle class returned by the fixture must be initialized
with a SafeEyesCore instance to be patched.
With this, all sleeping/blocking/thread starting calls inside SafeEyesCore are intercepted, and paused.
Additionally, all threads inside SafeEyesCore run sequentially.
The test code can use the next() method to unpause the thread,
which will run until the next sleeping/blocking/thread starting call,
after which it needs to be woken up using next() again.
The next() method blocks the test code while the thread is running.
"""
# executes instantly, on the same thread
# no need to switch threads, as we don't use any gtk things
monkeypatch.setattr(
core.utility,
"execute_main_thread",
lambda target_function, *args, **kwargs: target_function(*args, **kwargs)
)
handle = None
def utility_start_thread(target_function, **kwargs):
if not handle:
raise Exception("handle must be initialized before first thread")
handle.utility_start_thread(target_function, **kwargs)
def time_sleep(time):
if not handle:
raise Exception("handle must be initialized before first sleep call")
handle.sleep(time)
monkeypatch.setattr(
core.utility,
"start_thread",
utility_start_thread
)
monkeypatch.setattr(
core.time,
"sleep",
time_sleep
)
class PatchedCondition(threading.Condition):
def __init__(self, handle):
super().__init__()
self.handle = handle
def wait(self, timeout):
self.handle.wait_condvar(timeout)
class Handle:
thread = None
task_queue = deque()
running = True
condvar_in = threading.Condition()
condvar_out = threading.Condition()
def __init__(self, safe_eyes_core):
nonlocal handle
nonlocal time_machine
if handle:
raise Exception("only one handle is allowed per test call")
handle = self
self.time_machine = time_machine
self.safe_eyes_core = safe_eyes_core
self.safe_eyes_core.waiting_condition = PatchedCondition(self)
def background_thread(self):
while True:
with self.condvar_in:
success = self.condvar_in.wait(1)
if not success:
raise Exception("thread timed out")
if not self.running:
break
if self.task_queue:
(target_function, kwargs) = self.task_queue.popleft()
logging.debug(f"thread started {target_function}")
target_function(**kwargs)
logging.debug(f"thread finished {target_function}")
with self.condvar_out:
self.condvar_out.notify()
def sleep(self, time):
if self.thread is threading.current_thread():
with self.condvar_out:
self.condvar_out.notify()
self.time_machine.shift(delta=datetime.timedelta(seconds=time))
with self.condvar_in:
success = self.condvar_in.wait(1)
if not success:
raise Exception("thread timed out")
def wait_condvar(self, time):
if self.thread is not threading.current_thread():
raise Exception("waiting on condition may only happen in thread")
with self.condvar_out:
self.condvar_out.notify()
self.time_machine.shift(delta=datetime.timedelta(seconds=time))
with self.condvar_in:
success = self.condvar_in.wait(1)
if not success:
raise Exception("thread timed out")
def utility_start_thread(self, target_function, **kwargs):
self.task_queue.append((target_function, kwargs))
if self.thread is None:
self.thread = threading.Thread(target=self.background_thread, name="SequentialThreadingRunner", daemon=False, kwargs=kwargs)
self.thread.start()
def next(self):
assert self.thread
with self.condvar_in:
self.condvar_in.notify()
# wait until done:
with self.condvar_out:
success = self.condvar_out.wait(1)
if not success:
raise Exception("thread timed out")
def stop(self):
self.running = False
with self.condvar_in:
self.condvar_in.notify()
if self.thread:
self.thread.join(1)
yield Handle
if handle:
handle.stop()
def run_next_break(
self,
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
break_duration,
break_name_translated
):
"""Run one entire cycle of safe_eyes_core.
It must be waiting for __scheduler_job to run. (This is the equivalent of State.WAITING).
That means it must either be just started, or have finished the previous cycle.
"""
on_update_next_break = mock.Mock()
on_pre_break = mock.Mock(return_value=True)
on_start_break = mock.Mock(return_value=True)
start_break = mock.Mock()
on_count_down = mock.Mock()
safe_eyes_core.on_update_next_break += on_update_next_break
safe_eyes_core.on_pre_break += on_pre_break
safe_eyes_core.on_start_break += on_start_break
safe_eyes_core.start_break += start_break
safe_eyes_core.on_count_down += on_count_down
# start __scheduler_job
sequential_threading_handle.next()
# wait until it reaches the condvar
assert context['state'] == model.State.WAITING
on_update_next_break.assert_called_once()
assert isinstance(on_update_next_break.call_args[0][0], model.Break)
assert on_update_next_break.call_args[0][0].name == break_name_translated
on_update_next_break.reset_mock()
# continue after condvar
sequential_threading_handle.next()
# end of __scheduler_job
assert context['state'] == model.State.PRE_BREAK
on_pre_break.assert_called_once()
assert isinstance(on_pre_break.call_args[0][0], model.Break)
assert on_pre_break.call_args[0][0].name == break_name_translated
on_pre_break.reset_mock()
# start __wait_until_prepare
sequential_threading_handle.next()
# wait until it reaches the condvar
# continue after condvar
sequential_threading_handle.next()
# end of __wait_until_prepare
# start __start_break
sequential_threading_handle.next()
# first sleep in __start_break
sequential_threading_handle.next()
on_start_break.assert_called_once()
assert isinstance(on_start_break.call_args[0][0], model.Break)
assert on_start_break.call_args[0][0].name == break_name_translated
on_start_break.reset_mock()
start_break.assert_called_once()
assert isinstance(start_break.call_args[0][0], model.Break)
assert start_break.call_args[0][0].name == break_name_translated
start_break.reset_mock()
assert context['state'] == model.State.BREAK
# continue sleep in __start_break
for i in range(break_duration - 2):
sequential_threading_handle.next()
sequential_threading_handle.next()
# end of __start_break
on_count_down.assert_called()
assert on_count_down.call_count == break_duration
on_count_down.reset_mock()
assert context['state'] == model.State.BREAK
def assert_datetime(self, string):
if not string.endswith("+00:00"):
string += "+00:00"
assert datetime.datetime.now(datetime.timezone.utc) == datetime.datetime.fromisoformat(string)
def test_create_empty(self):
context = {}
config = {
"short_breaks": [],
"long_breaks": [],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": False,
"postpone_duration": 5,
}
safe_eyes_core = core.SafeEyesCore(context)
safe_eyes_core.initialize(config)
def test_start_empty(self, sequential_threading):
context = {}
config = {
"short_breaks": [],
"long_breaks": [],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": False,
"postpone_duration": 5,
}
on_update_next_break = mock.Mock()
safe_eyes_core = core.SafeEyesCore(context)
safe_eyes_core.on_update_next_break += mock
safe_eyes_core.initialize(config)
safe_eyes_core.start()
safe_eyes_core.stop()
on_update_next_break.assert_not_called()
def test_start(self, sequential_threading, time_machine):
context = {
"session": {},
}
config = {
"short_breaks": [
{"name": "break 1"},
{"name": "break 2"},
{"name": "break 3"},
{"name": "break 4"},
],
"long_breaks": [
{"name": "long break 1"},
{"name": "long break 2"},
{"name": "long break 3"},
],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": False,
"postpone_duration": 5,
}
on_update_next_break = mock.Mock()
safe_eyes_core = core.SafeEyesCore(context)
safe_eyes_core.on_update_next_break += on_update_next_break
safe_eyes_core.initialize(config)
sequential_threading_handle = sequential_threading(safe_eyes_core)
safe_eyes_core.start()
# start __scheduler_job
sequential_threading_handle.next()
assert context['state'] == model.State.WAITING
on_update_next_break.assert_called_once()
assert isinstance(on_update_next_break.call_args[0][0], model.Break)
assert on_update_next_break.call_args[0][0].name == "translated!: break 1"
on_update_next_break.reset_mock()
# wait for end of __scheduler_job - we cannot stop while waiting on the condvar
# this just moves us into waiting for __wait_until_prepare to start
sequential_threading_handle.next()
safe_eyes_core.stop()
assert context['state'] == model.State.STOPPED
def test_full_run_with_defaults(self, sequential_threading, time_machine):
context = {
"session": {},
}
short_break_duration = 15 # seconds
short_break_interval = 15 # minutes
pre_break_warning_time = 10 # seconds
long_break_duration = 60 # seconds
long_break_interval = 75 # minutes
config = {
"short_breaks": [
{"name": "break 1"},
{"name": "break 2"},
{"name": "break 3"},
{"name": "break 4"},
],
"long_breaks": [
{"name": "long break 1"},
{"name": "long break 2"},
{"name": "long break 3"},
],
"short_break_interval": short_break_interval,
"long_break_interval": long_break_interval,
"long_break_duration": long_break_duration,
"short_break_duration": short_break_duration,
"pre_break_warning_time": pre_break_warning_time,
"random_order": False,
"postpone_duration": 5,
}
self.assert_datetime("2024-08-25T13:00:00")
safe_eyes_core = core.SafeEyesCore(context)
sequential_threading_handle = sequential_threading(safe_eyes_core)
safe_eyes_core.initialize(config)
safe_eyes_core.start()
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 1"
)
# Time passed: 15min 25s
# 15min short_break_interval, 10 seconds pre_break_warning_time, 15 seconds short_break_duration
self.assert_datetime("2024-08-25T13:15:25")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 2"
)
self.assert_datetime("2024-08-25T13:30:50")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 3"
)
self.assert_datetime("2024-08-25T13:46:15")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 4"
)
self.assert_datetime("2024-08-25T14:01:40")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
long_break_duration,
"translated!: long break 1"
)
# Time passed: 16min 10s
# 15min short_break_interval (from previous, as long_break_interval must be multiple)
# 10 seconds pre_break_warning_time, 1 minute long_break_duration
self.assert_datetime("2024-08-25T14:17:50")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 1"
)
# Time passed: 15min 25s
# 15min short_break_interval, 10 seconds pre_break_warning_time, 15 seconds short_break_duration
self.assert_datetime("2024-08-25T14:33:15")
safe_eyes_core.stop()
assert context['state'] == model.State.STOPPED
def test_long_duration_is_bigger_than_short_interval(self, sequential_threading, time_machine):
"""Example taken from https://github.com/slgobinath/SafeEyes/issues/640"""
context = {
"session": {},
}
short_break_duration = 300 # seconds = 5min
short_break_interval = 25 # minutes
pre_break_warning_time = 10 # seconds
long_break_duration = 1800 # seconds = 30min
long_break_interval = 100 # minutes
config = {
"short_breaks": [
{"name": "break 1"},
{"name": "break 2"},
{"name": "break 3"},
{"name": "break 4"},
],
"long_breaks": [
{"name": "long break 1"},
{"name": "long break 2"},
{"name": "long break 3"},
],
"short_break_interval": short_break_interval,
"long_break_interval": long_break_interval,
"long_break_duration": long_break_duration,
"short_break_duration": short_break_duration,
"pre_break_warning_time": pre_break_warning_time,
"random_order": False,
"postpone_duration": 5,
}
self.assert_datetime("2024-08-25T13:00:00")
safe_eyes_core = core.SafeEyesCore(context)
sequential_threading_handle = sequential_threading(safe_eyes_core)
safe_eyes_core.initialize(config)
safe_eyes_core.start()
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 1"
)
# Time passed: 30m 10s
# 25min short_break_interval, 10 seconds pre_break_warning_time, 5 minutes short_break_duration
self.assert_datetime("2024-08-25T13:30:10")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 2"
)
self.assert_datetime("2024-08-25T14:00:20")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 3"
)
self.assert_datetime("2024-08-25T14:30:30")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
long_break_duration,
"translated!: long break 1"
)
# Time passed: 55min 10s
# 25min short_break_interval (from previous, as long_break_interval must be multiple)
# 10 seconds pre_break_warning_time, 30 minute long_break_duration
self.assert_datetime("2024-08-25T15:25:40")
self.run_next_break(
sequential_threading_handle,
time_machine,
safe_eyes_core,
context,
short_break_duration,
"translated!: break 4"
)
# Time passed: 30m 10s
# 15min short_break_interval, 10 seconds pre_break_warning_time, 15 seconds short_break_duration
self.assert_datetime("2024-08-25T15:55:50")
safe_eyes_core.stop()
assert context['state'] == model.State.STOPPED

View File

@ -0,0 +1,354 @@
# Safe Eyes is a utility to remind you to take break frequently
# to protect your eyes from eye strain.
# Copyright (C) 2017 Gobinath
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import random
from safeeyes import model
class TestBreak:
def test_break_short(self):
b = model.Break(
model.BreakType.SHORT_BREAK,
"test break",
15,
15,
None,
None
)
assert b.is_short_break()
assert not b.is_long_break()
def test_break_long(self):
b = model.Break(
model.BreakType.LONG_BREAK,
"long break",
75,
60,
None,
None
)
assert not b.is_short_break()
assert b.is_long_break()
class TestBreakQueue:
def test_create_empty(self):
config = {
"short_breaks": [],
"long_breaks": [],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": False,
}
context = {}
bq = model.BreakQueue(
config,
context
)
assert bq.is_empty()
assert bq.is_empty(model.BreakType.LONG_BREAK)
assert bq.is_empty(model.BreakType.SHORT_BREAK)
assert bq.next() is None
assert bq.get_break() is None
def get_bq_only_short(self, monkeypatch, random_seed=None):
if random_seed is not None:
random.seed(random_seed)
monkeypatch.setattr(model, "_", lambda message: "translated!: " + message, raising=False)
config = {
"short_breaks": [
{"name": "break 1"},
{"name": "break 2"},
{"name": "break 3"},
],
"long_breaks": [],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": random_seed is not None,
}
context = {
"session": {},
}
return model.BreakQueue(
config,
context
)
def get_bq_only_long(self, monkeypatch, random_seed=None):
if random_seed is not None:
random.seed(random_seed)
monkeypatch.setattr(model, "_", lambda message: "translated!: " + message, raising=False)
config = {
"short_breaks": [],
"long_breaks": [
{"name": "long break 1"},
{"name": "long break 2"},
{"name": "long break 3"},
],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": random_seed is not None,
}
context = {
"session": {},
}
return model.BreakQueue(
config,
context
)
def get_bq_full(self, monkeypatch, random_seed=None):
if random_seed is not None:
random.seed(random_seed)
monkeypatch.setattr(model, "_", lambda message: "translated!: " + message, raising=False)
config = {
"short_breaks": [
{"name": "break 1"},
{"name": "break 2"},
{"name": "break 3"},
{"name": "break 4"},
],
"long_breaks": [
{"name": "long break 1"},
{"name": "long break 2"},
{"name": "long break 3"},
],
"short_break_interval": 15,
"long_break_interval": 75,
"long_break_duration": 60,
"short_break_duration": 15,
"random_order": random_seed is not None,
}
context = {
"session": {},
}
return model.BreakQueue(
config,
context
)
def test_create_only_short(self, monkeypatch):
bq = self.get_bq_only_short(monkeypatch)
assert not bq.is_empty()
assert not bq.is_empty(model.BreakType.SHORT_BREAK)
assert bq.is_empty(model.BreakType.LONG_BREAK)
def test_only_short_repeat_get_break_no_change(self, monkeypatch):
bq = self.get_bq_only_short(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: break 1"
next = bq.get_break()
assert next.name == "translated!: break 1"
assert not bq.is_long_break()
def test_only_short_next_break(self, monkeypatch):
bq = self.get_bq_only_short(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
def test_only_short_next_break_random(self, monkeypatch):
random_seed = 5
bq = self.get_bq_only_short(monkeypatch, random_seed)
next = bq.get_break()
assert next.name == "translated!: break 3"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 1"
def test_create_only_long(self, monkeypatch):
bq = self.get_bq_only_long(monkeypatch)
assert not bq.is_empty()
assert not bq.is_empty(model.BreakType.LONG_BREAK)
assert bq.is_empty(model.BreakType.SHORT_BREAK)
def test_only_long_repeat_get_break_no_change(self, monkeypatch):
bq = self.get_bq_only_long(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: long break 1"
next = bq.get_break()
assert next.name == "translated!: long break 1"
assert bq.is_long_break()
def test_only_long_next_break(self, monkeypatch):
bq = self.get_bq_only_long(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: long break 1"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: long break 1"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: long break 1"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: long break 3"
def test_only_long_next_break_random(self, monkeypatch):
random_seed = 5
bq = self.get_bq_only_long(monkeypatch, random_seed)
next = bq.get_break()
assert next.name == "translated!: long break 3"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: long break 1"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: long break 1"
def test_create_full(self, monkeypatch):
bq = self.get_bq_full(monkeypatch)
assert not bq.is_empty()
assert not bq.is_empty(model.BreakType.LONG_BREAK)
assert not bq.is_empty(model.BreakType.SHORT_BREAK)
def test_full_repeat_get_break_no_change(self, monkeypatch):
bq = self.get_bq_full(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: break 1"
next = bq.get_break()
assert next.name == "translated!: break 1"
assert not bq.is_long_break()
def test_full_next_break(self, monkeypatch):
bq = self.get_bq_full(monkeypatch)
next = bq.get_break()
assert next.name == "translated!: break 1"
assert not bq.is_long_break()
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 1"
assert bq.is_long_break()
assert bq.next().name == "translated!: break 1"
assert not bq.is_long_break()
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 1"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: long break 1"
def test_full_next_break_random(self, monkeypatch):
random_seed = 5
bq = self.get_bq_full(monkeypatch, random_seed)
next = bq.get_break()
assert next.name == "translated!: break 1"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: long break 3"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: long break 2"
assert bq.next().name == "translated!: break 2"
assert bq.next().name == "translated!: break 4"
assert bq.next().name == "translated!: break 1"
assert bq.next().name == "translated!: break 3"
assert bq.next().name == "translated!: long break 1"

162
setup.py
View File

@ -1,103 +1,73 @@
import os, sys, site
import subprocess
import setuptools
#!/usr/bin/python3
import os
from pathlib import Path
from setuptools import Command, setup
from setuptools.command.build import build as OriginalBuildCommand
class BuildCommand(OriginalBuildCommand):
sub_commands = [('build_mo', None), *OriginalBuildCommand.sub_commands]
requires = [
'babel',
'psutil',
'croniter',
'PyGObject',
'packaging',
'python-xlib'
]
class BuildMoSubCommand(Command):
description = 'Compile .po files into .mo files'
_ROOT = os.path.abspath(os.path.dirname(__file__))
files = None
with open(os.path.join(_ROOT, 'README.md')) as f:
long_description = f.read()
def initialize_options(self):
self.files = None
self.editable_mode = False
self.build_lib = None
def finalize_options(self):
self.set_undefined_options("build_py", ("build_lib", "build_lib"))
def run(self):
files = self._get_files()
for build_file, source_file in files.items():
if not self.editable_mode:
# Parent directory required for msgfmt to work correctly
Path(build_file).parent.mkdir(parents=True, exist_ok=True)
self.spawn(['msgfmt', source_file, '-o', build_file])
def _get_files(self):
if self.files is not None:
return self.files
files = {}
localedir = Path('safeeyes/config/locale')
po_dirs = [l.joinpath('LC_MESSAGES') for l in localedir.iterdir() if l.is_dir()]
for po_dir in po_dirs:
po_files = [f
for f in po_dir.iterdir()
if f.is_file() and f.suffix == '.po']
for po_file in po_files:
mo_file = po_file.with_suffix(".mo")
source_file = po_file
build_file = mo_file
if not self.editable_mode:
build_file = Path(self.build_lib).joinpath(build_file)
files[str(build_file)] = str(source_file)
self.files = files
return files
def get_output_mapping(self):
return self._get_files()
def get_outputs(self):
return self._get_files().keys()
def get_source_files(self):
return self._get_files().values()
def __compile_po_files():
"""
Compile the *.po trainslation files.
"""
localedir = 'safeeyes/config/locale'
po_dirs = [localedir + '/' + l + '/LC_MESSAGES/'
for l in next(os.walk(localedir))[1]]
for po_dir in po_dirs:
po_files = [f
for f in next(os.walk(po_dir))[2]
if os.path.splitext(f)[1] == '.po']
for po_file in po_files:
filename, _ = os.path.splitext(po_file)
mo_file = filename + '.mo'
msgfmt_cmd = 'msgfmt {} -o {}'.format(
po_dir + po_file, po_dir + mo_file)
subprocess.call(msgfmt_cmd, shell=True)
def __data_files():
"""
Collect the data files.
"""
root_dir = sys.prefix
return [(os.path.join(root_dir, "share/applications"), ["safeeyes/platform/io.github.slgobinath.SafeEyes.desktop"]),
(os.path.join(root_dir, "share/icons/hicolor/24x24/status"), ["safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-disabled.png", "safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-enabled.png", "safeeyes/platform/icons/hicolor/24x24/status/io.github.slgobinath.SafeEyes-timer.png"]),
(os.path.join(root_dir, "share/icons/hicolor/24x24/apps"), ["safeeyes/platform/icons/hicolor/24x24/apps/io.github.slgobinath.SafeEyes.png"]),
(os.path.join(root_dir, "share/icons/hicolor/16x16/status"), ["safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-disabled.png", "safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-enabled.png", "safeeyes/platform/icons/hicolor/16x16/status/io.github.slgobinath.SafeEyes-timer.png"]),
(os.path.join(root_dir, "share/icons/hicolor/16x16/apps"), ["safeeyes/platform/icons/hicolor/16x16/apps/io.github.slgobinath.SafeEyes.png"]),
(os.path.join(root_dir, "share/icons/hicolor/32x32/status"), ["safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-disabled.png", "safeeyes/platform/icons/hicolor/32x32/status/io.github.slgobinath.SafeEyes-enabled.png"]),
(os.path.join(root_dir, "share/icons/hicolor/32x32/apps"), ["safeeyes/platform/icons/hicolor/32x32/apps/io.github.slgobinath.SafeEyes.png"]),
(os.path.join(root_dir, "share/icons/hicolor/64x64/apps"), ["safeeyes/platform/icons/hicolor/64x64/apps/io.github.slgobinath.SafeEyes.png"]),
(os.path.join(root_dir, "share/icons/hicolor/128x128/apps"), ["safeeyes/platform/icons/hicolor/128x128/apps/io.github.slgobinath.SafeEyes.png"]),
(os.path.join(root_dir, "share/icons/hicolor/48x48/status"), ["safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-disabled.png", "safeeyes/platform/icons/hicolor/48x48/status/io.github.slgobinath.SafeEyes-enabled.png"]),
(os.path.join(root_dir, "share/icons/hicolor/48x48/apps"), ["safeeyes/platform/icons/hicolor/48x48/apps/io.github.slgobinath.SafeEyes.png"]),]
def __package_files(directory):
"""
Collect the package files.
"""
paths = []
for (path, _, filenames) in os.walk(directory):
for filename in filenames:
paths.append(os.path.join('..', path, filename))
return paths
def __package_data():
"""
Return a list of package data.
"""
__compile_po_files()
data = ['glade/*.glade', 'resource/*']
data.extend(__package_files('safeeyes/config'))
data.extend(__package_files('safeeyes/plugins'))
data.extend(__package_files('safeeyes/platform'))
return data
setuptools.setup(
name="safeeyes",
version="2.2.2",
description="Protect your eyes from eye strain using this continuous breaks reminder.",
long_description=long_description,
long_description_content_type="text/markdown",
author="Gobinath Loganathan",
author_email="slgobinath@gmail.com",
url="https://github.com/slgobinath/SafeEyes",
download_url="https://github.com/slgobinath/SafeEyes/archive/v2.2.2.tar.gz",
packages=setuptools.find_packages(),
package_data={'safeeyes': __package_data()},
data_files=__data_files(),
install_requires=requires,
entry_points={'console_scripts': ['safeeyes = safeeyes.__main__:main']},
keywords='linux utility health eye-strain safe-eyes',
classifiers=[
"Operating System :: POSIX :: Linux",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Development Status :: 5 - Production/Stable",
"Environment :: X11 Applications :: GTK",
"Intended Audience :: End Users/Desktop",
"Topic :: Utilities"] + [('Programming Language :: Python :: %s' % x) for x in '3 3.5 3.6 3.7 3.8 3.9'.split()]
setup(
cmdclass={'build': BuildCommand, 'build_mo': BuildMoSubCommand}
)