Coverage for muutils / misc / string.py: 86%
36 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-22 18:25 -0700
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-22 18:25 -0700
1from __future__ import annotations
2from typing import Any, Callable, TypeVar
5from muutils.misc.hashing import stable_hash
8def sanitize_name(
9 name: str | None,
10 additional_allowed_chars: str = "",
11 replace_invalid: str = "",
12 when_none: str | None = "_None_",
13 leading_digit_prefix: str = "",
14) -> str:
15 """sanitize a string, leaving only alphanumerics and `additional_allowed_chars`
17 # Parameters:
18 - `name : str | None`
19 input string
20 - `additional_allowed_chars : str`
21 additional characters to allow, none by default
22 (defaults to `""`)
23 - `replace_invalid : str`
24 character to replace invalid characters with
25 (defaults to `""`)
26 - `when_none : str | None`
27 string to return if `name` is `None`. if `None`, raises an exception
28 (defaults to `"_None_"`)
29 - `leading_digit_prefix : str`
30 character to prefix the string with if it starts with a digit
31 (defaults to `""`)
33 # Returns:
34 - `str`
35 sanitized string
36 """
38 if name is None:
39 if when_none is None:
40 raise ValueError("name is None")
41 else:
42 return when_none
44 sanitized: str = ""
45 for char in name:
46 if char.isalnum():
47 sanitized += char
48 elif char in additional_allowed_chars:
49 sanitized += char
50 else:
51 sanitized += replace_invalid
53 if sanitized[0].isdigit():
54 sanitized = leading_digit_prefix + sanitized
56 return sanitized
59def sanitize_fname(
60 fname: str | None,
61 replace_invalid: str = "",
62 when_none: str | None = "_None_",
63 leading_digit_prefix: str = "",
64) -> str:
65 """sanitize a filename to posix standards
67 - leave only alphanumerics, `_` (underscore), '-' (dash) and `.` (period)
68 """
69 return sanitize_name(
70 name=fname,
71 additional_allowed_chars="._-",
72 replace_invalid=replace_invalid,
73 when_none=when_none,
74 leading_digit_prefix=leading_digit_prefix,
75 )
78def sanitize_identifier(
79 fname: str | None,
80 replace_invalid: str = "",
81 when_none: str | None = "_None_",
82) -> str:
83 """sanitize an identifier (variable or function name)
85 - leave only alphanumerics and `_` (underscore)
86 - prefix with `_` if it starts with a digit
87 """
88 return sanitize_name(
89 name=fname,
90 additional_allowed_chars="_",
91 replace_invalid=replace_invalid,
92 when_none=when_none,
93 leading_digit_prefix="_",
94 )
97def dict_to_filename(
98 data: dict[str, Any],
99 format_str: str = "{key}_{val}",
100 separator: str = ".",
101 max_length: int = 255,
102):
103 # Convert the dictionary items to a list of strings using the format string
104 formatted_items: list[str] = [
105 format_str.format(key=k, val=v)
106 for k, v in data.items() # pyright: ignore[reportAny]
107 ]
109 # Join the formatted items using the separator
110 joined_str: str = separator.join(formatted_items)
112 # Remove special characters and spaces
113 sanitized_str: str = sanitize_fname(joined_str)
115 # Check if the length is within limits
116 if len(sanitized_str) <= max_length:
117 return sanitized_str
119 # If the string is too long, generate a hash
120 return f"h_{stable_hash(sanitized_str)}"
123T_Callable = TypeVar("T_Callable", bound=Callable[..., Any])
126def dynamic_docstring(**doc_params: str) -> Callable[[T_Callable], T_Callable]:
127 def decorator(func: T_Callable) -> T_Callable:
128 if func.__doc__:
129 func.__doc__ = getattr(func, "__doc__", "").format(**doc_params)
130 return func
132 return decorator