Compare commits

...

17 Commits

Author SHA1 Message Date
deltragon a8b9242d4a
Merge f9e39081d8 into b0b624705e 2024-11-22 07:15:03 +00:00
AO Localisation Lab b0b624705e
Translated using Weblate (French)
Currently translated at 100.0% (135 of 135 strings)

Translation: Safe Eyes/Translations
Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/fr/
2024-11-13 00:00:33 +01:00
Priit Jõerüüt ff79402a2f
Translated using Weblate (Estonian)
Currently translated at 100.0% (135 of 135 strings)

Translation: Safe Eyes/Translations
Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/et/
2024-11-13 00:00:32 +01:00
Yaron Shahrabani dfe7270f57
Translated using Weblate (Hebrew)
Currently translated at 100.0% (135 of 135 strings)

Translation: Safe Eyes/Translations
Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/he/
2024-11-11 14:00:31 +01:00
deltragon f9e39081d8 exclude tests from package 2024-08-26 17:00:19 +02:00
deltragon 8953b2237f reorder methods 2024-08-26 16:53:07 +02:00
deltragon 60afefc35c test SafeEyesCore: add test from https://github.com/slgobinath/SafeEyes/issues/640 2024-08-26 16:53:07 +02:00
deltragon a823848a91 test SafeEyesCore: add handling for condvar, add explanation, check exact times 2024-08-26 16:53:07 +02:00
deltragon d43e8dc8bb test SafeEyesCore: refactor out run_next_break method 2024-08-26 16:53:07 +02:00
deltragon affd1af5e0 test SafeEyesCore: initial 2024-08-26 16:53:07 +02:00
deltragon 1d74ee2fbe pytest: test model.Break and model.BreakQueue 2024-08-26 16:53:06 +02:00
deltragon 4227212e89 fix deprecation notice from setuptools 2024-08-26 16:51:39 +02:00
deltragon 300cad2b8f setup.py: refactor mo building to use pathlib 2024-08-26 16:25:32 +02:00
deltragon 38b80b2ae9 fix typos 2024-08-26 15:52:32 +02:00
deltragon bf4a04a378 update release action to use python -m build 2024-08-21 20:16:42 +02:00
deltragon bb11f7674e read version from pyproject.toml 2024-08-21 20:16:42 +02:00
deltragon 6288c78aea migrate from setup.py to pyproject.toml 2024-08-21 20:16:42 +02:00
12 changed files with 1132 additions and 156 deletions

View File

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

View File

