@@ -185,7 +185,12 @@ def __missing__(self, key: str) -> t.NoReturn:
185185
186186
187187class ModelFile :
188- """Represents a single file in the model (i.e. a fragment)."""
188+ """Represents a single file in the model (i.e. a fragment).
189+
190+ This class loads the entire XML tree into memory. This makes it
191+ unsuitable for large trees with only small interesting segments,
192+ like ``.aird`` files. See :class:`VisualFile` for an alternative.
193+ """
189194
190195 __qtypecache : dict [etree .QName , dict [int , etree ._Element ]]
191196 __xtypecache : dict [str , dict [int , etree ._Element ]]
@@ -468,6 +473,99 @@ def unfollow_href(self, element_id: str) -> etree._Element:
468473 return self .__hrefsources [element_id ]
469474
470475
476+ class VisualFile :
477+ """Represents a visual (AIRD) fragment.
478+
479+ Visual fragments can rapidly grow very large, which makes it
480+ impractical to hold them in memory entirely all the time. This
481+ specialized class works similar to :class:`ModelFile`. However, it
482+ only keeps the central index in memory, and only loads and parses
483+ other data on request.
484+ """
485+
486+ fragment_type : t .Final = FragmentType .VISUAL
487+
488+ def __init__ (
489+ self ,
490+ filename : pathlib .PurePosixPath ,
491+ handler : filehandler .FileHandler ,
492+ ) -> None :
493+ self .filename = filename
494+ self .filehandler = handler
495+ if filename .suffix not in VISUAL_EXTS :
496+ raise ValueError (f"Bad filename for visual fragment: { filename } " )
497+
498+ with handler .open (filename ) as f :
499+ parser = etree .iterparse (f )
500+ for _ , element in parser :
501+ parent = element .getparent ()
502+ if parent is None or parent .getparent () is not None :
503+ continue
504+
505+ if element .tag == f"{{{ _n .NAMESPACES ['viewpoint' ]} }}DAnalysis" :
506+ self .__analysis = element
507+ break
508+ parent .remove (element )
509+ else :
510+ raise RuntimeError (
511+ "Broken XML: No 'viewpoint:DAnalysis' element found"
512+ )
513+ parent = self .__analysis .getparent ()
514+ assert parent is not None
515+ parent .remove (self .__analysis )
516+
517+ def __getitem__ (self , key : str ) -> etree ._Element :
518+ # TODO Return a diagram root element if it's found in this fragment
519+ raise KeyError (key )
520+
521+ def referenced_files (self ) -> cabc .Iterator [str ]:
522+ for i in self .__analysis :
523+ if i .tag == "semanticResources" and i .text :
524+ yield i .text
525+ elif i .tag == "referencedAnalysis" and (href := i .get ("href" )):
526+ yield href .split ("#" , maxsplit = 1 )[0 ]
527+
528+ def enumerate_uuids (self ) -> set [str ]:
529+ """Enumerate all UUIDs used in this fragment."""
530+ return set ()
531+
532+ def idcache_index (self , subtree : etree ._Element ) -> None :
533+ """Index the IDs of ``subtree``."""
534+ raise NotImplementedError ("Cannot modify visual fragments" )
535+
536+ def idcache_remove (self , source : str | etree ._Element ) -> None :
537+ """Remove the ID or all IDs below the source from the ID cache."""
538+ raise NotImplementedError ("Cannot modify visual fragments" )
539+
540+ def idcache_rebuild (self ) -> None :
541+ """Invalidate and rebuild this file's ID cache."""
542+ # Nothing to do
543+
544+ def idcache_reserve (self , new_id : str ) -> None :
545+ """Reserve the given ID for an element to be inserted later."""
546+ raise NotImplementedError ("Cannot modify visual fragments" )
547+
548+ def iterall_xt (
549+ self , xtypes : cabc .Container [str ]
550+ ) -> cabc .Iterator [etree ._Element ]:
551+ """Iterate over all elements in this tree by ``xsi:type``."""
552+ del xtypes
553+ yield from ()
554+
555+ def write_xml (
556+ self ,
557+ filename : pathlib .PurePosixPath ,
558+ encoding : str = "utf-8" ,
559+ ) -> None :
560+ """Do nothing."""
561+ del filename , encoding
562+
563+ # pylint: disable-next=useless-return
564+ def unfollow_href (self , element_id : str ) -> etree ._Element | None :
565+ del element_id
566+ return None
567+
568+
471569class MelodyLoader :
472570 """Facilitates extensive access to Polarsys / Capella projects."""
473571
@@ -551,7 +649,7 @@ def __init__(
551649 else :
552650 self .resources [resname ] = reshdl
553651
554- self .trees : dict [pathlib .PurePosixPath , ModelFile ] = {}
652+ self .trees : dict [pathlib .PurePosixPath , ModelFile | VisualFile ] = {}
555653 self .__load_referenced_files (
556654 pathlib .PurePosixPath ("\0 " , self .entrypoint )
557655 )
@@ -604,11 +702,17 @@ def __load_referenced_files(
604702
605703 handler = self .resources [resource_path .parts [0 ]]
606704 filename = pathlib .PurePosixPath (* resource_path .parts [1 :])
607- frag = ModelFile (
608- filename , handler , ignore_uuid_dups = self .__ignore_uuid_dups
609- )
705+ frag : VisualFile | ModelFile
706+ if filename .suffix in VISUAL_EXTS :
707+ frag = VisualFile (filename , handler )
708+ refs = list (frag .referenced_files ())
709+ else :
710+ frag = ModelFile (
711+ filename , handler , ignore_uuid_dups = self .__ignore_uuid_dups
712+ )
713+ refs = []
610714 self .trees [resource_path ] = frag
611- for ref in _find_refs ( frag . root ) :
715+ for ref in refs :
612716 ref_name = helpers .normalize_pure_path (
613717 _unquote_ref (ref ), base = resource_path .parent
614718 )
@@ -679,6 +783,7 @@ def update_namespaces(self) -> None:
679783 if fragment .fragment_type != FragmentType .SEMANTIC :
680784 continue
681785
786+ assert isinstance (fragment , ModelFile )
682787 LOGGER .debug ("Updating namespaces on fragment %s" , fname )
683788 fragment .update_namespaces (vp )
684789
@@ -955,7 +1060,7 @@ def iterall_xt(
9551060 """
9561061 xtset = self ._nonempty_hashset (xtypes )
9571062 if trees is None :
958- files : cabc .Iterable [ModelFile ] = self .trees .values ()
1063+ files : cabc .Iterable [ModelFile | VisualFile ] = self .trees .values ()
9591064 else :
9601065 files = (v for k , v in self .trees .items () if k in trees )
9611066 return itertools .chain .from_iterable (
@@ -1290,7 +1395,7 @@ def follow_links(
12901395
12911396 def _find_fragment (
12921397 self , element : etree ._Element
1293- ) -> tuple [pathlib .PurePosixPath , ModelFile ]:
1398+ ) -> tuple [pathlib .PurePosixPath , ModelFile | VisualFile ]:
12941399 root = collections .deque (
12951400 itertools .chain ([element ], element .iterancestors ()), 1
12961401 )[0 ]
0 commit comments