From d769440924c946d34ecb478c85141be75bf727e9 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 29 Oct 2025 04:23:18 -0700 Subject: [PATCH] Store asset keys relative to the MJCF. This CL fixes cases when loading a spec with "from_zip" that fails on zipped folders that contain a root directory. PiperOrigin-RevId: 825484553 Change-Id: I79c6f542edbd3ffd167d0b5cecc704c32c8d6824 --- python/mujoco/__init__.py | 10 ++++++++++ python/mujoco/specs_test.py | 20 ++++++++++++++++++++ python/mujoco/testdata/MJCF_NoRoot.zip | Bin 0 -> 5161 bytes python/mujoco/testdata/MJCF_Root.zip | Bin 0 -> 5626 bytes python/pyproject.toml | 1 + 5 files changed, 31 insertions(+) create mode 100644 python/mujoco/testdata/MJCF_NoRoot.zip create mode 100644 python/mujoco/testdata/MJCF_Root.zip diff --git a/python/mujoco/__init__.py b/python/mujoco/__init__.py index 1cb446f7..2d8d22ad 100644 --- a/python/mujoco/__init__.py +++ b/python/mujoco/__init__.py @@ -124,15 +124,25 @@ def from_zip(file: Union[str, IO[bytes]]) -> _specs.MjSpec: if not zipfile.is_zipfile(file): raise ValueError(f'File {file} is not a zip file.') with zipfile.ZipFile(file, 'r') as zip_file: + xml_dir = None for zip_info in zip_file.infolist(): if not zip_info.filename.endswith(os.path.sep): with zip_file.open(zip_info.filename) as f: if zip_info.filename.endswith('.xml'): xml_string = f.read() + xml_dir = os.path.dirname(zip_info.filename) else: assets[zip_info.filename] = f.read() + if not xml_string: raise ValueError('No XML file found in zip file.') + + relative_assets = {} + for key, value in assets.items(): + new_key = os.path.relpath(key, xml_dir) + relative_assets[new_key] = value + assets = relative_assets + return _specs.MjSpec.from_string(xml_string, assets=assets) diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index ee99ed25..e2610f34 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1615,5 +1615,25 @@ class SpecsTest(absltest.TestCase): self.assertEqual(wrap_joint2.type, mujoco.mjtWrap.mjWRAP_JOINT) self.assertEqual(wrap_joint2.coef, 2.0) + def test_from_zip(self): + """Tests that the assets are correctly parsed from a zip file.""" + model_path_root = ( + epath.resource_path("mujoco") / "testdata" / "MJCF_Root.zip" + ) + model_path_no_root = ( + epath.resource_path("mujoco") / "testdata" / "MJCF_NoRoot.zip" + ) + filenames = [model_path_root.as_posix(), model_path_no_root.as_posix()] + + for filename in filenames: + with self.subTest(filename): + spec = mujoco.MjSpec.from_zip(filename) + spec.compile() + assets = spec.assets + xml_string = spec.to_xml() + string_spec = mujoco.MjSpec.from_string(xml_string, assets=assets) + string_spec.compile() + self.assertEqual(spec.to_xml(), string_spec.to_xml()) + if __name__ == '__main__': absltest.main() diff --git a/python/mujoco/testdata/MJCF_NoRoot.zip b/python/mujoco/testdata/MJCF_NoRoot.zip new file mode 100644 index 0000000000000000000000000000000000000000..215b9a68d0bed878d8205fc2e44f82c9a8dbdc35 GIT binary patch literal 5161 zcmds*cQ{<>+sDt05{xc_kYIEYU9?0eTB3^@bugkc+9(->5iLUWn$_FrC2GhbN(j+Q zbP=LNlpy@Z?!LR(-RxfP?|T1x&vo7NOu5cy&V4`M=Q-zlYpUVk5&`GOYkd>rU*7!o zasglf9AWNuFn2yZT|xk7A`5_@2f`ZyfN%l-O8~Bp83@qC0xn`tAppt)Ce=gwMEVM` zpGv?0K=uc~3SkN3b@y=i1@u5i(D*$50&*|q?B?TExud1b(=(vw(xcL^rgD8qWJst= zcaW3!jyC_GH51`>)=uEbne)LFG^+0#(+%=1oQgou8bP>q+R@3@ zu4OK#9DC&AD zLn>+==Mu(S>9ph`KZ3dz%)nT0MK135#6O~h9(z7e7uC~qOU*w@Z0t1%2hfjLrJiihRm*4I@l)g3QR&C0dxHpTD3 zG3)6@7{k_y8k$ls|Cr6U2vQ>q@?FX5Pt9ew?3-=;^YC$3H(e-~J-q`VW~c%wWl1}9oO`U@z?bu zL4FE5kH4rXN~hN;n4i39l}EpYMlOYcJJwHe>(U&a$pr0$lH<|do)D<;4vTnC*)5^Q zS%Df8DcyV-?$8DG*_~eA%XA)U5rgW)tSPZ3o&X|$sSnTIE>gC)UWzIZ9s2N&aBHHa zd`}yZIb*|khlM?BM)(-Vpky;GP3Pn8@r4wX2?`O@#oQrftF_M*Udfk&BST3G=tiKl z2)F77(^Ba*_IRqh>NKa3s-kF-(N(%s-t;-HUb|2+HK_{AjL=43E3I86*2ijVQ3*kf zn(R@xZoj02Ey*oWDr&7XB&NEVAJ$2}r{SnSD_XkaBb0xtYgDdZ@t}zHkl)C>X=|n= z?Ub+`%Fna7WKM>`4SFvXQ0R1>w19PdJ#q)}Wcy*|`>#V&$EWf$QmFTAmd0T&l64tt z>_=afHw{ioFQqe)pHr#AhjN#+nX*wbgy=j4CQe5!EfynL zI9rI35n)Y^w&)}QHFME(rYvEi0@mxyBoT2>6ih3{2=8uqzrU5?k6d4iaix^ZsIYaQ zTJ5v@Vzcrlb$14tu$6|4MVp7RW+w}bY(&}3Sd1`^{Zy1ZL_ooMwG`~Gk~Y>uJrM$x zy@fuwJ>t^CPavj13ZfMRLLZ{h*n1CwGI6r0H7SIr2BM~# z1ErUS0BC@MfGJ7XGn2F$pBg0)3J?%efM}si{2^Zumt~H?xA6g{BzA9WC;w;7(_CnV z`w&3h_0xvtLal0|YM*~3l6~Ihj%(LP>BmHN_6s^Jl`^%T3+g*EFUX0_J_dxbAw13#6f=n_A|c!AuLLrrmL(q2Z&h*}}}Q>b)baa%0*A7vLi3Alc=*Jk>H^W{>653U+5&# zEaceg2#a>DUwwP4A40U0xK(5i;cTQ2W`O#=woLkRkk@^7^fWFyjWy&}bz?&|%kvM_ z>o1Lflf_tB>xRPNQTmD1EYeS6Ppxysu11FJp`>8K-J;mNJi)bQj{8}ZN8Rsz(y+&0KkV$JcXBqDFCSVsrdoSl&-zbiSr{- zq1H&qx{Pq#>B!|wg0|bG^EjpRz0MDswNZVU2l<_g8Ppa8^YN|%1vdEPdQtgO4vCL= zGIf!xhUQV*`Mv(;>+^3P zQ_4o&dt7WICt8+|2Sm@)1SmFGkFWsyb&ndMJqDP3?Z!dIk-RoPauoA6@vz3`kb9o~ z5UXA55+RGphun^4Xcu7jg{YoYs%VMg^bQ!myjr*Hz)+PT$arN46a{F%pu5+n*|W_N zIa@GFtt}FlryrD?D_Jyld=1@()3%dg;H6OG5?OY4F$*5ga?BjWow*#ld|#z)CthhK zxvvULZ?;a)oSJ{6b6e7iT|L%>q?2TX2@-P?XhQNbxjCG)7D>EjxDX$=f57WfT2{Wy zKW}BIpr@RZbv`|-YxZAxDLSC*8&axfA{f(5b4}gx!H+4~ zz8iM=WOLR?QS=~wHijjsUI2QRx}kkZosp~0|lmOhHFc=cQ!s!A66c+he^)_QlRt`dg9o{*gwbF z36hW9KPhSKxVuS6$+n;6R4-_y~o5 zhT*-1mUugK7EjJy#x}gbcE;u-smOWTI~_9?Fs*EBYf+XAQ1NF2b`d-Ou#xZWFftWSfamL4OhIKvs*(ffnpgxt=xzDK$|NRKk@?wz`^(z4`dy!b1w| z{T65P^7m_IOq`H5rWLQOjAdTmJFdG+apC+3T70!AM9b7wt1GuEax7bGb7mFt7?l!xX*p7vR z!Zn;NR_rR2lW%6V_2f1RvTPn#8TV#PkMZv4u!@1hZian#&~~hDVFTJtYj#E&(V(?iX`y z4d+*svPaDP_&!%5<>6`9%H!{{Q_$Dgpf#5-byi=x0`<~Jk0^`aJ)+*}4(e#fd)oOS zFUMl&Ik*@0)C*=2P@Q*i`v|PLMBZFE$VfjtTOID863Zr!oaw1b_tMMp*p+yjf!0qD z7Ym)CKzLj<(idj8j&Y6Zuh}Hgd$SHZMI5a}<;uG*x{0Xadd3#-JT?Sv69)zu`y4S1 zZah{I?&81{dt_cwH0?tSEo`O@hxLg-jaq9W?$l#EFhPNH2CXKMTC z=Hr(&pt&`zzvkzOC=@7WE-ZUhQ8+tBH$=&uJH@fB2+a|P%Sks zR7C@m&I8#nxCbUjn`>oz3^Rxe(RX_$nv%V9D05F8^SeUaVPZNz+w$VXz)P?&HBV*P zX@2F}WXG#~z58cIdozfPrJB7kai_K5u0(8wd|JV1NJNfgkpoq^(+$_&wY}q_*4850 zxywJZMb8(LUHj4_Cy$)whpS}65Dhn7J^R=CrctBYKOs}XA6lf$v=J0DXMGvz?tR9m z#JR-D>+x0XvZT{em_mvyV*v8!Jm0MrNGvL8XT%8E;Qm>aXG}iuG~{%96A~31PJv0% z0`drNpqlsRf=bmQqCMGtpHI&0ks3Qi9p8*LwP*j-OzyY&(>^N1#zIU75RS8N`Ybth z36r)c_Kt$Db{8R~wwdF=&EaKsnBzY7I=kLx&>;6So(wKS@Ko_F}O`~foA9>g)TGZlKeSon& zQR(Mb7lcy+@cTZtrK<-Aqy6rNd5rZgdC~7h$v6f`*p0x2W#xk8Iu|VMH55qLMVL4% zA$nKg(K;nNJ!)zW4_3s}F9qa5x6VK0)?cvqoJ&OAs{4FtZ5^JBYw z0XTmUKR?KUZ?fm(?}i!g*H+74+}*FRe`>V+4-5dRQRlSZV1F29Ec6F0m+zr}?`3=s z6~cPc@1ehRHn7kibT$4hZK^+|&5JcEzd`@pGx+aIK>s`VyadP}ga4ay{N0rQS`KU- zuz%o#wdwz8(Xm(m1AG2&Wg-0i>d)1{g$I1^C;zP&|KujWpE`%{aewxfSo9B7f%l7! Q39voAdTb?)DbDrzA98JAfB*mh literal 0 HcmV?d00001 diff --git a/python/mujoco/testdata/MJCF_Root.zip b/python/mujoco/testdata/MJCF_Root.zip new file mode 100644 index 0000000000000000000000000000000000000000..9bf4d0cedf977a6daf030df8ce9926531a310065 GIT binary patch literal 5626 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