Source code for context

"""Context managers for common odmlib load-modify-save workflows.

Provides :func:`open_odm` and :func:`open_define` context managers that
automatically load an ODM or Define-XML document on entry and, when a
write target is configured, write it back on clean exit.

Opening a document with only an ``input_file`` is read-only: nothing is
written when the ``with`` block exits.  Provide ``output_file`` to write
the (possibly modified) document to a different path, or pass
``write_on_exit=True`` to explicitly opt in to updating ``input_file``
in place.

Example — read an ODM file (read-only, nothing written)::

    from odmlib.context import open_odm

    with open_odm("study.xml") as odm:
        print(odm.FileOID)

Example — modify an ODM file, write to a different path::

    with open_odm("study.xml", output_file="study_updated.xml") as odm:
        odm.FileOID = "F.002"

Example — update an ODM file in place (explicit opt-in)::

    with open_odm("study.xml", write_on_exit=True) as odm:
        mdv = odm.Study[0].MetaDataVersion[0]
        mdv.ItemGroupDef.append(new_igd)
    # study.xml is overwritten with the modified content

Example — read a Define-XML 2.1 file::

    from odmlib.context import open_define

    with open_define("define.xml") as define:
        # Define-XML: Study and MetaDataVersion are single objects, not lists
        mdv = define.Study.MetaDataVersion
        print(len(mdv.ItemDef))
"""
from __future__ import annotations
from typing import Any, Optional, Union
import odmlib.mode as _mode


