Skip to content

Commit

Permalink
Add easy to use preprocessor support via pcpp
Browse files Browse the repository at this point in the history
- Fixes #60
  • Loading branch information
virtuald committed Aug 22, 2023
1 parent 1ba625a commit 5906821
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Non-goals:
headers that contain macros, you should preprocess your code using the
excellent pure python preprocessor [pcpp](https://github.com/ned14/pcpp)
or your favorite compiler
* See `cxxheaderparser.preprocessor` for how to use
* Probably won't be able to parse most IOCCC entries

There are two APIs available:
Expand Down
11 changes: 10 additions & 1 deletion cxxheaderparser/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@ def dumpmain() -> None:
parser.add_argument(
"--mode", choices=["json", "pprint", "repr", "brepr"], default="pprint"
)
parser.add_argument(
"--pcpp", default=False, action="store_true", help="Use pcpp preprocessor"
)

args = parser.parse_args()

options = ParserOptions(verbose=args.verbose)
preprocessor = None
if args.pcpp:
from .preprocessor import make_pcpp_preprocessor

preprocessor = make_pcpp_preprocessor()

options = ParserOptions(verbose=args.verbose, preprocessor=preprocessor)
data = parse_file(args.header, options=options)

if args.mode == "pprint":
Expand Down
8 changes: 8 additions & 0 deletions cxxheaderparser/options.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from dataclasses import dataclass
from typing import Callable, Optional

#: arguments are (filename, content)
PreprocessorFunction = Callable[[str, str], str]


@dataclass
Expand All @@ -12,3 +16,7 @@ class ParserOptions:

#: If true, converts a single void parameter to zero parameters
convert_void_to_zero_params: bool = True

#: A function that will preprocess the header before parsing. See
#: cxxheaderparser.preprocessor for available preprocessors
preprocessor: Optional[PreprocessorFunction] = None
6 changes: 4 additions & 2 deletions cxxheaderparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def __init__(
) -> None:
self.visitor = visitor
self.filename = filename
self.options = options if options else ParserOptions()

if options and options.preprocessor is not None:
content = options.preprocessor(filename, content)

self.lex: lexer.TokenStream = lexer.LexerTokenStream(filename, content)

Expand All @@ -90,8 +94,6 @@ def __init__(
self.state: State = NamespaceBlockState(None, global_ns)
self.anon_id = 0

self.options = options if options else ParserOptions()

self.verbose = True if self.options.verbose else False
if self.verbose:

Expand Down
99 changes: 99 additions & 0 deletions cxxheaderparser/preprocessor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Contains optional preprocessor support via pcpp
"""

import io
from os.path import relpath
import typing
from .options import PreprocessorFunction

from pcpp import Preprocessor, OutputDirective, Action


class PreprocessorError(Exception):
pass


class _CustomPreprocessor(Preprocessor):
def __init__(self):
Preprocessor.__init__(self)
self.errors = []

def on_error(self, file, line, msg):
self.errors.append(f"{file}:{line} error: {msg}")

def on_include_not_found(self, *ignored):
raise OutputDirective(Action.IgnoreAndPassThrough)

def on_comment(self, *ignored):
return True


def _filter_self(fname: str, fp: typing.TextIO) -> str:
# the output of pcpp includes the contents of all the included files, which
# isn't what a typical user of cxxheaderparser would want, so we strip out
# the line directives and any content that isn't in our original file

# Compute the filename to match based on how pcpp does it
try:
relfname = relpath(fname)
except Exception:
relfname = fname
relfname = relfname.replace("\\", "/")

relfname += '"\n'

new_output = io.StringIO()
keep = True

for line in fp:
if line.startswith("#line"):
keep = line.endswith(relfname)

if keep:
new_output.write(line)

new_output.seek(0)
return new_output.read()


def make_pcpp_preprocessor(
*,
defines: typing.List[str] = [],
include_paths: typing.List[str] = [],
retain_all_content: bool = False,
) -> PreprocessorFunction:
"""
Creates a preprocessor function that uses pcpp (which must be installed
separately) to preprocess the input text
"""

def _preprocess_file(filename: str, content: str) -> str:
pp = _CustomPreprocessor()
if include_paths:
for p in include_paths:
pp.add_path(p)

for define in defines:
pp.define(define)

if not retain_all_content:
pp.line_directive = "#line"

pp.parse(content, filename)

if pp.errors:
raise PreprocessorError("\n".join(pp.errors))
elif pp.return_code:
raise PreprocessorError("failed with exit code %d" % pp.return_code)

print("DID")
fp = io.StringIO()
pp.write(fp)
fp.seek(0)
if retain_all_content:
return fp.read()
else:
return _filter_self(filename, fp)

return _preprocess_file
9 changes: 8 additions & 1 deletion docs/custom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ Parser state

.. automodule:: cxxheaderparser.parserstate
:members:
:undoc-members:
:undoc-members:

Preprocessor
------------

.. automodule:: cxxheaderparser.preprocessor
:members:
:undoc-members:

0 comments on commit 5906821

Please sign in to comment.