66import io
77import os
88import sys
9- from typing import Iterator , Optional , TextIO , cast
9+ from typing import Iterator , Optional , TextIO , Type , Union , cast , get_args , get_origin
10+
11+ from pydantic import BaseModel
1012
1113
1214@contextlib .contextmanager
13- def open_cli_io_arg (path_or_dash : Optional [str ],
14- mode : str = 'r' ,
15- encoding : Optional [str ] = None ,
16- errors : Optional [str ] = None ,
17- default_stdin : bool = False ,
18- ) -> Iterator [TextIO ]:
15+ def open_cli_io_arg (
16+ path_or_dash : Optional [str ],
17+ mode : str = "r" ,
18+ encoding : Optional [str ] = None ,
19+ errors : Optional [str ] = None ,
20+ default_stdin : bool = False ,
21+ ) -> Iterator [TextIO ]:
1922 """
2023 Context manager for opening files with stdin/stdout support.
2124
@@ -28,6 +31,7 @@ def open_cli_io_arg(path_or_dash: Optional[str],
2831 manager.
2932
3033 Handles the common CLI pattern where:
34+
3135 - '-' means stdin (read mode) or stdout (write mode)
3236 - None means "argument not provided"; when default_stdin=True, it falls back
3337 to stdin/stdout
@@ -54,10 +58,10 @@ def open_cli_io_arg(path_or_dash: Optional[str],
5458 f.write(content)
5559 """
5660 # Valid text modes for file operations
57- _READ_FLAGS = frozenset ({'r' , '+' })
58- _WRITE_FLAGS = frozenset ({'w' , 'a' , 'x' , '+' })
61+ _READ_FLAGS = frozenset ({"r" , "+" })
62+ _WRITE_FLAGS = frozenset ({"w" , "a" , "x" , "+" })
5963
60- if 'b' in mode :
64+ if "b" in mode :
6165 raise ValueError (
6266 f"Binary mode '{ mode } ' is not supported. "
6367 "Use text modes ('r', 'w', 'a', 'x') instead."
@@ -66,9 +70,7 @@ def open_cli_io_arg(path_or_dash: Optional[str],
6670 needs_read = bool (set (mode ) & _READ_FLAGS )
6771 needs_write = bool (set (mode ) & _WRITE_FLAGS )
6872
69- should_use_stdio = path_or_dash == '-' or (
70- path_or_dash is None and default_stdin
71- )
73+ should_use_stdio = path_or_dash == "-" or (path_or_dash is None and default_stdin )
7274
7375 file_handle : Optional [TextIO ] = None
7476 should_close = False
@@ -83,11 +85,7 @@ def open_cli_io_arg(path_or_dash: Optional[str],
8385
8486 if needs_read :
8587 # Check for missing input when stdin is a terminal
86- if (
87- path_or_dash is None
88- and default_stdin
89- and sys .stdin .isatty ()
90- ):
88+ if path_or_dash is None and default_stdin and sys .stdin .isatty ():
9189 raise SystemExit ("error: No input provided." )
9290 file_handle = sys .stdin
9391
@@ -96,14 +94,15 @@ def open_cli_io_arg(path_or_dash: Optional[str],
9694
9795 else :
9896 raise ValueError (
99- f"Mode '{ mode } ' not supported with stdin/stdout "
100- "(use 'r' or 'w')"
97+ f"Mode '{ mode } ' not supported with stdin/stdout (use 'r' or 'w')"
10198 )
10299
103100 elif isinstance (path_or_dash , str ):
104101 if needs_read and not os .path .exists (path_or_dash ):
105102 raise FileNotFoundError (f"Input path does not exist: { path_or_dash } " )
106- file_handle = cast (TextIO , io .open (path_or_dash , mode , encoding = encoding , errors = errors ))
103+ file_handle = cast (
104+ TextIO , io .open (path_or_dash , mode , encoding = encoding , errors = errors )
105+ )
107106 should_close = True
108107
109108 elif path_or_dash is None :
@@ -117,13 +116,110 @@ def open_cli_io_arg(path_or_dash: Optional[str],
117116 "Expected str or None."
118117 )
119118
120- yield file_handle
119+ if file_handle is not None :
120+ yield file_handle
121121
122122 finally :
123123 if should_close and file_handle is not None :
124124 file_handle .close ()
125125
126126
127+ def generate_model_summary (model : Type [BaseModel ], indent : int = 0 ) -> str :
128+ lines = []
129+ prefix = " " * indent
130+
131+ # model_fields is a dictionary of FieldInfo objects
132+ for name , field in model .model_fields .items ():
133+ # Get the alias if available, otherwise use the field name
134+ field_name = field .alias if field .alias else name
135+
136+ # Get type annotation
137+ type_annotation = field .annotation
138+
139+ def format_type (t ) -> str :
140+ origin = get_origin (t )
141+ args = get_args (t )
142+
143+ # Handle Optional (Union[T, None])
144+ if origin is Union and type (None ) in args :
145+ non_none_args = [arg for arg in args if arg is not type (None )]
146+ if len (non_none_args ) == 1 :
147+ return f"{ format_type (non_none_args [0 ])} , optional"
148+
149+ # Handle List
150+ if origin is list :
151+ if args :
152+ return f"[{ format_type (args [0 ])} ]"
153+ return "[]"
154+
155+ # Handle Dict
156+ if origin is dict :
157+ return "obj"
158+
159+ # Handle Pydantic Models (Custom Classes)
160+ if isinstance (t , type ) and issubclass (t , BaseModel ):
161+ return "obj"
162+
163+ # Handle basic types and cleanup
164+ t_str = str (t )
165+ if t_str .startswith ("<class '" ):
166+ t_str = t_str [8 :- 2 ]
167+ if t_str .startswith ("typing." ):
168+ t_str = t_str [7 :]
169+
170+ # Remove module prefix if present
171+ if "." in t_str :
172+ t_str = t_str .split ("." )[- 1 ]
173+
174+ return t_str
175+
176+ display_type = format_type (type_annotation )
177+
178+ description = field .description if field .description else ""
179+
180+ line_content = f"{ prefix } - { field_name } ({ display_type } )"
181+ if description :
182+ line_content += f": { description } "
183+ lines .append (line_content )
184+
185+ # Check if it's a Pydantic model or a list/dict of Pydantic models
186+ origin = get_origin (type_annotation )
187+ args = get_args (type_annotation )
188+
189+ nested_model = None
190+ # Handle Optional wrappers for nesting check
191+ check_type = type_annotation
192+ if origin is Union and type (None ) in args :
193+ non_none_args = [arg for arg in args if arg is not type (None )]
194+ if len (non_none_args ) == 1 :
195+ check_type = non_none_args [0 ]
196+ origin = get_origin (check_type )
197+ args = get_args (check_type )
198+
199+ if isinstance (check_type , type ) and issubclass (check_type , BaseModel ):
200+ nested_model = check_type
201+ elif (
202+ origin is list
203+ and args
204+ and isinstance (args [0 ], type )
205+ and issubclass (args [0 ], BaseModel )
206+ ):
207+ nested_model = args [0 ]
208+ elif (
209+ origin is dict
210+ and args
211+ and len (args ) > 1
212+ and isinstance (args [1 ], type )
213+ and issubclass (args [1 ], BaseModel )
214+ ):
215+ nested_model = args [1 ]
216+
217+ if nested_model :
218+ lines .append (generate_model_summary (nested_model , indent + 4 ))
219+
220+ return "\n " .join (lines )
221+
222+
127223# keep imports of CLI modules for historical reasons
128224# keep them here in the bottom to avoid circular imports
129225from mmif .utils .cli import rewind
0 commit comments