{-# 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