From db94c25b449d821fee835984e5b8b1d73cf6af20 Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 10 Mar 2026 02:26:11 -0400 Subject: [PATCH] feat: add configuration module with YAML loading and defaults Made-with: Cursor --- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 309 bytes .../__pycache__/config.cpython-312.pyc | Bin 0 -> 2622 bytes src/cursor_flasher/config.py | 72 ++++++++++++++++++ tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 167 bytes .../conftest.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 547 bytes .../test_config.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 13313 bytes tests/test_config.py | 56 ++++++++++++++ 7 files changed, 128 insertions(+) create mode 100644 src/cursor_flasher/__pycache__/__init__.cpython-312.pyc create mode 100644 src/cursor_flasher/__pycache__/config.cpython-312.pyc create mode 100644 src/cursor_flasher/config.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-8.3.4.pyc create mode 100644 tests/__pycache__/test_config.cpython-312-pytest-8.3.4.pyc create mode 100644 tests/test_config.py diff --git a/src/cursor_flasher/__pycache__/__init__.cpython-312.pyc b/src/cursor_flasher/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67eb6b3705102d677f77c22eaca6587ceca018f7 GIT binary patch literal 309 zcmXw!u}T9$5QcXXLd1A>_S>eo+t`aBf?#8z2$E(vZfA2t;_k6~mm^mC4ua2MmDYFg z2}~vW0z$e}?j4sYX8!pGX8!q%qC*y9{jsJZ)ZeDr2k*r8D2O+v*pw+g<*VMfU)=_D zSvhZ=xLTw>M<;%sUxjY8D039uuqLByE$SRii!ZK4Izv+m@}f?uBr_8_EFn`=W%4wb zB&W&bX58Bh0hG-EHY0!~I#1RByt8fNX?G{@eP9wD&9y@nYBu*$;WAE1T7$9G+RLmp z7`LX-N$;`_!5#sSp%Ne|R=b0?kpB9d^Dp-BeSGpVa)Utr0s5$3 AUjP6A literal 0 HcmV?d00001 diff --git a/src/cursor_flasher/__pycache__/config.cpython-312.pyc b/src/cursor_flasher/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f549e9c3d07a0202a4301bf39f597ad8b00604b GIT binary patch literal 2622 zcmai0O>7&-6`uVkx%?AFiIS`z*0NhOwxpz$4n^EJf^6AtCCfqVqF%IJtT{t_r6re{ zUFr`MszVAesL;?zQL%u5wm^`kaDnF776p3fu@@;c3SyuX0@6#+3k3ym;ZtXZ+(>l; z^pSk?zW3(m&6_uG__tUr0w^he{Js4z6@X6}2#8`%#|O081$O}iP;fv|Fa&|gq9Yb1 zL*laJ$VJ6axGX!Wp&~&9*8x=C22gz>b|!k#X@H?Y4TfMCMxYL(j};h$ahQO~#}e#; zDcJj1glU+z1=#mkHbN+jBK#LAh=X*74o5odzf6$`khDC|o%mD}tOYZhIskBhaU+Ie z1py8s2@WBxAj30nD@J@n&JBOYM&<+(hL&&T9n145kZ)W5L)uESrHxzpQ*#5U3ywde z!Y)9-5Fju_8X>`ukc<>4GT)#?a~Yz}xut^rU{f2Ppa15}%P$kX zTyZ>P=3U3dpUJdapjoq_|L_4-Q2O_kKnD{uQ7X3;KnN3{Rf zX06{LG0&}(povP>eFs6JTxWqMk&d=x6;XHi;T00?C|6ylQbZ(Hb{)sGOFqJ@mecJO zdYy#wuIoT|tz=&ksP9p$;=3lay`t@Tw{pECWSVSR(<0zGJjIlV=%J-=cILUAiV=2aZ z8B0_4TX0M{c>c!JT@PVz3SoPtfDxo^e#M)DXm!G}r`!^na0>-*Dqq2#izf;+Fb@%) z@^HSZG6PkfpWtM918Zyz#@G%#OxcgYVJ5r%oqBIm&bBfG9ciF7I83F!raaslJG&?M z)hkW;>?!eZXmoqAKHZc@Tm2*3->yqdd89Qovb|70Po={%%sRuZgWET1s3{K~4xFLZ z@u%_`JQ_H`(J9*4t7~`J;Y`Kf+F~s{Ef7>Am7BYFnYI zR#jJlC+z4|wW_fKNa2tIe8zPTrUXaX1t6Zio(0bX?nCt-~A-0;if;LE` zU?T_eD(HpjY}tVD7@J#XzR>c|UIqL*)lAc$_ZP~x!J(db_tke^-TBr*qVFf`wZuQe zu^QYNs=sv*PrX}ur_^|(86T@j&m<5@AE_Xk_}TKWdf$gn`!99}9A?jM-#i#T|I4Lc zEd70WVm~xc)9RU4W@vZq(OP5eL$R5;Sc@Jc1{;@tCqGS0w4&+Oxr@J9{Pp4oOZ(Am z?M8jMmB=(Mf1LPQ>%#O$-`l_NT0Pu&>Eq=2BL$qFZim6h Config: + """Load config from YAML, falling back to defaults for missing values.""" + if not path.exists(): + return Config() + + with open(path) as f: + raw = yaml.safe_load(f) + + if not raw or not isinstance(raw, dict): + return Config() + + overrides: dict[str, Any] = {} + for section, mapping in FIELD_MAP.items(): + section_data = raw.get(section, {}) + if not isinstance(section_data, dict): + continue + for yaml_key, field_name in mapping.items(): + if yaml_key in section_data: + overrides[field_name] = section_data[yaml_key] + + return Config(**overrides) diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d552ce7a2014e510bfaec58e00fd7f8ae30207c4 GIT binary patch literal 167 zcmX@j%ge<81RHm(&jitrK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%S}JDIJKx)KeZ?` zJFO@+1xP1n7we~_mgy#D>gVUB>gK1V73(LL78U0g>89l*7H6au>6fGymlW&A$7kkc nmc+;F6;%G>u*uC&Da}c>D`Ev2%Lv59AjU^#Mn=XWW*`dyBQ`50 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/conftest.cpython-312-pytest-8.3.4.pyc b/tests/__pycache__/conftest.cpython-312-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d02b3f3411f2ffc781907944d2b3e5eaca00f04 GIT binary patch literal 547 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 literal 0 HcmV?d00001 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..3468f10 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,56 @@ +import pytest +from pathlib import Path +from cursor_flasher.config import Config, load_config, DEFAULT_CONFIG_PATH + + +class TestDefaultConfig: + def test_has_pulse_settings(self): + cfg = Config() + assert cfg.pulse_color == "#FF9500" + assert cfg.pulse_width == 4 + assert cfg.pulse_speed == 1.5 + assert cfg.pulse_opacity_min == 0.3 + assert cfg.pulse_opacity_max == 1.0 + + def test_has_sound_settings(self): + cfg = Config() + assert cfg.sound_enabled is True + assert cfg.sound_name == "Glass" + assert cfg.sound_volume == 0.5 + + def test_has_detection_settings(self): + cfg = Config() + assert cfg.poll_interval == 0.5 + assert cfg.cooldown == 3.0 + + def test_has_timeout_settings(self): + cfg = Config() + assert cfg.auto_dismiss == 300 + + +class TestLoadConfig: + def test_loads_from_yaml(self, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text( + "pulse:\n" + ' color: "#00FF00"\n' + " width: 8\n" + "sound:\n" + " enabled: false\n" + ) + cfg = load_config(config_file) + assert cfg.pulse_color == "#00FF00" + assert cfg.pulse_width == 8 + assert cfg.sound_enabled is False + assert cfg.pulse_speed == 1.5 + assert cfg.sound_name == "Glass" + + def test_missing_file_returns_defaults(self, tmp_path): + cfg = load_config(tmp_path / "nonexistent.yaml") + assert cfg.pulse_color == "#FF9500" + + def test_empty_file_returns_defaults(self, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text("") + cfg = load_config(config_file) + assert cfg.pulse_color == "#FF9500"