From a80231c8c87432812d8da57952bdbed80d809d0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:42:15 +0000 Subject: [PATCH] fix: production-level improvements to typography_analyzer Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com> --- .gitignore | 10 +++ .../typography_analyzer.cpython-312.pyc | Bin 29395 -> 30628 bytes src/typography_analyzer.py | 61 +++++++++++------- ...st_typography.cpython-312-pytest-9.0.2.pyc | Bin 42767 -> 46762 bytes tests/test_typography.py | 32 ++++++++- 5 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d311cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ +*.egg-info/ +dist/ +build/ +.coverage +htmlcov/ diff --git a/src/__pycache__/typography_analyzer.cpython-312.pyc b/src/__pycache__/typography_analyzer.cpython-312.pyc index dd7d38083c7b1e6721a27ae7c750438c6455ff95..2b708818a65a299ab372ad2d49f2ba8f54672bd6 100644 GIT binary patch delta 6217 zcmb7I4RBP~b$)l>zWvu~rPcrM>JMopt$slQ$pTAAh+ly~wqb?5UhR8AyWSshcZEO^ zS)1S#3buuBW1z+XC$;>gh+Aif?YgehOr^+;cAn`>lg6ERGOh(U4R+e5=e%Vl z* z5>f3p2U4A>0gKZTNOPtU4tRn;Jz#ZO6~s0g*SYl0wxsE`Ce-zHu^SumST!l+OQlq{ zndBFjNvSHXmlM;Da$@>Rszj@o?lg9mv!kS}tU|n3>=ZMO3eFW`mza4}?W`2ni&^lj z61&B0c-q=kq2tw!7^=E1X8+$841)4|j#mry6xNcC(~Wsf$9O;Wwsd=K;@ zm5vZ`v$zs$H;P-tRiND@ZWUYL*{sO6iLD@8snC7mYM@ss^mcI#&@BqRLu>=ORiXRE zcA!^_JH=h%JwUCI+Qg2Bt2mD8AOdS}WHAeWSw&uC*My^$R?!Sy8N>`RQ%nVQqnIUH zfMXJ~#T*EV;7mD`ZqH>N&D&%ZtJL~gbX}%q1gxCq*tK<8d67FIJzxKQ8r98lsnotPV^5kuch}Q9G)pxLjkQa5Uob z2E7rNYchR9C>W_zm`Pq6=!IJV9^vA~)F(TRcRX*HHCA6wFNmj?UFiPaw&%9Zl&zdu z+4GL?+TIz<&Re|7YF^+}Ddq*9Gu2QCD(6`oKD=n*pXWGos|_4E%}sGW_Vzj}dq?{- zbJdhu)I=@bC>;i2??YMru=3XGekFgSir)c%Lk@Nnu{`sPY*`kIk8CN`4=#|{L zqV`#9`>d&*eaqm}>;!Gv!nDR3vXM0#|C6+`Qd1Mz!tOKe*sYQ^REmt#psenL_}dAs z19iKa_F=111ZR@Eq`HfSSr`t9hd^_8Qoba$o7sZtKFey9DBa(W)E>4Yr9jgFWcUF_ zQ#NRCsnvq{GgIu16kfG*AF7NZcmeDhy2z0EN$(i-B}KA8L*qdamPsqSk=jItV&#?z z(mD?ENi~%M?O%|?Ul0RWIDk!-+G*TlV^UD06Ugobz?ib1r|lqOtStRyLdMyQb#3d{ zQS#-+iAK@8TO$1^LOH?_)|1gr9%heboQ`6|WVPt^MC@czKLTsf5%x|>5UWQ}b$L|j zUf|hH)PuFR*+p^v&~G681wti46_*Z(X+fnV z46sX?+lUnVeddH}?P6S)jM_C2RCjQnM3oUL0a6+t2MaRqmxAD6q8)oV$8N!x%0eU* zar(G(o9So}f_)x2nA>!e-B;KnTmtfR?0n(4 z%7~?kzKrk+E4GbjUPbaUJ7FspegNbfvCFo%NE4d0Yh}S7nvke0gd@~DCab66?7fI z{EB_NqL}nBePwYCcKES~fO>WveG4A+ZG?9a{vP37gnwYWDw}KG022Hf^Lr!oN67wL zWbonffZ|zV6}^Ch7ujo-8EKehvOX*YBT~>Kh3O&oi%OgEu1*-KS}(*wD<0ca#Z(!8 z4dkTO(NNdWBn9aEXz2u7sLm$mn6BpC{O5sT*UP4ebTHx?qtXGdG?57Vb@p-1o_o;} z#jsP1YQh~1nO&bqcx8sJqv+QFvS!3Dg*~IP#y{$!l81i8j?{JupMdBOv45;RM1-3_ zR$q�bNB{hx^Jr~g@rBg)Iin=^b)F-$0!>VOcU6W{7Cgzuk z4Wh72suqnABP*)Uj+!FI#ZLHS=aT|V1+c}GyQGU-UQ)V?E8`-XWb1N{rV{R}1~kDP zcsYAT=d= z_}JqOy9jxo{j#x(jI#2kigGM2uv6erX`)gfrBqNYRl(pXPgtNS2h_jWR4W*P(;oX_ z(=@M}hUlK=A{=|PwUGH+D(aTj*-}y+R_nPbRW~>EBrFzTN)Smd*gve9QnOS2nY?OB z!#-`zV!v&%Yqhj|iua}B!G-m;=Hv^a26ZvX&I!pZfF1F7c>JNTl-R1DV7YD8YELK_WSwnR z=4@*=I54j8AMR(T+icP6M)R?W6ZbsQy`ahzGUFM!H@S3u_Ob4GZs8fDp7ZamxeowEQyz=N~xxz>KBhmcU!hzt?c7*~dpu_!hKWisgoZvsmMn{CP|1tfe$w zTzaAL?178nD~*>9Tx-6z|0gZ+qS6bi&jzm=ZU}sN`tLYC&D!^wfh(vu-*L9%V&0XV zbNQ>Mb+R=-o>4rXv0^r3MZB!?O3u}i*Ye-1j+d^uaPYahtDB*5RTebP%)U_!t}aw@ z`jjUP#|etodC*tN7C>2ib(2uiwdM9P5>WyjZ+lK^{lZMDz>GjcZeuWPTU(u;9|Dw6te_A6)S zJ68+czj75-(xRq^wbTU+c;m~ zm@RP37c|ZmG`?On-?V{}7oMK! zAgIj5Vp~2XLrbZzY>g+;y71-LC@Tp~LLpuBPS%Y`5zi=|646BvExIGCX=ox$|G@sZ ztw?Zz@ZV#leZ!19uIIUEW?U;2!-6i*I+6-^AK>T+xw%*l&0L zn%E+E|51KEXOdca`t~lZDi^LQ^UJ+2-8u6dwnW^F40f=Y!7Mf~Seqgx+f5mLBX`=F zjou{J=VMTAh0Tg|c5oA9QvTjeB*dh>pO7&|He|4UL#ae!6GJ^;ZpGxGED~lnhC2SA zz0{EL*h>$7O2~h(XC7KX`q*0!H98)KSWK!N4*w`U2i{RURZi+1j)3G2k5lPmJb%D3 zWDL~ker9twrG6VVv zer6nAx9T9OLx&TRci(6vEb}WH8sO-^AK8M#vCk_icU`3(fg{ML53}g7U878^hrK%7 zPuidIG-Tp2+fHN++%e#!PPe1xZr0=3L)zGR&(oxxwTQ#f5ztard*a5n5?*D4a)o!f z>Ao=aVKY2CCyv@7e8r$7j@rT)h0~6#_Ju=1SrEqqV_}LnDjGn*d$G)W!eQmM2MO=? z`X|7h#Jvwsq11vWf#d-Js7tI)s&*?W_#>1nWmieFepK9vunS=T;XVYc2#Hj}gi}^G z++7ZZWwmq=E;Py}iP=HFhJe)>u1uGu!NRAp158|*L=ttT5rxeNdsyX2mEjzazk~nq zf5-NWl&gzsXR~Wx-aYSFJL_0G@93I!bj@Y2pU>Vlo4su=dwcA0-@7DgGtZIyA)EOF zP3BNV`UeH+fO13nP__C)opz{R{h>t-DC=CVfZH2%xga5N3DL(8l&jHy0cAHNZm!Do zDBl+p3XVT_W@~zp!rV+;numaj!bbq*KBVX?HMbNM6s==GL4U6z11sDz?wiP1(p4(p zpL|hKEbuglF;Wr)&mZ6{UI~@P(YtzA)`{W=o_znbM?A;h#YIix3~xo$li!Xyu*dk})X)Kn?V(M5eah_*F}a;x~OC@jh+&D+P&hTETT2s-?d zLc&}8A~WmQal66>d{aDzISwYxps68Y)tI4xF;lv#U-SEfN%{Vj-Zqryr?Jc6iS&$ zL0^WEho@uA?3!dk%>o@=a delta 5013 zcmb7I4RBP~b$)ku-|lL6rPc4Qkk+7I!%ABHpg&8p5Rwoe5VDW}o5f;T={-p+?~k~< zLLiF@$4-j_F_2>$5!@@^! zp=57zD8-u+vU}}JQkWtKQ$uOqG(j7K>7fj7Mkv#p8Orizg&bZ-$mw;W%;e1;mK5_h zn0J+uWt5nxQo-!(=1g=mwbEmkZU*m7k|3>!dA`Vt+zX zQZMSrD@nSTcu3xSH5tZOE-QeOtgydO*{HNCnNP^xBBf2qdcxo>R@xN@KJQgFDNcN@ zO_JzW=}0_RqOw`ZM$2`|7G)J`x|OX;4n9kiP9+zgrC`(fS3OGg z{pg9BH^h`}N)1|9DBG1<)UH&zlsbG?3E2*%9%btV-L2e*bhV%#P#Tb~5p<8zh;*%> zcPdRt*D1Y9pK?D^_3C{}^CRmdiJLJx^|bnUw*I?1_BFU@FN9yq{x#`JhLVD+79|ry zW$;>+EXARuBFAMvrZWl;}lbHzjCDK zq!)BhZn?sm;A2ZQGefSm)$EovLsSh8!4Ye_u>q~4hvBEz!L&Uyn`Sd+3uZcJntq_ z2Z=jL@D~JY2{6+n?k0Hn4(vc8+JXQvX9s&Y{sZTPZo^V2m&bM+F^>N5u*$`vL`=jW z$Iw>O2h}k8nXrRMjyqNOBgY~!e^6rqT<-atHu)*k4aaZhy{1E+Me=lfV9mJB`VHcKli*wMLH>~OB_dyj?t(S)B_!ivsNr8lcQt7K#X4|_AA8D zN5?~)JWC|U^F+Ns0^8=Uj=uJ-zP8rxO?~cUacs1ts2Yx`;XyUZ$Kaeh&$?nx_?f$1 zej82a;XwT zC(Dm8`391CV5!^*tk4Awh1O{nBR|9sRwA9E&yf_vow|Qicb8PJ$ahKgcS&W%c$d_m zm}2tM*t&}>K~Ex2G2fBEg;JZ1Sac+mq?imZR=KvjE#059LQcDy2;0*{J&th0h%o%a zXHVNF>#ns7zf{)l`t=7NwG8)%-u%-mkG|64)@#OKG{ysCnm!PY@f(m|?PUJ|4b>%m zx6rg0y@*aBaZFk;r(HK^&zZAlGUmC;^&Xx5!+u^CNuQZ6F*gVTF(X` zud&lHrQ0O+e_N7{8t~d+tI+(Y95cb!J2UjgDI;8cC`+%KlHt3JEqddq9S+)ZEKHg* z@uDgHXc}Bi%>Z*#HZwqelhZIBOP64EVQ%)6JS;8kwt31NWhb8!`q&#JCrz0ZJxw$v zPZ{7;lT&}mAmIcKc)#g9#P9#QmC`w}Lpty^7cw0@-dtfkP2AmZrMZad;TO#%*_u2U zj6`qOsAODNqS$^B)@&$o@bRFkSz>-Ztj2tUevHCkBpe3ch8jyDsw%W&&G5p8yy=D1 ztWTvZIpertA#2r4$y`?Pam$DH^waexrjV7Aek%B6aKYBMknEhve9?K%`Ey(TEm?23 z{LUy@GoD;`qGZt|CD|6uQd06M%afLqw)d?m3pVF<+v+*n>V@3ZGY8Mrov%KB@Iu{{ z=ufKNIJA(ndgk!C^2^(PBkS{0e!E4|C)+=>NZI*kkDfU?>%E*l@2ok#RkJ%6Qgg4T z7R;p3IvZd^E&8hYb@=e(dbUW?4wIN5&w-uwGy-ucnA)`Yi&h%0cvd}av9F^rFc^N zWnFp}l8Fm|^zqWpfomQ2$&JW+PV6TJBcV_r=GJM3F+W!LJY?Qg#2v`s z1mA>`&LOzABgyzBvs|K#n9{n@ zdj#{z?I-{5SXFi9x4c0fEXOLXVrJ*lg{S>z@10AlNW{u*7Y=b+057B%h)$rGs8%@B zRZwCaj}3++6IxOr8X1c4kUz%TNO=_kg@Wi@f;_m<<+5x;@>Tqco&)=i!MwKTw_bO7 z=3JiZu9`Vl&AhAbx@+T{Yva7DZ86D^v?)HjV}Thv(BOgzDtfcvt6eDvaGyk|gj3lx zHciI!$*G?ae1d>idn$1ga|=0wrXLC@Vhx%ej}tE?K?y+>8LlU22G&z7KY+}u@zS1~ zEOq%Hiw@v%DCx~FTk=3N52>-i5q~h4ShHbyO3lC{6H)#fnCM+C_u+u=#J}1*z)F{F zmqVqQQFRcTz8F@<6^_H&T}ASPX!_64v&*mhehREBE{b2@b;8V=;)DGkvP35A6Px~m zFPZhjMPD=94~y-Y@JF8~*^f&SKU3Dx0eEa%4*b`E$*SJrjt)WEpcAVo>7P1K%L4G` zfll@a*!};G&Eg;Zh0G86108o+92{`72s}5?{Qn%?$qvSM58h<#7to>J%Qiz$txEIZ z;wB9qPjG|-dd27AJF3G-m5r1!%0v9D5zEqgq}m>=b2 zNu_{{$Ppj7huuc8EnawNcqglcZx2^yT_qK!83SPj8y`+-%Ddp^@Lv49=^S~MHNd}) z47B#sXq1wo_0=rxt-Ct5JkZWz@XqKu z%NZm;!oTPzapNOJhSZ8VXT`j;>bkRO&e=5YY>sb@yv3$XDf2A1-<0x>F{|H}{*Egh zQFEsErx~;gQ@_KY)f))SK3~Wm2>X0^SSUR>buEdz_TP|lTM})(h`jjT64YOklGup9 zTB5{P+{5&`3t<{RibS6%^hK2`AeqpbMhfS9i5Oy(mE2c}yR1uc)Lr_RTrq;44^UJ@ zt`8xF&Mv7J z(zo~umkpeDbJEfkh{pD9j)Y_V%inuAr>OC0z2Yknx$S7e69d8fJg071OO)iLDKtZO zB&=fK#qknOu26u=sAFWsJI-?34<#@d>;K=(P)#!oL?S^>4Fh`fI4vOKfiWy0VSjL+ zSP8bm{kK;FD0Vl%ccQr^k0PlV@E#k{bdf0k9<6I5t>rO-X#!5Lg@G~FJ$;zyw@3-w zPl?n2hwyd;&7_781$ZRPsXfpPBax7*nKrfgdRljF<@uy0(oUS~S$Y+pE#mX;ZF&`@ zfWAo*jj}k>dYWu6@kFDD=dtw8F(iq&SHu|>$5)(Gx;r=>KE4P36uZarB9iz|ibkP$ v+ylGEhck1SWl=)7rI&QpMIF61!rb`lvdqeErNWmFy)G9p None: + if self.category not in CATEGORIES: + raise ValueError( + f"Invalid category {self.category!r}; must be one of {CATEGORIES}" + ) + def google_url(self, text: str = "") -> str: family = self.google_font_id or self.name.replace(" ", "+") wt = ":wght@" + ";".join(str(w) for w in sorted(self.weights)) url = GOOGLE_FONTS_BASE.format(family + wt) if text: - url += f"&text={text[:50]}" + url += "&text=" + urllib.parse.quote(text[:50]) return url def css_import(self) -> str: @@ -292,7 +299,14 @@ def _linearize_channel(c: int) -> float: def relative_luminance(hex_color: str) -> float: h = hex_color.lstrip("#") - r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16) + if len(h) == 3: + h = "".join(c * 2 for c in h) # expand #rgb → #rrggbb + if len(h) != 6: + raise ValueError(f"Invalid hex color: {hex_color!r}") + try: + r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16) + except ValueError: + raise ValueError(f"Invalid hex color: {hex_color!r}") return (0.2126 * _linearize_channel(r) + 0.7152 * _linearize_channel(g) + 0.0722 * _linearize_channel(b)) @@ -357,28 +371,29 @@ def suggest_pairing(font: Font, db_path: Path = DB_PATH) -> dict: """Suggest complementary fonts for a given font, using DB first then defaults.""" rules = _PAIRING_RULES.get(font.category, []) suggestions = [] - for rule in rules: - target_cat = rule["category"] - # Try DB first - conn = _db(db_path) - row = conn.execute( - "SELECT id,name,category FROM fonts WHERE category=? AND id!=? LIMIT 1", - (target_cat, font.id), - ).fetchone() - conn.close() + conn = _db(db_path) + try: + for rule in rules: + target_cat = rule["category"] + row = conn.execute( + "SELECT id,name,category FROM fonts WHERE category=? AND id!=? LIMIT 1", + (target_cat, font.id), + ).fetchone() - if row: - suggestions.append({ - "font_id": row[0], "name": row[1], "category": row[2], - "reason": rule["reason"], "source": "database", - }) - else: - defaults = _DEFAULTS.get(target_cat, []) - if defaults: + if row: suggestions.append({ - "font_id": None, "name": defaults[0], "category": target_cat, - "reason": rule["reason"], "source": "built-in", + "font_id": row[0], "name": row[1], "category": row[2], + "reason": rule["reason"], "source": "database", }) + else: + defaults = _DEFAULTS.get(target_cat, []) + if defaults: + suggestions.append({ + "font_id": None, "name": defaults[0], "category": target_cat, + "reason": rule["reason"], "source": "built-in", + }) + finally: + conn.close() return { "base_font": {"id": font.id, "name": font.name, "category": font.category}, @@ -441,7 +456,7 @@ def save_font(font: Font, db_path: Path = DB_PATH) -> None: ",".join(str(w) for w in font.weights), font.google_font_id, ",".join(font.tags), - font.created_at or datetime.datetime.utcnow().isoformat()), + font.created_at or datetime.datetime.now(datetime.timezone.utc).isoformat()), ) conn.commit(); conn.close() @@ -578,7 +593,7 @@ examples: weights=[int(w.strip()) for w in args.weights.split(",") if w.strip()], google_font_id=args.google_font_id, tags=[t.strip() for t in args.tags.split(",") if t.strip()], - created_at=datetime.datetime.utcnow().isoformat(), + created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(), ) save_font(font) print(f"✅ added font '{font.name}' → {font.id}") diff --git a/tests/__pycache__/test_typography.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_typography.cpython-312-pytest-9.0.2.pyc index f15a4eef5bd9f93bbeb409411e552eeafa546370..3ef972b13f803e52bef25fecb8681dae62f0e62e 100644 GIT binary patch delta 3242 zcmcgueN0=|6@T}A*am|!0TK!cG8hA|e3*~WIwVb?tZAC0B>AJ=f-*Ul-!pb<`=R>` z*cJ&a%NnXQh3Hl*TPii`x-?!YjmeYt$JSQb{#dq5nVAV7i?*a~HZ9#G0*ktlY1+B( zfmxS!+V)2u@BTRV+;h%7U%vN^-(Y|6XV!8(KR=g&=i_Ua1NUn#SW1!fquT3e{~4xt zr(^@%B~=1GCRG9Mmh6Cgq-wxU$sw2eSgGa=-`geC0_Sn54zODi0ly+W4ETg>Iq@R7 z`)pBH{Tb#e-3FfCC#42h+b1;wJ|#5)?w6c^2c%}egHj9N(^4y7kJJWuD8`E0lR@^I zUq=bDlEIkV5aaJ3O6YHmO_fcH0eCMWu!79s}VPrgPurWP<96gLV>U+ z?3LZBU%?T-CoJI%NWTn2rNk(+SXGzeT{8uhmuk`lB9K?Q0a69xmG0>RCy+0;r3##B zb2E@r?R3f9Jk>sJcFpnhon2$-vl0Ct;51d#XXyr#zQf(Uq# zrLS~nN$?;`Pqpi$j*ny~Uemlsn1{A(8Eg8|~3pyrs1*!DkHpFFQc>h}6QSjD$N;Z>^iA%%|s9th@3iN#=tDsON9-$EI} zAtl`B4$9%ah#&s}oWBG^{rploXenJYrfOPB2Q8(8meR5MAEJ(ex&NlU-(vr-2IqR> z*0p|Y^m-z4pMqlvlrgQ-B9Gq#5AgdGJ^;w%ggnp5?h&67j{JMU^%IM(xYv7p-OhTm z{|QW7hM_h9JlO2c1yfON!&?xm>ougD0ykH6R}b*dG|!4(LBugkd#Myo_drl2Hr>cpTm7`B*uuAp7oCZOXNtjZe*+W8|qV( zzM$*|t8QwPm=}i&eQCrg-4_5_!fjbGiQ8K+j>I@xc_)7A& zugq+QXz5t7A@ZSbjE$Jc1;bX7c&#{hz5h=Xl5bR&k(c@kP!XB76cOwB93Jk167o*p zJ^|kWZDRG;8@28g&18??Uc`l!p^PaK2+46JEJOVwUJ~$cx8hGgT7w@0q4txczqMk= zboGv()y;JrnC>_**KugN<50T#@LYB8baijK`dIRwzm&`WD~O9M{ySCw2Pq5M%kdJW zKid@eF6FI|31>0+X>hY(fik|GoDIIoLMxn7I*fmz>sCajo~#7{v(>Ar?m(y??ti=q zWPg!F{XuRM-3ia+Ou6kJqUlC44r+)V9+dAkd~KzV%c!xK8O;9)RBtue1q-)X&HY; zaDv{QP-(s{a=gh(Seagpp2T48-Dxj+za(Bk_ex&6u!gJ!poY>Prr-vP^F z{=Wou5Pq#&$c4#6LOUqmOWvNuY-JsgkQ{*R6zB(s)_%|!nq84ZFE&GfiHp_i?xip8 z6I>vCKWTgQtAdoRkjSm|(z=Zde^9|5Ot**|JJ)(5d$vH&z+RtiHt=^NHF|}pu_B15 zVVho{4C`xw(W(_HBhZsv)!T0i+4*{tVK-Q1P*Qn)5TV25_um^9wA+E4c*AGp=#By+ zb8ife%u>Vi6zGSk_JyNe^V$W8=^daQUAjNC>9uagv^6p9wv5pih(-pmtY?_^3Z|Wb zX~i)uAEq_JG}oA>3scXs*HO9t0kUe^6;h;upu8JD4a@K{>M`=G?=RYx4J_KRY+_MO V797iYEOIXA38-qxO0Hg({s|4$hdKZN delta 1261 zcmZ8fdu&r>6u;kX?`^lPUB@0YO4P9eyAII;;(}#vB3)VLgg+ErV!FE9YS~7YueWQ5 z6_m|XGKtQUHPOgO@G;noak)PDgJ`10M-XEN$;@?NhQ_}pk2J=J@o~;=1>(*9e)qfI zIltfe&SUyl@uhD?*D0s7T)=1X%eP})E5}_P);hc96k8qfL`C(sQET_TYW=9t?N?VK zZc!T$TU0ONeG%90XCd#dZd)}f%#Z{=-EHb>wB4_+LENe~B0iuhh}+cL5d&%yV!OH) zF{s{wxP4eu*6A;cBabr$YIr0R@g-Ew;YvIK zkEYkV{duqdjJIXp8_auyXS^Nyv9!nHoJR*m=H+T$7lK1l%9rO|OlSesU zg0yyzO_2>2I~Z?UiRNBC38q zu@7&zHP`Gm>1KIr%_uOIYm;2){Dq#+eJmGm&tT&4M~>^)FkshYvs{b}6O#{1%aHJm zerfV63VPyb0P&xreyJ9Z=k(S$nAJ}CD2y*a`_xX^RH}mKvY5uJWN_Y)MkkhPITWTI zk~~PC*V~VAvEG4OEC<4J+KIGVAf{+GidTR!eb;r}YWUGtR;JiXvT*lIP`Vk3=kx^aRzzOg^#RE^oKT~Tb0Ax3?; z)|+iIB$|MPIfNcKJiAV=$6yx(JwH3dSO*04jNOn2hqXvTgRk|d?PrqvMSxB35AC0$ z{aFIDgf!mf;Bp#6bDj)aG?vzqJW@=Zrh!xMIrWrNpEz}eQ*Akwgj0CFNc?7kO$b-* e{(-of?2oi?A6oGh5-o_Z{eyGj<#GsosQwE@9(Z;D diff --git a/tests/test_typography.py b/tests/test_typography.py index 43f5d57..8eb4557 100644 --- a/tests/test_typography.py +++ b/tests/test_typography.py @@ -145,13 +145,43 @@ def test_relative_luminance_white(): def test_relative_luminance_black(): assert abs(relative_luminance("#000000")) < 0.001 +def test_relative_luminance_shorthand(): + # #fff should expand to #ffffff + assert abs(relative_luminance("#fff") - 1.0) < 0.001 + +def test_relative_luminance_invalid_raises(): + with pytest.raises(ValueError): + relative_luminance("#xyz123") + +def test_relative_luminance_nonhex_chars(): + with pytest.raises(ValueError): + relative_luminance("#gggggg") + +def test_relative_luminance_wrong_length(): + with pytest.raises(ValueError): + relative_luminance("#12345") + + +# ── Font category validation ────────────────────────────────────────────────── +def test_font_invalid_category_raises(): + with pytest.raises(ValueError): + make_font(category="invalid-category") + + +# ── URL encoding in google_url ──────────────────────────────────────────────── +def test_google_url_text_encoded(): + f = make_font() + url = f.google_url(text="hello world & more") + assert " " not in url + assert "&text=" in url + assert "hello%20world" in url # ── Font dataclass ──────────────────────────────────────────────────────────── def make_font(**kw) -> Font: defaults = dict( id=str(uuid.uuid4()), name="Inter", category="sans-serif", weights=[400, 700], google_font_id="Inter", - tags=["modern"], created_at=datetime.datetime.utcnow().isoformat(), + tags=["modern"], created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(), ) defaults.update(kw) return Font(**defaults)