packages feed

nirum-0.3.0: src/Nirum/Targets/Python.hs

{-# LANGUAGE DeriveDataTypeable, ExtendedDefaultRules, OverloadedLists,
             QuasiQuotes, TypeFamilies, TypeSynonymInstances,
             MultiParamTypeClasses #-}
module Nirum.Targets.Python ( Code
                            , CodeGen
                            , CodeGenContext ( localImports
                                             , standardImports
                                             , thirdPartyImports
                                             )
                            , CompileError
                            , InstallRequires ( InstallRequires
                                              , dependencies
                                              , optionalDependencies
                                              )
                            , Source ( Source
                                     , sourceModule
                                     , sourcePackage
                                     )
                            , Python (Python)
                            , PythonVersion ( Python2
                                            , Python3
                                            )
                            , RenameMap
                            , addDependency
                            , addOptionalDependency
                            , compileModule
                            , compilePrimitiveType
                            , compileTypeDeclaration
                            , compileTypeExpression
                            , empty
                            , insertLocalImport
                            , insertStandardImport
                            , insertThirdPartyImports
                            , insertThirdPartyImportsA
                            , localImportsMap
                            , minimumRuntime
                            , parseModulePath
                            , renameModulePath
                            , runCodeGen
                            , stringLiteral
                            , toAttributeName
                            , toClassName
                            , toImportPath
                            , toImportPaths
                            , toNamePair
                            , unionInstallRequires
                            ) where

import Control.Monad (forM)
import qualified Control.Monad.State as ST
import qualified Data.List as L
import Data.Maybe (catMaybes, fromMaybe)
import Data.Typeable (Typeable)
import GHC.Exts (IsList (toList))
import Text.Printf (printf)

import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.SemVer as SV
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Text.Lazy (toStrict)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Function (on)
import System.FilePath (joinPath)
import Text.Blaze.Renderer.Text
import qualified Text.Email.Validate as E
import Text.Heterocephalus (compileText)
import Text.InterpolatedString.Perl6 (q, qq)

import qualified Nirum.CodeGen as C
import Nirum.CodeGen (Failure)
import qualified Nirum.Constructs.Annotation as A
import qualified Nirum.Constructs.DeclarationSet as DS
import qualified Nirum.Constructs.Identifier as I
import Nirum.Constructs.Declaration (Documented (docsBlock))
import Nirum.Constructs.ModulePath ( ModulePath
                                   , fromIdentifiers
                                   , hierarchy
                                   , hierarchies
                                   , replacePrefix
                                   )
import Nirum.Constructs.Name (Name (Name))
import qualified Nirum.Constructs.Name as N
import Nirum.Constructs.Service ( Method ( Method
                                         , errorType
                                         , methodAnnotations
                                         , methodName
                                         , parameters
                                         , returnType
                                         )
                                , Parameter (Parameter)
                                , Service (Service)
                                )
import Nirum.Constructs.TypeDeclaration ( EnumMember (EnumMember)
                                        , Field (Field, fieldName)
                                        , PrimitiveTypeIdentifier (..)
                                        , Tag (Tag)
                                        , Type ( Alias
                                               , EnumType
                                               , PrimitiveType
                                               , RecordType
                                               , UnboxedType
                                               , UnionType
                                               , primitiveTypeIdentifier
                                               )
                                        , TypeDeclaration (..)
                                        )
import Nirum.Constructs.TypeExpression ( TypeExpression ( ListModifier
                                                        , MapModifier
                                                        , OptionModifier
                                                        , SetModifier
                                                        , TypeIdentifier
                                                        )
                                       )
import Nirum.Docs.ReStructuredText (ReStructuredText, render)
import Nirum.Package hiding (target)
import Nirum.Package.Metadata ( Author (Author, name, email)
                              , Metadata ( Metadata
                                         , authors
                                         , target
                                         , version
                                         , description
                                         , license
                                         )
                              , MetadataError ( FieldError
                                              , FieldTypeError
                                              , FieldValueError
                                              )
                              , Node (VString)
                              , Target ( CompileError
                                       , CompileResult
                                       , compilePackage
                                       , parseTarget
                                       , showCompileError
                                       , targetName
                                       , toByteString
                                       )
                              , fieldType
                              , stringField
                              , tableField
                              , versionField
                              )
import qualified Nirum.Package.ModuleSet as MS
import qualified Nirum.Package.Metadata as MD
import Nirum.TypeInstance.BoundModule

minimumRuntime :: SV.Version
minimumRuntime = SV.version 0 6 0 [] []

data Python = Python { packageName :: T.Text
                     , minimumRuntimeVersion :: SV.Version
                     , renames :: RenameMap
                     } deriving (Eq, Ord, Show, Typeable)

type RenameMap = M.Map ModulePath ModulePath
type Package' = Package Python
type CompileError' = T.Text

data PythonVersion = Python2
                   | Python3
                   deriving (Eq, Ord, Show)

data Source = Source { sourcePackage :: Package'
                     , sourceModule :: BoundModule Python
                     } deriving (Eq, Ord, Show)

type Code = T.Text

instance Failure CodeGenContext CompileError' where
    fromString = return . T.pack

data CodeGenContext
    = CodeGenContext { standardImports :: S.Set T.Text
                     , thirdPartyImports :: M.Map T.Text (M.Map T.Text T.Text)
                     , localImports :: M.Map T.Text (S.Set T.Text)
                     , pythonVersion :: PythonVersion
                     }
    deriving (Eq, Ord, Show)

localImportsMap :: CodeGenContext -> M.Map T.Text (M.Map T.Text T.Text)
localImportsMap CodeGenContext { localImports = imports } =
    M.map (M.fromSet id) imports

sourceDirectory :: PythonVersion -> T.Text
sourceDirectory Python2 = "src-py2"
sourceDirectory Python3 = "src"

empty :: PythonVersion -> CodeGenContext
empty pythonVersion' = CodeGenContext { standardImports = []
                                      , thirdPartyImports = []
                                      , localImports = []
                                      , pythonVersion = pythonVersion'
                                      }

type CodeGen = C.CodeGen CodeGenContext CompileError'

