nirum-0.3.0: test/Nirum/Targets/PythonSpec.hs
{-# LANGUAGE OverloadedLists, OverloadedStrings, QuasiQuotes,
ScopedTypeVariables #-}
module Nirum.Targets.PythonSpec where
import Control.Monad (forM_)
import Data.Either (isRight)
import Data.Maybe (fromJust)
import qualified Data.Map.Strict as M
import qualified Data.SemVer as SV
import System.FilePath ((</>))
import Test.Hspec.Meta
import Text.Email.Validate (emailAddress)
import Text.InterpolatedString.Perl6 (q, qq)
import Nirum.Constructs.Annotation (empty)
import Nirum.Constructs.Identifier (Identifier)
import Nirum.Constructs.Module (Module (Module))
import Nirum.Constructs.ModulePath (ModulePath, fromIdentifiers)
import Nirum.Constructs.Name (Name (Name))
import Nirum.Constructs.TypeDeclaration ( Field (Field)
, PrimitiveTypeIdentifier (..)
, Type ( Alias
, RecordType
, UnboxedType
)
, TypeDeclaration ( Import
, TypeDeclaration
)
)
import Nirum.Constructs.TypeExpression ( TypeExpression ( ListModifier
, MapModifier
, OptionModifier
, SetModifier
, TypeIdentifier
)
)
import Nirum.Package hiding (target)
import Nirum.Package.Metadata ( Author (Author, email, name, uri)
, Metadata ( Metadata
, authors
, target
, version
, description
, license
, keywords
)
, Target (compilePackage)
)
import qualified Nirum.Package.ModuleSet as MS
import Nirum.PackageSpec (createPackage)
import qualified Nirum.Targets.Python as PY
import Nirum.Targets.Python ( Source (Source)
, CodeGen
, CodeGenContext ( localImports
, standardImports
, thirdPartyImports
)
, InstallRequires ( InstallRequires
, dependencies
, optionalDependencies
)
, Python (Python)
, PythonVersion (Python2, Python3)
, RenameMap
, addDependency
, addOptionalDependency
, compilePrimitiveType
, compileTypeExpression
, insertLocalImport
, insertStandardImport
, insertThirdPartyImports
, insertThirdPartyImportsA
, localImportsMap
, minimumRuntime
, parseModulePath
, renameModulePath
, runCodeGen
, stringLiteral
, toAttributeName
, toClassName
, toNamePair
, unionInstallRequires
)
import Nirum.TypeInstance.BoundModule
codeGen :: a -> CodeGen a
codeGen = return
makeDummySource' :: [Identifier] -> Module -> RenameMap -> Source
makeDummySource' pathPrefix m renames =
Source pkg $ fromJust $ resolveBoundModule ["foo"] pkg
where
mp :: [Identifier] -> ModulePath
mp identifiers = fromJust $ fromIdentifiers (pathPrefix ++ identifiers)
metadata' :: Metadata Python
metadata' = Metadata
{ version = SV.version 1 2 3 [] []
, authors =
[ Author
{ name = "John Doe"
, email = Just (fromJust $ emailAddress "john@example.com")
, uri = Nothing
}
]
, description = Just "Package description"
, license = Just "MIT"
, keywords = ["sample", "example", "nirum"]
, target = Python "sample-package" minimumRuntime renames
}
pkg :: Package Python
pkg = createPackage
metadata'
[ (mp ["foo"], m)
, ( mp ["foo", "bar"]
, Module [ Import (mp ["qux"]) "path" empty
, TypeDeclaration "path-unbox" (UnboxedType "path") empty
, TypeDeclaration "int-unbox"
(UnboxedType "bigint") empty
, TypeDeclaration "point"
(RecordType [ Field "x" "int64" empty
, Field "y" "int64" empty
])
empty
] Nothing
)
, ( mp ["qux"]
, Module
[ TypeDeclaration "path" (Alias "text") empty
, TypeDeclaration "name" (UnboxedType "text") empty
]
Nothing
)
]
makeDummySource :: Module -> Source
makeDummySource m = makeDummySource' [] m []
spec :: Spec
spec = parallel $ forM_ ([Python2, Python3] :: [PythonVersion]) $ \ ver -> do
let empty' = PY.empty ver
-- run' :: CodeGen a -> (Either CompileError a, CodeGenContext)
run' c = runCodeGen c empty'
-- code :: CodeGen a -> a
code = either (const undefined) id . fst . run'
-- compileError :: CodeGen a -> Maybe CompileError
compileError cg = either Just (const Nothing) $ fst $
runCodeGen cg empty'
describe [qq|CodeGen ($ver)|] $ do
specify "packages and imports" $ do
let c = do
insertStandardImport "sys"
insertThirdPartyImports
[("nirum", ["serialize_unboxed_type"])]
insertLocalImport ".." "Gender"
insertStandardImport "os"
insertThirdPartyImports [("nirum", ["serialize_enum_type"])]
insertThirdPartyImportsA
[("nirum.constructs", [("name_dict_type", "NameDict")])]
insertLocalImport ".." "Path"
let (e, ctx) = runCodeGen c empty'
e `shouldSatisfy` isRight
standardImports ctx `shouldBe` ["os", "sys"]
thirdPartyImports ctx `shouldBe`
[ ( "nirum"
, [ ("serialize_unboxed_type", "serialize_unboxed_type")
, ("serialize_enum_type", "serialize_enum_type")
]
)
, ( "nirum.constructs"
, [("name_dict_type", "NameDict")]
)
]
localImports ctx `shouldBe` [("..", ["Gender", "Path"])]
localImportsMap ctx `shouldBe`
[ ( ".."
, [ ("Gender", "Gender")
, ("Path", "Path")
]
)
]
specify "insertStandardImport" $ do
let codeGen1 = insertStandardImport "sys"
let (e1, ctx1) = runCodeGen codeGen1 empty'
e1 `shouldSatisfy` isRight
standardImports ctx1 `shouldBe` ["sys"]
thirdPartyImports ctx1 `shouldBe` []
localImports ctx1 `shouldBe` []
compileError codeGen1 `shouldBe` Nothing
let codeGen2 = codeGen1 >> insertStandardImport "os"
let (e2, ctx2) = runCodeGen codeGen2 empty'
e2 `shouldSatisfy` isRight
standardImports ctx2 `shouldBe` ["os", "sys"]
thirdPartyImports ctx2 `shouldBe` []
localImports ctx2 `shouldBe` []
compileError codeGen2 `shouldBe` Nothing
specify [qq|compilePrimitiveType ($ver)|] $ do
code (compilePrimitiveType Bool) `shouldBe` "bool"
code (compilePrimitiveType Bigint) `shouldBe` "int"
let (decimalCode, decimalContext) = run' (compilePrimitiveType Decimal)
decimalCode `shouldBe` Right "decimal.Decimal"
standardImports decimalContext `shouldBe` ["decimal"]
code (compilePrimitiveType Int32) `shouldBe` "int"
code (compilePrimitiveType Int64) `shouldBe`
case ver of
Python2 -> "numbers.Integral"
Python3 -> "int"
code (compilePrimitiveType Float32) `shouldBe` "float"
code (compilePrimitiveType Float64) `shouldBe` "float"
code (compilePrimitiveType Text) `shouldBe`
case ver of
Python2 -> "unicode"
Python3 -> "str"
code (compilePrimitiveType Binary) `shouldBe` "bytes"
let (dateCode, dateContext) = run' (compilePrimitiveType Date)
dateCode `shouldBe` Right "datetime.date"
standardImports dateContext `shouldBe` ["datetime"]
let (datetimeCode, datetimeContext) =
run' (compilePrimitiveType Datetime)
datetimeCode `shouldBe` Right "datetime.datetime"
standardImports datetimeContext `shouldBe` ["datetime"]
let (uuidCode, uuidContext) = run' (compilePrimitiveType Uuid)
uuidCode `shouldBe` Right "uuid.UUID"
standardImports uuidContext `shouldBe` ["uuid"]
code (compilePrimitiveType Uri) `shouldBe`
case ver of
Python2 -> "unicode"
Python3 -> "str"
describe [qq|compileTypeExpression ($ver)|] $ do
let s = makeDummySource $ Module [] Nothing
specify "TypeIdentifier" $ do
let (c, ctx) = run' $
compileTypeExpression s (Just $ TypeIdentifier "bigint")
standardImports ctx `shouldBe` []
localImports ctx `shouldBe` []
c `shouldBe` Right "int"
specify "OptionModifier" $ do
let (c', ctx') = run' $
compileTypeExpression s (Just $ OptionModifier "int32")
standardImports ctx' `shouldBe` ["typing"]
localImports ctx' `shouldBe` []
c' `shouldBe` Right "typing.Optional[int]"
specify "SetModifier" $ do
let (c'', ctx'') = run' $
compileTypeExpression s (Just $ SetModifier "int32")
standardImports ctx'' `shouldBe` ["typing"]
localImports ctx'' `shouldBe` []
c'' `shouldBe` Right "typing.AbstractSet[int]"
specify "ListModifier" $ do
let (c''', ctx''') = run' $
compileTypeExpression s (Just $ ListModifier "int32")
standardImports ctx''' `shouldBe` ["typing"]
localImports ctx''' `shouldBe` []
c''' `shouldBe` Right "typing.Sequence[int]"
specify "MapModifier" $ do
let (c'''', ctx'''') = run' $
compileTypeExpression s (Just $ MapModifier "uuid" "int32")
standardImports ctx'''' `shouldBe` ["typing", "uuid"]
localImports ctx'''' `shouldBe` []
c'''' `shouldBe` Right "typing.Mapping[uuid.UUID, int]"
describe [qq|toClassName ($ver)|] $ do
it "transform the facial name of the argument into PascalCase" $ do
toClassName "test" `shouldBe` "Test"
toClassName "hello-world" `shouldBe` "HelloWorld"
it "appends an underscore to the result if it's a reserved keyword" $ do
toClassName "true" `shouldBe` "True_"
toClassName "false" `shouldBe` "False_"
toClassName "none" `shouldBe` "None_"
describe [qq|toAttributeName ($ver)|] $ do
it "transform the facial name of the argument into snake_case" $ do
toAttributeName "test" `shouldBe` "test"
toAttributeName "hello-world" `shouldBe` "hello_world"
it "appends an underscore to the result if it's a reserved keyword" $ do
toAttributeName "def" `shouldBe` "def_"
toAttributeName "lambda" `shouldBe` "lambda_"
toAttributeName "nonlocal" `shouldBe` "nonlocal_"
describe [qq|toNamePair ($ver)|] $ do
it "transforms the name to a Python code string of facial/behind pair" $
do toNamePair "text" `shouldBe` "('text', 'text')"
toNamePair (Name "test" "hello") `shouldBe` "('test', 'hello')"
it "replaces hyphens to underscores" $ do
toNamePair "hello-world" `shouldBe` "('hello_world', 'hello_world')"
toNamePair (Name "hello-world" "annyeong-sesang") `shouldBe`
"('hello_world', 'annyeong_sesang')"
it "appends an underscore if the facial name is a Python keyword" $ do
toNamePair "def" `shouldBe` "('def_', 'def')"
toNamePair "lambda" `shouldBe` "('lambda_', 'lambda')"
toNamePair (Name "abc" "lambda") `shouldBe` "('abc', 'lambda')"
toNamePair (Name "lambda" "abc") `shouldBe` "('lambda_', 'abc')"
specify [qq|stringLiteral ($ver)|] $ do
stringLiteral "asdf" `shouldBe` [q|"asdf"|]
stringLiteral [q|Say 'hello world'|]
`shouldBe` [q|"Say 'hello world'"|]
stringLiteral [q|Say "hello world"|]
`shouldBe` [q|"Say \"hello world\""|]
stringLiteral "Say '\xc548\xb155'"
`shouldBe` [q|u"Say '\uc548\ub155'"|]
describe [qq|compilePackage ($ver)|] $ do
it "returns a Map of file paths and their contents to generate" $ do
let (Source pkg _) = makeDummySource $ Module [] Nothing
files = compilePackage pkg
directoryStructure =
[ "src-py2" </> "foo" </> "__init__.py"
, "src-py2" </> "foo" </> "bar" </> "__init__.py"
, "src-py2" </> "qux" </> "__init__.py"
, "src" </> "foo" </> "__init__.py"
, "src" </> "foo" </> "bar" </> "__init__.py"
, "src" </> "qux" </> "__init__.py"
, "setup.py"
, "MANIFEST.in"
]
M.keysSet files `shouldBe` directoryStructure
it "creates an emtpy Python package directory if necessary" $ do
let (Source pkg _) = makeDummySource' ["test"] (Module [] Nothing)
[]
files = compilePackage pkg
directoryStructure =
[ "src-py2" </> "test" </> "__init__.py"
, "src-py2" </> "test" </> "foo" </> "__init__.py"
, "src-py2" </> "test" </> "foo" </> "bar" </> "__init__.py"
, "src-py2" </> "test" </> "qux" </> "__init__.py"
, "src" </> "test" </> "__init__.py"
, "src" </> "test" </> "foo" </> "__init__.py"
, "src" </> "test" </> "foo" </> "bar" </> "__init__.py"
, "src" </> "test" </> "qux" </> "__init__.py"
, "setup.py"
, "MANIFEST.in"
]
M.keysSet files `shouldBe` directoryStructure
it "generates renamed package dirs if renames are configured" $ do
let (Source pkg _) = makeDummySource' [] (Module [] Nothing)
[(["foo"], ["quz"])]
files = compilePackage pkg
directoryStructure =
[ "src-py2" </> "quz" </> "__init__.py"
, "src-py2" </> "quz" </> "bar" </> "__init__.py"
, "src-py2" </> "qux" </> "__init__.py"
, "src" </> "quz" </> "__init__.py"
, "src" </> "quz" </> "bar" </> "__init__.py"
, "src" </> "qux" </> "__init__.py"
, "setup.py"
, "MANIFEST.in"
]
M.keysSet files `shouldBe` directoryStructure
let (Source pkg' _) = makeDummySource' [] (Module [] Nothing)
[(["foo", "bar"], ["bar"])]
files' = compilePackage pkg'
directoryStructure' =
[ "src-py2" </> "foo" </> "__init__.py"
, "src-py2" </> "bar" </> "__init__.py"
, "src-py2" </> "qux" </> "__init__.py"
, "src" </> "foo" </> "__init__.py"
, "src" </> "bar" </> "__init__.py"
, "src" </> "qux" </> "__init__.py"
, "setup.py"
, "MANIFEST.in"
]
M.keysSet files' `shouldBe` directoryStructure'
describe [qq|InstallRequires ($ver)|] $ do
let req = InstallRequires [] []
req2 = req { dependencies = ["six"] }
req3 = req { optionalDependencies = [((3, 4), ["enum34"])] }
specify "addDependency" $ do
addDependency req "six" `shouldBe` req2
addDependency req2 "six" `shouldBe` req2
addDependency req "nirum" `shouldBe`
req { dependencies = ["nirum"] }
addDependency req2 "nirum" `shouldBe`
req2 { dependencies = ["nirum", "six"] }
specify "addOptionalDependency" $ do
addOptionalDependency req (3, 4) "enum34" `shouldBe` req3
addOptionalDependency req3 (3, 4) "enum34" `shouldBe` req3
addOptionalDependency req (3, 4) "ipaddress" `shouldBe`
req { optionalDependencies = [((3, 4), ["ipaddress"])] }
addOptionalDependency req3 (3, 4) "ipaddress" `shouldBe`
req { optionalDependencies = [ ((3, 4), ["enum34", "ipaddress"])
]
}
addOptionalDependency req (3, 5) "typing" `shouldBe`
req { optionalDependencies = [((3, 5), ["typing"])] }
addOptionalDependency req3 (3, 5) "typing" `shouldBe`
req3 { optionalDependencies = [ ((3, 4), ["enum34"])
, ((3, 5), ["typing"])
]
}
specify "unionInstallRequires" $ do
(req `unionInstallRequires` req) `shouldBe` req
(req `unionInstallRequires` req2) `shouldBe` req2
(req2 `unionInstallRequires` req) `shouldBe` req2
(req `unionInstallRequires` req3) `shouldBe` req3
(req3 `unionInstallRequires` req) `shouldBe` req3
let req4 = req3 { dependencies = ["six"] }
(req2 `unionInstallRequires` req3) `shouldBe` req4
(req3 `unionInstallRequires` req2) `shouldBe` req4
let req5 = req { dependencies = ["nirum"]
, optionalDependencies = [((3, 4), ["ipaddress"])]
}
req6 = addOptionalDependency (addDependency req4 "nirum")
(3, 4) "ipaddress"
(req4 `unionInstallRequires` req5) `shouldBe` req6
(req5 `unionInstallRequires` req4) `shouldBe` req6
specify [qq|toImportPath ($ver)|] $ do
let (Source pkg _) = makeDummySource $ Module [] Nothing
target' = target $ metadata pkg
PY.toImportPath target' ["foo", "bar"] `shouldBe` "foo.bar"
describe [qq|toImportPath ($ver)|] $ do
it "adds ancestors of packages" $ do
let (Source pkg _) = makeDummySource $ Module [] Nothing
modulePaths = MS.keysSet $ modules pkg
target' = target $ metadata pkg
PY.toImportPaths target' modulePaths `shouldBe`
[ "foo"
, "foo.bar"
, "qux"
]
it "applies renames before add ancestors of packages" $ do
let (Source pkg _) = makeDummySource' [] (Module [] Nothing)
[(["foo"], ["f", "oo"])]
modulePaths = MS.keysSet $ modules pkg
target' = target $ metadata pkg
PY.toImportPaths target' modulePaths `shouldBe`
[ "f" -- > "f" should be added
, "f.oo"
, "f.oo.bar"
, "qux"
]
specify "parseModulePath" $ do
parseModulePath "" `shouldBe` Nothing
parseModulePath "foo" `shouldBe` Just ["foo"]
parseModulePath "foo.bar" `shouldBe` Just ["foo", "bar"]
parseModulePath "foo.bar-baz" `shouldBe` Just ["foo", "bar-baz"]
parseModulePath "foo." `shouldBe` Nothing
parseModulePath "foo.bar." `shouldBe` Nothing
parseModulePath ".foo" `shouldBe` Nothing
parseModulePath ".foo.bar" `shouldBe` Nothing
parseModulePath "foo..bar" `shouldBe` Nothing
parseModulePath "foo.bar>" `shouldBe` Nothing
parseModulePath "foo.bar-" `shouldBe` Nothing
specify "renameModulePath" $ do
let renames = [ (["foo"], ["poo"])
, (["foo", "bar"], ["foo"])
, (["baz"], ["p", "az"])
] :: RenameMap
renameModulePath renames ["foo"] `shouldBe` ["poo"]
renameModulePath renames ["foo", "baz"] `shouldBe` ["poo", "baz"]
renameModulePath renames ["foo", "bar"] `shouldBe` ["foo"]
renameModulePath renames ["foo", "bar", "qux"] `shouldBe` ["foo", "qux"]
renameModulePath renames ["baz"] `shouldBe` ["p", "az"]
renameModulePath renames ["baz", "qux"] `shouldBe` ["p", "az", "qux"]
renameModulePath renames ["qux", "foo"] `shouldBe` ["qux", "foo"]