Coverage for tests/unit/misc/test_misc.py: 97%

148 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-04 03:33 -0600

1from __future__ import annotations 

2from typing import Iterable, Any, Type, Union 

3from dataclasses import dataclass 

4import abc 

5import pytest 

6from pytest import mark, param 

7 

8from muutils.misc import ( 

9 dict_to_filename, 

10 freeze, 

11 sanitize_fname, 

12 sanitize_identifier, 

13 sanitize_name, 

14 stable_hash, 

15 flatten, 

16 get_all_subclasses, 

17 IsDataclass, 

18 isinstance_by_type_name, 

19 dataclass_set_equals, 

20) 

21 

22 

23def test_stable_hash(): 

24 # no real way to test this without running multiple interpreters, but I'm pretty sure it works lol 

25 assert stable_hash("test") == stable_hash( 

26 "test" 

27 ), "Hash should be stable for the same input" 

28 assert stable_hash("test") != stable_hash( 

29 "Test" 

30 ), "Hash should be different for different inputs" 

31 

32 

33def test_sanitize_fname(): 

34 assert ( 

35 sanitize_fname("filename.txt") == "filename.txt" 

36 ), "Alphanumeric characters and '.' should remain" 

37 assert ( 

38 sanitize_fname("file-name.txt") == "file-name.txt" 

39 ), "Alphanumeric characters, '-' and '.' should remain" 

40 assert ( 

41 sanitize_fname("file@name!?.txt") == "filename.txt" 

42 ), "Special characters should be removed" 

43 assert sanitize_fname(None) == "_None_", "None input should return '_None_'" 

44 

45 

46def test_sanitize_name(): 

47 assert sanitize_name("Hello World") == "HelloWorld" 

48 assert sanitize_name("Hello_World", additional_allowed_chars="_") == "Hello_World" 

49 assert sanitize_name("Hello!World", replace_invalid="-") == "Hello-World" 

50 assert sanitize_name(None) == "_None_" 

51 assert sanitize_name(None, when_none="Empty") == "Empty" 

52 with pytest.raises(ValueError): 

53 sanitize_name(None, when_none=None) 

54 assert sanitize_name("123abc") == "123abc" 

55 assert sanitize_name("123abc", leading_digit_prefix="_") == "_123abc" 

56 

57 

58def test_sanitize_fname_2(): 

59 assert sanitize_fname("file name.txt") == "filename.txt" 

60 assert sanitize_fname("file_name.txt") == "file_name.txt" 

61 assert sanitize_fname("file-name.txt") == "file-name.txt" 

62 assert sanitize_fname("file!name.txt") == "filename.txt" 

63 assert sanitize_fname(None) == "_None_" 

64 assert sanitize_fname(None, when_none="Empty") == "Empty" 

65 with pytest.raises(ValueError): 

66 sanitize_fname(None, when_none=None) 

67 assert sanitize_fname("123file.txt") == "123file.txt" 

68 assert sanitize_fname("123file.txt", leading_digit_prefix="_") == "_123file.txt" 

69 

70 

71def test_sanitize_identifier(): 

72 assert sanitize_identifier("variable_name") == "variable_name" 

73 assert sanitize_identifier("VariableName") == "VariableName" 

74 assert sanitize_identifier("variable!name") == "variablename" 

75 assert sanitize_identifier("123variable") == "_123variable" 

76 assert sanitize_identifier(None) == "_None_" 

77 assert sanitize_identifier(None, when_none="Empty") == "Empty" 

78 with pytest.raises(ValueError): 

79 sanitize_identifier(None, when_none=None) 

80 

81 

82def test_dict_to_filename(): 

83 data = {"key1": "value1", "key2": "value2"} 

84 assert ( 

85 dict_to_filename(data) == "key1_value1.key2_value2" 

86 ), "Filename should be formatted correctly" 

87 

88 long_data = {f"key{i}": f"value{i}" for i in range(100)} 

