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

1from __future__ import annotations 

2from typing import Any, Callable, TypeVar 

3 

4 

5from muutils.misc.hashing import stable_hash 

6 

7 

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` 

16 

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 `""`) 

32 

33 # Returns: 

34 - `str` 

35 sanitized string 

36 """ 

37 

38 if name is None: 

39 if when_none is None: 

40 raise ValueError("name is None") 

41 else: 

42 return when_none 

43 

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 

52 

53 if sanitized[0].isdigit(): 

54 sanitized = leading_digit_prefix + sanitized 

55 

56 return sanitized 

57 

58 

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 

66 

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 ) 

76 

77 

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) 

84 

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 ) 

95 

96 

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 ] 

108 

109 # Join the formatted items using the separator 

110 joined_str: str = separator.join(formatted_items) 

111 

112 # Remove special characters and spaces 

113 sanitized_str: str = sanitize_fname(joined_str) 

114 

115 # Check if the length is within limits 

116 if len(sanitized_str) <= max_length: 

117 return sanitized_str 

118 

119 # If the string is too long, generate a hash 

120 return f"h_{stable_hash(sanitized_str)}" 

121 

122 

123T_Callable = TypeVar("T_Callable", bound=Callable[..., Any]) 

124 

125 

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 

131 

132 return decorator