@ -1,2 +1,8 @@
include LICENSES/GPL-3.0-or-later.txt include LICENSES/GPL-3.0-or-later.txt
include README.md 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 1. Checkout the latest commits from the `master` branch
2. Run `python3 -m safeeyes` to make sure nothing is broken 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): 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) - [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L4)
- [setup.py](https://github.com/slgobinath/SafeEyes/blob/master/setup.py#L90) - [pyproject.toml](https://github.com/slgobinath/SafeEyes/blob/master/pyproject.toml#L35)
- [safeeyes.py](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/safeeyes.py#L42)
- [io.github.slgobinath.SafeEyes.metainfo.xml](https://github.com/slgobinath/SafeEyes/blob/master/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml#L56) - [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) - [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) 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

@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: 2021-02-18 00:50+0000\n" "PO-Revision-Date: 2024-11-12 23:00+0000\n"
"Last-Translator: Kristjan Räts <kristjanrats@gmail.com>\n" "Last-Translator: Priit Jõerüüt <hwlate@joeruut.com>\n"
"Language-Team: Estonian <https://hosted.weblate.org/projects/safe-eyes/" "Language-Team: Estonian <https://hosted.weblate.org/projects/safe-eyes/"
"translations/et/>\n" "translations/et/>\n"
"Language: et\n" "Language: et\n"
@ -15,11 +15,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5\n" "X-Generator: Weblate 5.9-dev\n"
# Short break # Short break
msgid "Gently close your eyes" msgid "Gently close your eyes"
msgstr "" msgstr "Sulge õrnalt oma silmad"
# Short break # Short break
msgid "Roll your eyes a few times to each side" msgid "Roll your eyes a few times to each side"
@ -103,7 +103,7 @@ msgid ""
"Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you " "Safe Eyes protects your eyes from eye strain (asthenopia) by reminding you "
"to take breaks while you're working long hours at the computer" "to take breaks while you're working long hours at the computer"
msgstr "" msgstr ""
"Safe Eyes aitab vähendada arvutiga töötamisel silmade väsimust, tuletades " "Safe Eyes aitab arvutiga töötamisel vähendada silmade väsimust, tuletades "
"meelde puhkepause" "meelde puhkepause"
# About dialog # About dialog
@ -112,11 +112,11 @@ msgstr "Litsents"
# About dialog # About dialog
msgid "List of Contributors" msgid "List of Contributors"
msgstr "" msgstr "Kaasautorite loend"
# About dialog # About dialog
msgid "Help us translate this app" msgid "Help us translate this app"
msgstr "" msgstr "Aita meil seda rakendust tõlkida"
# Break screen # Break screen
msgid "Skip" msgid "Skip"
@ -128,7 +128,7 @@ msgstr "Lükka edasi"
# Settings dialog # Settings dialog
msgid "Break duration (in seconds)" msgid "Break duration (in seconds)"
msgstr "Pausi kestvus (sekundites)" msgstr "Pausi kestus (sekundites)"
# Settings dialog # Settings dialog
msgid "Interval between two breaks (in minutes)" msgid "Interval between two breaks (in minutes)"
@ -244,7 +244,7 @@ msgstr "Palun vali pilt"
# Settings dialog # Settings dialog
msgid "Duration" msgid "Duration"
msgstr "Kestvus" msgstr "Kestus"
# Settings dialog # Settings dialog
msgid "Time to wait" msgid "Time to wait"
@ -256,11 +256,11 @@ msgstr "Muuda vaikimisi seadeid"
# Settings dialog # Settings dialog
msgid "Time (in seconds)" msgid "Time (in seconds)"
msgstr "Kestvus (sekundites)" msgstr "Kestus (sekundites)"
# Settings dialog # Settings dialog
msgid "Time (in minutes)" msgid "Time (in minutes)"
msgstr "Kestvus (minutites)" msgstr "Kestus (minutites)"
# Settings dialog # Settings dialog
msgid "Break Settings" msgid "Break Settings"
@ -497,15 +497,15 @@ msgstr "Tee nüüd paus"
#: plugins/trayicon #: plugins/trayicon
msgid "Any break" msgid "Any break"
msgstr "" msgstr "Iga puhkepaus"
#: plugins/trayicon #: plugins/trayicon
msgid "Short break" msgid "Short break"
msgstr "" msgstr "Lühike puhkepaus"
#: plugins/trayicon #: plugins/trayicon
msgid "Long break" msgid "Long break"
msgstr "" msgstr "Pikk puhkepaus"
#: plugins/trayicon #: plugins/trayicon
msgid "Until restart" msgid "Until restart"
@ -529,57 +529,61 @@ msgstr "Peata meedia"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "Limit Consecutive Skipping" msgid "Limit Consecutive Skipping"
msgstr "" msgstr "Piira järjest vahelejätmisi"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "How many skips or postpones are allowed in a row" msgid "How many skips or postpones are allowed in a row"
msgstr "" msgstr "Kui mitu edasilükkamist või vahelejätmist võid järjest teha"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "Limit how many breaks can be skipped or postponed in a row" msgid "Limit how many breaks can be skipped or postponed in a row"
msgstr "" msgstr "Kui mitu pausi võid järjest vahele jätta või edasi lükata"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
#, python-format #, python-format
msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row"
msgstr "" msgstr "Järjest vahelejäetud või edasilükatatud %(num)d/%(allowed)d pausi"
# safeeyes/platform/io.github.slgobinath.SafeEyes.desktop # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop
msgid "RSI Prevention" msgid "RSI Prevention"
msgstr "" msgstr "RSI ennetus"
msgid "" msgid ""
"Please install service providing tray icons for your desktop environment." "Please install service providing tray icons for your desktop environment."
msgstr "" msgstr ""
"Palun paigalda teenus, mis võimaldab sinu töölauakeskkonnas kasutada "
"süsteemisalve."
#, python-format #, python-format
msgid "Next long break at %s" msgid "Next long break at %s"
msgstr "" msgstr "Järgmine pikk puhkepaus %s"
#, python-format #, python-format
msgid "Next breaks at %(short)s/%(long)s" msgid "Next breaks at %(short)s/%(long)s"
msgstr "" msgstr "Järgmised puhkepausid %(short)s/%(long)s"
#, python-format #, python-format
msgid "The required plugin '%s' is missing dependencies!" msgid "The required plugin '%s' is missing dependencies!"
msgstr "" msgstr "Vajalikul „%s“ pluginal puuduvad sõltuvad komponendid!"
msgid "" msgid ""
"Please install the dependencies or disable the plugin. To hide this message, " "Please install the dependencies or disable the plugin. To hide this message, "
"you can also deactivate the plugin in the settings." "you can also deactivate the plugin in the settings."
msgstr "" msgstr ""
"Palun lisa vajalikud komponendid või lülita plugin välja. Viimast saad teha "
"seadistustest ja sellega ei teki enam ka antud teadet."
msgid "Click here for more information" msgid "Click here for more information"
msgstr "" msgstr "Lisateabeks klõpsi siin"
msgid "Disable plugin temporarily" msgid "Disable plugin temporarily"
msgstr "" msgstr "Lülita plugin ajutiselt välja"
msgid "Disable permanently" msgid "Disable permanently"
msgstr "" msgstr "Lülita püsivalt välja"
msgid "License:" msgid "License:"
msgstr "" msgstr "Litsents:"
# Short break # Short break
#~ msgid "Tightly close your eyes" #~ msgid "Tightly close your eyes"

View File

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: 2024-08-24 17:09+0000\n" "PO-Revision-Date: 2024-11-12 23:00+0000\n"
"Last-Translator: AO Localisation Lab <ao@localizationlab.org>\n" "Last-Translator: AO Localisation Lab <ao@localizationlab.org>\n"
"Language-Team: French <https://hosted.weblate.org/projects/safe-eyes/" "Language-Team: French <https://hosted.weblate.org/projects/safe-eyes/"
"translations/fr/>\n" "translations/fr/>\n"
@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.7.1-dev\n" "X-Generator: Weblate 5.9-dev\n"
# Short break # Short break
msgid "Gently close your eyes" msgid "Gently close your eyes"
@ -117,7 +117,7 @@ msgstr "Liste des contributeurs"
# About dialog # About dialog
msgid "Help us translate this app" msgid "Help us translate this app"
msgstr "" msgstr "Aidez-nous à traduire cette appli"
# Break screen # Break screen
msgid "Skip" msgid "Skip"
@ -526,7 +526,7 @@ msgstr "Jusquau redémarrage"
#: plugins/trayicon #: plugins/trayicon
msgid "Quit" msgid "Quit"
msgstr "Fermer" msgstr "Quitter"
# plugin/mediacontrol # plugin/mediacontrol
msgid "Media Control" msgid "Media Control"

View File

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: 2021-05-23 09:32+0000\n" "PO-Revision-Date: 2024-11-11 13:00+0000\n"
"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n" "Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
"Language-Team: Hebrew <https://hosted.weblate.org/projects/safe-eyes/" "Language-Team: Hebrew <https://hosted.weblate.org/projects/safe-eyes/"
"translations/he/>\n" "translations/he/>\n"
@ -15,11 +15,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Weblate 4.7-dev\n" "X-Generator: Weblate 5.8.2\n"
# Short break # Short break
msgid "Gently close your eyes" msgid "Gently close your eyes"
msgstr "" msgstr "לעצום את העיניים בעדינות"
# Short break # Short break
msgid "Roll your eyes a few times to each side" msgid "Roll your eyes a few times to each side"
@ -110,11 +110,11 @@ msgstr "רישיון"
# About dialog # About dialog
msgid "List of Contributors" msgid "List of Contributors"
msgstr "" msgstr "רשימת תורמים"
# About dialog # About dialog
msgid "Help us translate this app" msgid "Help us translate this app"
msgstr "" msgstr "נשמח לסיוע בתרגום היישום הזה"
# Break screen # Break screen
msgid "Skip" msgid "Skip"
@ -495,15 +495,15 @@ msgstr "לקחת הפסקה עכשיו"
#: plugins/trayicon #: plugins/trayicon
msgid "Any break" msgid "Any break"
msgstr "" msgstr "כל הפסקה שהיא"
#: plugins/trayicon #: plugins/trayicon
msgid "Short break" msgid "Short break"
msgstr "" msgstr "הפסקה קצרה"
#: plugins/trayicon #: plugins/trayicon
msgid "Long break" msgid "Long break"
msgstr "" msgstr "הפסקה ארוכה"
#: plugins/trayicon #: plugins/trayicon
msgid "Until restart" msgid "Until restart"
@ -527,57 +527,59 @@ msgstr "השהיית מדיה"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "Limit Consecutive Skipping" msgid "Limit Consecutive Skipping"
msgstr "" msgstr "הגבלת דילוג מחזורי"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "How many skips or postpones are allowed in a row" msgid "How many skips or postpones are allowed in a row"
msgstr "" msgstr "כמה דילוגים או השהיות מותר ברצף"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
msgid "Limit how many breaks can be skipped or postponed in a row" msgid "Limit how many breaks can be skipped or postponed in a row"
msgstr "" msgstr "הגבלת כמות ההפסקות שאפשר לדלג או להשהות ברצף"
# plugin/limitconsecutiveskipping # plugin/limitconsecutiveskipping
#, python-format #, python-format
msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row" msgid "Skipped or postponed %(num)d/%(allowed)d breaks in a row"
msgstr "" msgstr "דילגת או השהית %(num)d/%(allowed)d הפסקות ברצף"
# safeeyes/platform/io.github.slgobinath.SafeEyes.desktop # safeeyes/platform/io.github.slgobinath.SafeEyes.desktop
msgid "RSI Prevention" msgid "RSI Prevention"
msgstr "" msgstr "מניעת פציעת מאמץ חוזרני"
msgid "" msgid ""
"Please install service providing tray icons for your desktop environment." "Please install service providing tray icons for your desktop environment."
msgstr "" msgstr "נא להתקין שירות שמספק סמלי שורת מערכת לסביבת שולחן העבודה שלך."
#, python-format #, python-format
msgid "Next long break at %s" msgid "Next long break at %s"
msgstr "" msgstr "ההפסקה הארוכה הבאה ב־%s"
#, python-format #, python-format
msgid "Next breaks at %(short)s/%(long)s" msgid "Next breaks at %(short)s/%(long)s"
msgstr "" msgstr "ההפסקות הבאות ב־%(short)s/%(long)s"
#, python-format #, python-format
msgid "The required plugin '%s' is missing dependencies!" msgid "The required plugin '%s' is missing dependencies!"
msgstr "" msgstr "לתוסף ההכרחי %s חסרות תלויות!"
msgid "" msgid ""
"Please install the dependencies or disable the plugin. To hide this message, " "Please install the dependencies or disable the plugin. To hide this message, "
"you can also deactivate the plugin in the settings." "you can also deactivate the plugin in the settings."
msgstr "" msgstr ""
"נא להתקין את התלויות או להשבית את התוסף. כדי להסתיר את ההודעה הזאת ניתן גם "
"להשבית את התוסף בהגדרות."
msgid "Click here for more information" msgid "Click here for more information"
msgstr "" msgstr "לחיצה כאן תציג מידע נוסף"
msgid "Disable plugin temporarily" msgid "Disable plugin temporarily"
msgstr "" msgstr "השבתת תוסף זמנית"
msgid "Disable permanently" msgid "Disable permanently"
msgstr "" msgstr "השבתה לצמיתות"
msgid "License:" msgid "License:"
msgstr "" msgstr "רישיון:"
# Short break # Short break
#~ msgid "Tightly close your eyes" #~ msgid "Tightly close your eyes"

View File

@ -24,6 +24,7 @@ import atexit
import logging import logging
import os import os
from threading import Timer from threading import Timer
from importlib import metadata
import gi import gi
from safeeyes import utility from safeeyes import utility
@ -36,10 +37,11 @@ from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog from safeeyes.ui.settings_dialog import SettingsDialog
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib from gi.repository import Gtk, Gio, GLib
SAFE_EYES_VERSION = "2.2.2" SAFE_EYES_VERSION = metadata.version("safeeyes")
class SafeEyes(Gtk.Application): 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 #!/usr/bin/python3
import subprocess
import setuptools 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 = [ class BuildMoSubCommand(Command):
'babel', description = 'Compile .po files into .mo files'
'psutil',
'croniter',
'PyGObject',
'packaging',
'python-xlib'
]
_ROOT = os.path.abspath(os.path.dirname(__file__)) files = None
with open(os.path.join(_ROOT, 'README.md')) as f: def initialize_options(self):
long_description = f.read() 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(): setup(
""" cmdclass={'build': BuildCommand, 'build_mo': BuildMoSubCommand}
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()]
) )