Coverage for tdom / classnames.py: 95%
21 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-17 23:32 +0000
« 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.
5 Accepts strings, lists/tuples of strings, and dicts mapping class names to
6 boolean values. Ignores None and False values.
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"
14 Args:
15 *args: Variable length argument list containing strings, lists/tuples,
16 or dicts.
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)
25 while queue:
26 arg = queue.pop(0)
28 if not arg: # Handles None, False, empty strings/lists/dicts
29 continue
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__}")
49 # Filter out empty strings and join the result.
50 return " ".join(stripped for c in classes if (stripped := c.strip()))