From cb254c058497b8f8c582d124a5565d0d4ad65bf3 Mon Sep 17 00:00:00 2001 From: V1ki Date: Fri, 27 Feb 2026 10:02:55 +0800 Subject: [PATCH] refactor: extract validation and path helpers to utils (#3) - Create utils/validation.py with validate_description(), validate_task_file(), validate_task_id() - Create utils/paths.py with shared get_tasks_file() helper - Create utils/__init__.py - Update commands/add.py, commands/list.py, commands/done.py to import from utils - Update test_task.py imports to use utils.validation directly - No behavior changes; all existing tests pass Closes #3 --- commands/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 207 bytes commands/__pycache__/add.cpython-311.pyc | Bin 0 -> 1368 bytes commands/__pycache__/done.cpython-311.pyc | Bin 0 -> 1916 bytes commands/__pycache__/list.cpython-311.pyc | Bin 0 -> 1576 bytes commands/add.py | 18 +-------- commands/done.py | 16 +------- commands/list.py | 19 ++------- test_task.py | 4 +- utils/__init__.py | 1 + utils/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 161 bytes utils/__pycache__/paths.cpython-311.pyc | Bin 0 -> 592 bytes utils/__pycache__/validation.cpython-311.pyc | Bin 0 -> 1715 bytes utils/paths.py | 8 ++++ utils/validation.py | 37 ++++++++++++++++++ 14 files changed, 55 insertions(+), 48 deletions(-) create mode 100644 commands/__pycache__/__init__.cpython-311.pyc create mode 100644 commands/__pycache__/add.cpython-311.pyc create mode 100644 commands/__pycache__/done.cpython-311.pyc create mode 100644 commands/__pycache__/list.cpython-311.pyc create mode 100644 utils/__init__.py create mode 100644 utils/__pycache__/__init__.cpython-311.pyc create mode 100644 utils/__pycache__/paths.cpython-311.pyc create mode 100644 utils/__pycache__/validation.cpython-311.pyc create mode 100644 utils/paths.py create mode 100644 utils/validation.py diff --git a/commands/__pycache__/__init__.cpython-311.pyc b/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a7dcb99b7e5f576009d48a275ccfc6e5b58493a GIT binary patch literal 207 zcmZ3^%ge<81jl|Y$W#H+k3k$5V1zP0gOp5XNMVR#NMQ_S&}6ETb8U09$q++}_2c6+^D;}~eH@lY2O} literal 0 HcmV?d00001 diff --git a/commands/__pycache__/add.cpython-311.pyc b/commands/__pycache__/add.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8296fc8b177d6a482c34cd5d258b7a868f1801b3 GIT binary patch literal 1368 zcmZV;O>5jnbR>8?ZNPH^!u)Q0NCVOQ5v5>_rHV>S(Q%gl3$N zjSNdEw1FOaD8!Hi_T*BWgMUKlFOZDD%Yea9eDKZSTYBjmt=ICpPc!d(-prf#Mt@99 z6cE6@fBW_`8KHlKFanTMIJphvB_gPWh(u&ZYDqv9NA4&sMG`vf;Evi-fmeS*Esem&X=9bbJR7nzka-%uX65~T~q4JDNO z`OGWhLM2256ZN$+zD`1LmbCPl?~ofi`B@gBOtiQ9_>EkQ$fmPcBo`{L5Xpz=6?%g& zdXfS(NU?iKG>BPsttV-l%zs~#x$c{RE4@!&^HNW&NtAaM>ojc&|Y7>DdSs*ISwPAR!cGid;a4p@0a@?k#%T>oS3FY~~ zGKs;gr;IC(<#NsReaj`h@FcLIKU0HB);m7ssvp=ctKmRgB`;(h&DE6Wx*s5n9u z;N3zU);xwrud|L{H|@Dqn|VH+V-{s|%naaV)KA|Ft(%0*`_Fg|X!=}&4gZE0k zF9&*gPrtUSUyJpbM4vfElDfD*dHMIk=H0zYeYaAND~+Vmh$k14$%S9k!&3RTn=jhG zwl^Cu+}_s%od3D`V{=pADgN~^D&L0@<7R@J5pEvf(jK1P#nW$YyuQ_cxYLMc?!+^9 z62N#V!AlWd+Q<2*aC=h+1f&1ykjA)?;6^kWCy*ej>0CDq;;k4)AYK74t~1-A^S;R* z(P2d=hnmyh2mCaK)fN}^Ckj3U61)%SUG84TBkPX!S@1E4qNnJ$fQ}VOk_PB%gsvW- cVlVv$Xf_%j1JsDd$FX)vS~y1kN9mUT0TyLLV*mgE literal 0 HcmV?d00001 diff --git a/commands/__pycache__/done.cpython-311.pyc b/commands/__pycache__/done.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51f3acaac7ef0c7fde7c55ade9609dd0ac91194a GIT binary patch literal 1916 zcmZWp%}*Og6rcU_*DeN=29q|x8l{O0vT4<-G=!iYAQY6MN=;jdwsN`gtZbcf)r;yYbV^*FeKb)vv1J!JF!rDKmsge7!NAPt!yh)^9+3l@3n_E@72K>mRJ3!0sC zgo5^i_BL6o=UlSF!t(--``y&oL=|=x{jj{rRXEID>55j+I+Zc*VjfO}Y2W{axVoaj zXbE0)R9GDPBOLS=jEzA7Bq9~`6yYd%i=D>-X~w^;k^8B9Tw}xKA;AyRpOp#&uH4-gCtT_^%k%EJF z;4}XP0_`I);wDm!M5?;j6fYl%R}aOjt~lHfhg*mhdXFPLPJG0T$c>2XMC4{9z7={S z^=&WK?4#uPVRGC}W*W(iD`gu}wmNqlAKu-luekASBc83!zwW!-LV|FKk}bX|_SX3A zsomtEC_AFud~alHwl?tOZc`dKl2V6K>czF^V|!m8eCUkcc1LeFfJ!qBX~vOej-`at zzfeyCf%)g`!BFGldH;_qEjFY@M_O!3{abgo?zEyvO8p}tA?C#1uiXIRPnW;m;IBKK zNy|GO_QE;9C7;U~?*blPnDqOC_7iR1^qDCB6g7inJXNEho<*jMI^HO#GsZPIAmwiU w2&Ba^4AVqI4jOum#47zY(V!EYtq{w6&H%>eG$t?vsSfE&FgIH0|Aa>HAO5(wjsO4v literal 0 HcmV?d00001 diff --git a/commands/__pycache__/list.cpython-311.pyc b/commands/__pycache__/list.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d0533eb092cd48de20c1954f78c5b6371935aa3 GIT binary patch literal 1576 zcma)6&ub%99Di?qWtwT)rkc%GOPoqqh&2;&FUqc3aivnE2!e;o62`o-ojS>cd9P)6 zNFWCf5ppPa*n$;$S}Xexc=q5qvjjE_2u0b0w?Ofdli&9yO*XiqZ)QI4z3=xs@B96J z=KGx=Gnq7k@$2Z!?aB!K!8cb!8ZR#2gvA3y&@LjFNFLtBFw34)lXqo|i1H2ERfr0x z5)Duz(r2ic3SaodrOaeD{hFFpzgDyB#QKw;xl_ck@~O@Ct78vh;_e^>bh!!e05J^H zcpOaD4?*(iN(|wI20}yU69)&ceFMJeF1#LB*3bACdX+~w$Ixe>TWHgn3X)+pO>S&7-f zp2N!HL}}TB;c`c<8GDZS>Sw?7s8ut?sRp>yfc`wiOv0=LeDT z_RqH?W2>iZ1v;2LpQjzr% zqYPduoE)9L(aV;PbH};!4?7z8>Mgu~{2Bnzh4s z57p_BqI`>~I8~_-zgnpT*C4Gp#eYqn`=$8Lh`thxisS?SgAlI?az^^?nokZr=e^(+ zQ1H^F4!}^x7!S}&53O7vqb;rhy55_;hnj>x=Gjd#t>J4!G=b$M%+r59iZK2Lvg2ge literal 0 HcmV?d00001 diff --git a/commands/add.py b/commands/add.py index 1b1a943..dba12e4 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,22 +1,8 @@ """Add task command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_description(description): - """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if not description: - raise ValueError("Description cannot be empty") - if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() +from utils.paths import get_tasks_file +from utils.validation import validate_description def add_task(description): diff --git a/commands/done.py b/commands/done.py index c9dfd42..995f058 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,20 +1,8 @@ """Mark task done command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_id(tasks, task_id): - """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if task_id < 1 or task_id > len(tasks): - raise ValueError(f"Invalid task ID: {task_id}") - return task_id +from utils.paths import get_tasks_file +from utils.validation import validate_task_id def mark_done(task_id): diff --git a/commands/list.py b/commands/list.py index 714315d..d7921ba 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,27 +1,14 @@ """List tasks command.""" import json -from pathlib import Path - - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_file(): - """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - tasks_file = get_tasks_file() - if not tasks_file.exists(): - return [] - return tasks_file +from utils.paths import get_tasks_file +from utils.validation import validate_task_file def list_tasks(): """List all tasks.""" # NOTE: No --json flag support yet (feature bounty) - tasks_file = validate_task_file() + tasks_file = validate_task_file(get_tasks_file()) if not tasks_file: print("No tasks yet!") return diff --git a/test_task.py b/test_task.py index ba98e43..8a87dba 100644 --- a/test_task.py +++ b/test_task.py @@ -3,8 +3,8 @@ import json import pytest from pathlib import Path -from commands.add import add_task, validate_description -from commands.done import validate_task_id +from commands.add import add_task +from utils.validation import validate_description, validate_task_id def test_validate_description(): diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..db3e327 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# utils package diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b8b2ac0f5301987a93426c83451711ff6f6c92f GIT binary patch literal 161 zcmZ3^%ge<81V+CXWC{T3#~=<2FhUuh*?^4c3@Hr344RC7D;bKIfc(!O$zLY=1x1-< zi6yD}CAkIqiJ7|TnI-uJ#kwV_#U;8WiACwDCHkc$nK{M!@$s2?nI-Y@dIgogIBatB gQ%ZAE?TT1|hJdUo<_8iVm>C%vKQO?EB4(f%08LCL9RL6T literal 0 HcmV?d00001 diff --git a/utils/__pycache__/paths.cpython-311.pyc b/utils/__pycache__/paths.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bd3c620172aeb127d806898506cc33da6220480 GIT binary patch literal 592 zcmaJ;F;Ck-6uvt+XV7PYf(b?8(SP zi}(Zm8Oc^z3`i9NTcm8T^xcslvG8>Fy?0Na-uLBln2qL_b# zVuUDaBZ?{EctF|&BWf|yCDcB*+ZHXgP~FKlT5%*8^|DCCUd(vLg!HxlHO74%dyu@IP-JI=V>S6MM>(9$jeL5dBsruzD!T*L}+h?9j60! zgXY@8)2AR4$!VlmpaxkGC5>L9(o8lKld7R2(PJtYs)Wlx??w9A&teUFp&1)$aRFwL z16S%*pP>hL{c`7G_i}e!ow^%W&8fTjyFYce|LjfO=EQD_4?sJv+r>&4(zFwX#fl!y zlkdi!87IoXF+`z%@l$zkkkTP%pT!zH^(N&Zz|6uJ|3$S4s@&ube;6rS0YwDQW4V<)kbHg<-jw3VqW<3rrKr8P7;o40Si?|u6H z!UY?_Sb5#?-cKR)kJ_9M*z0k&4AWCY(I%ppVQMg3Nm280YSW;p$7s`}X_kW16r5(@ zG)=7yRLHhI*r++2QS#97J?cm=3`mXn4aNnjgq%o6Y!_YMTZ(d&372~f6%u7QlT9AD zc<^;KnSNt#&%uCs30rL%ftnM9ajRPOFC8S-8&;BiR%PGD9QIMzWnkpD7y7t zGD3zS@x!1>KCC;t!;GR(Fe7_ZQe^rph|)p=MFq?iC_-UPUfnJ=xcAVJtR(A=lH(Pt zo(vnJD4CE&>F_F(rKa?JQ5yBYQe!W=JlL{w0vfJ5Q86RF0<(|$lW20Tk8t|q7x}B* zYqyW{tEc(ZZg%yBWk31!*qS@F=6<|&V$FB0c^&b^aen+X zPIC+0)WR90L}NXro}s$rYz2(!sSeCVV7}`K3B1I7>z+;!{s4lvwsd$|g>P+|qFz5j2D@jB+ z>>SteeWfTMn@G+a2~AgVe6dGj9T7*6n?X3JY>&yCajO(?l!;4(l|3pXW|mI?64A_% zCgs<4@+ojk!%y4-10?0qigdc9_V)_sd8$x@lWVy<1H0q;L2?6Ks&2HA)u%R7%dL!Sp<|S3O*Tv zbYt8-(1UWHQ}_Q;9c2QMhD}Mr3gJ#rWiiYzl~7vK_W}*2&P(pyStA95Tj1dtFa;w@ zs}Bl(Q9WKkL`clQ#c>8a$}fX=UWI)EMvEjnbvS+SX#de+^*6I~_ZRzD`($>pJG&UG z=IC3HPEtL++|2%c&C2~kCbG(98oK3jWP-f40yV>t=><}M>kp)3bvduHpVdR!^x5b9 z8hl+nP1L~jO^k656}za|&)E1z$L%ASj^atL6Xu->JljV@b2W=EbvF74rlWY$>;E#k Fk~c Path: + """Return the path to the tasks JSON file.""" + return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..a89be55 --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,37 @@ +"""Shared validation helpers for task-cli.""" + + +def validate_description(description: str) -> str: + """Validate and normalise a task description. + + Raises: + ValueError: if the description is empty or exceeds 200 characters. + """ + if not description: + raise ValueError("Description cannot be empty") + if len(description) > 200: + raise ValueError("Description too long (max 200 chars)") + return description.strip() + + +def validate_task_file(tasks_file): + """Check whether the tasks file exists. + + Returns: + The ``tasks_file`` path if it exists, otherwise an empty list so + callers can treat a falsy return as "no tasks yet". + """ + if not tasks_file.exists(): + return [] + return tasks_file + + +def validate_task_id(tasks, task_id: int) -> int: + """Validate that *task_id* refers to an existing task. + + Raises: + ValueError: if *task_id* is out of range. + """ + if task_id < 1 or task_id > len(tasks): + raise ValueError(f"Invalid task ID: {task_id}") + return task_id