from typing import Any, List, Callable import json import ast import inspect import toml import re import configparser from pathlib import Path, PosixPath from pydantic.json_schema import GenerateJsonSchema from pydantic_core import to_jsonable_python JSONValue = str | bool | int | None | List['JSONValue'] TOML_HEADER = "# Converted from INI to TOML format: https://toml.io/en/\n\n" def load_ini_value(val: str) -> JSONValue: """Convert lax INI values into strict TOML-compliant (JSON) values""" if val.lower() in ('true', 'yes', '1'): return True if val.lower() in ('false', 'no', '0'): return False if val.isdigit(): return int(val) try: return ast.literal_eval(val) except Exception: pass try: return json.loads(val) except Exception: pass return val def convert(ini_str: str) -> str: """Convert a string of INI config into its TOML equivalent (warning: strips comments)""" config = configparser.ConfigParser() config.optionxform = str # capitalize key names config.read_string(ini_str) # Initialize an empty dictionary to store the TOML representation toml_dict = {} # Iterate over each section in the INI configuration for section in config.sections(): toml_dict[section] = {} # Iterate over each key-value pair in the section for key, value in config.items(section): parsed_value = load_ini_value(value) # Convert the parsed value to its TOML-compatible JSON representation toml_dict[section.upper()][key.upper()] = json.dumps(parsed_value) # Build the TOML string toml_str = TOML_HEADER for section, items in toml_dict.items(): toml_str += f"[{section}]\n" for key, value in items.items(): toml_str += f"{key} = {value}\n" toml_str += "\n" return toml_str.strip() class JSONSchemaWithLambdas(GenerateJsonSchema): """ Encode lambda functions in default values properly. Usage: >>> json.dumps(value, encoder=JSONSchemaWithLambdas()) """ def encode_default(self, default: Any) -> Any: config = self._config if isinstance(default, Callable): return '{{lambda ' + inspect.getsource(default).split('=lambda ')[-1].strip()[:-1] + '}}' return to_jsonable_python( default, timedelta_mode=config.ser_json_timedelta, bytes_mode=config.ser_json_bytes, serialize_unknown=True ) # for computed_field properties render them like this instead: # inspect.getsource(field.wrapped_property.fget).split('def ', 1)[-1].split('\n', 1)[-1].strip().strip('return '), def better_toml_dump_str(val: Any) -> str: try: return toml.encoder._dump_str(val) # type: ignore except Exception: # if we hit any of toml's numerous encoding bugs, # fall back to using json representation of string return json.dumps(str(val)) class CustomTOMLEncoder(toml.encoder.TomlEncoder): """ Custom TomlEncoder to work around https://github.com/uiri/toml's many encoding bugs. More info: https://github.com/fabiocaccamo/python-benedict/issues/439 >>> toml.dumps(value, encoder=CustomTOMLEncoder()) """ def __init__(self, **kwargs): super().__init__(**kwargs) self.dump_funcs[Path] = lambda x: json.dumps(str(x)) self.dump_funcs[PosixPath] = lambda x: json.dumps(str(x)) self.dump_funcs[str] = better_toml_dump_str self.dump_funcs[re.RegexFlag] = better_toml_dump_str