Coverage for tdom / classnames.py: 95%

21 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-17 23:32 +0000

1def classnames(*args: object) -> str: 

2 """ 

3 Construct a space-separated class string from various inputs. 

4 

5 Accepts strings, lists/tuples of strings, and dicts mapping class names to 

6 boolean values. Ignores None and False values. 

7 

8 Examples: 

9 classnames("btn", "btn-primary") -> "btn btn-primary" 

10 classnames("btn", {"btn-primary": True, "disabled": False}) -> "btn btn-primary" 

11 classnames(["btn", "btn-primary"], {"disabled": True}) -> "btn btn-primary disabled" 

12 classnames("btn", None, False, "active") -> "btn active" 

13 

14 Args: 

15 *args: Variable length argument list containing strings, lists/tuples, 

16 or dicts. 

17 

18 Returns: 

19 A single string with class names separated by spaces. 

20 """ 

21 classes: list[str] = [] 

22 # Use a queue to process arguments iteratively, preserving order. 

23 queue = list(args) 

24 

25 while queue: 

26 arg = queue.pop(0) 

27 

28 if not arg: # Handles None, False, empty strings/lists/dicts 

29 continue 

30 

31 if isinstance(arg, str): 

32 classes.append(arg) 

33 elif isinstance(arg, dict): 

34 for key, value in arg.items(): 

35 if value: 

36 if not isinstance(key, str): 

37 raise ValueError( 

38 f"Classnames dictionary keys must be strings, found {key!r} of type {type(key).__name__}" 

39 ) 

40 classes.append(key) 

41 elif isinstance(arg, (list, tuple)): 

42 # Add items to the front of the queue to process them next, in order. 

43 queue[0:0] = arg 

44 elif isinstance(arg, bool): 

45 pass # Explicitly ignore booleans not in a dict 

46 else: 

47 raise ValueError(f"Invalid class argument type: {type(arg).__name__}") 

48 

49 # Filter out empty strings and join the result. 

50 return " ".join(stripped for c in classes if (stripped := c.strip()))