runCodeGen :: CodeGen a
           -> CodeGenContext
           -> (Either CompileError' a, CodeGenContext)
runCodeGen = C.runCodeGen

insertStandardImport :: T.Text -> CodeGen ()
insertStandardImport module' = ST.modify insert'
  where
    insert' c@CodeGenContext { standardImports = si } =
        c { standardImports = S.insert module' si }

insertThirdPartyImports :: [(T.Text, S.Set T.Text)] -> CodeGen ()
insertThirdPartyImports imports =
    insertThirdPartyImportsA [ (from, M.fromSet id objects)
                             | (from, objects) <- imports
                             ]

insertThirdPartyImportsA :: [(T.Text, M.Map T.Text T.Text)] -> CodeGen ()
insertThirdPartyImportsA imports =
    ST.modify insert'
  where
    insert' c@CodeGenContext { thirdPartyImports = ti } =
        c { thirdPartyImports = L.foldl (M.unionWith M.union) ti importList }
    importList :: [M.Map T.Text (M.Map T.Text T.Text)]
    importList = [ M.singleton from objects
                 | (from, objects) <- imports
                 ]

insertLocalImport :: T.Text -> T.Text -> CodeGen ()
insertLocalImport module' object = ST.modify insert'
  where
    insert' c@CodeGenContext { localImports = li } =
        c { localImports = M.insertWith S.union module' [object] li }

importTypingForPython3 :: CodeGen ()
importTypingForPython3 = do
    pyVer <- getPythonVersion
    case pyVer of
        Python2 -> return ()
        Python3 -> insertStandardImport "typing"

getPythonVersion :: CodeGen PythonVersion
getPythonVersion = fmap pythonVersion ST.get

renameModulePath :: RenameMap -> ModulePath -> ModulePath
renameModulePath renameMap path' =
    rename (M.toDescList renameMap)
    -- longest paths should be processed first
  where
    rename :: [(ModulePath, ModulePath)] -> ModulePath
    rename ((from, to) : xs) = let r = replacePrefix from to path'
                               in if r == path'
                                  then rename xs
                                  else r
    rename [] = path'

renameMP :: Python -> ModulePath -> ModulePath
renameMP Python { renames = table } = renameModulePath table

thd3 :: (a, b, c) -> c
thd3 (_, _, v) = v

mangleVar :: Code -> T.Text -> Code
mangleVar expr arbitrarySideName = T.concat
    [ "__nirum_"
    , (`T.map` expr) $ \ c -> if 'A' <= c && c <= 'Z' ||
                                 'a' <= c && c <= 'z' || c == '_'
                              then c else '_'
    , "__"
    , arbitrarySideName
    , "__"
    ]

-- | The set of Python reserved keywords.
-- See also: https://docs.python.org/3/reference/lexical_analysis.html#keywords
keywords :: S.Set T.Text
keywords = [ "False", "None", "True"
           , "and", "as", "assert", "break", "class", "continue"
           , "def", "del" , "elif", "else", "except", "finally"
           , "for", "from", "global", "if", "import", "in", "is"
           , "lambda", "nonlocal", "not", "or", "pass", "raise"
           , "return", "try", "while", "with", "yield"
           ]

toClassName :: I.Identifier -> T.Text
toClassName identifier =
    if className `S.member` keywords then className `T.snoc` '_' else className
  where
    className :: T.Text
    className = I.toPascalCaseText identifier

toClassName' :: Name -> T.Text
toClassName' = toClassName . N.facialName

toAttributeName :: I.Identifier -> T.Text
toAttributeName identifier =
    if attrName `S.member` keywords then attrName `T.snoc` '_' else attrName
  where
    attrName :: T.Text
    attrName = I.toSnakeCaseText identifier

toAttributeName' :: Name -> T.Text
toAttributeName' = toAttributeName . N.facialName

toEnumMemberName :: Name -> T.Text
toEnumMemberName name'
  | attributeName `elem` memberKeywords = attributeName `T.snoc` '_'
  | otherwise = attributeName
  where
    memberKeywords :: [T.Text]
    memberKeywords = ["mro"]
    attributeName :: T.Text
    attributeName = toAttributeName' name'

toImportPath' :: ModulePath -> T.Text
toImportPath' = T.intercalate "." . map toAttributeName . toList

toImportPath :: Python -> ModulePath -> T.Text
toImportPath target' = toImportPath' . renameMP target'

toImportPaths :: Python -> S.Set ModulePath -> [T.Text]
toImportPaths target' paths =
    S.toAscList $ S.map toImportPath' $ hierarchies renamedPaths
  where
    renamedPaths :: S.Set ModulePath
    renamedPaths = S.map (renameMP target') paths

toNamePair :: Name -> T.Text
toNamePair (Name f b) = [qq|('{toAttributeName f}', '{I.toSnakeCaseText b}')|]

stringLiteral :: T.Text -> T.Text
stringLiteral string =
    open $ T.concatMap esc string `T.snoc` '"'
  where
    open :: T.Text -> T.Text
    open = if T.any (> '\xff') string then T.append [qq|u"|] else T.cons '"'
    esc :: Char -> T.Text
    esc '"' = "\\\""
    esc '\\' = "\\\\"
    esc '\t' = "\\t"
    esc '\n' = "\\n"
    esc '\r' = "\\r"
    esc c
        | c >= '\x10000' = T.pack $ printf "\\U%08x" c
        | c >= '\xff' = T.pack $ printf "\\u%04x" c
        | c < ' ' || c >= '\x7f' = T.pack $ printf "\\x%02x" c
        | otherwise = T.singleton c

toIndentedCodes :: (a -> T.Text) -> [a] -> T.Text -> T.Text
toIndentedCodes f traversable concatenator =
    T.intercalate concatenator $ map f traversable

compileParameters :: (ParameterName -> ParameterType -> Code)
                  -> [(T.Text, Code, Bool)]
                  -> Code
compileParameters gen nameTypeTriples =
    toIndentedCodes
        (\ (n, t, o) -> gen n t `T.append` if o then "=None" else "")
        nameTypeTriples ", "

compileFieldInitializers :: DS.DeclarationSet Field -> Int -> CodeGen Code
compileFieldInitializers fields depth = do
    initializers <- forM (toList fields) compileFieldInitializer
    return $ T.intercalate indentSpaces initializers
  where
    indentSpaces :: T.Text
    indentSpaces = "\n" `T.append` T.replicate depth "    "
    compileFieldInitializer :: Field -> CodeGen Code
    compileFieldInitializer (Field fieldName' fieldType' _) =
        case fieldType' of
            SetModifier _ ->
                return [qq|self.$attributeName = frozenset($attributeName)|]
            ListModifier _ -> do
                insertThirdPartyImportsA [ ( "nirum.datastructures"
                                           , [("list_type", "List")]
                                           )
                                         ]
                return [qq|self.$attributeName = list_type($attributeName)|]
            _ -> return [qq|self.$attributeName = $attributeName|]
      where
        attributeName :: Code
        attributeName = toAttributeName' fieldName'

compileDocs :: Documented a => a -> Maybe ReStructuredText
compileDocs = fmap render . docsBlock

quoteDocstring :: ReStructuredText -> Code
quoteDocstring rst = T.concat ["r'''", rst, "\n'''\n"]

compileDocstring' :: Documented a => Code -> a -> [ReStructuredText] -> Code
compileDocstring' indentSpace d extra =
    case (compileDocs d, extra) of
        (Nothing, []) -> "\n"
        (result, extra') -> indent indentSpace $ quoteDocstring $
            T.append (fromMaybe "" result) $
                     T.concat ['\n' `T.cons` e `T.snoc` '\n' | e <- extra']

compileDocstring :: Documented a => Code -> a -> Code
compileDocstring indentSpace d = compileDocstring' indentSpace d []

compileDocstringWithFields :: Documented a
                           => Code -> a -> DS.DeclarationSet Field -> Code
compileDocstringWithFields indentSpace decl fields =
    compileDocstring' indentSpace decl extra
  where
    extra :: [ReStructuredText]
    extra =
        [ case compileDocs f of
              Nothing -> T.concat [ ".. attribute:: "
                                  , toAttributeName' n
                                  , "\n"
                                  ]
              Just docs' -> T.concat [ ".. attribute:: "
                                     , toAttributeName' n
                                     , "\n\n"
                                     , indent "   " docs'
                                     ]
        | f@(Field n _ _) <- toList fields
        ]

compileDocsComment :: Documented a => Code -> a -> Code
compileDocsComment indentSpace d =
    case compileDocs d of
        Nothing -> "\n"
        Just rst -> indent (indentSpace `T.append` "#: ") rst

indent :: Code -> Code -> Code
indent space =
    T.intercalate "\n" . map indentLn . T.lines
  where
    indentLn :: Code -> Code
    indentLn line
      | T.null line = T.empty
      | otherwise = space `T.append` line

typeReprCompiler :: CodeGen (Code -> Code)
typeReprCompiler = do
    ver <- getPythonVersion
    case ver of
        Python2 -> return $ \ t -> [qq|($t.__module__ + '.' + $t.__name__)|]
        Python3 -> do
            insertStandardImport "typing"
            return $ \ t -> [qq|typing._type_repr($t)|]

type ParameterName = Code
type ParameterType = Code
type ReturnType = Code

parameterCompiler :: CodeGen (ParameterName -> ParameterType -> Code)
parameterCompiler = do
    ver <- getPythonVersion
    return $ \ n t -> case ver of
                          Python2 -> n
                          Python3 -> [qq|$n: '{t}'|]

returnCompiler :: CodeGen (ReturnType -> Code)
returnCompiler = do
    ver <- getPythonVersion
    return $ \ r ->
        case (ver, r) of
            (Python2, _) -> ""
            (Python3, "None") -> [qq| -> None|]
            (Python3, _) -> [qq| -> '{r}'|]


compileUnionTag :: Source -> Name -> Tag -> CodeGen Code
compileUnionTag source parentname d@(Tag typename' fields _) = do
    typeExprCodes <- mapM (compileTypeExpression source)
        [Just typeExpr | (Field _ typeExpr _) <- toList fields]
    let nameTypeTriples = L.sortBy (compare `on` thd3)
                                   (zip3 tagNames typeExprCodes optionFlags)
        slotTypes = toIndentedCodes
            (\ (n, t, _) -> [qq|('{n}', {t})|]) nameTypeTriples ",\n        "
    insertThirdPartyImportsA
        [ ("nirum.validate", [("validate_union_type", "validate_union_type")])
        , ("nirum.constructs", [("name_dict_type", "NameDict")])
        ]
    arg <- parameterCompiler
    ret <- returnCompiler
    pyVer <- getPythonVersion
    initializers <- compileFieldInitializers fields $ case pyVer of
        Python3 -> 2
        Python2 -> 3
    let initParams = compileParameters arg nameTypeTriples
        inits = case pyVer of
            Python2 -> [qq|
    def __init__(self, **kwargs):
        def __init__($initParams):
            $initializers
            pass
        __init__(**kwargs)
        validate_union_type(self)
            |]
            Python3 -> [qq|
    def __init__(self{ if null nameTypeTriples
                         then T.empty
                         else ", *, " `T.append` initParams }) -> None:
        $initializers
        validate_union_type(self)
            |]
    return [qq|
class $className($parentClass):
{compileDocstringWithFields "    " d fields}
    __slots__ = (
        $slots
    )
    __nirum_type__ = 'union'
    __nirum_tag__ = $parentClass.Tag.{toEnumMemberName typename'}
    __nirum_tag_names__ = name_dict_type([
        $nameMaps
    ])

    @staticmethod
    def __nirum_tag_types__():
        return [$slotTypes]

    { inits :: T.Text }

    def __nirum_serialize__(self):
        return \{
            '_type': '{behindParentTypename}',
            '_tag': '{behindTagName}',
            $fieldSerializers
        \}

    def __repr__(self){ ret "str" }:
        return (
            $parentClass.__module__ + '.$parentClass.$className(' +
            ', '.join('\{0\}=\{1!r\}'.format(attr, getattr(self, attr))
                      for attr in self.__slots__) +
            ')'
        )

    def __eq__(self, other){ ret "bool" }:
        return isinstance(other, $className) and all(
            getattr(self, attr) == getattr(other, attr)
            for attr in self.__slots__
        )

    def __ne__(self, other){ ret "bool" }:
        return not self == other

    def __hash__(self){ ret "int" }:
        return hash($hashTuple)


$parentClass.$className = $className
if hasattr($parentClass, '__qualname__'):
    $className.__qualname__ = $parentClass.__qualname__ + '.{className}'
|]
  where
    optionFlags :: [Bool]
    optionFlags = [ case typeExpr of
                        OptionModifier _ -> True
                        _ -> False
                  | (Field _ typeExpr _) <- toList fields
                  ]
    className :: T.Text
    className = toClassName' typename'
    behindParentTypename :: T.Text
    behindParentTypename = I.toSnakeCaseText $ N.behindName parentname
    tagNames :: [T.Text]
    tagNames = map (toAttributeName' . fieldName) (toList fields)
    behindTagName :: T.Text
    behindTagName = I.toSnakeCaseText $ N.behindName typename'
    slots :: Code
    slots = if length tagNames == 1
            then [qq|'{head tagNames}'|] `T.snoc` ','
            else toIndentedCodes (\ n -> [qq|'{n}'|]) tagNames ",\n        "
    hashTuple :: Code
    hashTuple = if null tagNames
        then "self.__nirum_tag__"
        else [qq|({toIndentedCodes (T.append "self.") tagNames ", "},)|]
    fieldList :: [Field]
    fieldList = toList fields
    nameMaps :: Code
    nameMaps = toIndentedCodes toNamePair
                               (map fieldName fieldList)
                               ",\n        "
    parentClass :: T.Text
    parentClass = toClassName' parentname
    fieldSerializers :: Code
    fieldSerializers = T.intercalate ",\n"
        [ T.concat [ "'", I.toSnakeCaseText (N.behindName fn), "': "
                   , compileSerializer source ft
                                       [qq|self.{toAttributeName' fn}|]
                   ]
        | Field fn ft _ <- fieldList
        ]
compilePrimitiveType :: PrimitiveTypeIdentifier -> CodeGen Code
compilePrimitiveType primitiveTypeIdentifier' = do
    pyVer <- getPythonVersion
    case (primitiveTypeIdentifier', pyVer) of
        (Bool, _) -> return "bool"
        (Bigint, _) -> return "int"
        (Decimal, _) -> do
            insertStandardImport "decimal"
            return "decimal.Decimal"
        (Int32, _) -> return "int"
        (Int64, Python2) -> do
            insertStandardImport "numbers"
            return "numbers.Integral"
        (Int64, Python3) -> return "int"
        (Float32, _) -> return "float"
        (Float64, _) -> return "float"
        (Text, Python2) -> return "unicode"
        (Text, Python3) -> return "str"
        (Binary, _) -> return "bytes"
        (Date, _) -> do
            insertStandardImport "datetime"
            return "datetime.date"
        (Datetime, _) -> do
            insertStandardImport "datetime"
            return "datetime.datetime"
        (Uuid, _) -> insertStandardImport "uuid" >> return "uuid.UUID"
        (Uri, Python2) -> return "unicode"
        (Uri, Python3) -> return "str"

compileTypeExpression :: Source -> Maybe TypeExpression -> CodeGen Code
compileTypeExpression Source { sourcePackage = Package { metadata = meta }
                             , sourceModule = boundModule
                             }
                      (Just (TypeIdentifier i)) =
    case lookupType i boundModule of
        Missing -> fail $ "undefined identifier: " ++ I.toString i
        Imported _ (PrimitiveType p _) -> compilePrimitiveType p
        Imported m _ -> do
            insertThirdPartyImports [(toImportPath target' m, [toClassName i])]
            return $ toClassName i
        Local _ -> return $ toClassName i
  where
    target' :: Python
    target' = target meta
compileTypeExpression source (Just (MapModifier k v)) = do
    kExpr <- compileTypeExpression source (Just k)
    vExpr <- compileTypeExpression source (Just v)
    insertStandardImport "typing"
    return [qq|typing.Mapping[$kExpr, $vExpr]|]
compileTypeExpression source (Just modifier) = do
    expr <- compileTypeExpression source (Just typeExpr)
    insertStandardImport "typing"
    return [qq|typing.$className[$expr]|]
  where
    typeExpr :: TypeExpression
    className :: T.Text
    (typeExpr, className) = case modifier of
        OptionModifier t' -> (t', "Optional")
        SetModifier t' -> (t', "AbstractSet")
        ListModifier t' -> (t', "Sequence")
        TypeIdentifier _ -> undefined  -- never happen!
        MapModifier _ _ -> undefined  -- never happen!
compileTypeExpression _ Nothing =
    return "None"

compileSerializer :: Source -> TypeExpression -> Code -> Code
compileSerializer Source { sourceModule = boundModule } =
    compileSerializer' boundModule

compileSerializer' :: BoundModule Python -> TypeExpression -> Code -> Code
compileSerializer' mod' (OptionModifier typeExpr) pythonVar =
    [qq|(None if ($pythonVar) is None
              else ({compileSerializer' mod' typeExpr pythonVar}))|]
compileSerializer' mod' (SetModifier typeExpr) pythonVar =
    compileSerializer' mod' (ListModifier typeExpr) pythonVar
compileSerializer' mod' (ListModifier typeExpr) pythonVar =
    [qq|list(($serializer) for $elemVar in ($pythonVar))|]
  where
    elemVar :: Code
    elemVar = mangleVar pythonVar "elem"
    serializer :: Code
    serializer = compileSerializer' mod' typeExpr elemVar
compileSerializer' mod' (MapModifier kt vt) pythonVar =
    [qq|list(
        \{
            'key': ({compileSerializer' mod' kt kVar}),
            'value': ({compileSerializer' mod' vt vVar}),
        \}
        for $kVar, $vVar in ($pythonVar).items()
    )|]
  where
    kVar :: Code
    kVar = mangleVar pythonVar "key"
    vVar :: Code
    vVar = mangleVar pythonVar "value"
compileSerializer' mod' (TypeIdentifier typeId) pythonVar =
    case lookupType typeId mod' of
        Missing -> "None"  -- must never happen
        Local (Alias t) -> compileSerializer' mod' t pythonVar
        Imported modulePath' (Alias t) ->
            case resolveBoundModule modulePath' (boundPackage mod') of
                Nothing -> "None"  -- must never happen
                Just foundMod -> compileSerializer' foundMod t pythonVar
        Local PrimitiveType { primitiveTypeIdentifier = p } ->
            compilePrimitiveTypeSerializer p pythonVar
        Imported _ PrimitiveType { primitiveTypeIdentifier = p } ->
            compilePrimitiveTypeSerializer p pythonVar
        Local EnumType {} -> serializerCall
        Imported _ EnumType {} -> serializerCall
        Local RecordType {} -> serializerCall
        Imported _ RecordType {} -> serializerCall
        Local UnboxedType {} -> serializerCall
        Imported _ UnboxedType {} -> serializerCall
        Local UnionType {} -> serializerCall
        Imported _ UnionType {} -> serializerCall
  where
    serializerCall :: Code
    serializerCall = [qq|$pythonVar.__nirum_serialize__()|]

compilePrimitiveTypeSerializer :: PrimitiveTypeIdentifier -> Code -> Code
compilePrimitiveTypeSerializer Bigint var = var
compilePrimitiveTypeSerializer Decimal var = [qq|str($var)|]
compilePrimitiveTypeSerializer Int32 var = var
compilePrimitiveTypeSerializer Int64 var = var
compilePrimitiveTypeSerializer Float32 var = var
compilePrimitiveTypeSerializer Float64 var = var
compilePrimitiveTypeSerializer Text var = var
compilePrimitiveTypeSerializer Binary var =
    [qq|__import__('base64').b64encode($var).decode('ascii')|]
compilePrimitiveTypeSerializer Date var = [qq|($var).isoformat()|]
compilePrimitiveTypeSerializer Datetime var = [qq|($var).isoformat()|]
compilePrimitiveTypeSerializer Bool var = var
compilePrimitiveTypeSerializer Uuid var = [qq|str($var)|]
compilePrimitiveTypeSerializer Uri var = var

compileTypeDeclaration :: Source -> TypeDeclaration -> CodeGen Code
compileTypeDeclaration _ TypeDeclaration { type' = PrimitiveType {} } =
    return ""  -- never used
compileTypeDeclaration src d@TypeDeclaration { typename = typename'
                                             , type' = Alias ctype
                                             } = do
    ctypeExpr <- compileTypeExpression src (Just ctype)
    return $ toStrict $ renderMarkup [compileText|
%{ case compileDocs d }
%{ of Just rst }
#: #{rst}
%{ of Nothing }
%{ endcase }
#{toClassName' typename'} = #{ctypeExpr}
    |]
compileTypeDeclaration src d@TypeDeclaration { typename = typename'
                                             , type' = UnboxedType itype
                                             } = do
    let className = toClassName' typename'
    itypeExpr <- compileTypeExpression src (Just itype)
    insertStandardImport "typing"
    insertThirdPartyImports
        [ ("nirum.validate", ["validate_unboxed_type"])
        , ("nirum.deserialize", ["deserialize_meta"])
        ]
    pyVer <- getPythonVersion
    return $ toStrict $ renderMarkup $ [compileText|
class #{className}(object):
#{compileDocstring "    " d}

    __nirum_type__ = 'unboxed'

    @staticmethod
    def __nirum_get_inner_type__():
        return #{itypeExpr}

%{ case pyVer }
%{ of Python2 }
    def __init__(self, value):
%{ of Python3 }
    def __init__(self, value: '#{itypeExpr}') -> None:
%{ endcase }
        validate_unboxed_type(value, #{itypeExpr})
        self.value = value  # type: #{itypeExpr}

%{ case pyVer }
%{ of Python2 }
    def __ne__(self, other):
        return not self == other

    def __eq__(self, other):
%{ of Python3 }
    def __eq__(self, other) -> bool:
%{ endcase }
        return (isinstance(other, #{className}) and
                self.value == other.value)

%{ case pyVer }
%{ of Python2 }
    def __hash__(self):
%{ of Python3 }
    def __hash__(self) -> int:
%{ endcase }
        return hash(self.value)

    def __nirum_serialize__(self):
        return (#{compileSerializer src itype "self.value"})

    @classmethod
%{ case pyVer }
%{ of Python2 }
    def __nirum_deserialize__(cls, value):
%{ of Python3 }
    def __nirum_deserialize__(cls: type, value: typing.Any) -> '#{className}':
%{ endcase }
        inner_type = cls.__nirum_get_inner_type__()
        deserializer = getattr(inner_type, '__nirum_deserialize__', None)
        if deserializer:
            value = deserializer(value)
        else:
            value = deserialize_meta(inner_type, value)
        return cls(value=value)

%{ case pyVer }
%{ of Python2 }
    def __repr__(self):
        return '{0.__module__}.{0.__name__}({1!r})'.format(
            type(self), self.value
        )
%{ of Python3 }
    def __repr__(self) -> str:
        return '{0}({1!r})'.format(typing._type_repr(type(self)), self.value)
%{ endcase }

%{ case pyVer }
%{ of Python2 }
    def __hash__(self):
%{ of Python3 }
    def __hash__(self) -> int:
%{ endcase }
        return hash(self.value)
|]
compileTypeDeclaration _ d@TypeDeclaration { typename = typename'
                                           , type' = EnumType members
                                           } = do
    let className = toClassName' typename'
    insertStandardImport "enum"
    pyVer <- getPythonVersion
    return $ toStrict $ renderMarkup [compileText|
class #{className}(enum.Enum):
#{compileDocstring "    " d}

%{ forall member@(EnumMember memberName@(Name _ behind) _) <- toList members }
#{compileDocsComment "    " member}
    #{toEnumMemberName memberName} = '#{I.toSnakeCaseText behind}'
%{ endforall }

%{ case pyVer }
%{ of Python2 }
    def __nirum_serialize__(self):
%{ of Python3 }
    def __nirum_serialize__(self) -> str:
%{ endcase }
        return self.value

    @classmethod
%{ case pyVer }
%{ of Python2 }
    def __nirum_deserialize__(cls, value):
%{ of Python3 }
    def __nirum_deserialize__(cls: type, value: str) -> '#{className}':
%{ endcase }
        return cls(value.replace('-', '_'))  # FIXME: validate input


# Since enum.Enum doesn't allow to define non-member when the class is defined,
# __nirum_type__ should be defined after the class is defined.
#{className}.__nirum_type__ = 'enum'
|]
compileTypeDeclaration src d@TypeDeclaration { typename = typename'
                                             , type' = RecordType fields
                                             } = do
    typeExprCodes <- mapM (compileTypeExpression src)
        [Just typeExpr | (Field _ typeExpr _) <- fieldList]
    let nameTypeTriples = L.sortBy (compare `on` thd3)
                                   (zip3 fieldNames typeExprCodes optionFlags)
        slotTypes = toIndentedCodes
            (\ (n, t, _) -> [qq|'{n}': {t}|]) nameTypeTriples ",\n        "
    importTypingForPython3
    insertThirdPartyImports [ ("nirum.validate", ["validate_record_type"])
                            , ("nirum.deserialize", ["deserialize_meta"])
                            ]
    insertThirdPartyImportsA [ ( "nirum.constructs"
                               , [("name_dict_type", "NameDict")]
                               )
                             ]
    arg <- parameterCompiler
    ret <- returnCompiler
    typeRepr <- typeReprCompiler
    pyVer <- getPythonVersion
    initializers <- compileFieldInitializers fields $ case pyVer of
        Python3 -> 2
        Python2 -> 3
    let initParams = compileParameters arg nameTypeTriples
        inits = case pyVer of
            Python2 -> [qq|
    def __init__(self, **kwargs):
        def __init__($initParams):
            $initializers
            pass
        __init__(**kwargs)
        validate_record_type(self)
            |]
            Python3 -> [qq|
    def __init__(self{ if null nameTypeTriples
                         then T.empty
                         else ", *, " `T.append` initParams }) -> None:
        $initializers
        validate_record_type(self)
            |]
    let clsType = arg "cls" "type"
    return [qq|
class $className(object):
{compileDocstringWithFields "    " d fields}
    __slots__ = (
        $slots,
    )
    __nirum_type__ = 'record'
    __nirum_record_behind_name__ = '{behindTypename}'
    __nirum_field_names__ = name_dict_type([$nameMaps])

    @staticmethod
    def __nirum_field_types__():
        return \{$slotTypes\}

    {inits :: T.Text}

    def __repr__(self){ret "bool"}:
        return '\{0\}(\{1\})'.format(
            {typeRepr "type(self)"},
            ', '.join('\{\}=\{\}'.format(attr, getattr(self, attr))
                      for attr in self.__slots__)
        )

    def __eq__(self, other){ret "bool"}:
        return isinstance(other, $className) and all(
            getattr(self, attr) == getattr(other, attr)
            for attr in self.__slots__
        )

    def __ne__(self, other){ ret "bool" }:
        return not self == other

    def __nirum_serialize__(self):
        return \{
            '_type': '{behindTypename}',
            $fieldSerializers
        \}

    @classmethod
    def __nirum_deserialize__($clsType, value){ ret className }:
        if '_type' not in value:
            raise ValueError('"_type" field is missing.')
        if not cls.__nirum_record_behind_name__ == value['_type']:
            raise ValueError(
                '%s expect "_type" equal to "%s"'
                ', but found %s.' % (
                    typing._type_repr(cls),
                    cls.__nirum_record_behind_name__,
                    value['_type']
                )
            )
        args = dict()
        behind_names = cls.__nirum_field_names__.behind_names
        field_types = cls.__nirum_field_types__
        if callable(field_types):
            field_types = field_types()
            # old compiler could generate non-callable dictionary
        errors = set()
        for attribute_name, item in value.items():
            if attribute_name == '_type':
                continue
            if attribute_name in behind_names:
                name = behind_names[attribute_name]
            else:
                name = attribute_name
            try:
                args[name] = deserialize_meta(field_types[name], item)
            except ValueError as e:
                errors.add('%s: %s' % (attribute_name, str(e)))
        if errors:
            raise ValueError('\\n'.join(sorted(errors)))
        return cls(**args)

    def __hash__(self){ret "int"}:
        return hash(($hashText,))
|]
  where
    className :: T.Text
    className = toClassName' typename'
    fieldList :: [Field]
    fieldList = toList fields
    behindTypename :: T.Text
    behindTypename = I.toSnakeCaseText $ N.behindName typename'
    optionFlags :: [Bool]
    optionFlags = [ case typeExpr of
                        OptionModifier _ -> True
                        _ -> False
                  | (Field _ typeExpr _) <- fieldList
                  ]
    fieldNames :: [T.Text]
    fieldNames = [toAttributeName' name' | Field name' _ _ <- fieldList]
    slots :: Code
    slots = toIndentedCodes (\ n -> [qq|'{n}'|]) fieldNames ",\n        "
    nameMaps :: Code
    nameMaps = toIndentedCodes
        toNamePair
        (map fieldName $ toList fields)
        ",\n        "
    hashText :: Code
    hashText = toIndentedCodes (\ n -> [qq|self.{n}|]) fieldNames ", "
    fieldSerializers :: Code
    fieldSerializers = T.intercalate ",\n"
        [ T.concat [ "'", I.toSnakeCaseText (N.behindName fn), "': "
                   , compileSerializer src ft [qq|self.{ toAttributeName' fn}|]
                   ]
        | Field fn ft _ <- fieldList
        ]
compileTypeDeclaration src
                       d@TypeDeclaration { typename = typename'
                                         , type' = UnionType tags
                                         , typeAnnotations = annotations
                                         } = do
    tagCodes <- mapM (compileUnionTag src typename') $ toList tags
    let className = toClassName' typename'
        tagCodes' = T.intercalate "\n\n" tagCodes
        tagClasses = T.intercalate ", " [ toClassName' tagName
                                        | Tag tagName _ _ <- toList tags
                                        ]
        enumMembers = toIndentedCodes
            (\ (t, b) -> [qq|$t = '{b}'|]) enumMembers' "\n        "
    importTypingForPython3
    insertStandardImport "enum"
    insertThirdPartyImports [ ("nirum.deserialize", ["deserialize_meta"])
                            ]
    insertThirdPartyImportsA [ ( "nirum.constructs"
                               , [("name_dict_type", "NameDict")]
                               )
                             ]
    insertThirdPartyImportsA [ ( "nirum.datastructures"
                               , [("map_type", "Map")]
                               )
                             ]
    typeRepr <- typeReprCompiler
    ret <- returnCompiler
    arg <- parameterCompiler
    return [qq|
class $className({T.intercalate "," $ compileExtendClasses annotations}):
{compileDocstring "    " d}

    __nirum_type__ = 'union'
    __nirum_union_behind_name__ = '{I.toSnakeCaseText $ N.behindName typename'}'
    __nirum_field_names__ = name_dict_type([$nameMaps])

    class Tag(enum.Enum):
        $enumMembers

    def __init__(self, *args, **kwargs):
        raise NotImplementedError(
            "\{0\} cannot be instantiated "
            "since it is an abstract class.  Instantiate a concrete subtype "
            "of it instead.".format({typeRepr "type(self)"})
        )

    def __nirum_serialize__(self):
        raise NotImplementedError(
            "\{0\} cannot be instantiated "
            "since it is an abstract class.  Instantiate a concrete subtype "
            "of it instead.".format({typeRepr "type(self)"})
        )

    @classmethod
    def __nirum_deserialize__(
        {arg "cls" "type"}, value
    ){ ret className }:
        if '_type' not in value:
            raise ValueError('"_type" field is missing.')
        if '_tag' not in value:
            raise ValueError('"_tag" field is missing.')
        if not hasattr(cls, '__nirum_tag__'):
            for sub_cls in cls.__subclasses__():
                if sub_cls.__nirum_tag__.value == value['_tag']:
                    cls = sub_cls
                    break
            else:
                raise ValueError(
                    '%r is not deserialzable tag of `%s`' % (
                        value, typing._type_repr(cls)
                    )
                )
        if not cls.__nirum_union_behind_name__ == value['_type']:
            raise ValueError(
                '%s expect "_type" equal to "%s", but found %s' % (
                    typing._type_repr(cls),
                    cls.__nirum_union_behind_name__,
                    value['_type']
                )
            )
        if not cls.__nirum_tag__.value == value['_tag']:
            raise ValueError(
                '%s expect "_tag" equal to "%s", but found %s' % (
                    typing._type_repr(cls),
                    cls.__nirum_tag__.value,
                    cls
                )
            )
        args = dict()
        behind_names = cls.__nirum_tag_names__.behind_names
        errors = set()
        for attribute_name, item in value.items():
            if attribute_name in ('_type', '_tag'):
                continue
            if attribute_name in behind_names:
                name = behind_names[attribute_name]
            else:
                name = attribute_name
            tag_types = dict(cls.__nirum_tag_types__())
            try:
                args[name] = deserialize_meta(tag_types[name], item)
            except ValueError as e:
                errors.add('%s: %s' % (attribute_name, str(e)))
        if errors:
            raise ValueError('\\n'.join(sorted(errors)))
        return cls(**args)


$tagCodes'

$className.__nirum_tag_classes__ = map_type(
    (tcls.__nirum_tag__, tcls)
    for tcls in [$tagClasses]
)
            |]
  where
    enumMembers' :: [(T.Text, T.Text)]
    enumMembers' = [ ( toEnumMemberName tagName
                     , I.toSnakeCaseText $ N.behindName tagName
                     )
                   | (Tag tagName _ _) <- toList tags
                   ]
    nameMaps :: T.Text
    nameMaps = toIndentedCodes
        toNamePair
        [name' | Tag name' _ _ <- toList tags]
        ",\n        "
    compileExtendClasses :: A.AnnotationSet -> [Code]
    compileExtendClasses annotations' =
        if null extendClasses
            then ["object"]
            else extendClasses
      where
        extendsClassMap :: M.Map I.Identifier Code
        extendsClassMap = [("error", "Exception")]
        extendClasses :: [Code]
        extendClasses = catMaybes
            [ M.lookup annotationName extendsClassMap
            | (A.Annotation annotationName _) <- A.toList annotations'
            ]
compileTypeDeclaration
    src@Source { sourcePackage = Package { metadata = metadata' } }
    d@ServiceDeclaration { serviceName = name'
                         , service = Service methods
                         } = do
    let methods' = toList methods
    methodMetadata <- mapM compileMethodMetadata methods'
    let methodMetadata' = commaNl methodMetadata
    dummyMethods <- mapM compileMethod methods'
    clientMethods <- mapM compileClientMethod methods'
    methodErrorTypes <- mapM compileErrorType methods'
    let dummyMethods' = T.intercalate "\n\n" dummyMethods
        clientMethods' = T.intercalate "\n\n" clientMethods
        methodErrorTypes' =
            T.intercalate "," $ catMaybes methodErrorTypes
    param <- parameterCompiler
    ret <- returnCompiler
    insertStandardImport "json"
    insertThirdPartyImports [ ("nirum.deserialize", ["deserialize_meta"])
                            ]
    insertThirdPartyImportsA
        [ ("nirum.constructs", [("name_dict_type", "NameDict")])
        , ("nirum.datastructures", [(nirumMapName, "Map")])
        , ("nirum.service", [("service_type", "Service")])
        , ("nirum.transport", [("transport_type", "Transport")])
        ]
    return [qq|
class $className(service_type):
{compileDocstring "    " d}
    __nirum_type__ = 'service'
    __nirum_schema_version__ = \'{SV.toText $ version metadata'}\'
    __nirum_service_methods__ = \{
        {methodMetadata'}
    \}
    __nirum_method_names__ = name_dict_type([
        $methodNameMap
    ])
    __nirum_method_annotations__ = $methodAnnotations'

    @staticmethod
    def __nirum_method_error_types__(k, d=None):
        return dict([
            $methodErrorTypes'
        ]).get(k, d)

    {dummyMethods'}


# FIXME client MUST be generated & saved on diffrent module
#       where service isn't included.
class {className}_Client($className):
    """The client object of :class:`{className}`."""

    def __init__(self,
                 { param "transport" "transport_type" }){ ret "None" }:
        if not isinstance(transport, transport_type):
            raise TypeError(
                'expected an instance of \{0.__module__\}.\{0.__name__\}, not '
                '\{1!r\}'.format(transport_type, transport)
            )
        self.__nirum_transport__ = transport  # type: transport_type

    {clientMethods'}

{className}.Client = {className}_Client
{className}.Client.__name__ = 'Client'
if hasattr({className}.Client, '__qualname__'):
    {className}.Client.__qualname__ = '{className}.Client'
|]
  where
    nirumMapName :: T.Text
    nirumMapName = "map_type"
    className :: T.Text
    className = toClassName' name'
    commaNl :: [T.Text] -> T.Text
    commaNl = T.intercalate ",\n"
    compileErrorType :: Method -> CodeGen (Maybe Code)
    compileErrorType (Method mn _ _ me _) =
        case me of
            Just errorTypeExpression -> do
                et <- compileTypeExpression src (Just errorTypeExpression)
                return $ Just [qq|('{toAttributeName' mn}', $et)|]
            Nothing -> return Nothing
    compileMethod :: Method -> CodeGen Code
    compileMethod m@(Method mName params rtype _etype _anno) = do
        let mName' = toAttributeName' mName
        params' <- mapM compileMethodParameter $ toList params
        let paramDocs = [ T.concat [ ":param "
                                   , toAttributeName' pName
                                   , maybe "" (T.append ": ") $ compileDocs p
                                   -- TODO: types
                                   ]
                        | p@(Parameter pName _ _) <- toList params
                        ]
        rtypeExpr <- compileTypeExpression src rtype
        ret <- returnCompiler
        return [qq|
    def {mName'}(self, {commaNl params'}){ ret rtypeExpr }:
{compileDocstring' "        " m paramDocs}
        raise NotImplementedError('$className has to implement {mName'}()')
|]
    compileMethodParameter :: Parameter -> CodeGen Code
    compileMethodParameter (Parameter pName pType _) = do
        pTypeExpr <- compileTypeExpression src (Just pType)
        arg <- parameterCompiler
        return [qq|{arg (toAttributeName' pName) pTypeExpr}|]
    compileMethodMetadata :: Method -> CodeGen Code
    compileMethodMetadata Method { methodName = mName
                                 , parameters = params
                                 , returnType = rtype
                                 } = do
        let params' = toList params :: [Parameter]
        rtypeExpr <- compileTypeExpression src rtype
        paramMetadata <- mapM compileParameterMetadata params'
        let paramMetadata' = commaNl paramMetadata
        insertThirdPartyImportsA
            [("nirum.constructs", [("name_dict_type", "NameDict")])]
        return [qq|'{toAttributeName' mName}': \{
            '_v': 2,
            '_return': lambda: $rtypeExpr,
            '_names': name_dict_type([{paramNameMap params'}]),
            {paramMetadata'}
        \}|]
    methodList :: [Method]
    methodList = toList methods
    compileParameterMetadata :: Parameter -> CodeGen Code
    compileParameterMetadata (Parameter pName pType _) = do
        let pName' = toAttributeName' pName
        pTypeExpr <- compileTypeExpression src (Just pType)
        return [qq|'{pName'}': lambda: $pTypeExpr|]
    methodNameMap :: T.Text
    methodNameMap = toIndentedCodes
        toNamePair
        [mName | Method { methodName = mName } <- methodList]
        ",\n        "
    paramNameMap :: [Parameter] -> T.Text
    paramNameMap params = toIndentedCodes
        toNamePair [pName | Parameter pName _ _ <- params] ",\n        "
    compileClientPayload :: Parameter -> CodeGen Code
    compileClientPayload (Parameter pName pt _) = do
        let pName' = toAttributeName' pName
        return [qq|'{I.toSnakeCaseText $ N.behindName pName}':
                   ({compileSerializer src pt pName'})|]
    compileClientMethod :: Method -> CodeGen Code
    compileClientMethod Method { methodName = mName
                               , parameters = params
                               , returnType = rtype
                               , errorType = etypeM
                               } = do
        let clientMethodName' = toAttributeName' mName
        params' <- mapM compileMethodParameter $ toList params
        rtypeExpr <- compileTypeExpression src rtype
        errorCode <- case etypeM of
             Just e -> do
                e' <- compileTypeExpression src (Just e)
                return $ "result_type = " `T.append` e'
             Nothing ->
                return "raise UnexpectedNirumResponseError(serialized)"
        payloadArguments <- mapM compileClientPayload $ toList params
        ret <- returnCompiler
        return [qq|
    def {clientMethodName'}(self, {commaNl params'}){ret rtypeExpr}:
        successful, serialized = self.__nirum_transport__(
            '{I.toSnakeCaseText $ N.behindName mName}',
            payload=\{{commaNl payloadArguments}\},
            # FIXME Give annotations.
            service_annotations=\{\},
            method_annotations=self.__nirum_method_annotations__,
            parameter_annotations=\{\}
        )
        if successful:
            result_type = $rtypeExpr
        else:
            $errorCode
        if result_type is None:
            result = None
        else:
            result = deserialize_meta(result_type, serialized)
        if successful:
            return result
        raise result
|]
    toKeyItem :: I.Identifier -> T.Text -> T.Text
    toKeyItem ident v = [qq|'{toAttributeName ident}': {v}|]
    wrapMap :: T.Text -> T.Text
    wrapMap items = [qq|$nirumMapName(\{$items\})|]
    compileAnnotation :: I.Identifier -> A.AnnotationArgumentSet -> T.Text
    compileAnnotation ident annoArgument =
        toKeyItem ident $
            wrapMap $ T.intercalate ","
                [ toKeyStr ident' value :: T.Text
                | (ident', value) <- M.toList annoArgument
                ]
      where
        escapeSingle :: T.Text -> T.Text
        escapeSingle = T.strip . T.replace "'" "\\'"
        toKeyStr :: I.Identifier -> T.Text -> T.Text
        toKeyStr k v =
            [qq|'{toAttributeName k}': u'''{escapeSingle v}'''|]
    compileMethodAnnotation :: Method -> T.Text
    compileMethodAnnotation Method { methodName = mName
                                   , methodAnnotations = annoSet
                                   } =
        toKeyItem (N.facialName mName) $ wrapMap annotationDict
      where
        annotationDict :: T.Text
        annotationDict = T.intercalate ","
            [ compileAnnotation ident annoArgSet :: T.Text
            | (ident, annoArgSet) <- M.toList $ A.annotations annoSet
            ]
    methodAnnotations' :: T.Text
    methodAnnotations' = wrapMap $ commaNl $ map compileMethodAnnotation
        methodList

compileTypeDeclaration _ Import {} =
    return ""  -- Nothing to compile

compileModuleBody :: Source -> CodeGen Code
compileModuleBody src@Source { sourceModule = boundModule } = do
    let types' = boundTypes boundModule
    typeCodes <- mapM (compileTypeDeclaration src) $ toList types'
    return $ T.intercalate "\n\n" typeCodes

data InstallRequires =
    InstallRequires { dependencies :: S.Set T.Text
                    , optionalDependencies :: M.Map (Int, Int) (S.Set T.Text)
                    } deriving (Eq, Ord, Show)

addDependency :: InstallRequires -> T.Text -> InstallRequires
addDependency requires package =
    requires { dependencies = S.insert package $ dependencies requires }

addOptionalDependency :: InstallRequires
                      -> (Int, Int)       -- ^ Python version already stasified
                      -> T.Text           -- ^ PyPI package name
                      -> InstallRequires
addOptionalDependency requires pyVer package =
    requires { optionalDependencies = newOptDeps }
  where
    oldOptDeps :: M.Map (Int, Int) (S.Set T.Text)
    oldOptDeps = optionalDependencies requires
    newOptDeps :: M.Map (Int, Int) (S.Set T.Text)
    newOptDeps = M.alter (Just . S.insert package . fromMaybe S.empty)
                         pyVer oldOptDeps

unionInstallRequires :: InstallRequires -> InstallRequires -> InstallRequires
unionInstallRequires a b =
    a { dependencies = S.union (dependencies a) (dependencies b)
      , optionalDependencies = M.unionWith S.union (optionalDependencies a)
                                                   (optionalDependencies b)
      }

compileModule :: PythonVersion
              -> Source
              -> Either CompileError' (InstallRequires, Code)
compileModule pythonVersion' source = do
    let (result, context) = runCodeGen (compileModuleBody source)
                                       (empty pythonVersion')
    let deps = require "nirum" "nirum" $ M.keysSet $ thirdPartyImports context
    let optDeps =
            [ ((3, 4), require "enum34" "enum" $ standardImports context)
            , ((3, 5), require "typing" "typing" $ standardImports context)
            ]
    let installRequires = InstallRequires deps optDeps
    let fromImports = M.assocs (localImportsMap context) ++
                      M.assocs (thirdPartyImports context)
    code <- result
    return $ (,) installRequires $ toStrict $ renderMarkup $
        [compileText|# -*- coding: utf-8 -*-
#{compileDocstring "" $ sourceModule source}
%{ forall i <- S.elems (standardImports context) }
import #{i}
%{ endforall }

%{ forall (from, nameMap) <- fromImports }
from #{from} import (
%{ forall (alias, name) <- M.assocs nameMap }
%{ if (alias == name) }
    #{name},
%{ else }
    #{name} as #{alias},
%{ endif }
%{ endforall }
)
%{ endforall }

#{code}
|]
  where
    has :: S.Set T.Text -> T.Text -> Bool
    has set module' = module' `S.member` set ||
                      any (T.isPrefixOf $ module' `T.snoc` '.') set
    require :: T.Text -> T.Text -> S.Set T.Text -> S.Set T.Text
    require pkg module' set =
        if set `has` module' then S.singleton pkg else S.empty

compilePackageMetadata :: Package' -> InstallRequires -> Code
compilePackageMetadata Package
                           { metadata = Metadata
                                 { authors = authors'
                                 , version = version'
                                 , description = description'
                                 , license = license'
                                 , MD.keywords = keywords'
                                 , target = target'@Python
                                       { packageName = packageName'
                                       , minimumRuntimeVersion = minRuntimeVer
                                       }
                                 }
                           , modules = modules'
                           }
                       (InstallRequires deps optDeps) =
    toStrict $ renderMarkup [compileText|# -*- coding: utf-8 -*-
import sys

from setuptools import setup, __version__ as setuptools_version

install_requires = [
%{ forall p <- S.toList deps }
%{ if (p == "nirum") }
    'nirum >= #{SV.toText minRuntimeVer}',
%{ else }
    (#{stringLiteral p}),
%{ endif }
%{ endforall }
]
polyfill_requires = {
%{ forall ((major, minor), deps') <- M.toList optDeps }
    (#{major}, #{minor}): [
%{ forall dep' <- S.toList deps' }
        (#{stringLiteral dep'}),
%{ endforall }
    ],
%{ endforall }
}

%{ if (not $ M.null optDeps) }
# '<' operator for environment markers are supported since setuptools 17.1.
# Read PEP 496 for details of environment markers.
setup_requires = ['setuptools >= 17.1']
if tuple(map(int, setuptools_version.split('.'))) < (17, 1):
    extras_require = {}
    if 'bdist_wheel' not in sys.argv:
        for (major, minor), deps in polyfill_requires.items():
            if sys.version_info < (major, minor):
                install_requires.extend(deps)
    envmarker = ":python_version=='{0}.{1}'"
    python_versions = [(2, 6), (2, 7),
                       (3, 3), (3, 4), (3, 5), (3, 6)]  # FIXME
    for pyver in python_versions:
        extras_require[envmarker.format(*pyver)] = list({
            d
            for v, vdeps in polyfill_requires.items()
            if pyver < v
            for d in vdeps
        })
else:
    extras_require = {
        ":python_version<'{0}.{1}'".format(*pyver): deps
        for pyver, deps in polyfill_requires.items()
    }
%{ else }
setup_requires = []
extras_require = {}
%{ endif }


SOURCE_ROOT = #{stringLiteral $ sourceDirectory Python3}

if sys.version_info < (3, 0):
    SOURCE_ROOT = #{stringLiteral $ sourceDirectory Python2}

# TODO: long_description, url, classifiers
setup(
    name=#{stringLiteral packageName'},
    version=#{stringLiteral $ SV.toText version'},
    description=#{nStringLiteral description'},
    license=#{nStringLiteral license'},
    keywords=#{stringLiteral $ T.intercalate " " keywords'},
    author=', '.join([
%{ forall Author { name = name } <- authors' }
    (#{stringLiteral name}),
%{ endforall }
    ]),
    author_email=', '.join([
%{ forall Author { email = email' } <- authors' }
%{ case email' }
%{ of Just authorEmail }
    (#{stringLiteral $ decodeUtf8 $ E.toByteString authorEmail}),
%{ of Nothing }
%{ endcase }
%{ endforall }
    ]),
    package_dir={'': SOURCE_ROOT},
    packages=[#{strings $ toImportPaths target' $ MS.keysSet modules'}],
    provides=[#{strings $ toImportPaths target' $ MS.keysSet modules'}],
    requires=[#{strings $ S.toList deps}],
    setup_requires=setup_requires,
    install_requires=install_requires,
    extras_require=extras_require,
)
|]
  where
    nStringLiteral :: Maybe T.Text -> T.Text
    nStringLiteral (Just value) = stringLiteral value
    nStringLiteral Nothing = "None"
    strings :: [Code] -> Code
    strings values = T.intercalate ", " $ map stringLiteral (L.sort values)

manifestIn :: Code
manifestIn = [q|recursive-include src *.py
recursive-include src-py2 *.py
|]

compilePackage' :: Package'
                -> M.Map FilePath (Either CompileError' Code)
compilePackage' package@Package { metadata = Metadata { target = target' } } =
    M.fromList $
        initFiles ++
        [ ( f
          , case cd of
                Left e -> Left e
                Right (_, cd') -> Right cd'
          )
        | (f, cd) <- modules'
        ] ++
        [ ("setup.py", Right $ compilePackageMetadata package installRequires)
        , ("MANIFEST.in", Right manifestIn)
        ]
  where
    toPythonFilename :: ModulePath -> [FilePath]
    toPythonFilename mp = [ T.unpack (toAttributeName i)
                          | i <- toList $ renameMP target' mp
                          ] ++ ["__init__.py"]
    versions :: [PythonVersion]
    versions = [Python2, Python3]
    toFilename :: T.Text -> ModulePath -> FilePath
    toFilename sourceRootDirectory mp =
        joinPath $ T.unpack sourceRootDirectory : toPythonFilename mp
    initFiles :: [(FilePath, Either CompileError' Code)]
    initFiles = [ (toFilename (sourceDirectory ver) mp', Right "")
                | mp <- MS.keys (modules package)
                , mp' <- S.elems (hierarchy mp)
                , ver <- versions
                ]
    modules' :: [(FilePath, Either CompileError' (InstallRequires, Code))]
    modules' =
        [ ( toFilename (sourceDirectory ver) modulePath'
          , compileModule ver $ Source package boundModule
          )
        | (modulePath', _) <- MS.toAscList (modules package)
        , Just boundModule <- [resolveBoundModule modulePath' package]
        , ver <- versions
        ]
    installRequires :: InstallRequires
    installRequires = foldl unionInstallRequires
                            (InstallRequires [] [])
                            [deps | (_, Right (deps, _)) <- modules']

parseModulePath :: T.Text -> Maybe ModulePath
parseModulePath string =
    mapM I.fromText identTexts >>= fromIdentifiers
  where
    identTexts :: [T.Text]
    identTexts = T.split (== '.') string

instance Target Python where
    type CompileResult Python = Code
    type CompileError Python = CompileError'
    targetName _ = "python"
    parseTarget table = do
        name' <- stringField "name" table
        minRuntime <- case versionField "minimum_runtime" table of
            Left (FieldError _) -> Right minimumRuntime
            otherwise' -> otherwise'
        renameTable <- case tableField "renames" table of
            Right t -> Right t
            Left (FieldError _) -> Right HM.empty
            otherwise' -> otherwise'
        renamePairs <- sequence
            [ case (parseModulePath k, v) of
                  (Just modulePath', VString v') -> case parseModulePath v' of
                      Just altPath -> Right (modulePath', altPath)
                      Nothing -> Left $ FieldValueError [qq|renames.$k|]
                          [qq|expected a module path, not "$v'"|]
                  (Nothing, _) -> Left $ FieldValueError [qq|renams.$k|]
                      [qq|expected a module path as a key, not "$k"|]
                  _ -> Left $ FieldTypeError [qq|renames.$k|] "string" $
                                             fieldType v
            | (k, v) <- HM.toList renameTable
            ]
        return Python { packageName = name'
                      , minimumRuntimeVersion = max minRuntime minimumRuntime
                      , renames = M.fromList renamePairs
                      }
    compilePackage = compilePackage'
    showCompileError _ e = e
    toByteString _ = encodeUtf8