From 742fcdfabaeed8f9f70e14165cc9740f5b17d5a8 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Fri, 5 Jun 2026 07:11:03 +0200 Subject: Add support for storing data permanently --- .ruff.toml | 49 ++++--------------- README.md | 3 ++ README.rst | 12 ----- pyproject.toml | 6 +-- src/scorecounter/__init__.py | 1 + src/scorecounter/app.py | 109 +++++++++++++++++++++++++++++++------------ tests/__init__.py | 0 tests/scorecounter.py | 35 -------------- tests/test_app.py | 3 -- 9 files changed, 94 insertions(+), 124 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 tests/__init__.py delete mode 100644 tests/scorecounter.py delete mode 100644 tests/test_app.py diff --git a/.ruff.toml b/.ruff.toml index 956737c..9018176 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -4,57 +4,29 @@ line-length = 79 target-version = "py314" [lint] -select = ["ALL", "D101", "D102", "D103", "D104"] +select = ["ALL"] ignore = [ - "ANN", "BLE001", "COM812", - "D", - "EM101", # Exception must not use a string literal, assign to variable first - "EM102", # Exception must not use an f-string literal, assign to variable first - "ERA001", - #"FBT001", - #"FBT002", - #"INP001", - #"ISC001", - #"N802", - #"N806", - #"PLR2004", - # "PTH111", - # "PTH123", - "RUF012", - #"RUF013", - #"S101", - "S308", - #"T201", - "TRY003", - "TRY300", - #"UP020" + "D2", + "D4", ] -# C901 - `outside_temperature` is too complex (12 > 10) +# ARG005 - Unused lambda argument: `args` +# BLE001 - Do not catch blind exception: `Exception` # D101 - Missing docstring in public class # D102 - Missing docstring in public method -# D200 - One-line docstring should fit on one line -# D203 - 1 blank line required before class docstring -# D205 - 1 blank line required between summary line and description -# D403 - First word of the first line should be capitalized: `str` -> `Str` +# E721 - Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks # ERA001 - Found commented-out code # FBT001 - Boolean-typed positional argument in function definition # FBT002 - Boolean default positional argument in function definition # INP001 - File `beinc_weechat.py` is part of an implicit namespace package. Add an `__init__.py` -# N802 - Function name `do_GET` should be lowercase -# N806 - Variable `POST_data` in function should be lowercase -# PTH111 - `os.path.expanduser()` should be replaced by `Path.expanduser()` -# PTH123 - `open()` should be replaced by `Path.open()` -# PLR0912 - Too many branches (13 > 12) -# PLR0913 - Too many arguments in function definition (6 > 5) +# PT008 - Use `return_value=` instead of patching with `lambda` # PLR2004 - Magic value used in comparison, consider replacing `200` with a constant variable # RUF012 - Mutable class attributes should be annotated with `typing.ClassVar` # RUF013 - PEP 484 prohibits implicit `Optional` # S101 - Use of `assert` detected -# S308 - Use of `mark_safe` may expose cross-site scripting vulnerabilities -# S603 - `subprocess` call: check for execution of untrusted input +# S324 - Probable use of insecure hash functions in `hashlib`: `md5` # T201 - `print` found # TRY003 - Avoid specifying long messages outside the exception class # TRY300 - Consider moving this statement to an `else` block @@ -75,7 +47,7 @@ indent-style = "space" skip-magic-trailing-comma = true # Like Black, automatically detect the appropriate line ending. -line-ending = "lf" +line-ending = "auto" # Enable auto-formatting of code examples in docstrings. Markdown, # reStructuredText code/literal blocks and doctests are all supported. @@ -93,6 +65,3 @@ docstring-code-line-length = "dynamic" [lint.flake8-quotes] inline-quotes = "single" - -[lint.isort] -split-on-trailing-comma = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccff7b1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# scorecounter + +*scorecounter* is a simple score counting app written in Python. diff --git a/README.rst b/README.rst deleted file mode 100644 index 9316f3e..0000000 --- a/README.rst +++ /dev/null @@ -1,12 +0,0 @@ -scorecounter -============ - -**This cross-platform app was generated by** `Briefcase`_ **- part of** -`The BeeWare Project`_. **If you want to see more tools like Briefcase, please -consider** `becoming a financial member of BeeWare`_. - -Simple application for counting scores for two teams - -.. _`Briefcase`: https://briefcase.readthedocs.io/ -.. _`The BeeWare Project`: https://beeware.org/ -.. _`becoming a financial member of BeeWare`: https://beeware.org/contributing/membership diff --git a/pyproject.toml b/pyproject.toml index 29103c1..449c6ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,10 @@ [tool.briefcase] project_name = "scorecounter" bundle = "org.pichove" -version = "1.1.0" +version = "1.2.0" url = "https://simeon.simeonov.no" -license.file = "LICENSE" +license = "GPL-3.0-or-later" +license-files = ["LICENSE"] author = "Simeon Simeonov" author_email = "sgs@pichove.org" @@ -191,4 +192,3 @@ build_gradle_dependencies = [ requires = [ "toga-web~=0.5.0", ] - diff --git a/src/scorecounter/__init__.py b/src/scorecounter/__init__.py index e69de29..9bb96e2 100644 --- a/src/scorecounter/__init__.py +++ b/src/scorecounter/__init__.py @@ -0,0 +1 @@ +"""scorecounter package""" diff --git a/src/scorecounter/app.py b/src/scorecounter/app.py index fecdf9d..124ae35 100644 --- a/src/scorecounter/app.py +++ b/src/scorecounter/app.py @@ -1,20 +1,31 @@ """Simple score counter for two teams""" +import dataclasses +import json + import toga from toga.colors import BLUE, RED, WHITE from toga.constants import COLUMN, ROW +@dataclasses.dataclass +class ScoreCounterTeam: + """A dataclass that represents the state of a team""" + + name: str + score: int + + def to_dict(self) -> dict: + """Returns a dictionary representation of the object""" + return {'name': self.name, 'score': self.score} + + class ScoreCounter(toga.App): """The main ScoreCounter application""" - def startup(self): + def startup(self) -> None: """startup entry point""" - self._score_team1 = 0 - self._score_team2 = 0 - - self._name_team1 = 'Team 1' - self._name_team2 = 'Team 2' + self._team1, self._team2 = self.load_teams() self.main_window = toga.MainWindow( title=self.formal_name, on_resize=self.cb_main_window_resize @@ -35,44 +46,48 @@ class ScoreCounter(toga.App): self.main_window.show() - def cb_clear(self, _widget): + def cb_clear(self, _widget: toga.Widget) -> None: """Clear on press callback""" - self._score_team1 = 0 - self._score_team2 = 0 + self._team1.score = 0 + self._team2.score = 0 + self.save() self.refresh() - def cb_score_team1(self, _widget): + def cb_score_team1(self, _widget: toga.Widget) -> None: """Team1 on press callback""" - self._score_team1 += 1 + self._team1.score += 1 + self.save() self.refresh() - def cb_score_team2(self, _widget): + def cb_score_team2(self, _widget: toga.Widget) -> None: """Team 2 on press callback""" - self._score_team2 += 1 + self._team2.score += 1 + self.save() self.refresh() - def cb_main_window_resize(self, widget) -> None: + def cb_main_window_resize(self, widget: toga.Widget) -> None: """On main window resize callback""" self._button_team1.style.height = int(widget.size.height / 5) self._button_team2.style.height = int(widget.size.height / 5) - def cb_set(self, _widget): + def cb_set(self, _widget: toga.Widget) -> None: """Set the settings callback""" - self._name_team1 = self._input_team1.value - self._name_team2 = self._input_team2.value + self._team1.name = self._input_team1.value + self._team2.name = self._input_team2.value + self.save() self.refresh() self._draw_main_view() - def cb_settings(self, _widget): + def cb_settings(self, _widget: toga.Widget) -> None: """Create settings window menu""" - self._input_team1 = toga.TextInput(flex=1, value=self._name_team1) + self._input_team1 = toga.TextInput(flex=1, value=self._team1.name) box_team1 = toga.Box( direction=ROW, margin=5, children=[toga.Label('Team 1:', margin=5), self._input_team1], ) - self._input_team2 = toga.TextInput(flex=1, value=self._name_team2) + self._input_team2 = toga.TextInput(flex=1, value=self._team2.name) box_team2 = toga.Box( direction=ROW, margin=5, @@ -93,24 +108,56 @@ class ScoreCounter(toga.App): children=[box_team1, box_team2, toga.Box(flex=1), button_box], ) - def refresh(self): + def load_teams(self) -> tuple[ScoreCounterTeam, ScoreCounterTeam]: + """ + Loads the teams from config of config exists + + Default teams are returned if config doesn't exist + + :return: Tuple of 2 teams + :rtype: tuple + """ + config_path = self.paths.config / 'scorecounter_config.json' + + try: + with config_path.open('r', encoding='utf-8') as fp: + config_dict = json.load(fp) + return ( + ScoreCounterTeam(**config_dict['team1']), + ScoreCounterTeam(**config_dict['team2']), + ) + except Exception: + return ( + ScoreCounterTeam(name='Team Red', score=0), + ScoreCounterTeam(name='Team Blue', score=0), + ) + + def refresh(self) -> None: """Refreshes the score labels""" - self._button_team1.text = f'{self._name_team1} (+1)' - self._button_team2.text = f'{self._name_team2} (+1)' - self._score_label1.text = f'{self._name_team1}: {self._score_team1}' - self._score_label2.text = f'{self._name_team2}: {self._score_team2}' + self._button_team1.text = f'{self._team1.name} (+1)' + self._button_team2.text = f'{self._team2.name} (+1)' + self._score_label1.text = f'{self._team1.name}: {self._team1.score}' + self._score_label2.text = f'{self._team2.name}: {self._team2.score}' + + def save(self) -> None: + """Saves the teams to storage""" + data = {'team1': self._team1.to_dict(), 'team2': self._team2.to_dict()} + + config_path = self.paths.config / 'scorecounter_config.json' + with config_path.open('w', encoding='utf-8') as fp: + json.dump(data, fp, indent=4) - def _draw_main_view(self): + def _draw_main_view(self) -> None: """Draws the main view in the main window""" self._score_label1 = toga.Label( - f'{self._name_team1}: {self._score_team1}', + f'{self._team1.name}: {self._team1.score}', margin=(0, 5), color=RED, font_weight='bold', font_size=24, ) self._score_label2 = toga.Label( - f'{self._name_team2}: {self._score_team2}', + f'{self._team2.name}: {self._team2.score}', margin=(0, 5), color=BLUE, font_weight='bold', @@ -124,7 +171,7 @@ class ScoreCounter(toga.App): ) self._button_team1 = toga.Button( - f'{self._name_team1} (+1)', + f'{self._team1.name} (+1)', on_press=self.cb_score_team1, background_color=RED, flex=1, @@ -132,7 +179,7 @@ class ScoreCounter(toga.App): color=WHITE, ) self._button_team2 = toga.Button( - f'{self._name_team2} (+1)', + f'{self._team2.name} (+1)', on_press=self.cb_score_team2, background_color=BLUE, flex=1, @@ -153,6 +200,6 @@ class ScoreCounter(toga.App): self._button_team2.style.height = int(self.main_window.size.height / 5) -def main(): +def main() -> ScoreCounter: """main entry point""" return ScoreCounter() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/scorecounter.py b/tests/scorecounter.py deleted file mode 100644 index d59e9f3..0000000 --- a/tests/scorecounter.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import sys -import tempfile -from pathlib import Path - -import pytest - - -def run_tests(): - project_path = Path(__file__).parent.parent - os.chdir(project_path) - - # Determine any args to pass to pytest. If there aren't any, - # default to running the whole test suite. - args = sys.argv[1:] - if len(args) == 0: - args = ["tests"] - - returncode = pytest.main( - [ - # Turn up verbosity - "-vv", - # Disable color - "--color=no", - # Overwrite the cache directory to somewhere writable - "-o", - f"cache_dir={tempfile.gettempdir()}/.pytest_cache", - ] + args - ) - - print(f">>>>>>>>>> EXIT {returncode} <<<<<<<<<<") - - -if __name__ == "__main__": - run_tests() diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index e1a335f..0000000 --- a/tests/test_app.py +++ /dev/null @@ -1,3 +0,0 @@ -def test_first(): - """An initial test for the app.""" - assert 1 + 1 == 2 -- cgit v1.3