1 from __future__ import annotations

2 from typing import Iterable, Any, Type, Union

3 from dataclasses import dataclass

4 import abc

5 import pytest

6 from pytest import mark, param

7

8 from 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

23 def 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

33 def 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

46 def 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

58 def 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

71 def 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

82 def 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

94 def 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

106 class A:

107 pass

108

109

110 class B(A):

111 pass

112

113

114 class C(B):

115 pass

116

117

118 def 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

124 def 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)

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

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

164

165

166 def 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

200 class DC1:

201 x: bool

202 y: bool = False

203

204

205@dataclass(frozen=True)

206 class DC2:

207 x: bool

208 y: bool = False

209

210

211@dataclass(frozen=True)

212 class DC3:

213 x: DC2 = DC2(False, False)

214

215

216@dataclass(frozen=True)

217 class DC4:

218 x: DC2

219 y: bool = False

220

221

222@dataclass(frozen=True)

223 class DC5:

224 x: int

225

226

227@dataclass(frozen=True)

228 class DC6:

229 x: DC5

230 y: bool = False

231

232

233@dataclass(frozen=True)

234 class DC7(abc.ABC):

235 x: bool

236

237 @abc.abstractmethod

238 def foo(self):

239 pass

240

241

242@dataclass(frozen=True)

243 class DC8(DC7):

244 x: bool = False

245

246 def foo(self):

247 pass

248

249

250@dataclass(frozen=True)

251 class 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)

318 def 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)

350 def 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