89 assert dict_to_filename(long_data).startswith( 

90 "h_" 

91 ), "Filename should be hashed if too long" 

92 

93 

94def test_freeze(): 

95 class TestClass: 

96 def __init__(self): 

97 self.attr = "value" 

98 

99 instance = TestClass() 

100 freeze(instance) 

101 with pytest.raises(AttributeError): 

102 instance.attr = "new_value" 

103 

104 

105# Testing the get_all_subclasses function 

106class A: 

107 pass 

108 

109 

110class B(A): 

111 pass 

112 

113 

114class C(B): 

115 pass 

116 

117 

118def test_get_all_subclasses(): 

119 assert get_all_subclasses(A) == {B, C} 

120 assert get_all_subclasses(B) == {C} 

121 assert get_all_subclasses(C) == set() 

122 

123 

124def test_get_all_subclasses_include_self(): 

125 assert get_all_subclasses(A, include_self=True) == {A, B, C} 

126 assert get_all_subclasses(B, include_self=True) == {B, C} 

127 assert get_all_subclasses(C, include_self=True) == {C} 

128 

129 

130@mark.parametrize( 

131 "deep, flat, depth", 

132 [ 

133 param( 

134 iter_tuple[0], 

135 iter_tuple[1], 

136 iter_tuple[2], 

137 id=f"{i}", 

138 ) 

139 for i, iter_tuple in enumerate( 

140 [ 

141 ([1, 2, 3, 4], [1, 2, 3, 4], None), 

142 ((1, 2, 3, 4), [1, 2, 3, 4], None), 

143 ((j for j in [1, 2, 3, 4]), [1, 2, 3, 4], None), 

144 (["a", "b", "c", "d"], ["a", "b", "c", "d"], None), 

145 ("funky duck", [c for c in "funky duck"], None), 

146 (["funky", "duck"], ["funky", "duck"], None), 

147 (b"funky duck", [b for b in b"funky duck"], None), 

148 ([b"funky", b"duck"], [b"funky", b"duck"], None), 

149 ([[1, 2, 3, 4]], [1, 2, 3, 4], None), 

150 ([[[[1, 2, 3, 4]]]], [1, 2, 3, 4], None), 

151 ([[[[1], 2], 3], 4], [1, 2, 3, 4], None), 

152 ([[1, 2], [[3]], (4,)], [1, 2, 3, 4], None), 

153 ([[[1, 2, 3, 4]]], [[1, 2, 3, 4]], 1), 

154 ([[[1, 2, 3, 4]]], [1, 2, 3, 4], 2), 

155 ([[1, 2], [[3]], (4,)], [1, 2, [3], 4], 1), 

156 ([[1, 2], [(3,)], (4,)], [1, 2, (3,), 4], 1), 

157 ([[[[1], 2], 3], 4], [[1], 2, 3, 4], 2), 

158 ] 

159 ) 

160 ], 

161) 

162def test_flatten(deep: Iterable[Any], flat: Iterable[Any], depth: Union[int, None]): 

163 assert list(flatten(deep, depth)) == flat 

164 

165 

166def test_get_all_subclasses2(): 

167 class A: 

168 pass 

169 

170 class B(A): 

171 pass 

172 

173 class C(A): 

174 pass 

175 

176 class D(B, C): 

177 pass 

178 

179 class E(B): 

180 pass 

181 

182 class F(D): 

183 pass 

184 

185 class Z: 

186 pass 

187 

188 assert get_all_subclasses(A) == {B, C, D, E, F} 

189 assert get_all_subclasses(A, include_self=True) == {A, B, C, D, E, F} 

190 assert get_all_subclasses(B) == {D, E, F} 

191 assert get_all_subclasses(C) == {D, F} 

192 assert get_all_subclasses(D) == {F} 

193 assert get_all_subclasses(D, include_self=True) == {D, F} 

