packages feed

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

{-# LANGUAGE ExtendedDefaultRules, OverloadedLists, OverloadedStrings,
             QuasiQuotes #-}
module Nirum.Targets.Python ( Code
                            , CodeGen( code
                                     , localImports
                                     , standardImports
                                     , thirdPartyImports
                                     )
                            , CompileError
                            , InstallRequires ( InstallRequires
                                              , dependencies
                                              , optionalDependencies
                                              )
                            , Source( Source
                                    , sourceModule
                                    , sourcePackage
                                    )
                            , addDependency
                            , addOptionalDependency
                            , compileError
                            , compileModule
                            , compilePackage
                            , compilePrimitiveType
                            , compileTypeDeclaration
                            , compileTypeExpression
                            , hasError
                            , toAttributeName
                            , toClassName
                            , toImportPath
                            , toNamePair
                            , unionInstallRequires
                            , withLocalImport
                            , withStandardImport
                            , withThirdPartyImports
                            ) where

import qualified Data.List as L
import Data.Maybe (fromMaybe)
import GHC.Exts (IsList(toList))

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import System.FilePath (joinPath)
import Text.InterpolatedString.Perl6 (qq)

import qualified Nirum.Constructs.DeclarationSet as DS
import Nirum.Constructs.Identifier ( Identifier
                                   , toPascalCaseText
                                   , toSnakeCaseText
                                   , toString
                                   )
import Nirum.Constructs.ModulePath (ModulePath, ancestors)
import Nirum.Constructs.Name (Name(Name))
import qualified Nirum.Constructs.Name as N
import Nirum.Constructs.Service ( Method(Method, methodName)
                                , Parameter(Parameter)
                                , Service(Service)
                                )
import Nirum.Constructs.TypeDeclaration ( EnumMember(EnumMember)
                                        , Field(Field)
                                        , PrimitiveTypeIdentifier(..)
                                        , Tag(Tag)
                                        , Type( Alias
                                              , BoxedType
                                              , EnumType
                                              , PrimitiveType
                                              , RecordType
                                              , UnionType
                                              )
                                        , TypeDeclaration( Import
                                                         , ServiceDeclaration
                                                         , TypeDeclaration
                                                         )
                                        )
import Nirum.Constructs.TypeExpression ( TypeExpression( ListModifier
                                                       , MapModifier
                                                       , OptionModifier
                                                       , SetModifier
                                                       , TypeIdentifier
                                                       )
                                       )
import Nirum.Package ( BoundModule
                     , Package(modules)
                     , TypeLookup(Imported, Local, Missing)
                     , lookupType
                     , resolveBoundModule
                     , types
                     )

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

type Code = T.Text
type CompileError = T.Text

data CodeGen a = CodeGen { standardImports :: S.Set T.Text
                         , thirdPartyImports :: M.Map T.Text (S.Set T.Text)
                         , localImports :: M.Map T.Text (S.Set T.Text)
                         , code :: a
                         }
               | CodeGenError CompileError
               deriving (Eq, Ord, Show)

instance Functor CodeGen where
    fmap f codeGen = pure f <*> codeGen

instance Applicative CodeGen where
    pure = return
    c@CodeGen { code = f } <*> codeGen = codeGen >>= \x -> c { code = f x }
    (CodeGenError m) <*> _ = CodeGenError m

instance Monad CodeGen where
    return code' = CodeGen { standardImports = []
                           , thirdPartyImports = []
                           , localImports = []
                           , code = code'
                           }
    (CodeGen si ti li c) >>= f = case f c of
        (CodeGen si' ti' li' code') ->
            let stdImports = S.union si si'
                thirdPartyImports' = M.unionWith S.union ti ti'
                localImports' = M.unionWith S.union li li'
            in
                CodeGen stdImports thirdPartyImports' localImports' code'
        (CodeGenError m) -> CodeGenError m
    (CodeGenError m) >>= _ = CodeGenError m

    fail = CodeGenError . T.pack

