From 9d220186e195d7a2b7a7ba0041beefc9d77e9154 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 5 Jan 2021 03:21:25 -0500 Subject: [PATCH] Add pretty formatting for types and template specializations - Fixes #3 --- cxxheaderparser/tokfmt.py | 23 +++- cxxheaderparser/types.py | 161 +++++++++++++++++++++--- tests/test_typefmt.py | 254 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+), 21 deletions(-) create mode 100644 tests/test_typefmt.py 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 d1f8f63..8782817 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 __str__(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 __str__(self) -> str: + return f"decltype({tokfmt(self.tokens)})" + @dataclass class FundamentalSpecifier: @@ -103,6 +90,9 @@ class FundamentalSpecifier: name: str + def __str__(self) -> str: + return self.name + @dataclass class NameSpecifier: @@ -120,6 +110,12 @@ class NameSpecifier: specialization: typing.Optional["TemplateSpecialization"] = None + def __str__(self) -> str: + if self.specialization: + return f"{self.name}{self.specialization}" + else: + return self.name + @dataclass class AutoSpecifier: @@ -129,6 +125,9 @@ class AutoSpecifier: name: str = "auto" + def __str__(self) -> str: + return self.name + @dataclass class AnonymousName: @@ -141,6 +140,10 @@ class AnonymousName: #: Unique id associated with this name (only unique per parser instance!) id: int + def __str__(self) -> str: + # TODO: not sure what makes sense here, subject to change + return f"<>" + PQNameSegment = typing.Union[ AnonymousName, FundamentalSpecifier, NameSpecifier, DecltypeSpecifier, AutoSpecifier @@ -166,6 +169,13 @@ class PQName: #: Set to true if the type was preceded with 'typename' has_typename: bool = False + def __str__(self) -> str: + tn = "typename " if self.has_typename else "" + if self.classkey: + return f"{tn}{self.classkey} {'::'.join(map(str, self.segments))}" + else: + return tn + "::".join(map(str, self.segments)) + @dataclass class TemplateArgument: @@ -185,6 +195,10 @@ class TemplateArgument: param_pack: bool = False + def __str__(self): + pp = "..." if self.param_pack else "" + return f"{self.arg}{pp}" + @dataclass class TemplateSpecialization: @@ -200,6 +214,9 @@ class TemplateSpecialization: args: typing.List[TemplateArgument] + def __str__(self): + return f"<{', '.join(map(str, self.args))}>" + @dataclass class FunctionType: @@ -234,6 +251,25 @@ class FunctionType: #: calling convention msvc_convention: typing.Optional[str] = None + def __format__(self, name: str) -> str: + if not name: + return self.__str__() + + vararg = "..." if self.vararg else "" + if self.has_trailing_return: + return f"auto {name}({', '.join(map(str, self.parameters))}{vararg}) -> {self.return_type}" + else: + return f"{self.return_type} {name}({', '.join(map(str, self.parameters))}{vararg})" + + def __str__(self) -> str: + vararg = "..." if self.vararg else "" + if self.has_trailing_return: + return f"auto ({', '.join(map(str, self.parameters))}{vararg}) -> {self.return_type}" + else: + return ( + f"{self.return_type} ({', '.join(map(str, self.parameters))}{vararg})" + ) + @dataclass class Type: @@ -246,6 +282,17 @@ class Type: const: bool = False volatile: bool = False + def __format__(self, name: str) -> str: + s = self.__str__() + if not name: + return s + return f"{s} {name}" + + def __str__(self) -> str: + c = "const " if self.const else "" + v = "volatile " if self.volatile else "" + return f"{c}{v}{self.typename}" + @dataclass class Array: @@ -265,6 +312,17 @@ class Array: #: ~~ size: typing.Optional[Value] + def __format__(self, name: str) -> str: + if not name: + return self.__str__() + + s = self.size if self.size else "" + return f"{self.array_of} {name}[{s}]" + + def __str__(self) -> str: + s = self.size if self.size else "" + return f"{self.array_of}[{s}]" + @dataclass class Pointer: @@ -278,6 +336,30 @@ class Pointer: const: bool = False volatile: bool = False + def __fmt(self) -> str: + c = " const" if self.const else "" + v = " volatile" if self.volatile else "" + return f"*{c}{v}" + + def __format__(self, name: str) -> str: + if not name: + return self.__str__() + + fmt = self.__fmt() + ptr_to = self.ptr_to + if isinstance(ptr_to, (Array, FunctionType)): + return ptr_to.__format__(f"({fmt} {name})") + else: + return f"{ptr_to} {fmt} {name}" + + def __str__(self) -> str: + fmt = self.__fmt() + ptr_to = self.ptr_to + if isinstance(ptr_to, (Array, FunctionType)): + return ptr_to.__format__(f"({fmt})") + else: + return f"{ptr_to} {fmt}" + @dataclass class Reference: @@ -287,6 +369,23 @@ class Reference: ref_to: typing.Union[Array, FunctionType, Pointer, Type] + def __format__(self, name: str) -> str: + if not name: + return self.__str__() + + ref_to = self.ref_to + if isinstance(ref_to, Array): + return ref_to.__format__(f"(& {name})") + else: + return f"{ref_to}& {name}" + + def __str__(self) -> str: + ref_to = self.ref_to + if isinstance(ref_to, Array): + return ref_to.__format__("(&)") + else: + return f"{ref_to}&" + @dataclass class MoveReference: @@ -296,11 +395,24 @@ class MoveReference: moveref_to: typing.Union[Array, FunctionType, Pointer, Type] + def __format__(self, name: str) -> str: + if not name: + return self.__str__() + return f"{self.moveref_to} && {name}" + + def __str__(self) -> str: + return f"{self.moveref_to} &&" + #: A type or function type that is decorated with various things #: #: .. note:: There can only be one of FunctionType or Type in a DecoratedType #: chain +#: +#: The ``__str__`` for these types returns a canonical representation of +#: the underlying type. To get a declaration (such as a parameter), use the +#: ``__format__`` function or an f-string with the name as the format +#: specifier (``f"{type:name}``) DecoratedType = typing.Union[Array, Pointer, MoveReference, Reference, Type] @@ -501,6 +613,15 @@ class Parameter: default: typing.Optional[Value] = None param_pack: bool = False + def __str__(self): + default = f" = {self.default}" if self.default else "" + pp = "... " if self.param_pack else "" + name = self.name + if name: + return f"{self.type:{pp}{name}}{default}" + else: + return f"{self.type}{pp}{default}" + @dataclass class Function: diff --git a/tests/test_typefmt.py b/tests/test_typefmt.py new file mode 100644 index 0000000..bc9c9ec --- /dev/null +++ b/tests/test_typefmt.py @@ -0,0 +1,254 @@ +import pytest +from cxxheaderparser.tokfmt import Token +from cxxheaderparser.types import ( + Array, + 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", + ), + ( + 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", + "std::function 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", + "foo name", + ), + ], +) +def test_typefmt(pytype, typestr, declstr): + # string conversion + assert str(pytype) == typestr + + # f-string with no format specifier should be same as string + assert f"{pytype}" == typestr + + # f-string with format specifier + assert f"{pytype:name}" == declstr