Skip to content

Commit

Permalink
feat: add general class for interacting with external tools
Browse files Browse the repository at this point in the history
  • Loading branch information
mbdevpl committed Feb 15, 2025
1 parent d50105e commit 7b19992
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 11 deletions.
34 changes: 27 additions & 7 deletions test/test_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,29 @@
import timing
import typed_astunparse

from transpyle.general.code_reader import CodeReader
from transpyle.general.binder import Binder
from transpyle.cpp.parser import CppParser
from transpyle.cpp.ast_generalizer import CppAstGeneralizer
from transpyle.cpp.unparser import Cpp14Unparser
from transpyle.cpp.compiler import CppSwigCompiler
from transpyle.cpp.compiler_interface import GppInterface
from transpyle.general import AstGeneralizer, Binder, CodeReader, Parser
from transpyle.general.exc import ExternalToolError

try:
from transpyle.cpp.parser import CppParser
except (ImportError, ExternalToolError):
pass
try:
from transpyle.cpp.ast_generalizer import CppAstGeneralizer
except ImportError:
pass
try:
from transpyle.cpp.unparser import Cpp14Unparser
except ImportError:
pass
try:
from transpyle.cpp.compiler import CppSwigCompiler
except ImportError:
pass
try:
from transpyle.cpp.compiler_interface import GppInterface
except ImportError:
pass

from .common import \
PERFORMANCE_RESULTS_ROOT, EXAMPLES_ROOT, EXAMPLES_ROOTS, \
Expand All @@ -29,6 +45,7 @@
_TIME = timing.get_timing_group(__name__)


@unittest.skipIf(Parser.find('C++') is None, 'skipping due to missing C++ language support')
class ParserTests(unittest.TestCase):

@execute_on_language_examples('cpp14')
Expand All @@ -53,6 +70,7 @@ def test_try_parse_invalid(self):
_LOG.debug('%s', err.exception)


@unittest.skipIf(AstGeneralizer.find('C++') is None, 'skipping due to missing C++ language support')
class AstGeneralizerTests(unittest.TestCase):

@execute_on_language_examples('cpp14')
Expand All @@ -71,6 +89,7 @@ def test_generalize_examples(self, input_path):
_LOG.debug('%s', typed_astunparse.unparse(syntax))


@unittest.skipIf(Unparser.find('C++') is None, 'skipping due to missing C++ language support')
class UnparserTests(unittest.TestCase):

@execute_on_language_examples('cpp14')
Expand All @@ -97,6 +116,7 @@ def test_unparse_examples(self, input_path):
_LOG.info('unparsed "%s" in %fs', input_path, timer.elapsed)


@unittest.skipIf(Compiler.find('C++') is None, 'skipping due to missing C++ language support')
class CompilerTests(unittest.TestCase):

def test_cpp_paths_exist(self):
Expand Down
3 changes: 2 additions & 1 deletion transpyle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

from .configuration import configure
from .general.exc import ExternalToolError

configure()

Expand All @@ -17,7 +18,7 @@

try:
from .cpp import *
except ImportError:
except (ImportError, ExternalToolError):
_LOG.warning("C++ unavailable")

# try:
Expand Down
26 changes: 23 additions & 3 deletions transpyle/cpp/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,33 @@
import xml.etree.ElementTree as ET

import argunparse
import version_query

from ..general import Parser
from ..general import ExternalTool, Parser
from ..general.exc import ExternalToolVersionError
from ..general.tools import run_tool

_LOG = logging.getLogger(__name__)

CASTXML_PATH = pathlib.Path('castxml')

class CastXml(ExternalTool):
"""Define how to execute CastXML tool.
https://github.com/CastXML/CastXML
"""

path = pathlib.Path('castxml')
_version_arg = '--version'

@classmethod
def _version_output_filter(cls, output: str) -> str:
for output_line in output.splitlines():
if output_line.startswith('castxml version '):
return output_line.replace('castxml version ', '')
raise ExternalToolVersionError(f'could not extract version from output: {output}')


CastXml.assert_version_at_least(version_query.Version(0, 4))


def run_castxml(input_path: pathlib.Path, output_path: pathlib.Path, gcc: bool = False):
Expand All @@ -29,7 +49,7 @@ def run_castxml(input_path: pathlib.Path, output_path: pathlib.Path, gcc: bool =
elif platform.system() == 'Darwin':
kwargs['castxml-cc-gnu'] = 'clang++'
kwargs['o'] = str(output_path)
return run_tool(CASTXML_PATH, args, kwargs,
return run_tool(CastXml.path, args, kwargs,
argunparser=argunparse.ArgumentUnparser(opt_value=' '))


Expand Down
2 changes: 2 additions & 0 deletions transpyle/general/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Language-agnostic modules and base classes for language-specific modules in transpyle."""

from .tools import temporarily_change_dir, redirect_stdout_and_stderr, run_tool, call_tool
from .external_tool import ExternalTool

from .language import Language

Expand All @@ -19,6 +20,7 @@
from .transpiler import Transpiler, AutoTranspiler

__all__ = ['temporarily_change_dir', 'redirect_stdout_and_stderr', 'run_tool', 'call_tool',
'ExternalTool',
'Language',
'CodeReader', 'Parser', 'AstGeneralizer', 'IdentityAstGeneralizer', 'XmlAstGeneralizer',
'GeneralizingAutoParser',
Expand Down
11 changes: 11 additions & 0 deletions transpyle/general/exc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
"""Non-standard exception types used in transpyle."""

class ExternalToolError(Exception):
"""Indicates an issue with an external tool."""


class ExternalToolMissingError(ExternalToolError, FileNotFoundError):
"""Raised when an external tool is not found."""


class ExternalToolVersionError(ExternalToolError, AssertionError):
"""Raised when an external tool doesn't satisfy the version requirements."""


class ContinueIteration(StopIteration):

Expand Down
55 changes: 55 additions & 0 deletions transpyle/general/external_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Evaluate external tool and ."""

import pathlib
import shutil

import version_query

from .exc import ExternalToolMissingError, ExternalToolVersionError
from .tools import run_tool


class ExternalTool:
"""Generic tool definition.
When inheriting, the following has to be defined:
* path: path to the tool
* _version_arg: argument to get the version of the tool when executing it
* _version_output_filter: function to filter the output of the version command
"""

path: pathlib.Path
_version_arg: str
_version: version_query.Version | None = None

@classmethod
def exists(cls) -> bool:
path = shutil.which(cls.path.as_posix())
return path is not None

@classmethod
def assert_exists(cls) -> None:
"""Assert that the external tool exists."""
if not cls.exists():
raise ExternalToolMissingError(f'{cls.path} not found')

@classmethod
def version(cls) -> version_query.Version:
"""Determine the version of the external tool."""
if cls._version is None:
result = run_tool(cls.path, [cls._version_arg])
version_str = cls._version_output_filter(result.stdout)
cls._version = version_query.Version.from_str(version_str)
return cls._version

@classmethod
def _version_output_filter(cls, output: str) -> str:
raise NotImplementedError('this method needs to be implemented')

@classmethod
def assert_version_at_least(cls, version: version_query.Version) -> None:
"""Assert that the external tool is at least the given version."""
cls.assert_exists()
if cls.version() < version:
raise ExternalToolVersionError(
f'{cls.path} version {cls.version} does not satisfy the requirement >= {version}')

0 comments on commit 7b19992

Please sign in to comment.