194 assert get_all_subclasses(Z) == set() 

195 assert get_all_subclasses(Z, include_self=True) == {Z} 

196 

197 

198# Test classes 

199@dataclass 

200class DC1: 

201 x: bool 

202 y: bool = False 

203 

204 

205@dataclass(frozen=True) 

206class DC2: 

207 x: bool 

208 y: bool = False 

209 

210 

211@dataclass(frozen=True) 

212class DC3: 

213 x: DC2 = DC2(False, False) 

214 

215 

216@dataclass(frozen=True) 

217class DC4: 

218 x: DC2 

219 y: bool = False 

220 

221 

222@dataclass(frozen=True) 

223class DC5: 

224 x: int 

225 

226 

227@dataclass(frozen=True) 

228class DC6: 

229 x: DC5 

230 y: bool = False 

231 

232 

233@dataclass(frozen=True) 

234class DC7(abc.ABC): 

235 x: bool 

236 

237 @abc.abstractmethod 

238 def foo(self): 

239 pass 

240 

241 

242@dataclass(frozen=True) 

243class DC8(DC7): 

244 x: bool = False 

245 

246 def foo(self): 

247 pass 

248 

249 

250@dataclass(frozen=True) 

251class DC9(DC7): 

252 y: bool = True 

253 

254 def foo(self): 

255 pass 

256 

257 

258@mark.parametrize( 

259 "coll1, coll2, result", 

260 [ 

261 param( 

262 c1, 

263 c2, 

264 res, 

265 id=f"{c1}_{c2}", 

266 ) 

267 for c1, c2, res in ( 

268 [ 

269 ( 

270 [ 

271 DC1(False, False), 

272 DC1(False, True), 

273 ], 

274 [ 

275 DC1(True, False), 

276 DC1(True, True), 

277 ], 

278 False, 

279 ), 

280 ( 

281 [ 

282 DC1(False, False), 

283 DC1(False, True), 

284 ], 

285 [ 

286 DC1(False, False), 

287 DC1(False, True), 

288 ], 

289 True, 

290 ), 

291 ( 

292 [ 

293 DC1(False, False), 

294 DC1(False, True), 

295 ], 

296 [ 

297 DC2(False, False), 

298 DC2(False, True), 

299 ], 

300 False, 

301 ), 

302 ( 

303 [ 

304 DC3(DC2(False)), 

305 DC3(DC2(False)), 

306 ], 

307 [ 

308 DC3(DC2(False)), 

309 ], 

310 True, 

311 ), 

312 ([], [], True), 

313 ([DC5], [DC5], AttributeError), 

314 ] 

315 ) 

316 ], 

317) 

318def test_dataclass_set_equals( 

319 coll1: Iterable[IsDataclass], 

320 coll2: Iterable[IsDataclass], 

321 result: Union[bool, Type[Exception]], 

322): 

323 if isinstance(result, type) and issubclass(result, Exception): 

324 with pytest.raises(result): 

325 dataclass_set_equals(coll1, coll2) 

326 else: 

327 assert dataclass_set_equals(coll1, coll2) == result 

328 

329 

330@mark.parametrize( 

331 "o, type_name, result", 

332 [ 

333 param( 

334 o, 

335 name, 

336 res, 

337 id=f"{o}_{name}", 

338 ) 

339 for o, name, res in ( 

340 [ 

341 (True, "bool", True), 

342 (True, "int", True), 

343 (1, "int", True), 

344 (1, "bool", False), 

345 ([], "list", True), 

346 ] 

347 ) 

348 ], 

349) 

350def test_isinstance_by_type_name( 

351 o: object, type_name: str, result: Union[bool, Type[Exception]] 

352): 

353 if isinstance(result, type) and issubclass(result, Exception): 

354 with pytest.raises(result): 

355 isinstance_by_type_name(o, type_name) 

356 else: 

357 assert isinstance_by_type_name(o, type_name) == result