Package: gekkota
This is a Python code-generation module.Generates any Python statement/expressionPlaces parens to ensure expression priorities are unchangedPlaces extra newlines before/after class/function definitions to conform with PEP 8100% coverage of type hints, passing strict Pyright checks on every PRMeaningful type hierarchy inspired by Python grammar to guarantee syntax validity almost in every case.Covered withdiamondstests completelyCompatible withwordstreamerrenderables via awrapperInstallationJust installgekkotapackage, e.g. withpython -m pip install gekkota(or any other package manager of your choice)Rendering and configurationTo render anyRenderableinto a string, you could use a few approaches:str(renderable): renders a Renderable with default configuration (check below)renderable.render_str(): also default configurationrenderable.render_str(config): overrides default config options with provided inconfigmapping. Unspecified keys remain at default valuesHere is current default config:default_config:Config={"tab_size":4,# how much chars to use in indentation"compact":False,# if True, renders without redundant whitespace (e.g "for [i, e] in enumerate(a)" renders as "for[i,e]in enumerate(a)")"tab_char":" ",# character used for indentation# "place_semicolons" and "inline_small_stmts" options have some performance impact, since those require checking for newlines in token stream before re-streaming tokens.# this impact is probably negligible, but be aware of it"place_semicolons":False,# if True, semicolons are placed after one-line statements"inline_small_stmts":False,# if True, one-line statements are inlined. Overrides "place_semicolons" if True.}ExpressionsBasic expressionsYour starting points would beto_expressionandName:fromgekkotaimportName,to_expression# Name(name: str, annotation: Optional[Expression] = None)# to_expression(value: int | float | complex | str | bytes | bool | None)a=Name("a")b=Name("b")six=to_expression(6)# prints 'a + b * 6'print((a+b*six))Name, as many other classes in the module, is anExpressioninstanceExpressions support most operations to combine with other expressions.Exceptions are:Attribute reference: for that you should useExpression.getattr(other: str)Indexing:Expression.index(index: Expression)Slicing:Expression.index(index: Union[SliceExpr, Sequence[SliceExpr]])Equality / Inequality:Expression.eq(right_hand_side)andExpression.neq(right_hand_side)respectivelyis:Expression.is_(right_hand_side),is not:Expression.is_not(right_hand_side)in:Expression.in_(right_hand_side)not in:Expression.not_in(right_hand_side)and:Expression.and_(right_hand_side)or:Expression.or_(right_hand_side)await:Expression.await_():=assignment:Expression.assign(value)Ternary operator:Expression.if_(condition, alternative)For example:fromgekkotaimportNamea=Name("a")b=Name("b")expression=a.await_().in_(b)print(expression)# await a in bFor any other operation on expressions you can just use familiar Python syntax:fromgekkotaimportNamea=Name("a")b=Name("b")c=Name("c")print((a+b*c/a(b,c))# 'a + b * c / a(b, c)')SequencesMost convenient way to create sequence literals is, again,to_expression:fromgekkotaimportto_expression,Namea=Name("a")b=Name("b")print(to_expression((a,b,6)),# '(a, b, 6)' (notice that to_expression is recursive)to_expression((a,)),# '(a, )'to_expression([a,b]),# '[a, b]'to_expression([]),# '[]'to_expression({a:b}),# '{a: b}'to_expression(set()),# 'set()'to_expression([a,[a,b]]),# '[a, [a, b]]')If you want to have more precise control, you can useTupleExpr,ListExpr,SetExprandDictExprfor this.
All have same constructor signature:(values: Sequence[Expression])(exceptDictExpr, which hasKeyValuevalues)To create comprehensions:fromgekkotaimportName,GeneratorFor,GeneratorIffromgekkotaimport(ListComprehension,DictComprehension,KeyValue,SetComprehension,# same usage as ListComprehension)a,b,c,d=map(Name,"abcd")# ListComprehension(generator_or_expr: GeneratorBase | Expression, parts: Sequence[GeneratorPart] = ())print(ListComprehension(a,[# GeneratorFor(target: AssignmentTarget, iterator: Expression, *, is_async: bool = False)GeneratorFor(b,c),# GeneratorIf(condition: Expression)GeneratorIf(b.eq(d))]))# [a for b in c if b == d]# DictComprehension(generator_or_expr: GeneratorBase | KeyValue, parts: Sequence[GeneratorPart] = ())# GeneratorPart == GeneratorFor | GeneratorIfprint(DictComprehension(KeyValue(a,b),[GeneratorFor(c,d),GeneratorIf(b.eq(d))]))# {a: b for c in d if b == d}Keyword call argsUseCallArgto provide keyword call args:fromgekkotaimportName,to_expressionprint_=Name("print")# CallArg(name: str, value: Optional[Expression] = None)print(print_(Name("a"),CallArg("b"),CallArg("sep",to_expression(", "))))# print(a, b, sep=', ')Type hintsTo annotate a name, just pass an additional parameter toName:fromgekkotaimportNamea=Name("a",Name("int"))print(a)# a: intNew in version 0.6:Nameis now generic, depending on the presence of the annotation.Some constructors now accept only annotatedName, while some others accept only unannotated (depends on syntax features allowed).This allows to ensure type soundness without breaking changes in API.StatementsTo render program code (with multiple statements), useCode:fromgekkotaimportCode,Assignment,Namea=Name("a")six=Literal(6)create_variable=Assignment([Name("a")],six+six)print_variable=Name("print")(a)print(Code([create_variable,print_variable,]))# prints:# a = 6 + 6# print(a)To render a block of code, useBlock:fromgekkotaimportBlock,IfStmt,Assignment,Namea=Name("a")b=Name("b")six=Literal(6)create_variable=Assignment([Name("a")],six+six)print_variable=Name("print")(a)print(IfStmt(b,Block([create_variable,print_variable,])))# prints:# if b:#     a = 6 + 6#     print(a)If the difference between two is not obvious:Codejust renders statements on separate lines, while block also adds a newline before the first statement and indentation to every line.
Moreover,Code([])renders into"", whileBlock([])— into"\n    pass"Small statementsHere is an example of a few small statements:fromgekkotaimportName,SequenceExprfromgekkotaimport(ReturnStmt,DelStmt,AssertStmt,BreakStmt,ContinueStmt,YieldStmt,YieldFromStmt,NonLocalStmt,GlobalStmt,PassStmt,RaiseStmt,AsyncStmt)a,b,c=map(Name,"abc")print(ReturnStmt(a))# 'return a'print(YieldStmt(a))# 'yield a'print(YieldFromStmt(b))# 'yield from b'print(DelStmt(a,b))# 'del a, b'print(AssertStmt(a))# 'assert a'print(BreakStmt())# 'break'print(ContinueStmt())# 'continue'print(GlobalStmt(a,b))# 'global a, b'print(NonLocalStmt(a,b))# 'nonlocal a, b'print(PassStmt())# 'pass'print(RaiseStmt())# 'raise'print(RaiseStmt(a))# 'raise a'print(RaiseStmt(a,b))# 'raise a from b'Type statementGekkota 0.6 supportsPEP 695which adds new syntax, includingtypestatement:typePoint=tuple[float,float]To generate such statement in gekkota, usegekkota.annotations.TypeStmt:fromgekkotaimportTypeStmt# type Point = tuple[float, float]print(TypeStmt("Point",[],TupleExpr([Name("float"),Name("float")])))The second argument is a sequence of type parameters (for genric types):Sequence[TypeParam]You can use several types as TypeParam:fromgekkotaimportTypeParam,TypeStmt,Name,TypeVarParam,TypeVarTupleParam,ParamSpecParamdefmake_type(type_params:Sequence[TypeParam]):returnTypeStmt("CoolAlias",type_params,Name("int"))print(make_type([]))# type CoolAlias = int# (both) type CoolAlias[T] = intprint(make_type([Name("T")]))print(make_type([TypeVarParam(Name("T"))]))# (both) type CoolAlias[T: float] = intprint(make_type([Name("T",Name("float"))]))print(make_type([TypeVarParam(name=Name("T"),value=Name("float"))]))# type CoolAlias[*T] = intprint(make_type([TypeVarTupleParam(Name("T"),)]))# type CoolAlias[**T] = intprint(make_type([ParamSpecParam(Name("T"),)]))Generic Functions and ClassesFuncDefandClassDefnow accept an optional keyword argumenttype_paramsacceptingSequence[TypeParam]:fromgekkotaimportFuncDef,Name,PassStmttypeparam=Name("T")newfunc=FuncDef(name="newfunc",args=[Name("x",typeparam)],body=PassStmt(),rtype=typeparam,type_params=[typeparam],)print(newfunc)"""def newfunc[T](x: T) -> T: pass"""AssignmentFor common assigment useAssignment:fromgekkotaimportAssignment,Namea,b,c=map(Name,"abc")# Assignment(targets: Sequence[AssignmentTarget] | AnnotatedTarget, value: Expression)print(Assignment([a],b),# a = bAssignment([a.index(b)],c)# a[b] = cAssignment([a,b],c),# a = b = c)To annotate assignment (or just annotate a variable), useAnnotatedTarget:fromgekkotaimportAssignment,AnnotatedTarget,Namea,b,c=map(Name,"abc")D=Name("D")# AnnotatedTarget(target: AssignmentTarget, annotation: Expression)print(Assignment(AnnotatedTarget(a,D),b),# a: D = bAssignment(AnnotatedTarget(a.index(b),D),c)# a[b]: D = cAssignment([a,b],c),# a = b = c)For augmented assignment (e.g.+=) useAugmentedAssignment:fromgekkotaimportAssignment,Namea,b,c=map(Name,"abc")# AugmentedAssignment(target: AugAssignmentTarget, op: str, expression: Expression)print(AugmentedAssignment(a,"+=",b),# a += bAugmentedAssignment(a.index(b),"*=",c)# a *= c)Control flowFor control flow you can useIfStmt,ElifStmtandElseStmt:fromgekkotaimportName,IfStmt,ElifStmt,ElseStmt,Codea,b,c=map(Name,"abc")# IfStmt(condition: Expression, body: Statement)# ElifStmt(condition: Expression, body: Statement)# ElseStmt(body: Statement)code=Code([IfStmt(a,b),ElifStmt(b,a),ElseStmt(c)])print(code)"""if a: belif b: aelse: c"""LoopsUseForStmtandWhileStmtfor loops:fromgekkotaimportForStmt,WhileStmt,Namea,b,c=map(Name,"abc")# ForStmt(target: Expression, iterator: Expression, body: Statement, *, is_async: bool = False)print(ForStmt(a,b,c))# for a in b: c# WhileStmt(condition: Expression, body: Statement)print(WhileStmt(a,b))# while a: bFunctionsTo render a function definition, you will need aFuncDef:fromgekkotaimportName,FuncDefa,b,c=map(Name,"abc")# FuncDef(#    name: str,#    args: Sequence[FuncArg],#    body: Statement,#    *,#    rtype: Optional[Expression] = None,#    is_async: bool = False,#    type_params: Sequence[TypeParam] = (),# )print(FuncDef("cool_func",[a],b,rtype=c,))# def cool_func(a) -> c: bNew in 0.6:Now FuncDef accepts atype_paramsargument (checkGeneric Functions and Classes)To provide a default value and/or annotations to arguments, useFuncArg:fromgekkotaimportName,FuncDef,FuncArg,to_expressiona,b,c=map(Name,"abc")# FuncDef(name: str, args: Sequence[FuncArg], body: Statement, *, rtype: Optional[Expression] = None, is_async: bool = False)# FuncArg(#     name: str,#     annotation: Optional[Expression] = None,#     default_value: Optional[Expression] = None,#     *,#     late_bound_default: bool = False,# )print(FuncDef("cool_func",[FuncArg("a",Name("int"),to_expression(0))],b,rtype=c,))# def cool_func(a: int = 0) -> c: bNew in version 0.6:support forPEP 671 (Draft) – Syntax for late-bound function argument defaultsOther argument types are:StarArg(value: T = None): generates*value,*by defaultDoubleStarArg(value): same asStarArg, but with**Slash()is/(a mark of positional-only arguments in Python 3.8+)Lambda functions are generated usingLambDef:fromgekkotaimportName,LambDefa,b,c=map(Name,"abc")# LambDef(args: Sequence[FuncArg], body: Expression)print(LambDef([a],b,))# lambda a: bTo decorate a function/class, useDecorated:fromgekkotaimportName,FuncDef,Decorateddecorator=Name("decorator")a,b,c=map(Name,"abc")# Decorated(decorator: Expression, statement: ClassDef | FuncDef)# FuncDef(name: str, args: Sequence[FuncArg], body: Statement, *, rtype: Optional[Expression] = None, is_async: bool = False)print(Decorated(decorator,FuncDef("cool_func",[a],b,rtype=c,)))# @decorator# def cool_func(a) -> c: bClassesTo define a class, useClassDef:fromgekkotaimportName,ClassDefa,b,c=map(Name,"abc")# ClassDef(#     name: str,#     args: Sequence[CallArg | Expression],#     body: Statement,#     *,#     type_params: Sequence[TypeParam] = (),# )print(ClassDef("MyClass1",[],a))# class MyClass1: aprint(ClassDef("MyClass2",[b],c))# class MyClass2(b): cNew in 0.6:Now ClassDef accepts atype_paramsargument (checkGeneric Functions and Classes)ImportsTo render imports, useImportStmtandFromImportStmt:fromgekkotaimportName,StarArg,ImportDots,ImportSource,ImportStmt,FromImportStmt,ImportAlias# ImportStmt(names: Sequence[ImportAlias | Name | StarArg[None]])print(ImportStmt([Name("a")]))# import aprint(ImportStmt([Name("a"),Name("b")]))# import a, b# FromImportStmt(source: ImportSource | Name, names: Sequence[ImportAlias | Name | StarArg[None]])# ImportAlias(name: Name, alias: Name | None = None)print(FromImportStmt(Name("math"),[Name("cos"),ImportAlias(Name("sin"),Name("tan"))# we do a little trolling]))# from math import cos, sin as tanprint(FromImportStmt(Name("gekkota"),[StarArg()]))# from gekkota import *# ImportDots(length: int = 1)print(FromImportStmt(ImportDots(),[StarArg()]))# from . import *# ImportSource(parts: Sequence[str])print(FromImportStmt(ImportSource(["","values"]),[Name("Name")]))# from .values import NameExceptionsfromgekkotaimportName,TryStmt,ExceptStmt,FinallyStmt,Blocka,b,e=map(Name,"abe")# TryStmt(body: Statement)print(TryStmt(a))# try: a# ExceptStmt(exceptions: Sequence[Expression] | None, alias: Name | None, body: Statement)print(ExceptStmt(None,None,a))# except: aprint(ExceptStmt(None,None,Block([])))# except:#     passprint(ExceptStmt([a],None,b))# except a: bprint(ExceptStmt([a],e,b))# except a as e: b# FinallyStmt(body: Statement)print(FinallyStmt(a))# finally: aContext ManagersfromgekkotaimportName,WithStmt,WithTargeta,b,e=map(Name,"abe")# WithStmt(targets: Sequence[WithTarget | Expression], body: Statement, *, is_async: bool = False,)print(WithStmt([a],b))# with a: b# WithTarget(expression: Expression, alias: str | None = None)print(WithStmt([WithTarget(a,"aaaa")],b))# with a as aaaa: bprint(WithStmt([a],b,is_async=True))# async with a: bPattern matchingThis section is currently unfinished, checkpattern_matching.pyCustom renderingIf your custom element can be meaningfully represented as a combination of existing elements, you can use a function instead of a class:fromgekkotaimportExpressiondefSquare(e:Expression)->Expression:returne*eThis is a pretty obvious approach, but often it works best.While being aimed at Python code generation,gekkotais pretty extensible, and can be used to render different things.You can build custom renderables, statements, expressions, and so on.The simplest example of a custom renderable would be:fromgekkotaimportRenderable,StrGen,ConfigclassRenderString(Renderable):"""It renders whatever is passed to it"""def__init__(self,value:str):self.value=valuedefrender(self,config:Config)->StrGen:yieldself.valueLet's suppose you want to render a custom expression: a custom sequence literal (obviously isn't valid in Python, but you need it for some reason).Suppose your custom literal would be in form of<|value1, value2, ...|>.You can extendSequenceExprfor that:fromgekkotaimportSequenceExpr,NameclassMyCoolSequence(SequenceExpr):parens="<|","|>"seq=MyCoolSequence([Name("a"),Name("b")])print(seq)# <|a,b|>That's it, you're ready to render this literal (which, again, isn't valid in Python but anyway).Or you could go further and write rendering by yourself (it's easier than it sounds):fromgekkotaimportExpression,ConfigclassMyCoolSequence(Expression):def__init__(self,values:Sequence[Expression]):self.values=values# could be rewritten to be simpler, check `Useful utils` section belowdefrender(self,config:Config)->StrGen:yield"<|"fori,iteminenumerate(self.values):yield fromitem.render(config)ifi+1<len(self.values):# no comma after last elementyield","yield" "yield"|>"It's fairly easy, just render every part in the right order:To render a string, useyield stringTo render aRenderable, useyield from renderable.render(config)Choosing a right base classTo choose a right base class, think in what context you want to use your renderable.If there is a similar context in Python (e.g. your renderable is a block statement, likefororif), extend that class.After choosing a right base class, check if it has a predefined render, maybe you won't need to write everything by yourself.For example, withBlockStmtyou need to providerender_headinstead:# that's the actual source from module, not an exampleclassBlockStmt(Statement):body:Statementdefrender_head(self,config:Config)->StrGen:returnNotImplementeddefrender(self,config:Config)->StrGen:yield fromself.render_head(config)yield":"yield" "yield fromself.body.render(config)[...]classElseStmt(BlockStmt):def__init__(self,body:Statement):self.body=bodydefrender_head(self,config:config)->StrGen:yield"else"Useful utilsgekkota.utilsprovidesUtilsclass which is useful for custom renderables. For example, customMyCoolSequencecould be implemented as:fromgekkotaimportExpression,UtilsclassMyCoolSequence(Expression):def__init__(self,values:Sequence[Expression]):self.values=valuesdefrender(self,config:config)->StrGen:yield fromUtils.wrap(["<|","|>"],Utils.comma_separated(self.values,config))Methods provided inUtils:add_tab(generator: StrGen, config: Config) -> StrGenAdds indentation to a stream of tokens, using providedconfigFor example,Utils.add_tab(Name("a").render(config), config)-> Iterable of [' ', 'a']separated(separator: Sequence[str], renderables: Sequence[Renderable], config: Config) -> StrGenInserts separator between renderables (and renders them in stream)For example:Utils.separated([",", " "], self.values, config)- inserts ", " between elements ofself.valuesseparated_str(separator: Sequence[str], strings: Sequence[str], config: Config)Same as previous, but forstrsequencescomma_separated(renderables: Sequence[Renderable], config: Config) -> StrGenAlias forUtils.separated([",", " "], renderables, config)make_compact(generator: StrGen, config: Config) -> StrGenFilters all unneccessary whitespace from stream (doesn't respectconfig["compact"]). Config is unused at the moment, but provided for compatibility with future updateswrap(parens: Sequence[str], generator: StrGen) -> StrGenWraps a token stream with strings fromparensarray (should have 2 elements).In other words, insertsparens[0]at the start of the stream, andparens[1]at the endwordstreamer compatibilitygekkota contains experimentalwordstreamercompatibility layer to use gekkota objects in wordstreamer and vice versa.Use gekkota.Renderable where wordstreamer.Renderable can be used:fromgekkotaimportWSRenderable,Literalws_compatible=WSRenderable(Literal(6))# this is a wordstreamer.Renderable nowUse wordstreamer.Renderable where gekkota.Renderable can be used:fromgekkotaimportWStoG,Literalfromwordstreamer.startkitimportStringify# create a class to define the type of renderableclassWSLiteral(WStoG,Literal):passsome_number=Stringify(6)print(WSLiteral(some_number)+Literal(8))# "6 + 8"Insert a wordstreamer.Renderable in a string (gekkota.Literal)fromgekkotaimportWSString,Literalfromwordstreamer.startkitimportStringifysome_number=Stringify(6)print(WSString(some_number))# '"""6"""'