From 2bfd32130e392ceb1c1972271ad0fc44d186e6e5 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Mon, 8 Jul 2024 11:45:15 +0200 Subject: [PATCH] Simplify GAMSInfo class - Use tempfile.TemporaryDirectory. - Log warning if gams is missing. - Create maximum one instance. - Sort globals, classes, and functions in .model.gams --- ixmp/model/gams.py | 130 ++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/ixmp/model/gams.py b/ixmp/model/gams.py index 5a09959cc..94b13903d 100644 --- a/ixmp/model/gams.py +++ b/ixmp/model/gams.py @@ -5,7 +5,8 @@ import tempfile from copy import copy from pathlib import Path -from subprocess import CalledProcessError, run +from subprocess import CalledProcessError, check_output, run +from tempfile import TemporaryDirectory from typing import Any, MutableMapping, Optional from ixmp.backend import ItemType @@ -15,65 +16,8 @@ log = logging.getLogger(__name__) -class GAMSInfo: - """Information about the GAMS installation.""" - - #: Version. - version: Optional[str] - - #: System directory. - system_dir: Path - - def __init__(self, output: str) -> None: - if match := re.search(r"^GAMS ([\d\.]+)\s*Copyright", output, re.MULTILINE): - self.version = match.group(1) - else: # pragma: no cover - self.version = None - - if match := re.search(r"^\s*SysDir (.*)", output, re.MULTILINE): - self.system_dir = Path(match.group(1)) - else: # pragma: no cover - self.system_dir = Path.cwd() - - @property - def java_api_dir(self) -> Path: - """Java API files subdirectory of :attr:`.system_dir`.""" - return self.system_dir.joinpath("apifiles", "Java", "api") - - -def gams_info() -> GAMSInfo: - # NB check_output(['gams'], ...) does not work, because GAMS writes directly to the - # console instead of to stdout. check_output(['gams', '-LogOption=3'], ...) does - # not work, because GAMS does not accept options without an input file to - # execute. - import os - from subprocess import check_output - from tempfile import mkdtemp - - # Create a temporary GAMS program that does nothing - tmp_dir = Path(mkdtemp()) - gms = tmp_dir / "null.gms" - gms.write_text("$exit;") - - # Execute, capturing stdout - output = check_output( - ["gams", "null", "-LogOption=3"], - shell=os.name == "nt", - cwd=tmp_dir, - universal_newlines=True, - ) - - # Clean up - gms.unlink() - gms.with_suffix(".lst").unlink() - tmp_dir.rmdir() - - return GAMSInfo(output) - - -def gams_version() -> Optional[str]: - """Return the GAMS version as a string, for instance "24.7.4".""" - return gams_info().version +# Singleton instance of GAMSInfo. +_GAMS_INFO: Optional["GAMSInfo"] = None #: Return codes used by GAMS, from @@ -120,6 +64,55 @@ def gams_version() -> Optional[str]: RETURN_CODE = {key % 256: value for key, value in RETURN_CODE.items()} +class GAMSInfo: + """Information about the GAMS installation.""" + + #: GAMS version as a string, for instance "24.7.4". + version: Optional[str] + + #: System directory. + system_dir: Path + + def __init__(self) -> None: + # Retrieve some `output` containing GAMS installation info + with TemporaryDirectory() as temp_dir: + # NB the following do not work: + # - check_output(['gams'], ...) —because GAMS writes directly to the console + # instead of to stdout. + # - check_output(['gams', '-LogOption=3'], ...) —because GAMS does not + # accept options without an input file to execute. + # …so instead create a GAMS source file that does nothing: + Path(temp_dir, "null.gms").write_text("$exit;") + + try: + # Execute this no-op file and capture stdout + output = check_output( + ["gams", "null.gms", "-LogOption=3"], + shell=os.name == "nt", + cwd=temp_dir, + universal_newlines=True, + ) + except FileNotFoundError as e: + log.warning(f"{e}") + + # Parse GAMS version from the copyright line + if match := re.search(r"^GAMS ([\d\.]+)\s*Copyright", output, re.MULTILINE): + self.version = match.group(1) + else: # pragma: no cover + self.version = None + + # Parse GAMS system directory path + if match := re.search(r"^\s*SysDir (.*)", output, re.MULTILINE): + self.system_dir = Path(match.group(1)) + else: # pragma: no cover + self.system_dir = Path.cwd() + + @property + def java_api_dir(self) -> Path: + """Java API files subdirectory of :attr:`.system_dir`.""" + return self.system_dir.joinpath("apifiles", "Java", "api") + + class GAMSModel(Model): """Generic base class for :mod:`ixmp` models using `GAMS `_. @@ -405,3 +398,20 @@ def run(self, scenario): # Finished: remove the temporary directory, if any self.remove_temp_dir() + + +def gams_info() -> GAMSInfo: + """Return an instance of :class:`.GAMSInfo`.""" + # Singleton pattern; ensure there is only one instance of GAMSInfo + global _GAMS_INFO + + if _GAMS_INFO is None: + # Create the singleton + _GAMS_INFO = GAMSInfo() + + return _GAMS_INFO + + +def gams_version() -> Optional[str]: + """Return :attr:`.GAMSInfo.version`.""" + return gams_info().version