Skip to content

Commit 674334e

Browse files
committed
feat: add general class for interacting with external tools
1 parent d50105e commit 674334e

File tree

6 files changed

+120
-11
lines changed

6 files changed

+120
-11
lines changed

test/test_cpp.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,29 @@
1111
import timing
1212
import typed_astunparse
1313

14-
from transpyle.general.code_reader import CodeReader
15-
from transpyle.general.binder import Binder
16-
from transpyle.cpp.parser import CppParser
17-
from transpyle.cpp.ast_generalizer import CppAstGeneralizer
18-
from transpyle.cpp.unparser import Cpp14Unparser
19-
from transpyle.cpp.compiler import CppSwigCompiler
20-
from transpyle.cpp.compiler_interface import GppInterface
14+
from transpyle.general import AstGeneralizer, Binder, CodeReader, Compiler, Parser, Unparser
15+
from transpyle.general.exc import ExternalToolError
16+
17+
try:
18+
from transpyle.cpp.parser import CppParser
19+
except (ImportError, ExternalToolError):
20+
pass
21+
try:
22+
from transpyle.cpp.ast_generalizer import CppAstGeneralizer
23+
except (ImportError, ExternalToolError):
24+
pass
25+
try:
26+
from transpyle.cpp.unparser import Cpp14Unparser
27+
except (ImportError, ExternalToolError):
28+
pass
29+
try:
30+
from transpyle.cpp.compiler import CppSwigCompiler
31+
except (ImportError, ExternalToolError):
32+
pass
33+
try:
34+
from transpyle.cpp.compiler_interface import GppInterface
35+
except (ImportError, ExternalToolError):
36+
pass
2137

2238
from .common import \
2339
PERFORMANCE_RESULTS_ROOT, EXAMPLES_ROOT, EXAMPLES_ROOTS, \
@@ -29,6 +45,7 @@
2945
_TIME = timing.get_timing_group(__name__)
3046

3147

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

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

5572

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

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

7391

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

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

99118

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

102122
def test_cpp_paths_exist(self):

transpyle/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44

55
from .configuration import configure
6+
from .general.exc import ExternalToolError
67

78
configure()
89

@@ -17,7 +18,7 @@
1718

1819
try:
1920
from .cpp import *
20-
except ImportError:
21+
except (ImportError, ExternalToolError):
2122
_LOG.warning("C++ unavailable")
2223

2324
# try:

transpyle/cpp/parser.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,33 @@
77
import xml.etree.ElementTree as ET
88

99
import argunparse
10+
import version_query
1011

11-
from ..general import Parser
12+
from ..general import ExternalTool, Parser
13+
from ..general.exc import ExternalToolVersionError
1214
from ..general.tools import run_tool
1315

1416
_LOG = logging.getLogger(__name__)
1517

16-
CASTXML_PATH = pathlib.Path('castxml')
18+
19+
class CastXml(ExternalTool):
20+
"""Define how to execute CastXML tool.
21+
22+
https://github.com/CastXML/CastXML
23+
"""
24+
25+
path = pathlib.Path('castxml')
26+
_version_arg = '--version'
27+
28+
@classmethod
29+
def _version_output_filter(cls, output: str) -> str:
30+
for output_line in output.splitlines():
31+
if output_line.startswith('castxml version '):
32+
return output_line.replace('castxml version ', '')
33+
raise ExternalToolVersionError(f'could not extract version from output: {output}')
34+
35+
36+
CastXml.assert_version_at_least(version_query.Version(0, 4))
1737

1838

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

3555

transpyle/general/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Language-agnostic modules and base classes for language-specific modules in transpyle."""
22

33
from .tools import temporarily_change_dir, redirect_stdout_and_stderr, run_tool, call_tool
4+
from .external_tool import ExternalTool
45

56
from .language import Language
67

@@ -19,6 +20,7 @@
1920
from .transpiler import Transpiler, AutoTranspiler
2021

2122
__all__ = ['temporarily_change_dir', 'redirect_stdout_and_stderr', 'run_tool', 'call_tool',
23+
'ExternalTool',
2224
'Language',
2325
'CodeReader', 'Parser', 'AstGeneralizer', 'IdentityAstGeneralizer', 'XmlAstGeneralizer',
2426
'GeneralizingAutoParser',

transpyle/general/exc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
"""Non-standard exception types used in transpyle."""
22

3+
class ExternalToolError(Exception):
4+
"""Indicates an issue with an external tool."""
5+
6+
7+
class ExternalToolMissingError(ExternalToolError, FileNotFoundError):
8+
"""Raised when an external tool is not found."""
9+
10+
11+
class ExternalToolVersionError(ExternalToolError, AssertionError):
12+
"""Raised when an external tool doesn't satisfy the version requirements."""
13+
314

415
class ContinueIteration(StopIteration):
516

transpyle/general/external_tool.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Evaluate external tool and ."""
2+
3+
import pathlib
4+
import shutil
5+
6+
import version_query
7+
8+
from .exc import ExternalToolMissingError, ExternalToolVersionError
9+
from .tools import run_tool
10+
11+
12+
class ExternalTool:
13+
"""Generic tool definition.
14+
15+
When inheriting, the following has to be defined:
16+
* path: path to the tool
17+
* _version_arg: argument to get the version of the tool when executing it
18+
* _version_output_filter: function to filter the output of the version command
19+
"""
20+
21+
path: pathlib.Path
22+
_version_arg: str
23+
_version: version_query.Version | None = None
24+
25+
@classmethod
26+
def exists(cls) -> bool:
27+
path = shutil.which(cls.path.as_posix())
28+
return path is not None
29+
30+
@classmethod
31+
def assert_exists(cls) -> None:
32+
"""Assert that the external tool exists."""
33+
if not cls.exists():
34+
raise ExternalToolMissingError(f'{cls.path} not found')
35+
36+
@classmethod
37+
def version(cls) -> version_query.Version:
38+
"""Determine the version of the external tool."""
39+
if cls._version is None:
40+
result = run_tool(cls.path, [cls._version_arg])
41+
version_str = cls._version_output_filter(result.stdout)
42+
cls._version = version_query.Version.from_str(version_str)
43+
return cls._version
44+
45+
@classmethod
46+
def _version_output_filter(cls, output: str) -> str:
47+
raise NotImplementedError('this method needs to be implemented')
48+
49+
@classmethod
50+
def assert_version_at_least(cls, version: version_query.Version) -> None:
51+
"""Assert that the external tool is at least the given version."""
52+
cls.assert_exists()
53+
if cls.version() < version:
54+
raise ExternalToolVersionError(
55+
f'{cls.path} version {cls.version} does not satisfy the requirement >= {version}')

0 commit comments

Comments
 (0)