toml_util.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. from typing import Any, List, Callable
  2. import json
  3. import ast
  4. import inspect
  5. import toml
  6. import re
  7. import configparser
  8. from pathlib import Path, PosixPath
  9. from pydantic.json_schema import GenerateJsonSchema
  10. from pydantic_core import to_jsonable_python
  11. JSONValue = str | bool | int | None | List['JSONValue']
  12. TOML_HEADER = "# Converted from INI to TOML format: https://toml.io/en/\n\n"
  13. def load_ini_value(val: str) -> JSONValue:
  14. """Convert lax INI values into strict TOML-compliant (JSON) values"""
  15. if val.lower() in ('true', 'yes', '1'):
  16. return True
  17. if val.lower() in ('false', 'no', '0'):
  18. return False
  19. if val.isdigit():
  20. return int(val)
  21. try:
  22. return ast.literal_eval(val)
  23. except Exception:
  24. pass
  25. try:
  26. return json.loads(val)
  27. except Exception:
  28. pass
  29. return val
  30. def convert(ini_str: str) -> str:
  31. """Convert a string of INI config into its TOML equivalent (warning: strips comments)"""
  32. config = configparser.ConfigParser()
  33. config.optionxform = str # capitalize key names
  34. config.read_string(ini_str)
  35. # Initialize an empty dictionary to store the TOML representation
  36. toml_dict = {}
  37. # Iterate over each section in the INI configuration
  38. for section in config.sections():
  39. toml_dict[section] = {}
  40. # Iterate over each key-value pair in the section
  41. for key, value in config.items(section):
  42. parsed_value = load_ini_value(value)
  43. # Convert the parsed value to its TOML-compatible JSON representation
  44. toml_dict[section.upper()][key.upper()] = json.dumps(parsed_value)
  45. # Build the TOML string
  46. toml_str = TOML_HEADER
  47. for section, items in toml_dict.items():
  48. toml_str += f"[{section}]\n"
  49. for key, value in items.items():
  50. toml_str += f"{key} = {value}\n"
  51. toml_str += "\n"
  52. return toml_str.strip()
  53. class JSONSchemaWithLambdas(GenerateJsonSchema):
  54. """
  55. Encode lambda functions in default values properly.
  56. Usage:
  57. >>> json.dumps(value, encoder=JSONSchemaWithLambdas())
  58. """
  59. def encode_default(self, default: Any) -> Any:
  60. config = self._config
  61. if isinstance(default, Callable):
  62. return '{{lambda ' + inspect.getsource(default).split('=lambda ')[-1].strip()[:-1] + '}}'
  63. return to_jsonable_python(
  64. default,
  65. timedelta_mode=config.ser_json_timedelta,
  66. bytes_mode=config.ser_json_bytes,
  67. serialize_unknown=True
  68. )
  69. # for computed_field properties render them like this instead:
  70. # inspect.getsource(field.wrapped_property.fget).split('def ', 1)[-1].split('\n', 1)[-1].strip().strip('return '),
  71. def better_toml_dump_str(val: Any) -> str:
  72. try:
  73. return toml.encoder._dump_str(val) # type: ignore
  74. except Exception:
  75. # if we hit any of toml's numerous encoding bugs,
  76. # fall back to using json representation of string
  77. return json.dumps(str(val))
  78. class CustomTOMLEncoder(toml.encoder.TomlEncoder):
  79. """
  80. Custom TomlEncoder to work around https://github.com/uiri/toml's many encoding bugs.
  81. More info: https://github.com/fabiocaccamo/python-benedict/issues/439
  82. >>> toml.dumps(value, encoder=CustomTOMLEncoder())
  83. """
  84. def __init__(self, **kwargs):
  85. super().__init__(**kwargs)
  86. self.dump_funcs[Path] = lambda x: json.dumps(str(x))
  87. self.dump_funcs[PosixPath] = lambda x: json.dumps(str(x))
  88. self.dump_funcs[str] = better_toml_dump_str
  89. self.dump_funcs[re.RegexFlag] = better_toml_dump_str