1 from __future__ import annotations

2

3

4 from muutils.misc.hashing import stable_hash

5

6

7 def sanitize_name(

8 name: str | None,

9 additional_allowed_chars: str = "",

10 replace_invalid: str = "",

11 when_none: str | None = "_None_",

12 leading_digit_prefix: str = "",

13) -> str:

14 """sanitize a string, leaving only alphanumerics and `additional_allowed_chars`

15

16 # Parameters:

17 - `name : str | None`

18 input string

19 - `additional_allowed_chars : str`

20 additional characters to allow, none by default

21 (defaults to `""`)

22 - `replace_invalid : str`

23 character to replace invalid characters with

24 (defaults to `""`)

25 - `when_none : str | None`

26 string to return if `name` is `None`. if `None`, raises an exception

27 (defaults to `"_None_"`)

28 - `leading_digit_prefix : str`

29 character to prefix the string with if it starts with a digit

30 (defaults to `""`)

31

32 # Returns:

33 - `str`

34 sanitized string

35 """

36

37 if name is None:

38 if when_none is None:

39 raise ValueError("name is None")

40 else:

41 return when_none

42

43 sanitized: str = ""

44 for char in name:

45 if char.isalnum():

46 sanitized += char

47 elif char in additional_allowed_chars:

48 sanitized += char

49 else:

50 sanitized += replace_invalid

51

52 if sanitized[0].isdigit():

53 sanitized = leading_digit_prefix + sanitized

54

55 return sanitized

56

57

58 def sanitize_fname(fname: str | None, **kwargs) -> str:

59 """sanitize a filename to posix standards

60

61 - leave only alphanumerics, `_` (underscore), '-' (dash) and `.` (period)

62 """

63 return sanitize_name(fname, additional_allowed_chars="._-", **kwargs)

64

65

66 def sanitize_identifier(fname: str | None, **kwargs) -> str:

67 """sanitize an identifier (variable or function name)

68

69 - leave only alphanumerics and `_` (underscore)

70 - prefix with `_` if it starts with a digit

71 """

72 return sanitize_name(

73 fname, additional_allowed_chars="_", leading_digit_prefix="_", **kwargs

74 )

75

76

77 def dict_to_filename(

78 data: dict,

79 format_str: str = "{key}_{val}",

80 separator: str = ".",

81 max_length: int = 255,

82):

83 # Convert the dictionary items to a list of strings using the format string

84 formatted_items: list[str] = [

85 format_str.format(key=k, val=v) for k, v in data.items()

86 ]

87

88 # Join the formatted items using the separator

89 joined_str: str = separator.join(formatted_items)

90

91 # Remove special characters and spaces

92 sanitized_str: str = sanitize_fname(joined_str)

93

94 # Check if the length is within limits

95 if len(sanitized_str) <= max_length:

96 return sanitized_str

97

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

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

100

101

102 def dynamic_docstring(**doc_params):

103 def decorator(func):

104 if func.__doc__:

105 func.__doc__ = func.__doc__.format(**doc_params)

106 return func

107

108 return decorator