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
« 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
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)
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"
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_'"
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"
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"
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)
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"
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"
94def test_freeze():
95 class TestClass:
96 def __init__(self):
97 self.attr = "value"
99 instance = TestClass()
100 freeze(instance)
101 with pytest.raises(AttributeError):
102 instance.attr = "new_value"
105# Testing the get_all_subclasses function
106class A:
107 pass
110class B(A):
111 pass
114class C(B):
115 pass
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()
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}
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
166def test_get_all_subclasses2():
167 class A:
168 pass
170 class B(A):
171 pass
173 class C(A):
174 pass
176 class D(B, C):
177 pass
179 class E(B):
180 pass
182 class F(D):
183 pass
185 class Z:
186 pass
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}
198# Test classes
199@dataclass
200class DC1:
201 x: bool
202 y: bool = False
205@dataclass(frozen=True)
206class DC2:
207 x: bool
208 y: bool = False
211@dataclass(frozen=True)
212class DC3:
213 x: DC2 = DC2(False, False)
216@dataclass(frozen=True)
217class DC4:
218 x: DC2
219 y: bool = False
222@dataclass(frozen=True)
223class DC5:
224 x: int
227@dataclass(frozen=True)
228class DC6:
229 x: DC5
230 y: bool = False
233@dataclass(frozen=True)
234class DC7(abc.ABC):
235 x: bool
237 @abc.abstractmethod
238 def foo(self):
239 pass
242@dataclass(frozen=True)
243class DC8(DC7):
244 x: bool = False
246 def foo(self):
247 pass
250@dataclass(frozen=True)
251class DC9(DC7):
252 y: bool = True
254 def foo(self):
255 pass
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
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