@@ -44,8 +44,10 @@ class EvaluableExpression(str):
4444 str, so it can be used as a string.
4545 """
4646
47- def __init__ (self , expr ):
48- self .expr = expr
47+ @property
48+ def expr (self ):
49+ """The expression string. Always reflects the underlying str value."""
50+ return str (self )
4951
5052
5153# Union of types that are allowed as value expressions for parameters.
@@ -62,7 +64,7 @@ def print_(text: str, print_it: bool = False):
6264
6365 prefix = "modelspec >>> "
6466 if not isinstance (text , str ):
65- text = ( "%s" % text ). decode ( "ascii" )
67+ text = "%s" % text
6668 print ("{}{}" .format (prefix , text .replace ("\n " , "\n " + prefix )))
6769
6870
@@ -111,9 +113,9 @@ def to_json(self) -> str:
111113 """
112114 return json .dumps (self .to_dict (), indent = 4 )
113115
114- def to_bson (self ) -> str :
116+ def to_bson (self ) -> bytes :
115117 """
116- Convert the Base object to a BSON string representation.
118+ Convert the Base object to a BSON (bytes) representation.
117119 """
118120 return bson .encode (self .to_dict ())
119121
@@ -238,7 +240,9 @@ def to_json_file(
238240
239241 return filename
240242
241- def to_bson_file (self , filename : str , include_metadata : bool = True ) -> str :
243+ def to_bson_file (
244+ self , filename : Optional [str ] = None , include_metadata : bool = True
245+ ) -> str :
242246 """Convert modelspec format to bson format
243247
244248 Args:
@@ -321,7 +325,8 @@ def to_xml_file(
321325 def from_file (cls , filename : str ) -> "Base" :
322326 """
323327 Create a :class:`.Base` from its representation stored in a file. Auto-detect the correct deserialization code
324- based on file extension. Currently supported formats are; JSON(.json) and YAML (.yaml or .yml)
328+ based on file extension. Currently supported formats are: JSON (.json), YAML (.yaml or .yml),
329+ BSON (.bson) and XML (.xml).
325330
326331 Args:
327332 filename: The name of the file to load.
@@ -340,7 +345,7 @@ def from_file(cls, filename: str) -> "Base":
340345 else :
341346 raise ValueError (
342347 f"Cannot auto-detect modelspec serialization format from filename ({ filename } ). The filename "
343- f"must have one of the following extensions: .json, .yml, or .yaml ."
348+ f"must have one of the following extensions: .json, .yaml, . yml, .bson, or .xml ."
344349 )
345350
346351 @classmethod
@@ -945,10 +950,19 @@ def insert_links(text, format=MARKDOWN_FORMAT):
945950 )
946951 )
947952
953+ # De-duplicate while preserving order, so a type referenced by more than
954+ # one field/child doesn't get its documentation section emitted twice.
955+ seen = set ()
956+ unique_referenced = []
948957 for r in referenced :
958+ if r not in seen :
959+ seen .add (r )
960+ unique_referenced .append (r )
961+
962+ for r in unique_referenced :
949963 if format in (MARKDOWN_FORMAT , RST_FORMAT ):
950964 doc_string += r ._cls_generate_documentation (format = format )
951- if format in ( DICT_FORMAT ) :
965+ if format == DICT_FORMAT :
952966 doc_dict .update (r ._cls_generate_documentation (format = format ))
953967
954968 if format in (MARKDOWN_FORMAT , RST_FORMAT ):
@@ -1090,7 +1104,14 @@ def _is_list_base(cl):
10901104 Check if a class is a list of Base objects. These will be serialized as dicts if the underlying class has an id
10911105 attribute.
10921106 """
1093- return get_origin (cl ) is list and issubclass (get_args (cl )[0 ], Base )
1107+ if get_origin (cl ) is not list :
1108+ return False
1109+
1110+ args = get_args (cl )
1111+ # Guard against a bare ``list`` annotation (no args) and against element
1112+ # types that aren't classes (e.g. List[Union[A, B]]), which would make
1113+ # issubclass() raise TypeError.
1114+ return len (args ) > 0 and isinstance (args [0 ], type ) and issubclass (args [0 ], Base )
10941115
10951116
10961117converter .register_unstructure_hook_factory (_is_list_base , _unstructure_list_base )
0 commit comments