[docs] class ODMContext: """Context manager for load-modify-save ODM workflows. The document is loaded when the ``with`` block is entered and written back to ``output_file`` when the block exits normally. If an exception propagates out of the ``with`` block the file is **not** written (the exception is re-raised unchanged). :param input_file: Path to the ODM file to load. :param output_file: Path to write on clean exit. When omitted, the document is opened read-only and nothing is written (unless ``write_on_exit=True`` explicitly opts in to updating ``input_file`` in place). :param model_package: odmlib model package name. Defaults to ``"odm_1_3_2"``. :param format: ``"xml"`` or ``"json"``. Auto-detected from the file extension when not specified. :param write_on_exit: When omitted, the document is written on clean exit only if ``output_file`` was provided. Pass ``True`` to force writing (to ``output_file``, or in place to ``input_file`` when no ``output_file`` is given). Pass ``False`` to suppress writing entirely. """ def __init__(self, input_file: str, output_file: Optional[str] = None, model_package: str = "odm_1_3_2", format: Optional[str] = None, permissive: Union[bool, _mode.ValidationMode] = False, write_on_exit: Optional[bool] = None) -> None: self.input_file = input_file self.output_file = output_file or input_file self.model_package = model_package self.format = format or self._detect_format(input_file) self._permissive = permissive # writing must be opted into: an explicit output_file, or an # explicit write_on_exit=True (in-place update of input_file) if write_on_exit is None: write_on_exit = output_file is not None self._write_on_exit = write_on_exit self._mode_token: Any = None self._odm: Any = None # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ @staticmethod def _detect_format(filename: str) -> str: if filename.lower().endswith(".json"): return "json" return "xml" def _load(self) -> Any: import odmlib.odm_loader as OL import odmlib.loader as LD if self.format == "json": loader = LD.ODMLoader(OL.JSONODMLoader(model_package=self.model_package)) else: loader = LD.ODMLoader(OL.XMLODMLoader(model_package=self.model_package)) loader.open_odm_document(self.input_file) return loader.root() def _save(self, odm: Any) -> None: if self.format == "json": odm.write_json(self.output_file) else: odm.write_xml(self.output_file) # ------------------------------------------------------------------ # Context protocol # ------------------------------------------------------------------ def __enter__(self) -> Any: if self._permissive: effective_mode = ( self._permissive if isinstance(self._permissive, _mode.ValidationMode) else _mode.ValidationMode.PERMISSIVE ) self._mode_token = _mode.set_mode(effective_mode) self._odm = self._load() return self._odm def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: # Always restore mode first, before any save logic if self._mode_token is not None: _mode._validation_mode.reset(self._mode_token) self._mode_token = None if exc_type is None and self._odm is not None and self._write_on_exit: self._save(self._odm) return False # never suppress exceptions
[docs] class DefineContext(ODMContext): """Context manager for load-modify-save Define-XML workflows. Identical to :class:`ODMContext` but uses Define-XML loaders by default. :param input_file: Path to the Define-XML file to load. :param output_file: Path to write on clean exit. When omitted, the document is opened read-only and nothing is written (unless ``write_on_exit=True`` explicitly opts in to updating ``input_file`` in place). :param model_package: odmlib model package name. Defaults to ``"define_2_1"``. :param format: ``"xml"`` or ``"json"``. :param write_on_exit: When omitted, the document is written on clean exit only if ``output_file`` was provided. ``True`` forces writing (in place when no ``output_file``); ``False`` suppresses writing entirely. """ def __init__(self, input_file: str, output_file: Optional[str] = None, model_package: str = "define_2_1", format: Optional[str] = None, permissive: Union[bool, _mode.ValidationMode] = False, write_on_exit: Optional[bool] = None) -> None: super().__init__(input_file, output_file, model_package, format, permissive=permissive, write_on_exit=write_on_exit) def _load(self) -> Any: import odmlib.define_loader as DL import odmlib.loader as LD if self.format == "json": loader = LD.ODMLoader(DL.JSONDefineLoader(model_package=self.model_package)) else: loader = LD.ODMLoader(DL.XMLDefineLoader(model_package=self.model_package)) loader.open_odm_document(self.input_file) return loader.root()
# --------------------------------------------------------------------------- # Convenience factory functions # ---------------------------------------------------------------------------
[docs] def open_odm(input_file: str, output_file: Optional[str] = None, model_package: str = "odm_1_3_2", format: Optional[str] = None, permissive: Union[bool, _mode.ValidationMode] = False, write_on_exit: Optional[bool] = None) -> ODMContext: """Open an ODM document as a context manager. :param input_file: Path to the ODM file. :param output_file: Write path on clean exit. When omitted, the document is opened read-only (nothing is written unless ``write_on_exit=True`` opts in to an in-place update). :param model_package: odmlib model package (default ``"odm_1_3_2"``). :param format: ``"xml"`` or ``"json"`` (auto-detected if omitted). :param permissive: If ``True``, load in fully permissive mode. Pass a :class:`~odmlib.mode.ValidationMode` flag combination for targeted relaxation. Defaults to ``False`` (strict). :param write_on_exit: When omitted, the document is written on clean exit only if ``output_file`` was provided. ``True`` forces writing (in place when no ``output_file``); ``False`` suppresses writing entirely. :returns: An :class:`ODMContext` instance. Example:: # Read-only inspection — input file is never modified with open_odm("study.xml") as odm: print(odm.FileOID) with open_odm("broken.xml", permissive=True) as odm: ... # permissive mode active during load # Modify and write to a new file with open_odm("study.xml", output_file="study_v2.xml") as odm: odm.FileOID = "F.002" # Update the input file in place (explicit opt-in) with open_odm("study.xml", write_on_exit=True) as odm: odm.FileOID = "F.002" """ return ODMContext(input_file, output_file, model_package, format, permissive=permissive, write_on_exit=write_on_exit)
[docs] def open_define(input_file: str, output_file: Optional[str] = None, model_package: str = "define_2_1", format: Optional[str] = None, permissive: Union[bool, _mode.ValidationMode] = False, write_on_exit: Optional[bool] = None) -> DefineContext: """Open a Define-XML document as a context manager. :param input_file: Path to the Define-XML file. :param output_file: Write path on clean exit. When omitted, the document is opened read-only (nothing is written unless ``write_on_exit=True`` opts in to an in-place update). :param model_package: odmlib model package (default ``"define_2_1"``). :param format: ``"xml"`` or ``"json"`` (auto-detected if omitted). :param permissive: If ``True``, load in fully permissive mode. Pass a :class:`~odmlib.mode.ValidationMode` flag combination for targeted relaxation. Defaults to ``False`` (strict). :param write_on_exit: When omitted, the document is written on clean exit only if ``output_file`` was provided. ``True`` forces writing (in place when no ``output_file``); ``False`` suppresses writing entirely. :returns: A :class:`DefineContext` instance. Example:: # Read-only inspection — input file is never modified with open_define("define.xml") as define: # Define-XML: Study/MetaDataVersion are single objects, not lists mdv = define.Study.MetaDataVersion with open_define("broken.xml", permissive=True) as define: ... # permissive mode active during load """ return DefineContext(input_file, output_file, model_package, format, permissive=permissive, write_on_exit=write_on_exit)