From 8245b64ebdd6f7f6bad385c02e5240d2e78b2157 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:45:48 -0800 Subject: [PATCH 1/5] Reduce GitHub CI flakiness --- tests/conftest.py | 2 ++ tests/test_client.py | 2 ++ tests/test_core/test_component.py | 2 ++ tests/test_core/test_events.py | 2 ++ tests/test_core/test_hooks.py | 2 ++ tests/test_html.py | 2 ++ tests/test_pyscript/test_components.py | 2 ++ tests/test_reactjs/test_modules.py | 2 ++ tests/test_sample.py | 2 ++ tests/test_testing.py | 2 ++ tests/test_web/test_module.py | 2 ++ 11 files changed, 22 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 96787a799..85aa2126d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,8 @@ ) from reactpy.testing.display import _playwright_visible +from . import pytestmark # noqa: F401 + REACTPY_ASYNC_RENDERING.set_current(True) REACTPY_DEBUG.set_current(True) diff --git a/tests/test_client.py b/tests/test_client.py index e05286f74..8098a3f98 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,8 @@ from tests.tooling.common import DEFAULT_TYPE_DELAY from tests.tooling.hooks import use_counter +from . import pytestmark # noqa: F401 + JS_DIR = Path(__file__).parent / "js" diff --git a/tests/test_core/test_component.py b/tests/test_core/test_component.py index 4cbfebd54..31e63fadc 100644 --- a/tests/test_core/test_component.py +++ b/tests/test_core/test_component.py @@ -1,6 +1,8 @@ import reactpy from reactpy.testing import DisplayFixture +from .. import pytestmark # noqa: F401 + def test_component_repr(): @reactpy.component diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index dd99018b5..52b515308 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -13,6 +13,8 @@ from reactpy.types import Event from tests.tooling.common import DEFAULT_TYPE_DELAY +from .. import pytestmark # noqa: F401 + def test_event_handler_repr(): handler = EventHandler(lambda: None) diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 8e5814152..c1816ea60 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -13,6 +13,8 @@ from reactpy.utils import Ref from tests.tooling.common import DEFAULT_TYPE_DELAY, update_message +from .. import pytestmark # noqa: F401 + async def test_must_be_rendering_in_layout_to_use_hooks(): @reactpy.component diff --git a/tests/test_html.py b/tests/test_html.py index 930b3e2fb..f97bf49b2 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -7,6 +7,8 @@ from tests.tooling.common import DEFAULT_TYPE_DELAY from tests.tooling.hooks import use_counter +from . import pytestmark # noqa: F401 + async def test_script_re_run_on_content_change(display: DisplayFixture): @component diff --git a/tests/test_pyscript/test_components.py b/tests/test_pyscript/test_components.py index 6d1080a57..cb8b729c2 100644 --- a/tests/test_pyscript/test_components.py +++ b/tests/test_pyscript/test_components.py @@ -8,6 +8,8 @@ from reactpy.testing import BackendFixture, DisplayFixture from reactpy.testing.backend import root_hotswap_component +from .. import pytestmark # noqa: F401 + @pytest.fixture(scope="module") async def display(browser): diff --git a/tests/test_reactjs/test_modules.py b/tests/test_reactjs/test_modules.py index 9094310a1..8152fdfd3 100644 --- a/tests/test_reactjs/test_modules.py +++ b/tests/test_reactjs/test_modules.py @@ -7,6 +7,8 @@ from reactpy.reactjs import component_from_string, import_reactjs from reactpy.testing import BackendFixture, DisplayFixture +from .. import pytestmark # noqa: F401 + JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" diff --git a/tests/test_sample.py b/tests/test_sample.py index 1e6654c0e..3fb95f9a2 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -1,6 +1,8 @@ from reactpy.testing import DisplayFixture from tests.sample import SampleApp +from . import pytestmark # noqa: F401 + async def test_sample_app(display: DisplayFixture): await display.show(SampleApp) diff --git a/tests/test_testing.py b/tests/test_testing.py index 60435a8cf..ffefde330 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,6 +10,8 @@ from reactpy.testing.display import DisplayFixture from tests.sample import SampleApp +from . import pytestmark # noqa: F401 + def test_assert_reactpy_logged_does_not_suppress_errors(): with pytest.raises(RuntimeError, match=r"expected error"): diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 9b5cb87d9..72f5518de 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -21,6 +21,8 @@ ) from reactpy.types import InlineJavaScript +from .. import pytestmark # noqa: F401 + JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" From 7be4f61e2efe78b933f197aec105faedad93b3e1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:46:57 -0800 Subject: [PATCH 2/5] Prevent falsey user inputs from breaking string_to_reactpy --- src/reactpy/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index 6ebf90fdc..bc2767109 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -9,7 +9,7 @@ from lxml import etree from lxml.html import fromstring -from reactpy import html +from reactpy import html as _html from reactpy.transforms import RequiredTransforms, attributes_to_reactjs from reactpy.types import Component, VdomDict @@ -105,9 +105,14 @@ def string_to_reactpy( event handler that prevents the browser from navigating to the link. This is useful if you would rather have `reactpy-router` handle your URL navigation. """ - if not isinstance(html, str): # nocov + if not html.strip(): + return _html.div() + if not isinstance(html, str): msg = f"Expected html to be a string, not {type(html).__name__}" raise TypeError(msg) + if "<" not in html or ">" not in html: + msg = "Expected html string to contain HTML tags, but no tags were found." + raise ValueError(msg) # If the user provided a string, convert it to a list of lxml.etree nodes try: @@ -153,7 +158,7 @@ def _etree_to_vdom( attributes = attributes_to_reactjs(dict(node.items())) # Convert the lxml node to a VDOM dict - constructor = getattr(html, str(node.tag)) + constructor = getattr(_html, str(node.tag)) el = constructor(attributes, children) # Perform necessary transformations on the VDOM attributes to meet VDOM spec @@ -241,8 +246,8 @@ def component_to_vdom(component: Component) -> VdomDict: if hasattr(result, "render"): return component_to_vdom(cast(Component, result)) elif isinstance(result, str): - return html.div(result) - return html() + return _html.div(result) + return _html.div() def _react_attribute_to_html(key: str, value: Any) -> tuple[str, str]: From 89d6b8c71669ddf44d547f09545793cdd0abf88f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:01:51 -0800 Subject: [PATCH 3/5] Add tests --- src/js/bun.lockb | Bin 92578 -> 91794 bytes src/reactpy/__init__.py | 3 ++- src/reactpy/utils.py | 14 +++++++------- tests/test_utils.py | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/js/bun.lockb b/src/js/bun.lockb index 21e1e7ca7c5366c5487d9147f350974be43da3c0..e7927347c3bc5834ce4d3e49bd620f3f20baccbd 100644 GIT binary patch delta 8563 zcmd5>d3;nww!YO#C%GXjfuuW4Oqv~Hmd-}lvKY)IB!q+!BcfrNB!q^Lg#-n0NFt*6 z5FwtC5(kHgIQq#FZ>w>~EUP zB=P&csVK7k#%cBXS0o17U`cA23YjmkthT~cStm)oJSC|Qcr)-W;3*FcUBD3Vdx2Kq zyTDN35tAhO0blfzq^`i%AnyiTqT}NNDzUHTMa6yZ;; zjhXG7ULiSWlvg`v)RsL4xd?xQzlN^@@oz|@HWtb3bcjXP$HQ$nWIceQX;%0Yk#xiGfWpU3v{YaT!WL+B$v^cjJEdbC0uwEn1{6KB4{{Ov#~Su* zaMi|?S5!@sUPtZ1aI zS7m9;%xcNoqE*uwreSqVMOBHj4#pL(>8?7d(?Y%;ch#T6Xp3=~;;fCCT;VML7QBdV zBf3Pi%52lz9)|ng;KO=IQcvJdklTSD08vxJL0}}X6)1XqD#lQRvl_g>G-R6ycXFv~ zh8SNxPlnt|l-jcCkCZ{R85Kpruo);a@f^Ggz5^HmdJ5dYoCrM~uL$$MRN*QX5fL?UwW#~ocRaDKahTJ3tQk!RF{pA6g z_%u-TNu{&0%2g^!Wwn!Lx++SgO?OKYMysJ2C??-R9cSt|QO8k05pD`lWNc3z19ZF= zt;xUF@e2*>8{XFiYjos5(atAztkJPp$2_3uuOT}2*D+K_PaS7eRa7A5q^rr=SRDn5 zvDyYizBIfJ443LLc)DV`uDA~<1~~;NmLrRfKPPF~br2{9f0K^OfWqz(Ao{rBKA_0P zWT42tP#v!%YIa9~B7m*H$a-8f>w>3%V&ua0A=#mie4VRuc5U@+iRmL*GR5h_z)C~X zwTy`acENq>tShUj5F`JHvqJhpzyE!p4E?qY&A!B0B5s%7ftJ% z6e#NP1LD4(4StHI5KCGqCIEpK5iiso<+ZkXFZQB>01H(IoM35G7^tuTv`?uAX*aW!!oF)tHK)T zD(Kgg86x~Y13H5`LKJfcc8X!-XbYi?pm^p_&Ta}Tq}8DF=wdfTeiPB=tNz-PGCaYI zpgrBQSSIc2uCVzO9ID9eXem0nfe}o6z>EarW7IE?!3YYqCpm)B*+O!JDXg7lhbeL= zG-=RysG1=pTNJj83N4E1M?ZSWlFg#&Bt#`Nz^btKsK%<8JJHMcP)tx3Q;3C&X61ms zLrvj|d=?{tjBfCymTu{6AlYpSdzxIJo2kX7n0^SPYqo4QiZXjB=GQUI1)?ePPq@lf zqi7$KE)T;#j`c@<>Bf(gf_bHQ{eA{(7J_$~zoji762*IkNy29q~K zy|pDSU5-ZH3C>52W-c{HD)MH?hNu<}Yr6R|xHLM}D~l!2>?q-@JxY-`AiqR+nW)*4 zE}sP_y2zUfd!@^RyK8}%jLv1x(n-4_?}a=SRvzLW7DY8Z6*d+$jDiCDt4Z)Av0e&0 zL^+^#Y66`=hd`Ui-dmAB!8(|N`^~D~_*|J9Z@TD6mou=zi3r_kIg1+mDe{Mq-vhZxO@TlP z?ys<18V@>{n)@qe>LE!F(8K++%x7?wtzJ2>*xe_tkbmt16!tO60~N-oaG)Y9cNslw z@0*V7I0;P)4T#3*KqCfGs|k-J(P9d)yA^Xl6jE08cRw|OUkAQGt*H}NYMt^ucFnJv z0+@m=F$&v8S7Q|U3N(Y@H;9~$bQVX?#44p>l$Q&jr4N+oipD(1XcNg74hQ?l4nx|ph<7D4+` z<6wnVs&u1DZ>ThznumzC3i=f}h6=y!pp#U(N>KAThgflh#{?=Irm#m;x>=<+Ku1#B zaD`ckr77mekirva>+mdgfSSO^Vpzth{5-O!E9T?i$E*AxY61UWNXUl;Z$5SFYQ$hk zDp74#Qb&ej_Q7;@s=SMwnTq)n@Z~BWLl?n6KU9(?sy;5>x+=i*RsFK{6r81)y@u;H ztc;q$zYo4h_0gRiBNTHHCRvI)f{Umf{DD{LsWfj&h{QP}5{1KORM6h$79spYVV+OZJNCHqK4-UykvU!ubB zbom@Ok;@*`k{5@y!$(~^`q0&piutsJEfTpmRFzGjXL8cjt0uCJQrIqXj{3FDiAb5< zLql@2Sui!`DyFkpv>VI_3eHpHXR$7bNiT`z%CvU`y^@zLXJO04UyFn-AjfD$-UR_d zf(&a2^QCsk^3es_&4o%jTSA5Tig_Po!{}v87Bb;vz9LV+E~~W)D-l~xHDeUhR|;(& zlg;|JvVu>&>d_c!D3c^FU?$KE%m#{|Kx}l<7@fZzVwaP~>-yh<9=PuzhGzqD@53OB zB8qIS83oV&zl&*G@mYmDSLV@`*ZnB3C6iu$stemsnJszr^2#tOU6qG<*0QRI9$4j1 z-B;(bgXCOYL_Vwi>0@w*D0oc~y$f#Unp}2-+QCg(<4=9o=CWf{v$lvVYyIf}xUb2+ zu84Mmo4+oXoum$Mv)1`j^7>r%Ej6w$qWd?+E;Heo3YqvjDrou3t)lI8 zo^*GMkNFgy!2)!PQe>%TgtMe-MkTs@E?z{$^A}u5w`8rsZyjW^7as61tPMe}7hB0> zc0{^s876#Ck4--24R$;r>Yj&DRGSZN+vI6}v=?qwrFs;YgE{Yh%lc7FTM4VDrnV5% z_JI`JcK@w#3tBX%k8klY-;;=^(*Mz*f|deNt-tY*J3GT2kkM?_g88+^YiWYF8udfZdXaLX-B_g6-!V`j6FJ#Ol}OTYUFHcyQ`O(s zpu$ef5j?S}zZ0NBCgu-1Tm78`6*6y>IVeK+4T_KjomIt>wER4UpBv>b=H4)r;V2nY zbM8&Mm~{%uNR%9uQ7E}6c_^b%@>?^{d$8d9aVaKb0ZJjtSQ>Ys-YjO)gLLS^K|Dck zy=e2@c^-A*m+>QN>&d=ol`kFitnYwCbU-ai9m+nG11Mjj2>E^#p%e1k*?tH4VU!~% z|BWI#@DR%1P!6Jeg(4b!0!4J1=uDv#cF;#lM{yx;Hk_QqwJ;L0Z&9j$1i=rd1%Ljz zZ-`Kdw}}G(*XAcKus1|`?V=+;TZgMlG2~;QM?N>q^lE-0YyPT5QC=qWg9Q@fcp}t1 zJ|aJ2-Yy?jJUBLSaI8b>O^z$&Hsio*d+lG|J(!-iANthTq(QOq(lflni&sH%;24iA+EYb#=ehsyt?~zvx1#>IjJKEB^&cuolNi25rkE!{ZmF=S~phqb9y47^b zL>I1El8sZJ%BzE~jGj8yhFg+iQ)2P-X`B;1__Y18!SDR^F*Nb9jud$Fqv5|;ZN_<% zb!U@&Z2pF|&?m+^Vle~zQ_U|{Hi#C&$~gZ@sA&D??$4)`!zvXaVJ|MC4`AhbVH&gA z)Z;h=e%x;D>N`BZq}G+-K>sY^cE-9zC5ecPL%ipj-x_!Q51)^NRg&iGUCQjV;*n}X zr`2X04c5LlZtWYUz>BIC>cq68#jxr{t2+~!i;i_h%TCNh8|qcNguO`7*Aum_PBzX3 zbB{Lm8t#2H2VN4;I_&Aj3E}t;&Y71V|DiuL2vlg=_!u$bJ7~xC(ONIF8j8DNwHb$k zsY_%2X#cS5E)$y@AB)f>N!8=o{|`4tn=W>t&u+ZL9;5P`dK7r{c;{wfvT^#i*8adN z*XG~st=6dap>aNV_R6GV6GMEG)V8Xcv=<^+BD23xh=4>NN3+rwII}0;VbqX_#yR4l z1$}R%O;{kqmk1J?_!oYFFLflWtb`9`7%}5i(7E0IvGStzN44fe(WE+F1p}r5e}%Dd z_B!9n*l0G8cQHXz&-<9*Pi;D%VS;`eUnCTB_)4MJ#gBlq83%y(7k--W40?M9YDm#W z+nWb^u+gSY3orIymXuoumBatIpl1J3VVH~oK`6$dV1C^8SFF1~?4|~(YR*~tIuDk> zHuA4MP_vm|g-4ryxYrPA`lh(>+_`Gl4@Q+qlC+vTJz>|%Ult?$0nd~H2YHPTvns}s zV%Pm2eKT=FB$C&d+s2_|ztTUgTsZChWp2%F<~a{EGdo;6%*<-~$;R`{%#wT?JIrys zp5Jbz&^79mGGd&{_n6VAX`G3`)3^;_yG;rwpq&_MHH8yxJF#Vp&HbbC@jv@MyKyl( z!l5Os-UHec*O$1x{EYBwoOCvA`tUCyi-SEf@Q{2`fHesd%)ehfL&!#L=btA>4Yy6~kf zSPBNks;kqLo_uE*QgSN)B@FE@=5ZEg<2!;uh+%#@_} z`5(HV>y2Z;$kciH2P&V8GBL4O;SEgsg15kc9pfwfQM+*fymVvkt3HE1`WcSmV-qp* z372~<^+Ud~-dc8jeVNe6$~8Fb97rf zZ3B-$GuUE2$RAyFgiix!Gfu}}8+d%-%$xzE&?vF%p?B0)C$i;yKg?}+ZhSts2f!`o zLjw@tJrEXG25LRRKgz)Ld{kD(IC`(dH`P+#4I-B zz&-lFt1~*Lz4eef{V<}Eg5_ns_17TaYq_TFgi==;>tx=YRJ zKW%YqhVYnR^p9z_b58eY@MhIIg!`yXF|XJEhF6`GQ{;w;BihN9Ik=b}T*I?lv$!0SxTgKanl` zw%M%-;JIB9mhsJihMw&;bgZm~Ao_3(`Je#_kk&76gxYc_G406(4_!V;5>v-mCDLc2b^c-a%4EGafQ zQ9J+{r}gjLJbTGEe0LwWW-EUg9+Hjo{jUIm-3G>2i0?D|`23hP;v=_a6Q3?_ zH9lJm>bE{M;g@~cZUf`PMxV(~7SB6!eurCQ;ahO4&G-^>-|-KcpT67}?l!oOpBKK3 zPbW{@o73^#``Z)U2FAygh{wjT?;bzU;MTm(hj)i><8w^lHyw_3XB=M~ia{O1SBJ7jJc>k%)pshN_^U;DT^I}J zBP}dRlUw;x3!8Eq8(UtM_JwQQiMb6umxMH_>#O>};pB~0)bc2|+eD_p4(HJg+dF?h z)_Kr*Nw-7ltl*!+F4_28GUjN(8*e)gEzu0{+7WMjP&wNk6H^d&1dmGMv0SVTAMuEA zbjFuiJT)B4mmi-7)@FQ2nYFNPYslGac+kRQIu?C=Q{gLw!JQpyu`tcA&G=H&uhILf zAL0 zr&KeFD&>bKv$TP`5Cc9AG@J&C7enI(u_UuDvtms7@2YkC4Vpb~C}&aF#GA`mSXbjI z?u21ehPTpz{Bk*);EP>P-K(TxK7I-t-P^D=-T)=>G>g|fNxD>~MRci*@1DZ`_AjS_ BJwN~e delta 8755 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 IXJ;q>2h<~S3jhEB diff --git a/src/reactpy/__init__.py b/src/reactpy/__init__.py index 0f2fe0d63..eb9033401 100644 --- a/src/reactpy/__init__.py +++ b/src/reactpy/__init__.py @@ -1,5 +1,5 @@ from reactpy import config, logging, reactjs, types, web, widgets -from reactpy._html import html +from reactpy._html import h, html from reactpy.core import hooks from reactpy.core.component import component from reactpy.core.events import event @@ -32,6 +32,7 @@ "config", "create_context", "event", + "h", "hooks", "html", "logging", diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index bc2767109..ccda7df62 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -9,7 +9,7 @@ from lxml import etree from lxml.html import fromstring -from reactpy import html as _html +from reactpy import h from reactpy.transforms import RequiredTransforms, attributes_to_reactjs from reactpy.types import Component, VdomDict @@ -105,11 +105,11 @@ def string_to_reactpy( event handler that prevents the browser from navigating to the link. This is useful if you would rather have `reactpy-router` handle your URL navigation. """ - if not html.strip(): - return _html.div() if not isinstance(html, str): msg = f"Expected html to be a string, not {type(html).__name__}" raise TypeError(msg) + if not html.strip(): + return h.div() if "<" not in html or ">" not in html: msg = "Expected html string to contain HTML tags, but no tags were found." raise ValueError(msg) @@ -158,7 +158,7 @@ def _etree_to_vdom( attributes = attributes_to_reactjs(dict(node.items())) # Convert the lxml node to a VDOM dict - constructor = getattr(_html, str(node.tag)) + constructor = getattr(h, str(node.tag)) el = constructor(attributes, children) # Perform necessary transformations on the VDOM attributes to meet VDOM spec @@ -241,13 +241,13 @@ def component_to_vdom(component: Component) -> VdomDict: """Convert the first render of a component into a VDOM dictionary""" result = component.render() + if result is None: + return h.fragment() if isinstance(result, dict): return result if hasattr(result, "render"): return component_to_vdom(cast(Component, result)) - elif isinstance(result, str): - return _html.div(result) - return _html.div() + return h.div(result) if isinstance(result, str) else h.div() def _react_attribute_to_html(key: str, value: Any) -> tuple[str, str]: diff --git a/tests/test_utils.py b/tests/test_utils.py index 52cea0f6a..c35272c77 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -92,6 +92,23 @@ def test_string_to_reactpy(case): assert utils.string_to_reactpy(case["source"]) == case["model"] +@pytest.mark.parametrize("source", ["", " ", "\n\t "]) +def test_string_to_reactpy_empty_source(source): + assert utils.string_to_reactpy(source) == html.div() + + +@pytest.mark.parametrize("source", [123, None, object()]) +def test_string_to_reactpy_non_string_source(source): + with pytest.raises(TypeError): + utils.string_to_reactpy(source) # type: ignore[arg-type] + + +@pytest.mark.parametrize("source", ["no tags", "plain text", "just words"]) +def test_string_to_reactpy_missing_tags(source): + with pytest.raises(ValueError): + utils.string_to_reactpy(source) + + @pytest.mark.parametrize( "case", [ From 9c0ef113202e0a7820fa1f1ebbdcc30e0dc5d65c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:04:24 -0800 Subject: [PATCH 4/5] Bump version string --- src/reactpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy/__init__.py b/src/reactpy/__init__.py index eb9033401..422f16781 100644 --- a/src/reactpy/__init__.py +++ b/src/reactpy/__init__.py @@ -23,7 +23,7 @@ from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy __author__ = "The Reactive Python Team" -__version__ = "2.0.0b9" +__version__ = "2.0.0b10" __all__ = [ "Ref", From 7fa0c8a99ab4dd84739be131dbb6e55e01d629d1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:06:58 -0800 Subject: [PATCH 5/5] return type correctness --- src/reactpy/utils.py | 2 +- tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index ccda7df62..4966f9f4e 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -109,7 +109,7 @@ def string_to_reactpy( msg = f"Expected html to be a string, not {type(html).__name__}" raise TypeError(msg) if not html.strip(): - return h.div() + return h.fragment() if "<" not in html or ">" not in html: msg = "Expected html string to contain HTML tags, but no tags were found." raise ValueError(msg) diff --git a/tests/test_utils.py b/tests/test_utils.py index c35272c77..f15277a9d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -94,7 +94,7 @@ def test_string_to_reactpy(case): @pytest.mark.parametrize("source", ["", " ", "\n\t "]) def test_string_to_reactpy_empty_source(source): - assert utils.string_to_reactpy(source) == html.div() + assert utils.string_to_reactpy(source) == html.fragment() @pytest.mark.parametrize("source", [123, None, object()])