33from pathlib import Path
44from datetime import datetime , date , time
55from typing import Any , get_origin , get_args , Union
6- import ast , types , copy
6+ import ast , types , copy , json , configparser
7+ try : import tomllib # type: ignore
8+ except ModuleNotFoundError : tomllib = None
9+ try : import toml # type: ignore
10+ except ModuleNotFoundError : toml = None
11+ try : import yaml # type: ignore
12+ except ModuleNotFoundError : yaml = None
713
814
915# =========================================================
@@ -53,29 +59,29 @@ def deserialize(self, value_str: str, typ: type):
5359registry .register (
5460 "datetime" ,
5561 datetime ,
56- lambda v : f'" { v .isoformat ()} " ' ,
57- lambda v : datetime .fromisoformat (v . strip ( '"' ) )
62+ lambda v : f'{ v .isoformat ()} ' ,
63+ lambda v : datetime .fromisoformat (v )
5864)
5965
6066registry .register (
6167 "date" ,
6268 date ,
63- lambda v : f'" { v .isoformat ()} " ' ,
64- lambda v : date .fromisoformat (v . strip ( '"' ) )
69+ lambda v : f'{ v .isoformat ()} ' ,
70+ lambda v : date .fromisoformat (v )
6571)
6672
6773registry .register (
6874 "time" ,
6975 time ,
70- lambda v : f'" { v .isoformat ()} " ' ,
71- lambda v : time .fromisoformat (v . strip ( '"' ) )
76+ lambda v : f'{ v .isoformat ()} ' ,
77+ lambda v : time .fromisoformat (v )
7278)
7379
7480registry .register (
7581 "Path" ,
7682 Path ,
77- lambda v : f'" { str (v )} " ' ,
78- lambda v : Path (v . strip ( '"' ) )
83+ lambda v : f'{ str (v )} ' ,
84+ lambda v : Path (v )
7985)
8086
8187
@@ -441,21 +447,127 @@ def load(cls, filename: str | Path, default: dict | Obj | bool | None = None) ->
441447
442448 return obj
443449
444- @classmethod
445- def from_dict (cls , data : dict ) -> Obj :
446- """
447- Create Obj from a nested dictionary.
448-
449- Format:
450- {
451- "section": {
452- "key": (type, value)
453- }
454- }
455- """
456- obj = cls ()
457- for section_name , items in data .items ():
458- section = obj .section (section_name )
459- for key , (typ , value ) in items .items ():
460- section .set (key , typ , value )
461- return obj
450+ @classmethod
451+ def from_dict (cls , data : dict , default_section = "not sectioned" ) -> Obj :
452+ """
453+ Create Obj from a dictionary.
454+
455+ Rules:
456+ - Top-level dict values become sections
457+ - Top-level non-dict values go into `default_section`
458+ - Values may be:
459+ (type, value) tuples
460+ or plain values (type inferred)
461+ """
462+ obj = cls ()
463+
464+ if not isinstance (data , dict ):
465+ raise TypeError ("Input data must be a dictionary" )
466+
467+ for key , value in data .items ():
468+
469+ # -------- Case 1: Proper section --------
470+ if isinstance (value , dict ):
471+ section = obj .section (key )
472+
473+ for subkey , entry in value .items ():
474+
475+ # (type, value)
476+ if (
477+ isinstance (entry , tuple )
478+ and len (entry ) == 2
479+ and isinstance (entry [0 ], type )
480+ ):
481+ typ , val = entry
482+ else :
483+ typ = type (entry )
484+ val = entry
485+
486+ section .set (subkey , typ , val )
487+
488+ # -------- Case 2: Top-level value --------
489+ else :
490+ section = obj .section (default_section )
491+
492+ if (
493+ isinstance (value , tuple )
494+ and len (value ) == 2
495+ and isinstance (value [0 ], type )
496+ ):
497+ typ , val = value
498+ else :
499+ typ = type (value )
500+ val = value
501+
502+ section .set (key , typ , val )
503+
504+ return obj
505+
506+ @classmethod
507+ def convert_file (cls , filename : str | Path , default_section = "not sectioned" , overwrite = False ) -> Obj :
508+ """
509+ Convert JSON, TOML, YAML, or INI into .rdm format.
510+
511+ If overwrite=True, deletes original file after conversion.
512+ """
513+
514+ path = Path (filename )
515+
516+ if not path .exists ():
517+ raise FileNotFoundError (path )
518+
519+ suffix = path .suffix .lower ()
520+
521+ # -------- Load Data --------
522+
523+ if suffix == ".json" :
524+ with open (path , "r" , encoding = "utf-8" ) as f :
525+ data = json .load (f )
526+
527+ elif suffix == ".toml" :
528+ if tomllib :
529+ with open (path , "rb" ) as f :
530+ data = tomllib .load (f )
531+ elif toml :
532+ with open (path , "r" , encoding = "utf-8" ) as f :
533+ data = toml .load (f )
534+ else :
535+ raise RuntimeError ("TOML requires Python 3.11+ or 'toml' package." )
536+
537+ elif suffix in (".yaml" , ".yml" ):
538+ if not yaml :
539+ raise RuntimeError ("YAML support requires PyYAML." )
540+ with open (path , "r" , encoding = "utf-8" ) as f :
541+ data = yaml .safe_load (f )
542+
543+ elif suffix == ".ini" :
544+ parser = configparser .ConfigParser ()
545+ parser .read (path )
546+
547+ data = {}
548+
549+ # Sections
550+ for section in parser .sections ():
551+ data [section ] = dict (parser [section ])
552+
553+ # Handle DEFAULT section
554+ if parser .defaults ():
555+ data ["default" ] = dict (parser .defaults ())
556+
557+ else :
558+ raise ValueError (f"Unsupported file type: { suffix } " )
559+
560+ if not isinstance (data , dict ):
561+ raise TypeError ("Top-level structure must be a dictionary" )
562+
563+ # -------- Convert --------
564+
565+ obj = cls .from_dict (data , default_section )
566+
567+ new_path = path .with_suffix (".rdm" )
568+ obj .save (new_path )
569+
570+ if overwrite :
571+ path .unlink ()
572+
573+ return obj
0 commit comments