Source code for gemseo.wrappers.matlab.matlab_parser
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# -*-mode: python; py-indent-offset: 4; tab-width: 8; coding:utf-8 -*-
# Copyright (c) 2018 IRT-AESE.
# All rights reserved.
# Contributors:
# INITIAL AUTHORS - API and implementation and/or documentation
# :author: François Gallard : initial Scilab version
# :author: Arthur Piat : conversion Scilab to Matlab
# :author: Nicolas Roussouly: GEMSEO integration
"""Definition of the Matlab parser.
Overview
--------
This module contains the :class:`.MatlabParser`
which enables to parse Matlab files in order to automatically
detect inputs and outputs.
This class is basically used through the :class:`.MatlabDiscipline` class
in order to build a discipline based on the Matlab function.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
LOGGER = logging.getLogger(__name__)
[docs]class MatlabParser:
"""Parse Matlab file to identify inputs and outputs.
Examples:
>>> # Parse the matlab function "fucntion.m"
>>> parser = MatlabParser("function.m")
>>> print(parser.inputs)
>>> print(parser.outputs)
"""
RE_FILE_FMT = re.compile(r".*(\\.*)*\.m\b")
RE_ENCRYPTED_FCT = re.compile(r".*(\\.*)*\.p\b")
RE_OUTPUTS = re.compile(r"(\[(.*?)\])|(function( *?)(.*?)=)")
RE_FUNCTION = re.compile(r"=(.*?)\(")
RE_ARGS = re.compile(r"\((.*?)\)")
def __init__(self, full_path: str | None = None) -> None:
# noqa: D205,D212,D415
"""
Args:
full_path: The path to the matlab file.
If ``None``, the user shall parse the file explicitly.
"""
self.__inputs = None
self.__outputs = None
self.__fct_name = None
self.__fct_dir = None
if full_path is not None:
self.parse(full_path)
@property
def function_name(self) -> str:
"""Return the name of the function."""
return self.__fct_name
@property
def directory(self) -> Path:
"""Return the directory of the function."""
return self.__fct_dir
@property
def inputs(self):
"""Return the inputs."""
return self.__inputs
@property
def outputs(self):
"""Return the outputs."""
return self.__outputs
def __check_path(self, file_path: str | Path) -> None:
"""Check the format of the file.
Args:
file_path: The path to the matlab the file.
Raises:
IOError: If the matlab file does not exist.
ValueError:
* If the matlab file is encrypted;
* If the matlab function is neither a script nor a function.
"""
if not file_path.exists():
raise OSError(
"The function directory for Matlab "
"sources {} does not exists.".format(str(file_path))
)
file_path = str(file_path)
re_encrypted_file_groups = self.RE_ENCRYPTED_FCT.search(file_path)
if re_encrypted_file_groups is not None:
raise ValueError(
"The given file {} is encrypted "
"and cannot be parsed.".format(file_path)
)
re_file_groups = self.RE_FILE_FMT.search(file_path)
if re_file_groups is None:
raise ValueError(
"The given file {} should "
"either be a matlab function or script.".format(file_path)
)
def __parse_function_inputs_outputs(
self,
line: str,
function_name: str,
) -> None:
"""Parse inputs and outputs.
Args:
line: The line containing the declaration of the function.
function_name: The name of the function according to file.m.
Raises:
NameError:
* If function has no name;
* If function and file name are different.
ValueError:
* If function has no output;
* If function has no input.
"""
# TODO: refactor this function because some exceptions are not useful
re_func_groups = self.RE_FUNCTION.search(line)
if re_func_groups is None:
raise NameError("Matlab function has no name.")
fname = re_func_groups.group(0).strip()[1:-1].strip()
if fname != function_name:
raise NameError(
"Function name {} does not match with file name {}.".format(
function_name, fname
)
)
LOGGER.debug("Detected function: %s", fname)
self.__fct_name = fname
re_output_groups = self.RE_OUTPUTS.search(line)
if re_output_groups is None:
raise ValueError(f"Function {fname} has no output")
arg_str = re_output_groups.group(0).strip()
arg_str = arg_str.replace("[", "").replace("]", "")
arg_str = arg_str.replace("function", "").replace(" ", "")
arg_str = arg_str.replace("=", "")
outs = arg_str.split(",")
self.__outputs = [out_str.strip() for out_str in outs]
LOGGER.debug("Outputs are: %s", outs)
re_args_groups = self.RE_ARGS.search(line)
if re_args_groups is None:
raise ValueError(f"Function {fname} has no argument.")
arg_str = re_args_groups.group(0).strip()[1:-1].strip()
args = arg_str.split(",")
self.__inputs = [args_str.strip() for args_str in args]
LOGGER.debug("And arguments are: %s", args)
[docs] def parse(self, path: str) -> None:
"""Parse a .m file in order to get inputs and outputs.
Args:
path: The path to the matlab file.
Raises:
ValueError: Raised if the file is not a matlab function.
"""
path = Path(path).absolute()
self.__check_path(path)
self.__fct_dir = path.parent
fct_name = path.stem
is_parsed = False
with path.open("r", errors="ignore") as file_handle:
for line in file_handle.readlines():
if line.strip().startswith("function"):
self.__parse_function_inputs_outputs(line, fct_name)
is_parsed = True
break
if not is_parsed:
raise ValueError(f"The given file {path} is not a matlab function.")