Repository: robotpy/cxxheaderparser (language: python)

Issue:
Add helper to render types as strings
Undecided on how this looks at the moment, leaning towards putting it on the type dataclass itself.

Don't really want to overload `__str__`, but I'm not sure I have a good reason for that. Probably because I want an optional 'name' parameter which would insert the name in the correct place (inside for a function pointer, to the right for anything else).


Discussion / hints:

Fix patch:
diff --git a/cxxheaderparser/tokfmt.py b/cxxheaderparser/tokfmt.py
index f2bb67c..c607f93 100644
--- a/cxxheaderparser/tokfmt.py
+++ b/cxxheaderparser/tokfmt.py
@@ -1,7 +1,7 @@
+from dataclasses import dataclass, field
 import typing
 
 from .lexer import LexToken, PlyLexer, LexerTokenStream
-from .types import Token
 
 # key: token type, value: (left spacing, right spacing)
 _want_spacing = {
@@ -35,6 +35,27 @@
 _want_spacing.update(dict.fromkeys(PlyLexer.keywords, (2, 2)))
 
 
+@dataclass
+class Token:
+    """
+    In an ideal world, this Token class would not be exposed via the user
+    visible API. Unfortunately, getting to that point would take a significant
+    amount of effort.
+
+    It is not expected that these will change, but they might.
+
+    At the moment, the only supported use of Token objects are in conjunction
+    with the ``tokfmt`` function. As this library matures, we'll try to clarify
+    the expectations around these. File an issue on github if you have ideas!
+    """
+
+    #: Raw value of the token
+    value: str
+
+    #: Lex type of the token
+    type: str = field(repr=False, compare=False, default="")
+
+
 def tokfmt(toks: typing.List[Token]) -> str:
     """
     Helper function that takes a list of tokens and converts them to a string
diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py
index 1b8765e..c594001 100644
--- a/cxxheaderparser/types.py
+++ b/cxxheaderparser/types.py
@@ -1,26 +1,7 @@
 import typing
 from dataclasses import dataclass, field
 
-
-@dataclass
-class Token:
-    """
-    In an ideal world, this Token class would not be exposed via the user
-    visible API. Unfortunately, getting to that point would take a significant
-    amount of effort.
-
-    It is not expected that these will change, but they might.
-
-    At the moment, the only supported use of Token objects are in conjunction
-    with the ``tokfmt`` function. As this library matures, we'll try to clarify
-    the expectations around these. File an issue on github if you have ideas!
-    """
-
-    #: Raw value of the token
-    value: str
-
-    #: Lex type of the token
-    type: str = field(repr=False, compare=False, default="")
+from .tokfmt import tokfmt, Token
 
 
 @dataclass
@@ -37,6 +18,9 @@ class Value:
     #: Tokens corresponding to the value
     tokens: typing.List[Token]
 
+    def format(self) -> str:
+        return tokfmt(self.tokens)
+
 
 @dataclass
 class NamespaceAlias:
@@ -94,6 +78,9 @@ class DecltypeSpecifier:
     #: Unparsed tokens within the decltype
     tokens: typing.List[Token]
 
+    def format(self) -> str:
+        return f"decltype({tokfmt(self.tokens)})"
+
 
 @dataclass
 class FundamentalSpecifier:
@@ -107,6 +94,9 @@ class FundamentalSpecifier:
 
     name: str
 
+    def format(self) -> str:
+        return self.name
+
 
 @dataclass
 class NameSpecifier:
@@ -124,6 +114,12 @@ class NameSpecifier:
 
     specialization: typing.Optional["TemplateSpecialization"] = None
 
+    def format(self) -> str:
+        if self.specialization:
+            return f"{self.name}{self.specialization.format()}"
+        else:
+            return self.name
+
 
 @dataclass
 class AutoSpecifier:
@@ -133,6 +129,9 @@ class AutoSpecifier:
 
     name: str = "auto"
 
+    def format(self) -> str:
+        return self.name
+
 
 @dataclass
 class AnonymousName:
@@ -145,6 +144,10 @@ class AnonymousName:
     #: Unique id associated with this name (only unique per parser instance!)
     id: int
 
+    def format(self) -> str:
+        # TODO: not sure what makes sense here, subject to change
+        return f"<<id={self.id}>>"
+
 
 PQNameSegment = typing.Union[
     AnonymousName, FundamentalSpecifier, NameSpecifier, DecltypeSpecifier, AutoSpecifier
@@ -170,6 +173,13 @@ class PQName:
     #: Set to true if the type was preceded with 'typename'
     has_typename: bool = False
 
+    def format(self) -> str:
+        tn = "typename " if self.has_typename else ""
+        if self.classkey:
+            return f"{tn}{self.classkey} {'::'.join(seg.format() for seg in self.segments)}"
+        else:
+            return tn + "::".join(seg.format() for seg in self.segments)
+
 
 @dataclass
 class TemplateArgument:
@@ -189,6 +199,12 @@ class TemplateArgument:
 
     param_pack: bool = False
 
+    def format(self) -> str:
+        if self.param_pack:
+            return f"{self.arg.format()}..."
+        else:
+            return self.arg.format()
+
 
 @dataclass
 class TemplateSpecialization:
@@ -204,6 +220,9 @@ class TemplateSpecialization:
 
     args: typing.List[TemplateArgument]
 
+    def format(self) -> str:
+        return f"<{', '.join(arg.format() for arg in self.args)}>"
+
 
 @dataclass
 class FunctionType:
@@ -238,6 +257,23 @@ class FunctionType:
     #:            calling convention
     msvc_convention: typing.Optional[str] = None
 
+    def format(self) -> str:
+        vararg = "..." if self.vararg else ""
+        params = ", ".join(p.format() for p in self.parameters)
+        if self.has_trailing_return:
+            return f"auto ({params}{vararg}) -> {self.return_type.format()}"
+        else:
+            return f"{self.return_type.format()} ({params}{vararg})"
+
+    def format_decl(self, name: str) -> str:
+        """Format as a named declaration"""
+        vararg = "..." if self.vararg else ""
+        params = ", ".join(p.format() for p in self.parameters)
+        if self.has_trailing_return:
+            return f"auto {name}({params}{vararg}) -> {self.return_type.format()}"
+        else:
+            return f"{self.return_type.format()} {name}({params}{vararg})"
+
 
 @dataclass
 class Type:
@@ -250,6 +286,17 @@ class Type:
     const: bool = False
     volatile: bool = False
 
+    def format(self) -> str:
+        c = "const " if self.const else ""
+        v = "volatile " if self.volatile else ""
+        return f"{c}{v}{self.typename.format()}"
+
+    def format_decl(self, name: str):
+        """Format as a named declaration"""
+        c = "const " if self.const else ""
+        v = "volatile " if self.volatile else ""
+        return f"{c}{v}{self.typename.format()} {name}"
+
 
 @dataclass
 class Array:
@@ -269,6 +316,14 @@ class Array:
     #:          ~~
     size: typing.Optional[Value]
 
+    def format(self) -> str:
+        s = self.size.format() if self.size else ""
+        return f"{self.array_of.format()}[{s}]"
+
+    def format_decl(self, name: str) -> str:
+        s = self.size.format() if self.size else ""
+        return f"{self.array_of.format()} {name}[{s}]"
+
 
 @dataclass
 class Pointer:
@@ -282,6 +337,25 @@ class Pointer:
     const: bool = False
     volatile: bool = False
 
+    def format(self) -> str:
+        c = " const" if self.const else ""
+        v = " volatile" if self.volatile else ""
+        ptr_to = self.ptr_to
+        if isinstance(ptr_to, (Array, FunctionType)):
+            return ptr_to.format_decl(f"(*{c}{v})")
+        else:
+            return f"{ptr_to.format()}*{c}{v}"
+
+    def format_decl(self, name: str):
+        """Format as a named declaration"""
+        c = " const" if self.const else ""
+        v = " volatile" if self.volatile else ""
+        ptr_to = self.ptr_to
+        if isinstance(ptr_to, (Array, FunctionType)):
+            return ptr_to.format_decl(f"(*{c}{v} {name})")
+        else:
+            return f"{ptr_to.format()}*{c}{v} {name}"
+
 
 @dataclass
 class Reference:
@@ -291,6 +365,22 @@ class Reference:
 
     ref_to: typing.Union[Array, FunctionType, Pointer, Type]
 
+    def format(self) -> str:
+        ref_to = self.ref_to
+        if isinstance(ref_to, Array):
+            return ref_to.format_decl("(&)")
+        else:
+            return f"{ref_to.format()}&"
+
+    def format_decl(self, name: str):
+        """Format as a named declaration"""
+        ref_to = self.ref_to
+
+        if isinstance(ref_to, Array):
+            return ref_to.format_decl(f"(& {name})")
+        else:
+            return f"{ref_to.format()}& {name}"
+
 
 @dataclass
 class MoveReference:
@@ -300,6 +390,13 @@ class MoveReference:
 
     moveref_to: typing.Union[Array, FunctionType, Pointer, Type]
 
+    def format(self) -> str:
+        return f"{self.moveref_to.format()}&&"
+
+    def format_decl(self, name: str):
+        """Format as a named declaration"""
+        return f"{self.moveref_to.format()}&& {name}"
+
 
 #: A type or function type that is decorated with various things
 #:
@@ -505,6 +602,15 @@ class Parameter:
     default: typing.Optional[Value] = None
     param_pack: bool = False
 
+    def format(self) -> str:
+        default = f" = {self.default.format()}" if self.default else ""
+        pp = "... " if self.param_pack else ""
+        name = self.name
+        if name:
+            return f"{self.type.format_decl(f'{pp}{name}')}{default}"
+        else:
+            return f"{self.type.format()}{pp}{default}"
+
 
 @dataclass
 class Function:


Test patch:
diff --git a/tests/test_typefmt.py b/tests/test_typefmt.py
new file mode 100644
index 0000000..8aa377a
--- /dev/null
+++ b/tests/test_typefmt.py
@@ -0,0 +1,313 @@
+import typing
+
+import pytest
+
+from cxxheaderparser.tokfmt import Token
+from cxxheaderparser.types import (
+    Array,
+    DecoratedType,
+    FunctionType,
+    FundamentalSpecifier,
+    Method,
+    MoveReference,
+    NameSpecifier,
+    PQName,
+    Parameter,
+    Pointer,
+    Reference,
+    TemplateArgument,
+    TemplateSpecialization,
+    TemplateDecl,
+    Type,
+    Value,
+)
+
+
+@pytest.mark.parametrize(
+    "pytype,typestr,declstr",
+    [
+        (
+            Type(typename=PQName(segments=[FundamentalSpecifier(name="int")])),
+            "int",
+            "int name",
+        ),
+        (
+            Type(
+                typename=PQName(segments=[FundamentalSpecifier(name="int")]), const=True
+            ),
+            "const int",
+            "const int name",
+        ),
+        (
+            Type(
+                typename=PQName(segments=[NameSpecifier(name="S")], classkey="struct")
+            ),
+            "struct S",
+            "struct S name",
+        ),
+        (
+            Pointer(
+                ptr_to=Type(
+                    typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                )
+            ),
+            "int*",
+            "int* name",
+        ),
+        (
+            Pointer(
+                ptr_to=Pointer(
+                    ptr_to=Type(
+                        typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                    )
+                )
+            ),
+            "int**",
+            "int** name",
+        ),
+        (
+            Reference(
+                ref_to=Type(
+                    typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                )
+            ),
+            "int&",
+            "int& name",
+        ),
+        (
+            Reference(
+                ref_to=Array(
+                    array_of=Type(
+                        typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                    ),
+                    size=Value(tokens=[Token(value="3")]),
+                )
+            ),
+            "int (&)[3]",
+            "int (& name)[3]",
+        ),
+        (
+            MoveReference(
+                moveref_to=Type(
+                    typename=PQName(
+                        segments=[NameSpecifier(name="T"), NameSpecifier(name="T")]
+                    )
+                )
+            ),
+            "T::T&&",
+            "T::T&& name",
+        ),
+        (
+            Pointer(
+                ptr_to=Array(
+                    array_of=Type(
+                        typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                    ),
+                    size=Value(tokens=[Token(value="3")]),
+                )
+            ),
+            "int (*)[3]",
+            "int (* name)[3]",
+        ),
+        (
+            Pointer(
+                ptr_to=Array(
+                    array_of=Type(
+                        typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                    ),
+                    size=Value(tokens=[Token(value="3")]),
+                ),
+                const=True,
+            ),
+            "int (* const)[3]",
+            "int (* const name)[3]",
+        ),
+        (
+            FunctionType(
+                return_type=Type(
+                    typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                ),
+                parameters=[
+                    Parameter(
+                        type=Type(
+                            typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                        )
+                    )
+                ],
+            ),
+            "int (int)",
+            "int name(int)",
+        ),
+        (
+            FunctionType(
+                return_type=Type(
+                    typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                ),
+                parameters=[
+                    Parameter(
+                        type=Type(
+                            typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                        )
+                    )
+                ],
+                has_trailing_return=True,
+            ),
+            "auto (int) -> int",
+            "auto name(int) -> int",
+        ),
+        (
+            FunctionType(
+                return_type=Type(
+                    typename=PQName(
+                        segments=[FundamentalSpecifier(name="void")],
+                    ),
+                ),
+                parameters=[
+                    Parameter(
+                        type=Type(
+                            typename=PQName(
+                                segments=[FundamentalSpecifier(name="int")],
+                            ),
+                        ),
+                        name="a",
+                    ),
+                    Parameter(
+                        type=Type(
+                            typename=PQName(
+                                segments=[FundamentalSpecifier(name="int")],
+                            ),
+                        ),
+                        name="b",
+                    ),
+                ],
+            ),
+            "void (int a, int b)",
+            "void name(int a, int b)",
+        ),
+        (
+            Pointer(
+                ptr_to=FunctionType(
+                    return_type=Type(
+                        typename=PQName(segments=[FundamentalSpecifier(name="int")])
+                    ),
+                    parameters=[
+                        Parameter(
+                            type=Type(
+                                typename=PQName(
+                                    segments=[FundamentalSpecifier(name="int")]
+                                )
+                            )
+                        )
+                    ],
+                )
+            ),
+            "int (*)(int)",
+            "int (* name)(int)",
+        ),
+        (
+            Type(
+                typename=PQName(
+                    segments=[
+                        NameSpecifier(name="std"),
+                        NameSpecifier(
+                            name="function",
+                            specialization=TemplateSpecialization(
+                                args=[
+                                    TemplateArgument(
+                                        arg=FunctionType(
+                                            return_type=Type(
+                                                typename=PQName(
+                                                    segments=[
+                                                        FundamentalSpecifier(name="int")
+                                                    ]
+                                                )
+                                            ),
+                                            parameters=[
+                                                Parameter(
+                                                    type=Type(
+                                                        typename=PQName(
+                                                            segments=[
+                                                                FundamentalSpecifier(
+                                                                    name="int"
+                                                                )
+                                                            ]
+                                                        )
+                                                    )
+                                                )
+                                            ],
+                                        )
+                                    )
+                                ]
+                            ),
+                        ),
+                    ]
+                )
+            ),
+            "std::function<int (int)>",
+            "std::function<int (int)> name",
+        ),
+        (
+            Type(
+                typename=PQName(
+                    segments=[
+                        NameSpecifier(
+                            name="foo",
+                            specialization=TemplateSpecialization(
+                                args=[
+                                    TemplateArgument(
+                                        arg=Type(
+                                            typename=PQName(
+                                                segments=[
+                                                    NameSpecifier(name=""),
+                                                    NameSpecifier(name="T"),
+                                                ],
+                                            )
+                                        ),
+                                    )
+                                ]
+                            ),
+                        )
+                    ]
+                ),
+            ),
+            "foo<::T>",
+            "foo<::T> name",
+        ),
+        (
+            Type(
+                typename=PQName(
+                    segments=[
+                        NameSpecifier(
+                            name="foo",
+                            specialization=TemplateSpecialization(
+                                args=[
+                                    TemplateArgument(
+                                        arg=Type(
+                                            typename=PQName(
+                                                segments=[
+                                                    NameSpecifier(name=""),
+                                                    NameSpecifier(name="T"),
+                                                ],
+                                                has_typename=True,
+                                            )
+                                        ),
+                                    )
+                                ]
+                            ),
+                        )
+                    ]
+                ),
+            ),
+            "foo<typename ::T>",
+            "foo<typename ::T> name",
+        ),
+    ],
+)
+def test_typefmt(
+    pytype: typing.Union[DecoratedType, FunctionType], typestr: str, declstr: str
+):
+    # basic formatting
+    assert pytype.format() == typestr
+
+    # as a type declaration
+    assert pytype.format_decl("name") == declstr