├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .gitkeep ├── Package.swift ├── PythonCodable └── PythonDecoder.swift ├── README.md └── Tests └── PythonCodableTests ├── PythonCodableTests+Python.swift ├── PythonCodableTests+Swift.swift ├── PythonCodableTests.swift ├── PythonDecoderTests.swift └── Resources └── PythonCodableTests.py /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | continuous-integration: 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | - macos-latest 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Install SSH Key 17 | uses: shimataro/ssh-key-action@v2 18 | with: 19 | key: ${{ secrets.SSH_KEY }} 20 | known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} 21 | - uses: actions/checkout@v1 22 | - name: Test 23 | run: swift test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden Files 2 | .* 3 | !.gitignore 4 | !.gitkeep 5 | !.github 6 | !.travis.yml 7 | 8 | # Swift 9 | /build/ 10 | /Package.resolved 11 | 12 | # Python 13 | *.pyc 14 | 15 | # Temporary Items 16 | *.tmp 17 | *.tmp.* 18 | 19 | # Virtual Environments 20 | /venv*/ 21 | 22 | # Configuration Override 23 | *.override.* 24 | 25 | # Extra Directories 26 | /Assets/ 27 | /Extra/ 28 | 29 | # Xcode 30 | xcuserdata/ 31 | *.xcscmblueprint 32 | *.xccheckout 33 | -------------------------------------------------------------------------------- /.gitkeep: -------------------------------------------------------------------------------- 1 | 2D0F96C3-5D13-4E04-BF20-1160EEB790CA 2 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PythonCodable", 7 | platforms: [ 8 | .macOS(.v11) 9 | ], 10 | products: [ 11 | .library( 12 | name: "PythonCodable", 13 | targets: ["PythonCodable"] 14 | ) 15 | ], 16 | dependencies: [ 17 | .package(url: "git@github.com:pvieito/FoundationKit.git", branch: "master"), 18 | .package(url: "git@github.com:pvieito/PythonKit.git", branch: "master"), 19 | .package(url: "https://github.com/tattn/MoreCodable.git", branch: "master"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "PythonCodable", 24 | dependencies: ["PythonKit", "MoreCodable"], 25 | path: "PythonCodable" 26 | ), 27 | .testTarget( 28 | name: "PythonCodableTests", 29 | dependencies: ["PythonCodable", "PythonKit", "FoundationKit"], 30 | resources: [.process("Resources")] 31 | ) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /PythonCodable/PythonDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PythonDecoder.swift 3 | // PythonCodable 4 | // 5 | // Created by Pedro José Pereira Vieito on 11/12/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PythonKit 11 | import MoreCodable 12 | 13 | public struct PythonDecoder { 14 | /// Tries to decodes the input `pythonObject` into the Swift `type` type. 15 | /// - Parameters: 16 | /// - type: Decodable Swift type. 17 | /// - pythonObject: Input Python object. 18 | public static func decode(_ type: T.Type, from pythonObject: PythonObject) throws -> T { 19 | let decodableDictionary = try pythonObject.bridgeToDecodableDictionary() 20 | return try DictionaryDecoder().decode(type, from: decodableDictionary) 21 | } 22 | } 23 | 24 | extension PythonObject { 25 | /// Tries to bridge the `PythonObject` into an equivalent Swift type. 26 | public func bridgeToSwift() throws -> Any? { 27 | return try self.bridgeToDecodableValue() 28 | } 29 | } 30 | 31 | extension PythonDecoder { 32 | enum Error: Swift.Error { 33 | case unsupportedType 34 | case unsupportedListType 35 | case unsupportedDictionaryType 36 | } 37 | } 38 | 39 | extension PythonObject { 40 | func bridgeToDecodableDictionaryKey() throws -> String { 41 | guard self.isPythonString else { 42 | throw PythonError.invalidCall(self) 43 | } 44 | 45 | return String(self)! 46 | } 47 | 48 | func bridgeToDecodableValue() throws -> Any? { 49 | if self.isPythonString { 50 | return String(self)! 51 | } 52 | else if self.isPythonBool { 53 | return Bool(self)! 54 | } 55 | else if self.isPythonInteger { 56 | return Int(self)! 57 | } 58 | else if self.isPythonFloat { 59 | return Double(self)! 60 | } 61 | else if self.isPythonNone { 62 | return nil 63 | } 64 | // Try dictionary before list, since dictionaries are list convertible. 65 | else if self.isPythonDictionaryOrDictionaryConvertible { 66 | return try self.bridgeToDecodableDictionary() 67 | } 68 | else if self.isPythonListOrListConvertible { 69 | return try self.bridgeToDecodableArray() 70 | } 71 | else { 72 | throw PythonDecoder.Error.unsupportedType 73 | } 74 | } 75 | 76 | func bridgeToDecodableArray() throws -> Array { 77 | let pythonList = try self.convertToPythonList() 78 | var bridgedArray: [Any?] = [] 79 | 80 | for item in pythonList { 81 | let bridgedValue = try item.bridgeToDecodableValue() 82 | bridgedArray.append(bridgedValue) 83 | } 84 | 85 | return bridgedArray 86 | } 87 | 88 | func bridgeToDecodableDictionary() throws -> Dictionary { 89 | let pythonDictionary = try self.convertToPythonDictionary() 90 | var bridgedDictionary: [String : Any] = [:] 91 | 92 | for item in pythonDictionary.items() { 93 | let (key, value) = item.tuple2 94 | 95 | if key.isPythonString, let bridgedValue = try value.bridgeToDecodableValue() { 96 | bridgedDictionary[String(key)!] = bridgedValue 97 | } 98 | } 99 | 100 | return bridgedDictionary 101 | } 102 | } 103 | 104 | extension PythonObject { 105 | var isPythonListOrListConvertible: Bool { 106 | return self.isPythonList || self.isPythonListConvertible 107 | } 108 | 109 | var isPythonListConvertible: Bool { 110 | return Bool(Python.hasattr(self, "__iter__"))! 111 | } 112 | 113 | func convertToPythonList() throws -> PythonObject { 114 | if self.isPythonList { 115 | return self 116 | } 117 | else if self.isPythonListConvertible { 118 | return Python.list(pythonObject) 119 | } 120 | else { 121 | throw PythonDecoder.Error.unsupportedListType 122 | } 123 | } 124 | } 125 | 126 | extension PythonObject { 127 | var isPythonDictionaryOrDictionaryConvertible: Bool { 128 | return self.isPythonDictionary || self.isPythonDictionaryConvertible 129 | } 130 | 131 | var isPythonNamedTupleConvertible: Bool { 132 | return Bool(Python.hasattr(self, "_asdict"))! 133 | } 134 | 135 | var isPythonDictionaryConvertible: Bool { 136 | return Bool(Python.hasattr(self, "__dict__"))! 137 | } 138 | 139 | func convertToPythonDictionary() throws -> PythonObject { 140 | if self.isPythonDictionary { 141 | return self 142 | } 143 | else if self.isPythonDictionaryConvertible { 144 | return Python.vars(pythonObject) 145 | } 146 | else if self.isPythonNamedTupleConvertible { 147 | return Python.dict(pythonObject._asdict()) 148 | } 149 | else { 150 | throw PythonDecoder.Error.unsupportedDictionaryType 151 | } 152 | } 153 | } 154 | 155 | extension PythonObject { 156 | var isPythonString: Bool { 157 | return Bool(Python.isinstance(pythonObject, Python.str))! 158 | } 159 | 160 | var isPythonInteger: Bool { 161 | return Bool(Python.isinstance(pythonObject, Python.int))! 162 | } 163 | 164 | var isPythonFloat: Bool { 165 | return Bool(Python.isinstance(pythonObject, Python.float))! 166 | } 167 | 168 | var isPythonBool: Bool { 169 | return Bool(Python.isinstance(pythonObject, Python.bool))! 170 | } 171 | 172 | var isPythonList: Bool { 173 | return Bool(Python.isinstance(pythonObject, Python.list))! 174 | } 175 | 176 | var isPythonDictionary: Bool { 177 | return Bool(Python.isinstance(pythonObject, Python.dict))! 178 | } 179 | 180 | var isPythonNone: Bool { 181 | return Bool(Python.isinstance(pythonObject, Python.type(Python.None)))! 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonCodable 2 | 3 | Swift framework to efficiently bridge objects from Python to Swift. 4 | 5 | ## Requirements 6 | 7 | `PythonCodable` requires [**Swift 5**](https://swift.org/download/) or higher. 8 | 9 | ## Usage 10 | 11 | `PythonCodable` builds on `PythonKit` to allow bridging arbitrary Python objects into Swift types efficiently using `PythonDecoder`: 12 | 13 | ```swift 14 | import PythonKit 15 | import PythonCodable 16 | 17 | // 1. Get a valid Python object: 18 | 19 | let urllibParse = Python.import("urllib.parse") 20 | let pythonParsedURL = urllibParse.urlparse("http://www.cwi.nl:80/%7Eguido/Python.html") 21 | 22 | print(pythonParsedURL) // ParseResult(scheme='http', netloc='www.cwi.nl:80'... 23 | print(Python.type(pythonParsedURL)) // 24 | 25 | // 2. Define a compatible Swift struct conforming to `Decodable`: 26 | 27 | struct ParsedURL: Decodable { 28 | let scheme: String 29 | let netloc: String 30 | let path: String 31 | let params: String? 32 | let query: String? 33 | let fragment: String? 34 | } 35 | 36 | // 3. Decode the Python object as a Swift type using `PythonDecoder`: 37 | 38 | let parsedURL = try PythonDecoder.decode(ParsedURL.self, from: pythonParsedURL) 39 | 40 | XCTAssertEqual(parsedURL.scheme, "http") 41 | XCTAssertEqual(parsedURL.netloc, "www.cwi.nl:80") 42 | XCTAssertEqual(parsedURL.path, "/%7Eguido/Python.html") 43 | ``` 44 | 45 | `PythonDecoder` supports multiple Python to Swift type conversions: 46 | 47 | - `int` to `Int` 48 | - `float` to `Double` 49 | - `bool` to `Bool` 50 | - `None` to `nil` 51 | - `list[t]` to `Array` where `t` is one of the supported types. 52 | - `dict[k, v]` to `Dictionary` where `k` is a `str` and `v` is one of the supported types. 53 | - `object` to `Any : Decodable` where `object` is a `dict`, a named tuple or an object with a dictionary representation. 54 | 55 | ### Swift Package Manager 56 | 57 | Add the following dependency to your `Package.swift` manifest: 58 | 59 | ```swift 60 | .package(url: "https://github.com/pvieito/PythonCodable.git", .branch("master")), 61 | ``` 62 | 63 | ## References 64 | 65 | - [**PythonKit**](https://github.com/pvieito/PythonKit) 66 | -------------------------------------------------------------------------------- /Tests/PythonCodableTests/PythonCodableTests+Python.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PythonCodableTests+Python.swift 3 | // PythonCodableTests 4 | // 5 | // Created by Pedro José Pereira Vieito on 12/12/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PythonKit 11 | 12 | extension PythonCodableTests { 13 | static let pythonModule: PythonObject = { 14 | let sys = Python.import("sys") 15 | sys.path.insert(0, testBundle.resourcePath!) 16 | let pythonCodableTestsModule = Python.import("PythonCodableTests") 17 | return pythonCodableTestsModule 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PythonCodableTests/PythonCodableTests+Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PythonCodableTests+Swift.swift 3 | // PythonCodableTests 4 | // 5 | // Created by Pedro José Pereira Vieito on 12/12/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FoundationKit 11 | import PythonKit 12 | 13 | extension PythonCodableTests { 14 | struct Struct: Codable, Equatable { 15 | struct SubStruct: Codable, Equatable { 16 | struct SubSubStruct: Codable, Equatable { 17 | let string: String 18 | } 19 | 20 | let bool: Bool 21 | let string: String? 22 | let double: Double? 23 | let float: Double? 24 | let int: Int? 25 | let intArray: Array? 26 | let stringArrayArray: Array>? 27 | let subSubStruct: SubSubStruct? 28 | 29 | init( 30 | bool: Bool, 31 | string: String? = nil, 32 | double: Double? = nil, 33 | float: Double? = nil, 34 | int: Int? = nil, 35 | intArray: Array? = nil, 36 | stringArrayArray: Array>? = nil, 37 | subSubStruct: SubSubStruct? = nil) { 38 | self.bool = bool 39 | self.string = string 40 | self.double = double 41 | self.float = float 42 | self.int = int 43 | self.intArray = intArray 44 | self.stringArrayArray = stringArrayArray 45 | self.subSubStruct = subSubStruct 46 | } 47 | } 48 | 49 | let int: Int 50 | let string: String? 51 | let bool: Bool? 52 | let subStruct: SubStruct? 53 | 54 | init( 55 | int: Int, 56 | string: String? = nil, 57 | bool: Bool? = nil, 58 | subStruct: SubStruct? = nil) { 59 | self.int = int 60 | self.string = string 61 | self.bool = bool 62 | self.subStruct = subStruct 63 | } 64 | } 65 | 66 | struct Struct2: Codable, Equatable { 67 | let arrayOfStructs: [Struct]? 68 | let arrayOfDicts: [[String: String]]? 69 | 70 | init( 71 | arrayOfStructs: [Struct]? = nil, 72 | arrayOfDicts: [[String: String]]? = nil) { 73 | self.arrayOfStructs = arrayOfStructs 74 | self.arrayOfDicts = arrayOfDicts 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/PythonCodableTests/PythonCodableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PythonCodableTests.swift 3 | // PythonCodableTests 4 | // 5 | // Created by Pedro José Pereira Vieito on 12/12/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PythonCodableTests { 12 | static let testBundle = Bundle.module 13 | } 14 | -------------------------------------------------------------------------------- /Tests/PythonCodableTests/PythonDecoderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PythonDecoderTests.swift 3 | // PythonCodableTests 4 | // 5 | // Created by Pedro José Pereira Vieito on 11/12/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import PythonKit 12 | import PythonCodable 13 | 14 | final class PythonDecoderTests: XCTestCase { 15 | static func decodeTestStruct(_ pythonObject: PythonObject) throws -> PythonCodableTests.Struct { 16 | return try PythonDecoder.decode(PythonCodableTests.Struct.self, from: pythonObject) 17 | } 18 | 19 | static func decodeTestStruct2(_ pythonObject: PythonObject) throws -> PythonCodableTests.Struct2 { 20 | return try PythonDecoder.decode(PythonCodableTests.Struct2.self, from: pythonObject) 21 | } 22 | 23 | func testPythonDecoderTestReadmeExample() throws { 24 | // 1. Get a valid Python object: 25 | 26 | let urllibParse = Python.import("urllib.parse") 27 | let pythonParsedURL = urllibParse.urlparse("http://www.cwi.nl:80/%7Eguido/Python.html") 28 | 29 | print(pythonParsedURL) // ParseResult(scheme='http', netloc='www.cwi.nl:80'... 30 | print(Python.type(pythonParsedURL)) // 31 | 32 | // 2. Define a compatible Swift struct conforming to `Decodable`: 33 | 34 | struct ParsedURL: Decodable { 35 | let scheme: String 36 | let netloc: String 37 | let path: String 38 | let params: String? 39 | let query: String? 40 | let fragment: String? 41 | } 42 | 43 | // 3. Decode the Python object as a Swift type using `PythonDecoder`: 44 | 45 | let parsedURL = try PythonDecoder.decode(ParsedURL.self, from: pythonParsedURL) 46 | 47 | XCTAssertEqual(parsedURL.scheme, "http") 48 | XCTAssertEqual(parsedURL.netloc, "www.cwi.nl:80") 49 | XCTAssertEqual(parsedURL.path, "/%7Eguido/Python.html") 50 | 51 | XCTAssertEqual(parsedURL.params, "") 52 | XCTAssertEqual(parsedURL.query, "") 53 | XCTAssertEqual(parsedURL.fragment, "") 54 | } 55 | 56 | func testPythonDecoderTestStruct() throws { 57 | let pyA = try Self.decodeTestStruct(PythonCodableTests.pythonModule.Struct( 58 | int: 1, 59 | string: "asb")) 60 | let swA = PythonCodableTests.Struct( 61 | int: 1, 62 | string: "asb") 63 | XCTAssertEqual(pyA, swA) 64 | 65 | let pyB = try Self.decodeTestStruct(PythonObject([ 66 | "int": -1_993_030_200, 67 | "string": "TEST_å∫∂ƒñ", 68 | "bool": false, 69 | "_fake_": "454"])) 70 | let swB = PythonCodableTests.Struct( 71 | int: -1_993_030_200, 72 | string: "TEST_å∫∂ƒñ", 73 | bool: false) 74 | XCTAssertEqual(pyB, swB) 75 | 76 | let pyC = try Self.decodeTestStruct(PythonObject([ 77 | "int": 0, 78 | "string": Python.None, 79 | "bool": true, 80 | "_fake_": "454"])) 81 | let swC = PythonCodableTests.Struct( 82 | int: 0, 83 | bool: true) 84 | XCTAssertEqual(pyC, swC) 85 | 86 | let pyD_SSS = PythonCodableTests.pythonModule.Struct.SubStruct.SubSubStruct(string: "0987") 87 | let swD_SSS = PythonCodableTests.Struct.SubStruct.SubSubStruct(string: "0987") 88 | let pyD_SS = PythonCodableTests.pythonModule.Struct.SubStruct( 89 | subSubStruct: pyD_SSS, 90 | bool: Python.True, 91 | string: "123", 92 | double: 1.334, 93 | float: 1.9876, 94 | intArray: [1, 2, 3], 95 | stringArrayArray: [["stringA"], ["string_text", "3"], [], ["None", Python.None]]) 96 | let swD_SS = PythonCodableTests.Struct.SubStruct( 97 | bool: true, 98 | string: "123", 99 | double: 1.334, 100 | float: 1.9876, 101 | intArray: [1, 2, 3], 102 | stringArrayArray: [["stringA"], ["string_text", "3"], [], ["None", nil]], 103 | subSubStruct: swD_SSS) 104 | let pyD_S = PythonCodableTests.pythonModule.Struct( 105 | int: 0, 106 | subStruct: pyD_SS) 107 | let pyD = try Self.decodeTestStruct(pyD_S) 108 | let swD = PythonCodableTests.Struct( 109 | int: 0, 110 | subStruct: swD_SS) 111 | XCTAssertEqual(pyD, swD) 112 | } 113 | 114 | func testArrayOfStructs() throws { 115 | let pyMod = PythonCodableTests.pythonModule 116 | let pyLS = try Self.decodeTestStruct2(pyMod.Struct2( 117 | arrayOfStructs: [pyMod.Struct(int: 1), pyMod.Struct(int: 2)] 118 | )) 119 | let swLS = PythonCodableTests.Struct2( 120 | arrayOfStructs: [PythonCodableTests.Struct(int: 1), PythonCodableTests.Struct(int: 2)] 121 | ) 122 | XCTAssertEqual(pyLS, swLS) 123 | } 124 | 125 | func testArrayOfDictionaries() throws { 126 | let pyMod = PythonCodableTests.pythonModule 127 | let dict1: [String: String] = ["key1": "value1"] 128 | let dict2: [String: String] = ["key2": "value2"] 129 | let pyLD = try Self.decodeTestStruct2(pyMod.Struct2( 130 | arrayOfDicts: [dict1, dict2] 131 | )) 132 | let swLD = PythonCodableTests.Struct2( 133 | arrayOfDicts: [dict1, dict2] 134 | ) 135 | XCTAssertEqual(pyLD, swLD) 136 | } 137 | 138 | func testPythonDecoderFailures() throws { 139 | let decodeFailureObjects = [ 140 | PythonObject([]), 141 | PythonObject(["string": "TEXT"]), 142 | PythonObject(["int": "TEXT"]), 143 | PythonObject(["int": 1.0]), 144 | PythonObject(["Int": 1]), 145 | PythonObject(["INT": 1]), 146 | PythonObject(["int": 1, "string": 1]), 147 | PythonObject(["int": 1, "string": "TEXT", "subStruct": ["bool": "FALSE"]]), 148 | PythonObject(["int": 1, "string": "TEXT", "subStruct": ["bool": Python.False, "stringArrayArray": [[1]]]]), 149 | ] 150 | 151 | for decodeFailureObject in decodeFailureObjects { 152 | XCTAssertThrowsError(try Self.decodeTestStruct(decodeFailureObject)) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Tests/PythonCodableTests/Resources/PythonCodableTests.py: -------------------------------------------------------------------------------- 1 | # 2 | # PythonCodableTestsClass.swift 3 | # PythonCodableTests 4 | # 5 | # Created by Pedro José Pereira Vieito on 12/12/2019. 6 | # Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | # 8 | 9 | from typing import * 10 | 11 | class Struct: 12 | class SubStruct: 13 | class SubSubStruct: 14 | def __init__(self, string: str): 15 | self.string = string 16 | 17 | def __init__( 18 | self, 19 | bool: bool, 20 | string: str = None, 21 | double: float = None, 22 | float: float = None, 23 | int: int = None, 24 | intArray: List[int] = None, 25 | stringArrayArray: List[List[str]] = None, 26 | subSubStruct: SubSubStruct = None): 27 | self.bool = bool 28 | self.string = string 29 | self.double = double 30 | self.float = float 31 | self.int = int 32 | self.intArray = intArray 33 | self.stringArrayArray = stringArrayArray 34 | self.subSubStruct = subSubStruct 35 | 36 | def __init__( 37 | self, 38 | int: int, 39 | string: str = None, 40 | bool: bool = None, 41 | subStruct: SubStruct = None): 42 | self.int = int 43 | self.string = string 44 | self.bool = bool 45 | self.subStruct = subStruct 46 | 47 | class Struct2: 48 | def __init__( 49 | self, 50 | arrayOfStructs: List[Struct] = None, 51 | arrayOfDicts: List[Dict[str, str]] = None): 52 | self.arrayOfStructs = arrayOfStructs 53 | self.arrayOfDicts = arrayOfDicts 54 | --------------------------------------------------------------------------------