hasError :: CodeGen a -> Bool
hasError (CodeGenError _) = True
hasError _ = False

compileError :: CodeGen a -> Maybe CompileError
compileError CodeGen {} = Nothing
compileError (CodeGenError m) = Just m

withStandardImport :: T.Text -> CodeGen a -> CodeGen a
withStandardImport module' c@CodeGen { standardImports = si } =
    c { standardImports = S.insert module' si }
withStandardImport _ c@(CodeGenError _) = c

withThirdPartyImports :: [(T.Text, S.Set T.Text)] -> CodeGen a -> CodeGen a
withThirdPartyImports imports c@CodeGen { thirdPartyImports = ti } =
    c { thirdPartyImports = L.foldl (M.unionWith S.union) ti importList }
  where
    importList :: [M.Map T.Text (S.Set T.Text)]
    importList = map (uncurry M.singleton) imports
withThirdPartyImports _ c@(CodeGenError _) = c

withLocalImport :: T.Text -> T.Text -> CodeGen a -> CodeGen a
withLocalImport module' object c@CodeGen { localImports = li } =
    c { localImports = M.insertWith S.union module' [object] li }
withLocalImport _ _ c@(CodeGenError _) = c

-- | 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 :: Identifier -> T.Text
toClassName identifier =
    if className `S.member` keywords then className `T.snoc` '_' else className
  where
    className :: T.Text
    className = toPascalCaseText identifier

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

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

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

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

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

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


compileUnionTag :: Source
                -> Name
                -> Name
                -> DS.DeclarationSet Field
                -> CodeGen Code
compileUnionTag source parentname typename fields = do
    typeExprCodes <- mapM (compileTypeExpression source)
        [typeExpr | (Field _ typeExpr _) <- toList fields]
    let className = toClassName' typename
        tagNames = map toAttributeName' [ name
                                        | (Field name _ _) <- toList fields
                                        ]
        nameNTypes = zip tagNames typeExprCodes
        slotTypes = toIndentedCodes
            (\(n, t) -> [qq|'{n}': {t}|]) nameNTypes ",\n        "
        slots = toIndentedCodes (\n -> [qq|'{n}'|]) tagNames ",\n        "
        initialArgs = toIndentedCodes
            (\(n, t) -> [qq|{n}: {t}|]) nameNTypes ", "
        initialValues =
            toIndentedCodes (\n -> [qq|self.{n} = {n}|]) tagNames "\n        "
        nameMaps = toIndentedCodes
            toNamePair
            [name | Field name _ _ <- toList fields]
            ",\n        "
        parentClass = toClassName' parentname
    withStandardImport "typing" $
        withThirdPartyImports [ ("nirum.validate", ["validate_union_type"])
                              , ("nirum.constructs", ["name_dict_type"])
                              ] $
            return [qq|
class $className($parentClass):
    # TODO: docstring

    __slots__ = (
        $slots,
    )
    __nirum_tag__ = $parentClass.Tag.{toAttributeName' typename}
    __nirum_tag_types__ = \{
        $slotTypes
    \}
    __nirum_tag_names__ = name_dict_type([
        $nameMaps
    ])

    def __init__(self, $initialArgs) -> None:
        $initialValues
        validate_union_type(self)

    def __repr__(self) -> str:
        return '\{0.__module__\}.\{0.__qualname__\}(\{1\})'.format(
            type(self),
            ', '.join('\{\}=\{\}'.format(attr, getattr(self, attr))
                      for attr in self.__slots__)
        )

    def __eq__(self, other) -> bool:
        return isinstance(other, $className) and all(
            getattr(self, attr) == getattr(other, attr)
            for attr in self.__slots__
        )
            |]

compilePrimitiveType :: PrimitiveTypeIdentifier -> CodeGen Code
compilePrimitiveType primitiveTypeIdentifier =
    case primitiveTypeIdentifier of
        Bool -> return "bool"
        Bigint -> return "int"
        Decimal -> withStandardImport "decimal" $ return "decimal.Decimal"
        Int32 -> return "int"
        Int64 -> return "int"
        Float32 -> return "float"
        Float64 -> return "float"
        Text -> return "str"
        Binary -> return "bytes"
        Date -> withStandardImport "datetime" $ return "datetime.date"
        Datetime -> withStandardImport "datetime" $ return "datetime.datetime"
        Uuid -> withStandardImport "uuid" $ return"uuid.UUID"
        Uri -> return "str"

compileTypeExpression :: Source -> TypeExpression -> CodeGen Code
compileTypeExpression Source { sourceModule = boundModule } (TypeIdentifier i) =
    case lookupType i boundModule of
        Missing -> fail $ "undefined identifier: " ++ toString i
        Imported _ (PrimitiveType p _) -> compilePrimitiveType p
        Imported m _ ->
            withThirdPartyImports [(toImportPath m, [toClassName i])] $
                return $ toClassName i
        Local _ -> return $ toClassName i
compileTypeExpression source (MapModifier k v) = do
    kExpr <- compileTypeExpression source k
    vExpr <- compileTypeExpression source v
    withStandardImport "typing" $ return [qq|typing.Mapping[$kExpr, $vExpr]|]
compileTypeExpression source modifier = do
    expr <- compileTypeExpression source typeExpr
    withStandardImport "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!

compileTypeDeclaration :: Source -> TypeDeclaration -> CodeGen Code
compileTypeDeclaration _ (TypeDeclaration _ (PrimitiveType _ _) _ _) =
    return ""  -- never used
compileTypeDeclaration src (TypeDeclaration typename (Alias ctype) _ _) = do
    ctypeExpr <- compileTypeExpression src ctype
    return [qq|
# TODO: docstring
{toClassName' typename} = $ctypeExpr
    |]
compileTypeDeclaration src (TypeDeclaration typename (BoxedType itype) _ _) = do
    let className = toClassName' typename
    itypeExpr <- compileTypeExpression src itype
    withStandardImport "typing" $
        withThirdPartyImports [ ("nirum.validate", ["validate_boxed_type"])
                              , ("nirum.serialize", ["serialize_boxed_type"])
                              , ( "nirum.deserialize"
                                , ["deserialize_boxed_type"]
                                )
                              ] $
            return [qq|
class $className:
    # TODO: docstring

    __nirum_boxed_type__ = $itypeExpr

    def __init__(self, value: $itypeExpr) -> None:
        validate_boxed_type(value, $itypeExpr)
        self.value = value  # type: $itypeExpr

    def __eq__(self, other) -> bool:
        return (isinstance(other, $className) and
                self.value == other.value)

    def __hash__(self) -> int:
        return hash(self.value)

    def __nirum_serialize__(self) -> typing.Any:
        return serialize_boxed_type(self)

    @classmethod
    def __nirum_deserialize__(cls: type, value: typing.Any) -> '{className}':
        return deserialize_boxed_type(cls, value)

    def __repr__(self) -> str:
        return '\{0.__module__\}.\{0.__qualname__\}(\{1!r\})'.format(
            type(self), self.value
        )
            |]
compileTypeDeclaration _ (TypeDeclaration typename (EnumType members) _ _) = do
    let className = toClassName' typename
        memberNames = T.intercalate
            "\n    "
            [ [qq|{toAttributeName' memberName} = '{toSnakeCaseText bn}'|]
            | EnumMember memberName@(Name _ bn) _ <- toList members
            ]
    withStandardImport "enum" $ return [qq|
class $className(enum.Enum):
    # TODO: docstring

    $memberNames

    def __nirum_serialize__(self) -> str:
        return self.value

    @classmethod
    def __nirum_deserialize__(cls: type, value: str) -> '{className}':
        return cls(value.replace('-', '_'))  # FIXME: validate input
    |]
compileTypeDeclaration src (TypeDeclaration typename (RecordType fields) _ _) = do
    typeExprCodes <- mapM (compileTypeExpression src)
        [typeExpr | (Field _ typeExpr _) <- toList fields]
    let className = toClassName' typename
        fieldNames = map toAttributeName' [ name
                                          | (Field name _ _) <- toList fields
                                          ]
        nameNTypes = zip fieldNames typeExprCodes
        slotTypes = toIndentedCodes
            (\(n, t) -> [qq|'{n}': {t}|]) nameNTypes ",\n        "
        slots = toIndentedCodes (\n -> [qq|'{n}'|]) fieldNames ",\n        "
        initialArgs = toIndentedCodes
            (\(n, t) -> [qq|{n}: {t}|]) nameNTypes ", "
        initialValues = toIndentedCodes
            (\n -> [qq|self.{n} = {n}|]) fieldNames "\n        "
        nameMaps = toIndentedCodes
            toNamePair
            [name | Field name _ _ <- toList fields]
            ",\n        "
    withStandardImport "typing" $
        withThirdPartyImports [ ( "nirum.validate"
                                , ["validate_record_type"]
                                )
                              , ("nirum.serialize", ["serialize_record_type"])
                              , ( "nirum.deserialize"
                                , ["deserialize_record_type"])
                              , ("nirum.constructs", ["name_dict_type"])
                              ] $
            return [qq|
class $className:
    # TODO: docstring

    __slots__ = (
        $slots,
    )
    __nirum_record_behind_name__ = '{toSnakeCaseText $ N.behindName typename}'
    __nirum_field_types__ = \{
        $slotTypes
    \}
    __nirum_field_names__ = name_dict_type([
        $nameMaps
    ])

    def __init__(self, $initialArgs) -> None:
        $initialValues
        validate_record_type(self)

    def __repr__(self) -> str:
        return '\{0.__module__\}.\{0.__qualname__\}(\{1\})'.format(
            type(self),
            ', '.join('\{\}=\{\}'.format(attr, getattr(self, attr))
                      for attr in self.__slots__)
        )

    def __eq__(self, other) -> bool:
        return isinstance(other, $className) and all(
            getattr(self, attr) == getattr(other, attr)
            for attr in self.__slots__
        )

    def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]:
        return serialize_record_type(self)

    @classmethod
    def __nirum_deserialize__(cls: type, value) -> '{className}':
        return deserialize_record_type(cls, value)
                        |]
compileTypeDeclaration src (TypeDeclaration typename (UnionType tags) _ _) = do
    fieldCodes <- mapM (uncurry (compileUnionTag src typename)) tagNameNFields
    let className = toClassName' typename
        fieldCodes' = T.intercalate "\n\n" fieldCodes
        enumMembers = toIndentedCodes
            (\(t, b) -> [qq|$t = '{b}'|]) enumMembers' "\n        "
    withStandardImport "typing" $
        withStandardImport "enum" $
            withThirdPartyImports [ ( "nirum.serialize"
                                    , ["serialize_union_type"])
                                  , ( "nirum.deserialize"
                                    , ["deserialize_union_type"])
                                  , ("nirum.constructs", ["name_dict_type"])
                                  ] $
                return [qq|
class $className:

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

    class Tag(enum.Enum):
        $enumMembers

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

    def __nirum_serialize__(self) -> typing.Mapping[str, typing.Any]:
        return serialize_union_type(self)

    @classmethod
    def __nirum_deserialize__(cls: type, value) -> '{className}':
        return deserialize_union_type(cls, value)


$fieldCodes'
            |]
  where
    tagNameNFields :: [(Name, DS.DeclarationSet Field)]
    tagNameNFields = [ (tagName, fields)
                     | (Tag tagName fields _) <- toList tags
                     ]
    enumMembers' :: [(T.Text, T.Text)]
    enumMembers' = [ ( toAttributeName' tagName
                     , toSnakeCaseText $ N.behindName tagName
                     )
                   | (Tag tagName _ _) <- toList tags
                   ]
    nameMaps :: T.Text
    nameMaps = toIndentedCodes
        toNamePair
        [name | (name, _) <- tagNameNFields]
        ",\n        "
compileTypeDeclaration src (ServiceDeclaration name (Service methods) _ _) = do
    let methods' = toList methods
    methodMetadata <- mapM compileMethodMetadata methods'
    let methodMetadata' = commaNl methodMetadata
    dummyMethods <- mapM compileMethod methods'
    let dummyMethods' = T.intercalate "\n\n" dummyMethods
    withThirdPartyImports [ ("nirum.constructs", ["name_dict_type"])
                          , ("nirum.rpc", ["service_type"])
                          ] $
        return [qq|
class $className(service_type):

    __nirum_service_methods__ = \{
        {methodMetadata'}
    \}
    __nirum_method_names__ = name_dict_type([
        $methodNameMap
    ])

    {dummyMethods'}
|]
  where
    className :: T.Text
    className = toClassName' name
    commaNl :: [T.Text] -> T.Text
    commaNl = T.intercalate ",\n"
    compileMethod :: Method -> CodeGen Code
    compileMethod (Method mName params rtype _etype _docs _anno) = do
        let mName' = toAttributeName' mName
        params' <- mapM compileParameter $ toList params
        rtypeExpr <- compileTypeExpression src rtype
        return [qq|
    def {mName'}(self, {commaNl params'}) -> $rtypeExpr:
        raise NotImplementedError('$className has to implement {mName'}()')
|]
    compileParameter :: Parameter -> CodeGen Code
    compileParameter (Parameter pName pType _) = do
        pTypeExpr <- compileTypeExpression src pType
        return [qq|{toAttributeName' pName}: $pTypeExpr|]
    compileMethodMetadata :: Method -> CodeGen Code
    compileMethodMetadata (Method mName params rtype _etype _docs _anno) = do
        let params' = toList params :: [Parameter]
        rtypeExpr <- compileTypeExpression src rtype
        paramMetadata <- mapM compileParameterMetadata params'
        let paramMetadata' = commaNl paramMetadata
        withThirdPartyImports [("nirum.constructs", ["name_dict_type"])] $
            return [qq|'{toAttributeName' mName}': \{
                '_return': $rtypeExpr,
                '_names': name_dict_type([{paramNameMap params'}]),
                {paramMetadata'}
            \}|]
    compileParameterMetadata :: Parameter -> CodeGen Code
    compileParameterMetadata (Parameter pName pType _) = do
        let pName' = toAttributeName' pName
        pTypeExpr <- compileTypeExpression src pType
        return [qq|'{pName'}': $pTypeExpr|]
    methodNameMap :: T.Text
    methodNameMap = toIndentedCodes
        toNamePair
        [mName | Method { methodName = mName } <- toList methods]
        ",\n        "
    paramNameMap :: [Parameter] -> T.Text
    paramNameMap params = toIndentedCodes
        toNamePair [pName | Parameter pName _ _ <- params] ",\n        "
compileTypeDeclaration _ (Import _ _) =
    return ""  -- Nothing to compile

compileModuleBody :: Source -> CodeGen Code
compileModuleBody src@Source { sourceModule = boundModule } = do
    let types' = types boundModule
    typeCodes <- mapM (compileTypeDeclaration src) $ toList types'
    let moduleCode = T.intercalate "\n\n" typeCodes
    return [qq|
# TODO: docs
$moduleCode
    |]

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 :: Source -> Either CompileError (InstallRequires, Code)
compileModule source =
    case code' of
        CodeGenError errMsg -> Left errMsg
        CodeGen {} -> codeWithDeps $ [qq|
{imports $ standardImports code'}

{fromImports $ localImports code'}

{fromImports $ thirdPartyImports code'}

{code code'}
    |]
  where
    code' :: CodeGen T.Text
    code' = compileModuleBody source
    imports :: S.Set T.Text -> T.Text
    imports importSet =
        if S.null importSet
        then ""
        else "import " `T.append` T.intercalate "," (S.elems importSet)
    fromImports :: M.Map T.Text (S.Set T.Text) -> T.Text
    fromImports importMap =
        T.intercalate "\n"
            [ [qq|from $from import {T.intercalate ", " $ S.elems vars}|]
            | (from, vars) <- M.assocs importMap
            ]
    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
    deps :: S.Set T.Text
    deps = require "nirum" "nirum" $ M.keysSet $ thirdPartyImports code'
    optDeps :: M.Map (Int, Int) (S.Set T.Text)
    optDeps = [ ((3, 4), require "enum34" "enum" $ standardImports code')
              , ((3, 5), require "typing" "typing" $ standardImports code')
              ]
    codeWithDeps :: Code -> Either CompileError (InstallRequires, Code)
    codeWithDeps c = Right (InstallRequires deps optDeps, c)

compilePackageMetadata :: Package -> InstallRequires -> Code
compilePackageMetadata package (InstallRequires deps optDeps) = [qq|
import sys

from setuptools import setup, __version__ as setuptools_version

install_requires = [$pInstallRequires]
polyfill_requires = \{$pPolyfillRequires}

if polyfill_requires:
    # '<' 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 = \{}

# TODO: description, long_description, url, author, author_email, license,
#       keywords, classifiers
setup(
    name='{pName}',
    version='{pVersion}',
    packages=[$pPackages],
    provides=[$pPackages],
    requires=[$pInstallRequires],
    setup_requires=setup_requires,
    install_requires=install_requires,
    extras_require=extras_require,
)
|]
  where
    pName :: Code
    pName = "TestPackage"  -- FIXME
    pVersion :: Code
    pVersion = "0.1.0"  -- FIXME
    strings :: [Code] -> Code
    strings values = T.intercalate ", " . L.sort $ [[qq|'{v}'|] | v <- values]
    pPackages :: Code
    pPackages = strings $ map toImportPath $ M.keys $ modules package
    pInstallRequires :: Code
    pInstallRequires = strings $ S.toList deps
    pPolyfillRequires :: Code
    pPolyfillRequires = T.intercalate ", "
        [ [qq|($major, $minor): [{strings $ S.toList deps'}]|]
        | ((major, minor), deps') <- M.toList optDeps
        ]

compilePackage :: Package
               -> M.Map FilePath (Either CompileError Code)
compilePackage package =
    M.fromList $
        initFiles ++
        [ ( f
          , case cd of
                Left e -> Left e
                Right (_, cd') -> Right cd'
          )
        | (f, cd) <- modules'
        ] ++
        [("setup.py", Right $ compilePackageMetadata package installRequires)]
  where
    toFilename :: ModulePath -> FilePath
    toFilename mp =
        joinPath $ [ T.unpack (toAttributeName i)
                   | i <- toList mp
                   ] ++ ["__init__.py"]
    initFiles :: [(FilePath, Either CompileError Code)]
    initFiles = [ (toFilename mp', Right "")
                | mp <- M.keys (modules package)
                , mp' <- S.elems (ancestors mp)
                ]
    modules' :: [(FilePath, Either CompileError (InstallRequires, Code))]
    modules' =
        [ ( toFilename modulePath'
          , compileModule $ Source package boundModule
          )
        | (modulePath', _) <- M.assocs (modules package)
        , Just boundModule <- [resolveBoundModule modulePath' package]
        ]
    installRequires :: InstallRequires
    installRequires = foldl unionInstallRequires
                            (InstallRequires [] [])
                            [deps | (_, Right (deps, _)) <- modules']