diff --git a/cxxheaderparser/preprocessor.py b/cxxheaderparser/preprocessor.py index e1c720c..30f0fbc 100644 --- a/cxxheaderparser/preprocessor.py +++ b/cxxheaderparser/preprocessor.py @@ -8,32 +8,37 @@ import os import typing from .options import PreprocessorFunction -from pcpp import Preprocessor, OutputDirective, Action - class PreprocessorError(Exception): pass -class _CustomPreprocessor(Preprocessor): - def __init__( - self, - encoding: typing.Optional[str], - passthru_includes: typing.Optional["re.Pattern"], - ): - Preprocessor.__init__(self) - self.errors: typing.List[str] = [] - self.assume_encoding = encoding - self.passthru_includes = passthru_includes +try: + import pcpp + from pcpp import Preprocessor, OutputDirective, Action - def on_error(self, file, line, msg): - self.errors.append(f"{file}:{line} error: {msg}") + class _CustomPreprocessor(Preprocessor): + def __init__( + self, + encoding: typing.Optional[str], + passthru_includes: typing.Optional["re.Pattern"], + ): + Preprocessor.__init__(self) + self.errors: typing.List[str] = [] + self.assume_encoding = encoding + self.passthru_includes = passthru_includes - def on_include_not_found(self, *ignored): - raise OutputDirective(Action.IgnoreAndPassThrough) + def on_error(self, file, line, msg): + self.errors.append(f"{file}:{line} error: {msg}") - def on_comment(self, *ignored): - return True + def on_include_not_found(self, *ignored): + raise OutputDirective(Action.IgnoreAndPassThrough) + + def on_comment(self, *ignored): + return True + +except ImportError: + pcpp = None def _filter_self(fname: str, fp: typing.TextIO) -> str: @@ -82,6 +87,9 @@ def make_pcpp_preprocessor( """ + if pcpp is None: + raise PreprocessorError("pcpp is not installed") + def _preprocess_file(filename: str, content: str) -> str: pp = _CustomPreprocessor(encoding, passthru_includes) if include_paths: diff --git a/tests/test_preprocessor.py b/tests/test_preprocessor.py index 2e0e591..f7d775b 100644 --- a/tests/test_preprocessor.py +++ b/tests/test_preprocessor.py @@ -1,9 +1,13 @@ import os import pathlib +import pytest import re +import shutil +import subprocess +import typing -from cxxheaderparser.options import ParserOptions -from cxxheaderparser.preprocessor import make_pcpp_preprocessor +from cxxheaderparser.options import ParserOptions, PreprocessorFunction +from cxxheaderparser import preprocessor from cxxheaderparser.simple import ( NamespaceScope, ParsedData, @@ -22,12 +26,26 @@ from cxxheaderparser.types import ( ) -def test_basic_preprocessor() -> None: +@pytest.fixture(params=["pcpp"]) +def make_pp(request) -> typing.Callable[..., PreprocessorFunction]: + param = request.param + if param == "pcpp": + if preprocessor.pcpp is None: + pytest.skip("pcpp not installed") + return preprocessor.make_pcpp_preprocessor + else: + assert False + + +def test_basic_preprocessor( + make_pp: typing.Callable[..., PreprocessorFunction] +) -> None: content = """ #define X 1 int x = X; """ - options = ParserOptions(preprocessor=make_pcpp_preprocessor()) + + options = ParserOptions(preprocessor=make_pp()) data = parse_string(content, cleandoc=True, options=options) assert data == ParsedData( @@ -45,7 +63,10 @@ def test_basic_preprocessor() -> None: ) -def test_preprocessor_omit_content(tmp_path: pathlib.Path) -> None: +def test_preprocessor_omit_content( + make_pp: typing.Callable[..., PreprocessorFunction], + tmp_path: pathlib.Path, +) -> None: """Ensure that content in other headers is omitted""" h_content = '#include "t2.h"' "\n" "int x = X;\n" h2_content = "#define X 2\n" "int omitted = 1;\n" @@ -56,7 +77,7 @@ def test_preprocessor_omit_content(tmp_path: pathlib.Path) -> None: with open(tmp_path / "t2.h", "w") as fp: fp.write(h2_content) - options = ParserOptions(preprocessor=make_pcpp_preprocessor()) + options = ParserOptions(preprocessor=make_pp()) data = parse_file(tmp_path / "t1.h", options=options) assert data == ParsedData( @@ -74,7 +95,10 @@ def test_preprocessor_omit_content(tmp_path: pathlib.Path) -> None: ) -def test_preprocessor_omit_content2(tmp_path: pathlib.Path) -> None: +def test_preprocessor_omit_content2( + make_pp: typing.Callable[..., PreprocessorFunction], + tmp_path: pathlib.Path, +) -> None: """ Ensure that content in other headers is omitted while handling pcpp relative path quirk @@ -91,9 +115,7 @@ def test_preprocessor_omit_content2(tmp_path: pathlib.Path) -> None: with open(tmp_path2 / "t2.h", "w") as fp: fp.write(h2_content) - options = ParserOptions( - preprocessor=make_pcpp_preprocessor(include_paths=[str(tmp_path)]) - ) + options = ParserOptions(preprocessor=make_pp(include_paths=[str(tmp_path)])) # Weirdness happens here os.chdir(tmp_path) @@ -114,7 +136,9 @@ def test_preprocessor_omit_content2(tmp_path: pathlib.Path) -> None: ) -def test_preprocessor_encoding(tmp_path: pathlib.Path) -> None: +def test_preprocessor_encoding( + make_pp: typing.Callable[..., PreprocessorFunction], tmp_path: pathlib.Path +) -> None: """Ensure we can handle alternate encodings""" h_content = b"// \xa9 2023 someone\n" b'#include "t2.h"' b"\n" b"int x = X;\n" @@ -126,7 +150,7 @@ def test_preprocessor_encoding(tmp_path: pathlib.Path) -> None: with open(tmp_path / "t2.h", "wb") as fp: fp.write(h2_content) - options = ParserOptions(preprocessor=make_pcpp_preprocessor(encoding="cp1252")) + options = ParserOptions(preprocessor=make_pp(encoding="cp1252")) data = parse_file(tmp_path / "t1.h", options=options, encoding="cp1252") assert data == ParsedData( @@ -144,6 +168,7 @@ def test_preprocessor_encoding(tmp_path: pathlib.Path) -> None: ) +@pytest.mark.skipif(preprocessor.pcpp is None, reason="pcpp not installed") def test_preprocessor_passthru_includes(tmp_path: pathlib.Path) -> None: """Ensure that all #include pass through""" h_content = '#include "t2.h"\n' @@ -155,7 +180,9 @@ def test_preprocessor_passthru_includes(tmp_path: pathlib.Path) -> None: fp.write("") options = ParserOptions( - preprocessor=make_pcpp_preprocessor(passthru_includes=re.compile(".+")) + preprocessor=preprocessor.make_pcpp_preprocessor( + passthru_includes=re.compile(".+") + ) ) data = parse_file(tmp_path / "t1.h", options=options)