Target - Python

Note

The Python target should be considered experimental. Though the code generated works, the future versions might generate code completely different with possible static type-checking support, because the types of generated code cannot be well-expressed with typing module.

Quick Start

gugugu-python \
        --input=dir/containing/gugugu/definitions \
        --output=dir/containing/python/code \
        --package-prefix=some.package.prefix \
        --with-codec \
        --with-server \
        --with-client \
        ;

Module

Gugugu module is represented by Python modules, with module name lower-cased, without underscores.

Types

Primitives

Gugugu Type

Python Type

Unit

object

Bool

bool

Int32

int

Double

float

String

str

Maybe A

Optional[A]

List A

List[A]

Record Type

Record type are represented by dataclass.

data Book
  = Book
    { id   :: Int32
    , name :: String
    }

becomes

from dataclasses import dataclass

@dataclass()
class Book:
    id: int
    name: str

Enum Type

Enum type are represented by IntEnum.

data Color
  = Red
  | Green
  | Blue

becomes

from enum import IntEnum

class Color(IntEnum):
    RED = 0
    GREEN = 1
    BLUE = 2

Foreign Type

data DateTime
  {-# FOREIGN python datetime.datetime #-}

Foreign type generates no Python codes. Gugugu just replaces the DateTime with the corresponding python type.

Encoder and Decoder

All types in this section are located in module gugugu.lang.python.runtime.codec with default configuration.

The generated codecs have type which cannot be expressed with module typing

Encoder = Callable[[S, A, EncoderImpl[S, R]], S]
Decoder = Callable[[S, EncoderImpl[S, R]], Tuple[S, A]]

def encode(a: A, impl: EncoderImpl[S, R], encoder: Encoder) -> R:
    pass


def decode(r: R, impl: DecoderImpl[S, R], decoder: Decoder) -> A:
    pass

The encoder and decoder type should have a type argument, A, which should be the type of the type to encode or decode. But it cannot be expressed well in Python.

The encoders and decoders are defined at:

Gugugu Type

Python Type

Encoder

Decoder

Unit

object

Encoders.encode_unit

Decoders.decode_unit

Bool

bool

Encoders.encode_bool

Decoders.decode_bool

Int32

int

Encoders.encode_int32

Decoders.decode_int32

Double

float

Encoders.encode_double

Decoders.decode_double

String

str

Encoders.encode_string

Decoders.decode_string

Maybe A

Optional[A]

Encoders.encode_maybe(encoderA)

Decoders.decode_maybe(decoderA)

List A

List[A]

Encoders.encode_list(encoderA)

Decoders.decode_list(decoderA)

Foo

Foo

Foo.encode_foo

Foo.decode_foo

Foo (foreign)

(foreign)

FooCodec.encode_foo

FooCodec.decode_foo

The EncoderImpl[S, R] and DecoderImpl[S, R] are two values you have to provide to describe how to encode and decode a value.

Use the encode to encode a value of type A to type R, with the encoder and the EncoderImpl[S, R]. Likewise, use the decode to decode a value of type A from type R, with the decoder and the DecoderImpl[S, R].

The encoder and decoder are polymorphic over S and R. With different EncoderImpl/DecoderImpl provided, you can encode/decode values to/from different types you want.

EncoderImpl and DecoderImpl

The S is the state used in encoding/decoding. The R is the serialized type encoding to or decoding from.

You might find examples/lang/python/src/guguguexamples/codec/json.py useful to write a EncoderImpl/DecoderImpl.

Most functions in the types works with only S except the following two.

class EncoderImpl(Generic[S, R]):

    @abstractmethod
    def encode_with_state(self, k: Callable[[S], S]) -> R:
        raise NotImplementedError

class DecoderImpl(Generic[S, R]):

    @abstractmethod
    def decode_with_state(self, r: R, k: Callable[[S], Tuple[S, A]]) -> A:
        raise NotImplementedError

For an EncoderImpl.encode_with_state, you usually should

  • Provide an initial state

  • Feed it to the function provided

  • Transform the state returned to serialized type, R

For a DecoderImpl.decode_with_state, you usually should

  • Transform the R into an initial state S

  • Feed it to the function provided

  • Make sure the returned state did not go wrong

  • Return the decoded value returned by the function provided.

The generated encoders/decoders are designed to be compatible with either an immutable state type or a mutable one. The state object will never be reused in generated code. The modification can happen in place if you take care of it in your code.

The generated code will never throw any exceptions, but you usually want to do so in your EncoderImpl/DecoderImpl when things go wrong.

Encode/Decode Record Type

class EncoderImpl(Generic[S, R]):

    @abstractmethod
    def encode_record(self, s: S, n_fields: int, k: Callable[[S], S]) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_record_field(self, s: S,
                            i: int, name: str,
                            k: Callable[[S], S]) -> S:
        raise NotImplementedError

class DecoderImpl(Generic[S, R]):

    @abstractmethod
    def decode_record(self, s: S, n_fields: int,
                      k: Callable[[S], Tuple[S, A]]) -> Tuple[S, A]:
        raise NotImplementedError

    @abstractmethod
    def decode_record_field(self, s: S,
                            i: int, name: str,
                            k: Callable[[S], Tuple[S, A]]) -> Tuple[S, A]:
        raise NotImplementedError

The generated encoder/decoder for record type consists of a call to EncoderImpl.encode_record/DecoderImpl.decode_record. And the provided callback will call the EncoderImpl.encode_record_field/DecoderImpl.decode_record_field several times with indices and names of the fields.

Encode/Decode Enum Type

class EncoderImpl(Generic[S, R]):

    @abstractmethod
    def encode_enum(self, s: S, a: A,
                    as_index: Callable[[A], int],
                    as_name: Callable[[A], str]) -> S:
        raise NotImplementedError

class DecoderImpl(Generic[S, R]):

    @abstractmethod
    def decode_enum(self, s: S,
                    by_index: Callable[[int], Optional[A]],
                    by_name: Callable[[str], Optional[A]]) -> Tuple[S, A]:
        raise NotImplementedError

The generated encoder/decoder for enum type consists of a call to EncoderImpl.encode_enum/DecoderImpl.decode_enum. You should encode/decode the value with the name or the index.

Encode/Decode Primitive and Foreign Types

class EncoderImpl(Generic[S, R]):

    @abstractmethod
    def encode_unit(self, s: S, v: object) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_bool(self, s: S, v: bool) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_int32(self, s: S, v: int) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_double(self, s: S, v: float) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_string(self, s: S, v: str) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_maybe(self, s: S, v: Optional[A], k: Callable[[S, A], S]) -> S:
        raise NotImplementedError

    @abstractmethod
    def encode_list(self, s: S, v: List[A], k: Callable[[S, A], S]) -> S:
        raise NotImplementedError

class DecoderImpl(Generic[S, R]):

    @abstractmethod
    def decode_unit(self, s: S) -> Tuple[S, object]:
        raise NotImplementedError

    @abstractmethod
    def decode_bool(self, s: S) -> Tuple[S, bool]:
        raise NotImplementedError

    @abstractmethod
    def decode_int32(self, s: S) -> Tuple[S, int]:
        raise NotImplementedError

    @abstractmethod
    def decode_double(self, s: S) -> Tuple[S, float]:
        raise NotImplementedError

    @abstractmethod
    def decode_string(self, s: S) -> Tuple[S, str]:
        raise NotImplementedError

    @abstractmethod
    def decode_maybe(self, s: S,
                     k: Callable[[S], Tuple[S, A]]) -> Tuple[S, Optional[A]]:
        raise NotImplementedError

    @abstractmethod
    def decode_list(self, s: S,
                    k: Callable[[S], Tuple[S, A]]) -> Tuple[S, List[A]]:
        raise NotImplementedError

The primitive types and foreign types will generate functions like above. And the encoder/decoder simply calls the function you provide.

The Maybe and List functions is also provided with the encoder/decoder of the parameter type.

Client and Server

All types in this section are located in package gugugu.lang.python.runtime.transport with default configuration.

module Hello where

foo :: FooReq -> IO FooRes
bar :: BarReq -> IO BarRes

becomes

class HelloModule(ABC):

    @abstractmethod
    def foo(self, fa):
        raise NotImplementedError

    @abstractmethod
    def bar(self, fa):
        raise NotImplementedError

    @classmethod
    def to_transport(cls, impl: HelloModule, decoder_impl, encoder_impl) -> ServerTransport:
        pass

    @classmethod
    def from_transport(cls, transport, encoder_impl, decoder_impl) -> HelloModule:
        pass

The HelloModule can be used as the client when used in client code, or as the server implementation in server code.

The fa should be a type about the incoming type, usually a tuple of metadata and the incoming value or something similar. The return type should be a type about the outgoing type, you can also use async functions.

Given the incoming type of the function as A, the outgoing type of the function IO B.

The type of fa can be

  • A, simply itself.

  • Tuple[SomeMeta, A], where SomeMeta is some metadata.

  • List[A], when you want to process many data in one request.

or their combination decnoted by F[A] later.

The return type can be

  • A, simply itself.

  • Tuple[SomeMeta, A], where SomeMeta is some metadata.

  • Future[A], if used with concurrent.futures

  • Awaitable[A], if async function is used.

  • Callable[[Callable[[A], None]], None], if used with continuation.

or their combination, decnoted by G[B] later.

Of course you have many choices, but be sure use only one not mixed of them.

Warning

Do not use any type that contains more than one values of the corresponding type if you want to work with other target that does not support it. And most targets do not support it.

Server Usage

@dataclass()
class QualName(Generic[A]):
    namespace: List[A]
    name: A

class ServerTransport(ABC):
    """ The type of this handler cannot be expressed in Python.
        Haskell notation:

        type ServerCodecHandler f g m ra rb =
            forall a b. (ra -> a)           -- ^ request decoder
                     -> (b -> rb)           -- ^ response encoder
                     -> (f a -> m (g b))    -- ^ request handler
                     -> f ra                -- ^ request encoded
                     -> m (g rb)            -- ^ response encoded
        type ServerTransport f g m ra rb = QualName String
                                        -> ServerCodecHandler f g m ra rb
                                        -> Maybe (f ra -> m (g rb))
    """
    @abstractmethod
    def ask(self, name: QualName[str], k):
        """ The self is expected to be type: ServerTransport f g m ra rb
            The k is expected to be type: ServerCodecHandler f g m ra rb
        """
        raise NotImplementedError

class HelloModule(ABC):

    @classmethod
    def to_transport(cls, impl: HelloModule, decoder_impl, encoder_impl) -> ServerTransport:
        pass

HelloModule.to_transport converts a HelloModule into a ServerTransport. ServerTransport.ask returns a handler with given name or None if no matching handler can be found. The handler has type Callable[[F[RA], G[RB]]. The decoder_impl should be a type of DecoderImpl[SA, RA] where RA is the type of the handler accepts. The encoder_impl should be a type of EncoderImpl[SB, RB] where RB is the type of the handler returns.

The k required by ServerTransport.ask is a callback to handle decoding and encoding. It should have type

def handle_encoding(decode_request: Callable[[RA], A],
                    encode_response: Callable[[B], RB],
                    handler: Callable[[F[A]], G[B]],
                    fr: F[RA]):
    pass

The decoding function and the encoding function may throw exceptions if you do that in the corresponding EncoderImpl/DecoderImpl, and you are responsible to handle that. The function should be polymorphic over type A and B.

Please consult examples/lang/python/src/guguguexamples/jsonhttp/server.py for how to use the it.

Client Usage

@dataclass()
class QualName(Generic[A]):
    namespace: List[A]
    name: A

class ClientTransport(ABC):
    """ The type of this handler cannot be expressed in Python.
        Haskell notation:

        type ClientTransport f g m ra rb =
            forall a b. QualName String
                     -> (a -> ra)
                     -> (rb -> b)
                     -> f a
                     -> m (g b)
    """
    @abstractmethod
    def send(self, name: QualName[str], encoder, decoder, fa):
        raise NotImplementedError

class HelloModule(ABC):

    @classmethod
    def from_transport(cls, transport, encoder_impl, decoder_impl) -> HelloModule:
        pass

HelloModule.from_transport converts a ClientTransport into a HelloModule. Like ServerTransport, it can handle request about type RA and return an response about type RB. Like ServerCodecHandler, you are responsible to handle possible exceptions.

Please consult examples/lang/python/src/guguguexamples/jsonhttp/client.py for how to write a ClientTransport.

Command Line Options

Usage: gugugu-python (-i|--input INPUT) (-o|--output OUTPUT)
                     (-p|--package-prefix PACKAGE_PREFIX)
                     [-r|--runtime-package RUNTIME_PACKAGE] [--with-codec]
                     [--with-server] [--with-client] [--trans-module-code ARG]
                     [--trans-module-value ARG] [--trans-module-type ARG]
                     [--trans-func-code ARG] [--trans-func-value ARG]
                     [--trans-type-code ARG] [--trans-type-func ARG]
                     [--trans-field-code ARG] [--trans-field-value ARG]
                     [--trans-enum-code ARG] [--trans-enum-value ARG]
                     [--version]

Available options:
  -i,--input INPUT         the directory containing the definition files
  -o,--output OUTPUT       the directory to put the generated sources
  -p,--package-prefix PACKAGE_PREFIX
                           the package prefix, e.g. some.package.prefix
  -r,--runtime-package RUNTIME_PACKAGE
                           location of gugugu runtime
                           package (default: "gugugu.lang.python.runtime")
  --with-codec             pass this flag to generate codecs, default to false
  --with-server            pass this flag to generate server, default to false,
                           implies with-codec
  --with-client            pass this flag to generate client, default to false,
                           implies with-codec
  --trans-module-code ARG  module name transformer for code (default: lower)
  --trans-module-value ARG module name transformer for value (default: snake)
  --trans-module-type ARG  module name transformer for type of
                           client/server (default: id)
  --trans-func-code ARG    function name transformer for code (default: snake)
  --trans-func-value ARG   function name transformer for value (default: snake)
  --trans-type-code ARG    type name transformer for code (default: id)
  --trans-type-func ARG    type name transformer in function (default: snake)
  --trans-field-code ARG   record field name transformer for
                           code (default: snake)
  --trans-field-value ARG  record field name transformer for
                           value (default: snake)
  --trans-enum-code ARG    enum name transformer for code (default: upper-snake)
  --trans-enum-value ARG   enum name transformer for
                           value (default: upper-snake)
  -h,--help                Show this help text
  --help-transformers      list available name transformers
  --version                show version