diff --git a/.gitignore b/.gitignore index 9395b21..93f5503 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__ .cache +.pytest_cache .stack-work *.tix *.pyc diff --git a/.hlint.yaml b/.hlint.yaml index 85c0ab6..f4b536d 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -55,6 +55,7 @@ within: - Nirum.Parser - Nirum.ParserSpec + - Nirum.Targets.Python.CodeGenSpec - ignore: {name: Unnecessary hiding} diff --git a/.travis.yml b/.travis.yml index b7d8f5b..eac679e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -78,6 +78,7 @@ before_install: brew update || true brew upgrade || true brew update || true + brew install --with-default-names gnu-sed brew tap dahlia/homebrew-deadsnakes brew install python34 python35 python3 || \ brew upgrade python34 python35 python3 || \ diff --git a/CHANGES.md b/CHANGES.md index cb77267..6d8980e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,9 +23,38 @@ To be released. classes. Nirum type names are qualified and their leading module paths are also normalized (the same rule to `nirum.modules` applies here). + - All integral types (`int32`, `int64`, and `bigint`) became represented + as [`numbers.Integral`][python2-numbers-integral] instead of + [`int`][python2-int]. + + There's no change to Python 3. + + - The `uri` type became represented as [`basestring`][python2-basestring] + instead of [`unicode`][python2-unicode] in Python 2, since URI (unlike IRI) + is limited to a subset of ASCII character set. + + There's no change to Python 3. + + - Generated type constructors became to validate field value's range or format + besides class checks: range checks for `int32`/`int64`, time zone + (``tzinfo``) awareness check for `datetime`, and basic format check for + `uri`. + + - Fixed a bug that generated service methods hadn't checked its arguments + before its transport sends a payload. [[#220]] + + - Fixed a bug that field/parameter names that use a module name of the Python + standard library cause runtime `TypeError`s (due to name shadowing). + Under the hood, all generated `import`s are now aliased with a name prefixed + an underscore. + [#13]: https://github.com/spoqa/nirum/issues/13 +[#220]: https://github.com/spoqa/nirum/issues/220 [#227]: https://github.com/spoqa/nirum/pull/227 [entry points]: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points +[python2-numbers-integral]: https://docs.python.org/2/library/numbers.html#numbers.Integral +[python2-basestring]: https://docs.python.org/2/library/functions.html#basestring +[python2-unicode]: https://docs.python.org/2/library/functions.html#unicode Version 0.3.1 diff --git a/src/Nirum/Targets/Python.hs b/src/Nirum/Targets/Python.hs index 5a159bd..e0192e4 100644 --- a/src/Nirum/Targets/Python.hs +++ b/src/Nirum/Targets/Python.hs @@ -93,7 +93,8 @@ import qualified Nirum.Package.Metadata as MD import Nirum.Targets.Python.CodeGen import Nirum.Targets.Python.Serializers import Nirum.Targets.Python.TypeExpression -import Nirum.TypeInstance.BoundModule +import Nirum.Targets.Python.Validators +import Nirum.TypeInstance.BoundModule as BM type Package' = Package Python type CompileError' = Nirum.Targets.Python.CodeGen.CompileError @@ -102,6 +103,10 @@ data Source = Source { sourcePackage :: Package' , sourceModule :: BoundModule Python } deriving (Eq, Ord, Show) +sourceImportPath :: Source -> T.Text +sourceImportPath (Source (Package MD.Metadata { MD.target = t } _) bm) = + toImportPath t (BM.modulePath bm) + sourceDirectory :: PythonVersion -> T.Text sourceDirectory Python2 = "src-py2" sourceDirectory Python3 = "src" @@ -109,6 +114,9 @@ sourceDirectory Python3 = "src" thd3 :: (a, b, c) -> c thd3 (_, _, v) = v +enumerate :: [a] -> [(Int, a)] +enumerate = zip [0 ..] + toEnumMemberName :: Name -> T.Text toEnumMemberName name' | attributeName `elem` memberKeywords = attributeName `T.snoc` '_' @@ -152,13 +160,10 @@ compileParameters gen nameTypeTriples = (\ (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 +compileFieldInitializers :: DS.DeclarationSet Field -> CodeGen [Code] +compileFieldInitializers fields' = + forM (toList fields') compileFieldInitializer where - indentSpaces :: T.Text - indentSpaces = "\n" `T.append` T.replicate depth " " compileFieldInitializer :: Field -> CodeGen Code compileFieldInitializer (Field fieldName' fieldType' _) = case fieldType' of @@ -227,15 +232,6 @@ indent space = | 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 @@ -260,88 +256,161 @@ returnCompiler = do compileUnionTag :: Source -> Name -> Tag -> CodeGen Code compileUnionTag source parentname d@(Tag typename' fields' _) = do typeExprCodes <- mapM (compileTypeExpression' source) - [Just typeExpr | (Field _ typeExpr _) <- toList fields'] + [Just typeExpr | (Field _ typeExpr _) <- fieldList] 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")]) - ] + [("nirum.constructs", [("name_dict_type", "NameDict")])] arg <- parameterCompiler - ret <- returnCompiler pyVer <- getPythonVersion - initializers <- compileFieldInitializers fields' $ case pyVer of - -- These numbers don't mean version but indentation depth - 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'} + validators <- sequence + [ do + v <- compileValidator' source typeExpr $ toAttributeName' fName + return (fName, typeExprCode, v) + | (typeExprCode, Field fName typeExpr _) <- zip typeExprCodes fieldList + ] + initializers <- compileFieldInitializers fields' + return $ toStrict $ renderMarkup $ [compileText| +class #{className}(#{parentClass}): +#{compileDocstringWithFields " " d fields'} __slots__ = ( - $slots +%{ forall Field fName _ _ <- fieldList } + '#{toAttributeName' fName}', +%{ endforall } ) __nirum_type__ = 'union' - __nirum_tag__ = $parentClass.Tag.{toEnumMemberName typename'} + __nirum_tag__ = #{parentClass}.Tag.#{toEnumMemberName typename'} + + # FIXME: __nirum_tag_names__ becomes unnecessary when deserializers + # become independent from the nirum-python runtime library. + # https://github.com/spoqa/nirum/issues/160 __nirum_tag_names__ = name_dict_type([ - $nameMaps +%{ forall Field (Name fFacial fBehind) _ _ <- fieldList } + ('#{toAttributeName fFacial}', '#{I.toSnakeCaseText fBehind}'), +%{ endforall } ]) @staticmethod def __nirum_tag_types__(): - return [$slotTypes] + return [ +%{ forall (n, t, _) <- nameTypeTriples } + ('#{n}', #{t}), +%{ endforall } + ] - { inits :: T.Text } +%{ case pyVer } +%{ of Python2 } + def __init__(self, **kwargs): +%{ of Python3 } + def __init__( +%{ if null nameTypeTriples } + self +%{ else } + self, *, #{compileParameters arg nameTypeTriples} +%{ endif } + ) -> None: +%{ endcase } + _type_repr = __import__('typing')._type_repr + # typing module can be masked by field name of the same name, e.g.: + # union foo = bar ( text typing ); + # As Nirum identifier disallows to begin with dash/underscore, + # we can avoid such name overwrapping by defining _type_repr, + # an underscore-leaded alias of typing._type_repr and using it + # in the below __init__() inner function. + def __init__(#{compileParameters arg nameTypeTriples}): +%{ forall (fName, fType, (Validator fTypePred fValueValidators)) <- validators } + if not (#{fTypePred}): + raise TypeError( + '#{toAttributeName' fName} must be a value of ' + + _type_repr(#{fType}) + ', not ' + + repr(#{toAttributeName' fName}) + ) +%{ forall ValueValidator fValuePredCode fValueErrorMsg <- fValueValidators } + elif not (#{fValuePredCode}): + raise ValueError( + 'invalid #{toAttributeName' fName}: ' + #{stringLiteral fValueErrorMsg} + ) +%{ endforall } +%{ endforall } +%{ forall initializer <- initializers } + #{initializer} +%{ endforall } + pass # it's necessary when there are no parameters at all +%{ case pyVer } +%{ of Python2 } + __init__(**kwargs) +%{ of Python3 } + __init__( +%{ forall Field fName _ _ <- fieldList } + #{toAttributeName' fName}=#{toAttributeName' fName}, +%{ endforall } + ) +%{ endcase } 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__) + - ')' - ) + return { + '_type': '#{behindParentTypename}', + '_tag': '#{behindTagName}', +%{ forall Field fName@(Name _ fBehind) fType _ <- fieldList } + '#{ I.toSnakeCaseText fBehind}': +#{compileSerializer' source fType $ T.append "self." $ toAttributeName' fName}, +%{ endforall } + } - def __eq__(self, other){ ret "bool" }: - return isinstance(other, $className) and all( +%{ case pyVer } +%{ of Python2 } + def __eq__(self, other): +%{ of Python3 } + def __eq__(self, other: '#{parentClass}') -> bool: +%{ endcase } + return isinstance(other, #{className}) and all( getattr(self, attr) == getattr(other, attr) for attr in self.__slots__ ) - def __ne__(self, other){ ret "bool" }: +%{ case pyVer } +%{ of Python2 } + def __ne__(self, other): +%{ of Python3 } + def __ne__(self, other: '#{parentClass}') -> bool: +%{ endcase } return not self == other - def __hash__(self){ ret "int" }: - return hash($hashTuple) +%{ case pyVer } +%{ of Python2 } + def __hash__(self): +%{ of Python3 } + def __hash__(self) -> int: +%{ endcase } + return hash(( +%{ forall Field fName _ _ <- fieldList } + self.#{toAttributeName' fName}, +%{ endforall } + )) + +%{ case pyVer } +%{ of Python2 } + def __repr__(self): +%{ of Python3 } + def __repr__(self) -> bool: +%{ endcase } + return ''.join([ + '#{sourceImportPath source}.#{parentClass}.#{className}(', +%{ forall (i, Field fName _ _) <- enumerate fieldList } +%{ if i > 0 } + ', ', +%{ endif } + '#{toAttributeName' fName}=', + repr(self.#{toAttributeName' fName }), +%{ endforall } + ')' + ]) -$parentClass.$className = $className -if hasattr($parentClass, '__qualname__'): - $className.__qualname__ = $parentClass.__qualname__ + '.{className}' +#{parentClass}.#{className} = #{className} +if hasattr(#{parentClass}, '__qualname__'): + (#{className}).__qualname__ = '#{parentClass}.#{className}' |] where optionFlags :: [Bool] @@ -355,33 +424,13 @@ if hasattr($parentClass, '__qualname__'): behindParentTypename :: T.Text behindParentTypename = I.toSnakeCaseText $ N.behindName parentname tagNames :: [T.Text] - tagNames = map (toAttributeName' . fieldName) (toList fields') + tagNames = map (toAttributeName' . fieldName) fieldList 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 - ] compileTypeExpression' :: Source -> Maybe TypeExpression -> CodeGen Code compileTypeExpression' Source { sourceModule = boundModule } = @@ -391,6 +440,10 @@ compileSerializer' :: Source -> TypeExpression -> Code -> Code compileSerializer' Source { sourceModule = boundModule } = compileSerializer boundModule +compileValidator' :: Source -> TypeExpression -> Code -> CodeGen Validator +compileValidator' Source { sourceModule = boundModule } = + compileValidator boundModule + compileTypeDeclaration :: Source -> TypeDeclaration -> CodeGen Code compileTypeDeclaration _ TypeDeclaration { type' = PrimitiveType {} } = return "" -- never used @@ -412,11 +465,9 @@ compileTypeDeclaration src d@TypeDeclaration { typename = typename' let className = toClassName' typename' itypeExpr <- compileTypeExpression' src (Just itype) insertStandardImport "typing" - insertThirdPartyImports - [ ("nirum.validate", ["validate_unboxed_type"]) - , ("nirum.deserialize", ["deserialize_meta"]) - ] + insertThirdPartyImports [("nirum.deserialize", ["deserialize_meta"])] pyVer <- getPythonVersion + Validator typePred valueValidators' <- compileValidator' src itype "value" return $ toStrict $ renderMarkup $ [compileText| class #{className}(object): #{compileDocstring " " d} @@ -433,7 +484,17 @@ class #{className}(object): %{ of Python3 } def __init__(self, value: '#{itypeExpr}') -> None: %{ endcase } - validate_unboxed_type(value, #{itypeExpr}) + if not (#{typePred}): + raise TypeError( + 'expected {0}, not {1!r}'.format( + typing._type_repr(#{itypeExpr}), + value + ) + ) +%{ forall ValueValidator predCode msg <- valueValidators' } + if not (#{predCode}): + raise ValueError(#{stringLiteral msg}) +%{ endforall } self.value = value # type: #{itypeExpr} %{ case pyVer } @@ -530,89 +591,165 @@ class #{className}(enum.Enum): # __nirum_type__ should be defined after the class is defined. #{className}.__nirum_type__ = 'enum' |] -compileTypeDeclaration src d@TypeDeclaration { typename = typename' +compileTypeDeclaration src d@TypeDeclaration { typename = Name tnFacial tnBehind , 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")] - ) - ] + let nameTypeTriples = L.sortBy + (compare `on` thd3) + (zip3 [toAttributeName' name' | Field name' _ _ <- fieldList] + typeExprCodes optionFlags) + insertStandardImport "typing" + insertThirdPartyImportsA + [ ("nirum.constructs", [("name_dict_type", "NameDict")]) + , ("nirum.deserialize", [("deserialize_meta", "deserialize_meta")]) + ] arg <- parameterCompiler - ret <- returnCompiler - typeRepr <- typeReprCompiler pyVer <- getPythonVersion - initializers <- compileFieldInitializers fields' $ case pyVer of - -- These numbers don't mean version but indentation depth - 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'} + validators <- sequence + [ do + v <- compileValidator' src typeExpr $ toAttributeName' fName + return (fName, typeExprCode, v) + | (typeExprCode, Field fName typeExpr _) <- zip typeExprCodes fieldList + ] + initializers <- compileFieldInitializers fields' + return $ toStrict $ renderMarkup $ [compileText| +class #{className}(object): +#{compileDocstringWithFields " " d fields'} __slots__ = ( - $slots, +%{ forall Field fName _ _ <- fieldList } + '#{toAttributeName' fName}', +%{ endforall } ) __nirum_type__ = 'record' - __nirum_record_behind_name__ = '{behindTypename}' - __nirum_field_names__ = name_dict_type([$nameMaps]) + __nirum_record_behind_name__ = '#{I.toSnakeCaseText tnBehind}' + + # FIXME: __nirum_field_names__ becomes unnecessary when deserializers + # become independent from the nirum-python runtime library. + # https://github.com/spoqa/nirum/issues/160 + __nirum_field_names__ = name_dict_type([ +%{ forall Field (Name fFacial fBehind) _ _ <- fieldList } + ('#{toAttributeName fFacial}', '#{I.toSnakeCaseText fBehind}'), +%{ endforall } + ]) @staticmethod +%{ case pyVer } +%{ of Python2 } def __nirum_field_types__(): - return \{$slotTypes\} - - {inits :: T.Text} +%{ of Python3 } + def __nirum_field_types__() -> typing.Mapping[str, typing.Any]: +%{ endcase } + return { +%{ forall (n, t, _) <- nameTypeTriples } + '#{n}': #{t}, +%{ endforall } + } - def __repr__(self){ret "bool"}: - return '\{0\}(\{1\})'.format( - {typeRepr "type(self)"}, - ', '.join('\{\}=\{\}'.format(attr, getattr(self, attr)) - for attr in self.__slots__) +%{ case pyVer } +%{ of Python2 } + def __init__(self, **kwargs): +%{ of Python3 } + def __init__( +%{ if null nameTypeTriples } + self +%{ else } + self, *, #{compileParameters arg nameTypeTriples} +%{ endif } + ) -> None: +%{ endcase } + _type_repr = __import__('typing')._type_repr + # typing module can be masked by field name of the same name, e.g.: + # record foo ( text typing ); + # As Nirum identifier disallows to begin with dash/underscore, + # we can avoid such name overwrapping by defining _type_repr, + # an underscore-leaded alias of typing._type_repr and using it + # in the below __init__() inner function. + def __init__(#{compileParameters arg nameTypeTriples}): +%{ forall (fName, fType, (Validator fTypePred fValueValidators)) <- validators } + if not (#{fTypePred}): + raise TypeError( + '#{toAttributeName' fName} must be a value of ' + + _type_repr(#{fType}) + ', not ' + + repr(#{toAttributeName' fName}) + ) +%{ forall ValueValidator fValuePredCode fValueErrorMsg <- fValueValidators } + elif not (#{fValuePredCode}): + raise ValueError( + 'invalid #{toAttributeName' fName}: ' + #{stringLiteral fValueErrorMsg} + ) +%{ endforall } +%{ endforall } +%{ forall initializer <- initializers } + #{initializer} +%{ endforall } + pass # it's necessary when there are no parameters at all +%{ case pyVer } +%{ of Python2 } + __init__(**kwargs) +%{ of Python3 } + __init__( +%{ forall Field fName _ _ <- fieldList } + #{toAttributeName' fName}=#{toAttributeName' fName}, +%{ endforall } ) +%{ endcase } + +%{ case pyVer } +%{ of Python2 } + def __repr__(self): +%{ of Python3 } + def __repr__(self) -> bool: +%{ endcase } + return ''.join([ + '#{sourceImportPath src}.#{className}(', +%{ forall (i, Field fName _ _) <- enumerate fieldList } +%{ if i > 0 } + ', ', +%{ endif } + '#{toAttributeName' fName}=', + repr(self.#{toAttributeName' fName }), +%{ endforall } + ')' + ]) - def __eq__(self, other){ret "bool"}: - return isinstance(other, $className) and all( +%{ case pyVer } +%{ of Python2 } + def __eq__(self, other): +%{ of Python3 } + def __eq__(self, other: '#{className}') -> bool: +%{ endcase } + return isinstance(other, #{className}) and all( getattr(self, attr) == getattr(other, attr) for attr in self.__slots__ ) - def __ne__(self, other){ ret "bool" }: +%{ case pyVer } +%{ of Python2 } + def __ne__(self, other): +%{ of Python3 } + def __ne__(self, other: '#{className}') -> bool: +%{ endcase } return not self == other def __nirum_serialize__(self): - return \{ - '_type': '{behindTypename}', - $fieldSerializers - \} + return { + '_type': '#{I.toSnakeCaseText tnBehind}', +%{ forall Field fName@(Name _ fBehind) fType _ <- fieldList } + '#{ I.toSnakeCaseText fBehind}': +#{compileSerializer' src fType $ T.append "self." $ toAttributeName' fName}, +%{ endforall } + } @classmethod - def __nirum_deserialize__($clsType, value){ ret className }: +%{ case pyVer } +%{ of Python2 } + def __nirum_deserialize__(cls, value): +%{ of Python3 } + def __nirum_deserialize__(cls, value) -> '#{className}': +%{ endcase } if '_type' not in value: raise ValueError('"_type" field is missing.') if not cls.__nirum_record_behind_name__ == value['_type']: @@ -639,67 +776,66 @@ class $className(object): field_type = field_types[name] except KeyError: continue + if (field_type.__module__ == 'numbers' and + field_type.__name__ == 'Integral'): + # FIXME: deserialize_meta() cannot determine the Nirum type + # from the given Python class, since there are 1:N relationships + # between Nirum types and Python classes. A Python class can + # have more than one corresponds and numbers.Integral is + # the case: bigint, int32, and int64 all corresponds to + # numbers.Integral (on Python 2). + # It's the essential reason why we should be free from + # deserialize_meta() and generate actual deserializer code + # for each field instead. + # See also: https://github.com/spoqa/nirum/issues/160 + try: + args[name] = int(item) + except ValueError as e: + errors.add('%s: %s' % (attribute_name, e)) + continue try: args[name] = deserialize_meta(field_type, item) except ValueError as e: errors.add('%s: %s' % (attribute_name, str(e))) if errors: - raise ValueError('\\n'.join(sorted(errors))) + raise ValueError('\n'.join(sorted(errors))) return cls(**args) - def __hash__(self){ret "int"}: - return hash(($hashText,)) +%{ case pyVer } +%{ of Python2 } + def __hash__(self): +%{ of Python3 } + def __hash__(self) -> int: +%{ endcase } + return hash(( +%{ forall Field fName _ _ <- fieldList } + self.#{toAttributeName' fName}, +%{ endforall } + )) |] where - className :: T.Text - className = toClassName' typename' + className = toClassName tnFacial 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' = union + , type' = union@UnionType {} , typeAnnotations = annotations } = do tagCodes <- mapM (compileUnionTag src typename') tags' - importTypingForPython3 + insertStandardImport "typing" insertStandardImport "enum" - insertThirdPartyImports [ ("nirum.deserialize", ["deserialize_meta"]) - ] - insertThirdPartyImportsA [ ( "nirum.constructs" - , [("name_dict_type", "NameDict")] - ) - ] - insertThirdPartyImportsA [ ( "nirum.datastructures" - , [("map_type", "Map")] - ) - ] - typeRepr <- typeReprCompiler + insertThirdPartyImports [("nirum.deserialize", ["deserialize_meta"])] + insertThirdPartyImportsA + [ ("nirum.constructs", [("name_dict_type", "NameDict")]) + , ("nirum.datastructures", [("map_type", "Map")]) + ] pyVer <- getPythonVersion return $ toStrict $ renderMarkup $ [compileText| class #{className}(#{T.intercalate "," $ compileExtendClasses annotations}): @@ -718,18 +854,28 @@ class #{className}(#{T.intercalate "," $ compileExtendClasses annotations}): #{toEnumMemberName tn} = '#{toBehindSnakeCaseText tn}' %{ endforall } +%{ case pyVer } +%{ of Python2 } def __init__(self, *args, **kwargs): +%{ of Python3 } + def __init__(self, *args, **kwargs) -> None: +%{ endcase } raise NotImplementedError( "{0} cannot be instantiated " "since it is an abstract class. Instantiate a concrete subtype " - "of it instead.".format(#{typeRepr "type(self)"}) + "of it instead.".format(typing._type_repr(self)) ) +%{ case pyVer } +%{ of Python2 } def __nirum_serialize__(self): +%{ of Python3 } + def __nirum_serialize__(self) -> typing.Mapping[str, object]: +%{ endcase } raise NotImplementedError( "{0} cannot be instantiated " "since it is an abstract class. Instantiate a concrete subtype " - "of it instead.".format(#{typeRepr "type(self)"}) + "of it instead.".format(typing._type_repr(self)) ) @classmethod @@ -991,21 +1137,50 @@ if hasattr({className}.Client, '__qualname__'): Nothing -> return "raise UnexpectedNirumResponseError(serialized)" payloadArguments <- mapM compileClientPayload $ toList params + validators <- sequence + [ do + v <- compileValidator' src pTypeExpr $ toAttributeName' pName + pTypeExprCode <- compileTypeExpression' src $ Just pTypeExpr + return (pName, pTypeExprCode, v) + | Parameter pName pTypeExpr _ <- toList params + ] ret <- returnCompiler - return [qq| - def {clientMethodName'}(self, {commaNl params'}){ret rtypeExpr}: + return $ toStrict $ renderMarkup $ [compileText| + def #{clientMethodName'}(self, #{commaNl params'})#{ret rtypeExpr}: + _type_repr = __import__('typing')._type_repr + # typing module can be masked by parameter of the same name, e.g.: + # service foo-service ( bar (text typing) ); + # As Nirum identifier disallows to begin with dash/underscore, + # we can avoid such name overwrapping by defining _type_repr, + # an underscore-leaded alias of typing._type_repr and using it + # in the below. +%{ forall (pName, pType, (Validator pTypePred pValueValidators)) <- validators } + if not (#{pTypePred}): + raise TypeError( + '#{toAttributeName' pName} must be a value of ' + + _type_repr(#{pType}) + ', not ' + + repr(#{toAttributeName' pName}) + ) +%{ forall ValueValidator pValuePredCode pValueErrorMsg <- pValueValidators } + elif not (#{pValuePredCode}): + raise ValueError( + 'invalid #{toAttributeName' pName}: ' + #{stringLiteral pValueErrorMsg} + ) +%{ endforall } +%{ endforall } successful, serialized = self.__nirum_transport__( - '{I.toSnakeCaseText $ N.behindName mName}', - payload=\{{commaNl payloadArguments}\}, + '#{I.toSnakeCaseText $ N.behindName mName}', + payload={#{commaNl payloadArguments}}, # FIXME Give annotations. - service_annotations=\{\}, + service_annotations={}, method_annotations=self.__nirum_method_annotations__, - parameter_annotations=\{\} + parameter_annotations={} ) if successful: - result_type = $rtypeExpr + result_type = #{rtypeExpr} else: - $errorCode + #{errorCode} if result_type is None: result = None else: @@ -1091,9 +1266,10 @@ compileModule pythonVersion' source = do let (result, context) = runCodeGen (compileModuleBody source) (empty pythonVersion') let deps = require "nirum" "nirum" $ M.keysSet $ thirdPartyImports context + let standardImportSet' = standardImportSet context let optDeps = - [ ((3, 4), require "enum34" "enum" $ standardImports context) - , ((3, 5), require "typing" "typing" $ standardImports context) + [ ((3, 4), require "enum34" "enum" standardImportSet') + , ((3, 5), require "typing" "typing" standardImportSet') ] let installRequires = InstallRequires deps optDeps let fromImports = M.assocs (localImportsMap context) ++ @@ -1102,8 +1278,12 @@ compileModule pythonVersion' source = do return $ (,) installRequires $ toStrict $ renderMarkup $ [compileText|# -*- coding: utf-8 -*- #{compileDocstring "" $ sourceModule source} -%{ forall i <- S.elems (standardImports context) } -import #{i} +%{ forall (alias, import') <- M.assocs (standardImports context) } +%{ if import' == alias } +import #{import'} +%{ else } +import #{import'} as #{alias} +%{ endif } %{ endforall } %{ forall (from, nameMap) <- fromImports } diff --git a/src/Nirum/Targets/Python/CodeGen.hs b/src/Nirum/Targets/Python/CodeGen.hs index e682b6e..8c655c1 100644 --- a/src/Nirum/Targets/Python/CodeGen.hs +++ b/src/Nirum/Targets/Python/CodeGen.hs @@ -11,11 +11,14 @@ module Nirum.Targets.Python.CodeGen , RenameMap , empty , getPythonVersion + , importBuiltins + , importStandardLibrary + , importTypingForPython3 , insertLocalImport , insertStandardImport + , insertStandardImportA , insertThirdPartyImports , insertThirdPartyImportsA - , importTypingForPython3 , keywords , localImportsMap , mangleVar @@ -75,7 +78,8 @@ type CompileError = Text type Code = Text data CodeGenContext - = CodeGenContext { standardImports :: Set Text + = CodeGenContext { standardImports :: Map Text Text + , standardImportSet :: Set Text , thirdPartyImports :: Map Text (Map Text Text) , localImports :: Map Text (Set Text) , pythonVersion :: PythonVersion @@ -88,6 +92,7 @@ instance Nirum.CodeGen.Failure CodeGenContext CompileError where empty :: PythonVersion -> CodeGenContext empty pythonVer = CodeGenContext { standardImports = [] + , standardImportSet = [] , thirdPartyImports = [] , localImports = [] , pythonVersion = pythonVer @@ -104,11 +109,36 @@ runCodeGen :: CodeGen a -> (Either CompileError a, CodeGenContext) runCodeGen = Nirum.CodeGen.runCodeGen +importStandardLibrary :: Text -> CodeGen Code +importStandardLibrary module' = do + insertStandardImportA alias module' + return alias + where + alias :: Code + alias + | "_" `isPrefixOf` module' = module' + | otherwise = '_' `cons` Data.Text.replace "." "_" module' + +importBuiltins :: CodeGen Code +importBuiltins = do + pyVer <- getPythonVersion + case pyVer of + Python3 -> do + insertStandardImportA "__builtin__" "builtins" + return "__builtin__" + Python2 -> importStandardLibrary "__builtin__" + insertStandardImport :: Text -> CodeGen () -insertStandardImport module' = modify insert' +insertStandardImport module' = + insertStandardImportA module' module' + +insertStandardImportA :: Code -> Text -> CodeGen () +insertStandardImportA alias module' = modify insert' where - insert' c@CodeGenContext { standardImports = si } = - c { standardImports = Data.Set.insert module' si } + insert' c@CodeGenContext { standardImports = si, standardImportSet = ss } = + c { standardImports = Data.Map.Strict.insert alias module' si + , standardImportSet = Data.Set.insert module' ss + } insertThirdPartyImports :: [(Text, Set Text)] -> CodeGen () insertThirdPartyImports imports = diff --git a/src/Nirum/Targets/Python/TypeExpression.hs b/src/Nirum/Targets/Python/TypeExpression.hs index 43ba90a..dd14ba9 100644 --- a/src/Nirum/Targets/Python/TypeExpression.hs +++ b/src/Nirum/Targets/Python/TypeExpression.hs @@ -33,12 +33,12 @@ compileTypeExpression mod' (Just (TypeIdentifier i)) = compileTypeExpression mod' (Just (MapModifier k v)) = do kExpr <- compileTypeExpression mod' (Just k) vExpr <- compileTypeExpression mod' (Just v) - insertStandardImport "typing" - return [qq|typing.Mapping[$kExpr, $vExpr]|] + typing <- importStandardLibrary "typing" + return [qq|$typing.Mapping[$kExpr, $vExpr]|] compileTypeExpression mod' (Just modifier) = do expr <- compileTypeExpression mod' (Just typeExpr) - insertStandardImport "typing" - return [qq|typing.$className[$expr]|] + typing <- importStandardLibrary "typing" + return [qq|$typing.$className[$expr]|] where typeExpr :: TypeExpression className :: Text @@ -55,27 +55,34 @@ compilePrimitiveType :: PrimitiveTypeIdentifier -> CodeGen Code compilePrimitiveType primitiveTypeIdentifier' = do pyVer <- getPythonVersion case (primitiveTypeIdentifier', pyVer) of - (Bool, _) -> return "bool" - (Bigint, _) -> return "int" + (Bool, _) -> builtins "bool" + (Bigint, Python2) -> do + numbers <- importStandardLibrary "numbers" + return [qq|$numbers.Integral|] + (Bigint, Python3) -> builtins "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" + decimal <- importStandardLibrary "decimal" + return [qq|$decimal.Decimal|] + (Int32, _) -> compilePrimitiveType Bigint + (Int64, _) -> compilePrimitiveType Bigint + (Float32, _) -> builtins "float" + (Float64, _) -> builtins "float" + (Text, Python2) -> builtins "unicode" + (Text, Python3) -> builtins "str" + (Binary, _) -> builtins "bytes" (Date, _) -> do - insertStandardImport "datetime" - return "datetime.date" + datetime <- importStandardLibrary "datetime" + return [qq|$datetime.date|] (Datetime, _) -> do - insertStandardImport "datetime" - return "datetime.datetime" - (Uuid, _) -> insertStandardImport "uuid" >> return "uuid.UUID" - (Uri, Python2) -> return "unicode" - (Uri, Python3) -> return "str" + datetime <- importStandardLibrary "datetime" + return [qq|$datetime.datetime|] + (Uuid, _) -> do + uuid <- importStandardLibrary "uuid" + return [qq|$uuid.UUID|] + (Uri, Python2) -> builtins "basestring" + (Uri, Python3) -> builtins "str" + where + builtins :: Code -> CodeGen Code + builtins typename' = do + builtinsMod <- importBuiltins + return [qq|$builtinsMod.$typename'|] diff --git a/src/Nirum/Targets/Python/Validators.hs b/src/Nirum/Targets/Python/Validators.hs new file mode 100644 index 0000000..33ad54b --- /dev/null +++ b/src/Nirum/Targets/Python/Validators.hs @@ -0,0 +1,146 @@ +{-# LANGUAGE QuasiQuotes #-} +module Nirum.Targets.Python.Validators + ( Validator (..) + , ValueValidator (..) + , compilePrimitiveTypeValidator + , compileValidator + ) where + +import Data.Text (Text, intercalate) +import Text.InterpolatedString.Perl6 (qq) + +import Nirum.Constructs.Identifier +import Nirum.Constructs.TypeDeclaration +import Nirum.Constructs.TypeExpression +import {-# SOURCE #-} Nirum.Targets.Python () +import Nirum.Targets.Python.CodeGen +import Nirum.Targets.Python.TypeExpression +import Nirum.TypeInstance.BoundModule + +data Validator = Validator + { typePredicateCode :: Code + , valueValidators :: [ValueValidator] + } deriving (Eq, Show) + +data ValueValidator = ValueValidator + { predicateCode :: Code + , errorMessage :: Text + } deriving (Eq, Show) + +compileValidator :: BoundModule Python + -> TypeExpression + -> Code + -> CodeGen Validator +compileValidator mod' (OptionModifier typeExpr) pythonVar = do + Validator typePred vvs <- compileValidator mod' typeExpr pythonVar + let typeValidator = [qq|(($pythonVar) is None or $typePred)|] + valueValidators' = + [ ValueValidator [qq|(($pythonVar) is None or ($vPredCode))|] msg + | ValueValidator vPredCode msg <- vvs + ] + return $ Validator typeValidator valueValidators' +compileValidator mod' (SetModifier typeExpr) pythonVar = do + abc <- collectionsAbc + Validator typePred vvs <- + multiplexValidators mod' pythonVar [(typeExpr, "elem")] + return $ Validator + [qq|(isinstance($pythonVar, $abc.Set) and $typePred)|] + vvs +compileValidator mod' (ListModifier typeExpr) pythonVar = do + abc <- collectionsAbc + Validator typePred vvs <- + multiplexValidators mod' pythonVar [(typeExpr, "item")] + return $ Validator + [qq|(isinstance($pythonVar, $abc.Sequence) and $typePred)|] + vvs +compileValidator mod' (MapModifier keyTypeExpr valueTypeExpr) pythonVar = do + abc <- collectionsAbc + Validator typePred vvs <- + multiplexValidators mod' [qq|(($pythonVar).items())|] + [(keyTypeExpr, "key"), (valueTypeExpr, "value")] + return $ Validator + [qq|(isinstance($pythonVar, $abc.Mapping) and $typePred)|] + vvs +compileValidator mod' (TypeIdentifier typeId) pythonVar = + case lookupType typeId mod' of + Missing -> return $ Validator "False" [] -- must never happen + Local (Alias typeExpr') -> compileValidator mod' typeExpr' pythonVar + Imported modulePath' (Alias typeExpr') -> + case resolveBoundModule modulePath' (boundPackage mod') of + Nothing -> return $ Validator "False" [] -- must never happen + Just foundMod -> compileValidator foundMod typeExpr' pythonVar + Local PrimitiveType { primitiveTypeIdentifier = pId } -> + compilePrimitiveTypeValidator pId pythonVar + Imported _ PrimitiveType { primitiveTypeIdentifier = pId } -> + compilePrimitiveTypeValidator pId pythonVar + _ -> + compileInstanceValidator mod' typeId pythonVar + +compilePrimitiveTypeValidator :: PrimitiveTypeIdentifier + -> Code + -> CodeGen Validator +compilePrimitiveTypeValidator primitiveTypeId pythonVar = do + typeName <- compilePrimitiveType primitiveTypeId + return $ Validator + [qq|(isinstance(($pythonVar), ($typeName)))|] + (vv primitiveTypeId pythonVar) + where + vv :: PrimitiveTypeIdentifier -> Code -> [ValueValidator] + vv Int32 var = + [ ValueValidator [qq|(-0x80000000 <= ($var) < 0x80000000)|] + "out of range of 32-bit integer" + ] + vv Int64 var = + [ ValueValidator + [qq|(-0x8000000000000000 <= ($var) < 0x8000000000000000)|] + "out of range of 64-bit integer" + ] + vv Datetime var = + [ ValueValidator [qq|(($var).tzinfo is not None)|] + "naive datetime (lacking tzinfo)" + ] + vv Uri var = + [ ValueValidator [qq|('\\n' not in ($var))|] + "URI cannot contain new line characters" + ] + vv _ _ = [] + +compileInstanceValidator :: BoundModule Python + -> Identifier + -> Code + -> CodeGen Validator +compileInstanceValidator mod' typeId pythonVar = do + cls <- compileTypeExpression mod' (Just (TypeIdentifier typeId)) + return $ Validator [qq|(isinstance(($pythonVar), ($cls)))|] [] + +collectionsAbc :: CodeGen Code +collectionsAbc = do + ver <- getPythonVersion + importStandardLibrary $ case ver of + Python2 -> "collections" + Python3 -> "collections.abc" + +multiplexValidators :: BoundModule Python + -> Code + -> [(TypeExpression, Code)] + -> CodeGen Validator +multiplexValidators mod' iterableExpr elements = do + validators <- sequence + [ do + v <- compileValidator mod' tExpr elemVar + return (elemVar, v) + | (tExpr, var) <- elements + , elemVar <- [mangleVar iterableExpr var] + ] + let csElemVars = intercalate "," [v | (v, _) <- validators] + typePredLogicalAnds = intercalate + " and " + [typePred | (_, Validator typePred _) <- validators] + return $ Validator + [qq|(all(($typePredLogicalAnds) for ($csElemVars) in $iterableExpr))|] + [ ValueValidator + [qq|(all(($typePred) for ($csElemVars) in $iterableExpr))|] + [qq|invalid elements ($msg)|] + | (_, Validator _ vvs) <- validators + , ValueValidator typePred msg <- vvs + ] diff --git a/test/Nirum/Targets/Python/CodeGenSpec.hs b/test/Nirum/Targets/Python/CodeGenSpec.hs index 3fe0fa4..e9e5ff9 100644 --- a/test/Nirum/Targets/Python/CodeGenSpec.hs +++ b/test/Nirum/Targets/Python/CodeGenSpec.hs @@ -103,7 +103,7 @@ spec' = pythonVersionSpecs $ \ ver -> do insertLocalImport ".." "Path" let (e, ctx) = runCodeGen c empty' e `shouldSatisfy` isRight - standardImports ctx `shouldBe` ["os", "sys"] + standardImports ctx `shouldBe` [("os", "os"), ("sys", "sys")] thirdPartyImports ctx `shouldBe` [ ( "nirum" , [ ("serialize_unboxed_type", "serialize_unboxed_type") @@ -122,18 +122,83 @@ spec' = pythonVersionSpecs $ \ ver -> do ] ) ] + specify "importStandardLibrary" $ do + let codeGen1 = importStandardLibrary "io" + let (e1, ctx1) = runCodeGen codeGen1 empty' + e1 `shouldBe` Right "_io" + standardImports ctx1 `shouldBe` [("_io", "io")] + standardImportSet ctx1 `shouldBe` ["io"] + thirdPartyImports ctx1 `shouldBe` [] + localImports ctx1 `shouldBe` [] + compileError codeGen1 `shouldBe` Nothing + -- importing a nested module, i.e., import path that contains "." + let codeGen2 = codeGen1 >> importStandardLibrary "os.path" + let (e2, ctx2) = runCodeGen codeGen2 empty' + e2 `shouldBe` Right "_os_path" + standardImports ctx2 `shouldBe` + [("_os_path", "os.path"), ("_io", "io")] + standardImportSet ctx2 `shouldBe` ["os.path", "io"] + thirdPartyImports ctx2 `shouldBe` [] + localImports ctx2 `shouldBe` [] + compileError codeGen2 `shouldBe` Nothing + -- importing a module that begins with "_" + let codeGen3 = codeGen2 >> importStandardLibrary "__builtin__" + let (e3, ctx3) = runCodeGen codeGen3 empty' + e3 `shouldBe` Right "__builtin__" + standardImports ctx3 `shouldBe` + [ ("__builtin__", "__builtin__") + , ("_os_path", "os.path") + , ("_io", "io") + ] + standardImportSet ctx3 `shouldBe` ["__builtin__", "os.path", "io"] + thirdPartyImports ctx3 `shouldBe` [] + localImports ctx3 `shouldBe` [] + compileError codeGen3 `shouldBe` Nothing + specify "importBuiltins" $ do + let codeGen1 = importBuiltins + let (e1, ctx1) = runCodeGen codeGen1 empty' + e1 `shouldSatisfy` isRight + standardImports ctx1 `shouldBe` + [ if ver == Python2 + then ("__builtin__", "__builtin__") + else ("__builtin__", "builtins") + ] + standardImportSet ctx1 `shouldBe` + [if ver == Python2 then "__builtin__" else "builtins"] + thirdPartyImports ctx1 `shouldBe` [] + localImports ctx1 `shouldBe` [] + compileError codeGen1 `shouldBe` Nothing specify "insertStandardImport" $ do let codeGen1 = insertStandardImport "sys" let (e1, ctx1) = runCodeGen codeGen1 empty' e1 `shouldSatisfy` isRight - standardImports ctx1 `shouldBe` ["sys"] + standardImports ctx1 `shouldBe` [("sys", "sys")] + standardImportSet 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"] + standardImports ctx2 `shouldBe` [("os", "os"), ("sys", "sys")] + standardImportSet ctx2 `shouldBe` ["os", "sys"] + thirdPartyImports ctx2 `shouldBe` [] + localImports ctx2 `shouldBe` [] + compileError codeGen2 `shouldBe` Nothing + specify "insertStandardImportA" $ do + let codeGen1 = insertStandardImportA "_csv" "csv" + let (e1, ctx1) = runCodeGen codeGen1 empty' + e1 `shouldSatisfy` isRight + standardImports ctx1 `shouldBe` [("_csv", "csv")] + standardImportSet ctx1 `shouldBe` ["csv"] + thirdPartyImports ctx1 `shouldBe` [] + localImports ctx1 `shouldBe` [] + compileError codeGen1 `shouldBe` Nothing + let codeGen2 = codeGen1 >> insertStandardImportA "_gc" "gc" + let (e2, ctx2) = runCodeGen codeGen2 empty' + e2 `shouldSatisfy` isRight + standardImports ctx2 `shouldBe` [("_gc", "gc"), ("_csv", "csv")] + standardImportSet ctx2 `shouldBe` ["gc", "csv"] thirdPartyImports ctx2 `shouldBe` [] localImports ctx2 `shouldBe` [] compileError codeGen2 `shouldBe` Nothing diff --git a/test/Nirum/Targets/Python/TypeExpressionSpec.hs b/test/Nirum/Targets/Python/TypeExpressionSpec.hs index 2279ac8..87f5f57 100644 --- a/test/Nirum/Targets/Python/TypeExpressionSpec.hs +++ b/test/Nirum/Targets/Python/TypeExpressionSpec.hs @@ -20,69 +20,82 @@ spec = pythonVersionSpecs $ \ ver -> do run' c = runCodeGen c empty' -- code :: CodeGen a -> a code = either (const undefined) id . fst . run' + builtinsPair = + ( "__builtin__" + , case ver of + Python2 -> "__builtin__" + Python3 -> "builtins" + ) specify [qq|compilePrimitiveType ($ver)|] $ do - code (compilePrimitiveType Bool) `shouldBe` "bool" - code (compilePrimitiveType Bigint) `shouldBe` "int" + let (boolCode, boolContext) = run' $ compilePrimitiveType Bool + let intTypeCode = case ver of + Python2 -> "_numbers.Integral" + Python3 -> "__builtin__.int" + boolCode `shouldBe` Right "__builtin__.bool" + standardImports boolContext `shouldBe` [builtinsPair] + code (compilePrimitiveType Bigint) `shouldBe` intTypeCode 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" + decimalCode `shouldBe` Right "_decimal.Decimal" + standardImports decimalContext `shouldBe` [("_decimal", "decimal")] + code (compilePrimitiveType Int32) `shouldBe` intTypeCode + code (compilePrimitiveType Int64) `shouldBe` intTypeCode + code (compilePrimitiveType Float32) `shouldBe` "__builtin__.float" + code (compilePrimitiveType Float64) `shouldBe` "__builtin__.float" code (compilePrimitiveType Text) `shouldBe` case ver of - Python2 -> "unicode" - Python3 -> "str" - code (compilePrimitiveType Binary) `shouldBe` "bytes" + Python2 -> "__builtin__.unicode" + Python3 -> "__builtin__.str" + code (compilePrimitiveType Binary) `shouldBe` "__builtin__.bytes" let (dateCode, dateContext) = run' (compilePrimitiveType Date) - dateCode `shouldBe` Right "datetime.date" - standardImports dateContext `shouldBe` ["datetime"] + dateCode `shouldBe` Right "_datetime.date" + standardImports dateContext `shouldBe` [("_datetime", "datetime")] let (datetimeCode, datetimeContext) = run' (compilePrimitiveType Datetime) - datetimeCode `shouldBe` Right "datetime.datetime" - standardImports datetimeContext `shouldBe` ["datetime"] + datetimeCode `shouldBe` Right "_datetime.datetime" + standardImports datetimeContext `shouldBe` [("_datetime", "datetime")] let (uuidCode, uuidContext) = run' (compilePrimitiveType Uuid) - uuidCode `shouldBe` Right "uuid.UUID" - standardImports uuidContext `shouldBe` ["uuid"] + uuidCode `shouldBe` Right "_uuid.UUID" + standardImports uuidContext `shouldBe` [("_uuid", "uuid")] code (compilePrimitiveType Uri) `shouldBe` case ver of - Python2 -> "unicode" - Python3 -> "str" + Python2 -> "__builtin__.basestring" + Python3 -> "__builtin__.str" describe [qq|compileTypeExpression ($ver)|] $ do let Source { sourceModule = bm } = makeDummySource $ Module [] Nothing specify "TypeIdentifier" $ do let (c, ctx) = run' $ - compileTypeExpression bm (Just $ TypeIdentifier "bigint") - standardImports ctx `shouldBe` [] + compileTypeExpression bm (Just $ TypeIdentifier "binary") + standardImports ctx `shouldBe` [builtinsPair] localImports ctx `shouldBe` [] - c `shouldBe` Right "int" + c `shouldBe` Right "__builtin__.bytes" specify "OptionModifier" $ do let (c', ctx') = run' $ - compileTypeExpression bm (Just $ OptionModifier "int32") - standardImports ctx' `shouldBe` ["typing"] + compileTypeExpression bm (Just $ OptionModifier "binary") + standardImports ctx' `shouldBe` + [builtinsPair, ("_typing", "typing")] localImports ctx' `shouldBe` [] - c' `shouldBe` Right "typing.Optional[int]" + c' `shouldBe` Right "_typing.Optional[__builtin__.bytes]" specify "SetModifier" $ do let (c'', ctx'') = run' $ - compileTypeExpression bm (Just $ SetModifier "int32") - standardImports ctx'' `shouldBe` ["typing"] + compileTypeExpression bm (Just $ SetModifier "float32") + standardImports ctx'' `shouldBe` + [builtinsPair, ("_typing", "typing")] localImports ctx'' `shouldBe` [] - c'' `shouldBe` Right "typing.AbstractSet[int]" + c'' `shouldBe` Right "_typing.AbstractSet[__builtin__.float]" specify "ListModifier" $ do let (c''', ctx''') = run' $ - compileTypeExpression bm (Just $ ListModifier "int32") - standardImports ctx''' `shouldBe` ["typing"] + compileTypeExpression bm (Just $ ListModifier "float64") + standardImports ctx''' `shouldBe` + [builtinsPair, ("_typing", "typing")] localImports ctx''' `shouldBe` [] - c''' `shouldBe` Right "typing.Sequence[int]" + c''' `shouldBe` Right "_typing.Sequence[__builtin__.float]" specify "MapModifier" $ do - let (c'''', ctx'''') = run' $ - compileTypeExpression bm (Just $ MapModifier "uuid" "int32") - standardImports ctx'''' `shouldBe` ["typing", "uuid"] + let (c'''', ctx'''') = run' $ compileTypeExpression bm $ + Just $ MapModifier "uuid" "binary" + standardImports ctx'''' `shouldBe` + [builtinsPair, ("_typing", "typing"), ("_uuid", "uuid")] localImports ctx'''' `shouldBe` [] - c'''' `shouldBe` Right "typing.Mapping[uuid.UUID, int]" + c'''' `shouldBe` + Right "_typing.Mapping[_uuid.UUID, __builtin__.bytes]" diff --git a/test/nirum_fixture/fixture/foo.nrm b/test/nirum_fixture/fixture/foo.nrm index b7bfbeb..61de585 100644 --- a/test/nirum_fixture/fixture/foo.nrm +++ b/test/nirum_fixture/fixture/foo.nrm @@ -133,3 +133,11 @@ service sample-service ( record record-with-map ( {text: text} text-to-text, ); + +record name-shadowing-field-record ( + {text: [text]} typing, + {text: {text}} collections, + bigint numbers, + uuid uuid, + binary bytes, +); diff --git a/test/nirum_fixture/fixture/types.nrm b/test/nirum_fixture/fixture/types.nrm index 8f0ddb2..980f0bb 100644 --- a/test/nirum_fixture/fixture/types.nrm +++ b/test/nirum_fixture/fixture/types.nrm @@ -1 +1,5 @@ -record uuid-list ([uuid] values); +unboxed uuid-list ([uuid]); + +unboxed int32-unboxed (int32); + +unboxed datetime-unboxed (datetime); diff --git a/test/python/primitive_test.py b/test/python/primitive_test.py index 103f823..fd50601 100644 --- a/test/python/primitive_test.py +++ b/test/python/primitive_test.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import enum +import uuid from pytest import raises from nirum.service import Service @@ -8,7 +9,8 @@ from fixture.foo import (Album, CultureAgnosticName, Dog, EastAsianName, EvaChar, FloatUnbox, Gender, ImportedTypeUnbox, Irum, - Line, MixedName, Mro, Music, NoMro, NullService, + Line, MixedName, Mro, Music, NoMro, + NameShadowingFieldRecord, NullService, Person, People, Point1, Point2, Point3d, Pop, PingService, Product, RecordWithMap, RecordWithOptionalRecordField, @@ -17,6 +19,7 @@ WesternName) from fixture.foo.bar import PathUnbox, IntUnbox, Point from fixture.qux import Path, Name +from fixture.types import UuidList def test_float_unbox(): @@ -29,8 +32,8 @@ def test_float_unbox(): assert {float_unbox, float_unbox, float1} == {float_unbox, float1} assert float_unbox.__nirum_serialize__() == 3.14 assert FloatUnbox.__nirum_deserialize__(3.14) == float_unbox - assert hash(float_unbox) - assert hash(float_unbox) != 3.14 + assert hash(float_unbox) == hash(FloatUnbox(3.14)) + assert hash(float_unbox) != hash(float1) with raises(ValueError): FloatUnbox.__nirum_deserialize__('a') with raises(TypeError): @@ -69,6 +72,20 @@ def test_boxed_alias(): assert Irum.__nirum_deserialize__(u'khj') == Irum(u'khj') +def test_unboxed_list(): + assert list(UuidList([]).value) == [] + uuids = [uuid.uuid1() for _ in range(3)] + assert list(UuidList(uuids).value) == uuids + with raises(TypeError) as ei: + UuidList(['not uuid']) + assert (str(ei.value) == + "expected typing.Sequence[uuid.UUID], not ['not uuid']") + with raises(TypeError) as ei: + UuidList(uuids + ['not uuid']) + assert str(ei.value) == \ + "expected typing.Sequence[uuid.UUID], not %r" % (uuids + ['not uuid']) + + def test_enum(): assert type(Gender) is enum.EnumMeta assert set(Gender) == {Gender.male, Gender.female} @@ -102,7 +119,9 @@ def test_record(): assert point != Point1(left=4, top=14) assert point != Point1(left=4, top=15) assert point != 'foo' - assert hash(Point1(left=3, top=14)) + assert hash(point) == hash(Point1(left=3, top=14)) + assert hash(point) != hash(Point1(left=0, top=14)) + assert hash(point) != hash(Point1(left=3, top=0)) point_serialized = {'_type': 'point1', 'x': 3, 'top': 14} assert point.__nirum_serialize__() == point_serialized assert Point1.__nirum_deserialize__(point_serialized) == point @@ -124,6 +143,7 @@ def test_record(): Point1(left='a', top=1) with raises(TypeError): Point1(left='a', top='b') + assert repr(point) == 'fixture.foo.Point1(left=3, top=14)' assert isinstance(Point2, type) int_three = IntUnbox(3) int_four_teen = IntUnbox(14) @@ -135,6 +155,9 @@ def test_record(): assert point2 != Point2(left=IntUnbox(4), top=IntUnbox(14)) assert point2 != Point2(left=IntUnbox(4), top=IntUnbox(15)) assert point2 != 'foo' + assert hash(point2) == hash(Point2(left=IntUnbox(3), top=IntUnbox(14))) + assert hash(point2) != hash(Point2(left=IntUnbox(0), top=IntUnbox(14))) + assert hash(point2) != hash(Point2(left=IntUnbox(3), top=IntUnbox(0))) point2_serialize = {'_type': 'point2', 'left': 3, 'top': 14} assert point2.__nirum_serialize__() == point2_serialize assert Point2.__nirum_deserialize__(point2_serialize) == point2 @@ -154,6 +177,10 @@ def test_record(): Point2(left=IntUnbox(1), top='a') with raises(TypeError): Point2(left=IntUnbox(1), top=2) + assert repr(point2) == ( + 'fixture.foo.Point2(left=fixture.foo.bar.IntUnbox(3), ' + 'top=fixture.foo.bar.IntUnbox(14))' + ) assert isinstance(Point3d, type) point3d = Point3d(xy=Point(x=1, y=2), z=3) assert point3d.xy == Point(x=1, y=2) @@ -169,6 +196,8 @@ def test_record(): } assert point3d.__nirum_serialize__() == point3d_serialize assert Point3d.__nirum_deserialize__(point3d_serialize) == point3d + assert (repr(point3d) == + 'fixture.foo.Point3d(xy=fixture.foo.bar.Point(x=1, y=2), z=3)') # Optional fields can be empty r = RecordWithOptionalRecordField(f=None) @@ -228,7 +257,16 @@ def test_union(): middle_name=u'wrong', last_name=u'wrong') assert hash(WesternName(first_name=u'foo', middle_name=u'bar', - last_name=u'baz')) + last_name=u'baz')) == hash(western_name) + assert hash(western_name) != hash( + WesternName(first_name=u'', middle_name=u'bar', last_name=u'baz') + ) + assert hash(western_name) != hash( + WesternName(first_name=u'foo', middle_name=u'', last_name=u'baz') + ) + assert hash(western_name) != hash( + WesternName(first_name=u'foo', middle_name=u'bar', last_name=u'') + ) if PY3: assert repr(western_name) == ( "fixture.foo.MixedName.WesternName(first_name='foo', " @@ -444,3 +482,61 @@ def test_map_serializer(): }, ], } + + +def test_name_shadowing_field(): + NameShadowingFieldRecord( + typing={u'a': [u'b', u'c']}, + collections={u'a': {u'b', u'c'}}, + numbers=1234, + uuid=uuid.uuid4(), + bytes=b'binary', + ) + with raises(TypeError) as ei: + NameShadowingFieldRecord( + typing={u'invalid'}, + collections={u'a': {u'b', u'c'}}, + numbers=1234, + uuid=uuid.uuid4(), + bytes=b'binary', + ) + assert str(ei.value).startswith('typing must be a value of typing.Mapping') + with raises(TypeError) as ei: + NameShadowingFieldRecord( + typing={u'a': [u'b', u'c']}, + collections={u'invalid'}, + numbers=1234, + uuid=uuid.uuid4(), + bytes=b'binary', + ) + assert str(ei.value).startswith( + 'collections must be a value of typing.Mapping[' + ) + with raises(TypeError): + NameShadowingFieldRecord( + typing={u'a': [u'b', u'c']}, + collections={u'a': {u'b', u'c'}}, + numbers='invalid', + uuid=uuid.uuid4(), + bytes=b'binary', + ) + with raises(TypeError) as ei: + NameShadowingFieldRecord( + typing={u'a': [u'b', u'c']}, + collections={u'a': {u'b', u'c'}}, + numbers=1234, + uuid='invalid', + bytes=b'binary', + ) + assert str(ei.value) == "uuid must be a value of uuid.UUID, not 'invalid'" + with raises(TypeError) as ei: + NameShadowingFieldRecord( + typing={u'a': [u'b', u'c']}, + collections={u'a': {u'b', u'c'}}, + numbers=1234, + uuid=uuid.uuid4(), + bytes=['invalid'], + ) + assert "bytes must be a value of {0}, not ['invalid']".format( + 'bytes' if PY3 else 'str' + ) == str(ei.value) diff --git a/test/python/service_test.py b/test/python/service_test.py index 25c4b1f..f35c6d2 100644 --- a/test/python/service_test.py +++ b/test/python/service_test.py @@ -1,6 +1,7 @@ import uuid from nirum.transport import Transport +from pytest import raises from six import PY2 from fixture.foo import (Dog, Gender, PingService, Product, RpcError, @@ -87,6 +88,44 @@ def test_service_client_payload_serialization(): } +def test_service_client_validation(): + """https://github.com/spoqa/nirum/issues/220""" + t = DumbTransport() + c = SampleService.Client(t) + kwargs = dict( + a=Dog(name=u'Dog.name', age=3), + b=Product(name=u'Product.name', sale=False), + c=Gender.female, + d=Way(u'way/path/text'), + e=uuid.UUID('F7DB93E3-731E-48EF-80A2-CAC81E02F1AE'), + f=b'binary data', + g=1234, + h=u'text data' + ) + c.sample_method(**kwargs) # ok + + # Missing argument raises TypeError + missing_arg = dict(kwargs) + del missing_arg['a'] + with raises(TypeError): + c.sample_method(**missing_arg) + + # Passing a value of unmatched type raises TypeError + unmatched_type = dict(kwargs, g='not bigint') + with raises(TypeError): + c.sample_method(**unmatched_type) + + # Passing None to non-optional parameter raises TypeError + passing_none = dict(kwargs, a=None) + with raises(TypeError): + c.sample_method(**passing_none) + + # Passing integer greater than 2^31-1 raises ValueError + pc = PingService.Client(t) + with raises(ValueError): + pc.no_return_method(0x80000000) + + def test_service_client_representation(): if PY2: assert repr(SampleService.Client) == "" diff --git a/test/python/validation_test.py b/test/python/validation_test.py new file mode 100644 index 0000000..f159509 --- /dev/null +++ b/test/python/validation_test.py @@ -0,0 +1,51 @@ +import datetime + +from pytest import raises + +from fixture.foo import Product +from fixture.foo.bar import Point +from fixture.types import DatetimeUnboxed, Int32Unboxed + + +try: + UTC = datetime.timezone.utc +except AttributeError: + from dateutil.tz import tzutc + UTC = tzutc() + + +def test_int32_value_error(): + Int32Unboxed(0) + Int32Unboxed(2 ** 15) + Int32Unboxed(2 ** 31 - 1) + Int32Unboxed(-(2 ** 31)) + with raises(ValueError): + Int32Unboxed(2 ** 31) + with raises(ValueError): + Int32Unboxed(-(2 ** 31 + 1)) + + +def test_int64_value_error(): + Point(x=0, y=0) + Point(x=2 ** 31, y=0) + Point(x=2 ** 63 - 1, y=0) + Point(x=-(2 ** 63), y=0) + with raises(ValueError): + Point(x=2 ** 63, y=0) + with raises(ValueError): + Point(x=-(2 ** 63 + 1), y=0) + + +def test_datetime_value_error(): + DatetimeUnboxed(datetime.datetime(2018, 3, 11, 5, 27, tzinfo=UTC)) + with raises(ValueError): + # Naive datetime is disallowed + DatetimeUnboxed(datetime.datetime(2018, 3, 11, 5, 27)) + + +def test_uri_value_error(): + Product(name=u'', sale=True, url=None) # url field is optional here + Product(name=u'', sale=True, url='http://example.com/') + with raises(ValueError): + # URI cannot contain new lines + Product(name=u'', sale=True, url='http://example.com/\n') diff --git a/test/serialization/primitive-types/uuid.json b/test/serialization/primitive-types/uuid.json index e7c0a86..caf7b44 100644 --- a/test/serialization/primitive-types/uuid.json +++ b/test/serialization/primitive-types/uuid.json @@ -1,22 +1,16 @@ { "description": "Although UUID strings can have various forms, its normalization form is lowercase hex digits in standard form (e.g., 118fe23b-0380-4d15-8f2d-9f8c6f55e5a5).", "type": "fixture.types.uuid-list", - "input": { - "_type": "uuid_list", - "values": [ - "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", - "118FE23B-0380-4D15-8F2D-9F8C6F55E5A5", - "118fe23b03804d158f2d-9f8c6f55e5a5", - "118FE23B03804D158F2D9F8C6F55E5A5" - ] - }, - "normal": { - "_type": "uuid_list", - "values": [ - "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", - "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", - "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", - "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5" - ] - } + "input": [ + "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", + "118FE23B-0380-4D15-8F2D-9F8C6F55E5A5", + "118fe23b03804d158f2d-9f8c6f55e5a5", + "118FE23B03804D158F2D9F8C6F55E5A5" + ], + "normal": [ + "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", + "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", + "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5", + "118fe23b-0380-4d15-8f2d-9f8c6f55e5a5" + ] } diff --git a/tox.ini b/tox.ini index d73fdc6..014e7e6 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = six flake8 pytest + py27: python-dateutil commands = pip install -f {distdir} -e {distdir}/nirum_fixture flake8 test/python