Skip to content

Commit 864b0a2

Browse files
Merge pull request #2345 from dbrattli/python
Python support for Fable 🎉
2 parents 38dd35e + 72c972b commit 864b0a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+8550
-122
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,7 @@ docs/tools/FSharp.Formatting.svclog
201201
.ionide.debug
202202
*.bak
203203
project.lock.json
204+
205+
# Python
206+
.eggs/
207+
__pycache__/

build.fsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,32 @@ let buildLibraryTs() =
170170
runInDir buildDirTs "npm run tsc -- --init --target es2020 --module es2020 --allowJs"
171171
runInDir buildDirTs ("npm run tsc -- --outDir ../../" + buildDirJs)
172172

173+
let buildLibraryPy() =
174+
let libraryDir = "src/fable-library-py"
175+
let projectDir = libraryDir + "/fable"
176+
let buildDirPy = "build/fable-library-py"
177+
178+
cleanDirs [buildDirPy]
179+
180+
runFableWithArgs projectDir [
181+
"--outDir " + buildDirPy </> "fable"
182+
"--fableLib " + buildDirPy </> "fable"
183+
"--lang Python"
184+
"--exclude Fable.Core"
185+
]
186+
// Copy *.py from projectDir to buildDir
187+
copyDirRecursive libraryDir buildDirPy
188+
copyDirNonRecursive (buildDirPy </> "fable/fable-library") (buildDirPy </> "fable")
189+
//copyFile (buildDirPy </> "fable/fable-library/*.py") (buildDirPy </> "fable")
190+
copyFile (buildDirPy </> "fable/system.text.py") (buildDirPy </> "fable/system_text.py")
191+
copyFile (buildDirPy </> "fable/fsharp.core.py") (buildDirPy </> "fable/fsharp_core.py")
192+
copyFile (buildDirPy </> "fable/fsharp.collections.py") (buildDirPy </> "fable/fsharp_collections.py")
193+
//copyFile (buildDirPy </> "fable/async.py") (buildDirPy </> "fable/async_.py")
194+
removeFile (buildDirPy </> "fable/system.text.py")
195+
196+
runInDir buildDirPy ("python3 --version")
197+
runInDir buildDirPy ("python3 ./setup.py develop")
198+
173199
// Like testJs() but doesn't create bundles/packages for fable-standalone & friends
174200
// Mainly intended for CI
175201
let testJsFast() =
@@ -368,6 +394,24 @@ let test() =
368394
if envVarOrNone "APPVEYOR" |> Option.isSome then
369395
testJsFast()
370396

397+
let testPython() =
398+
buildLibraryIfNotExists() // NOTE: fable-library-py needs to be built seperatly.
399+
400+
let projectDir = "tests/Python"
401+
let buildDir = "build/tests/Python"
402+
403+
cleanDirs [buildDir]
404+
runInDir projectDir "dotnet test"
405+
runFableWithArgs projectDir [
406+
"--outDir " + buildDir
407+
"--exclude Fable.Core"
408+
"--lang Python"
409+
]
410+
411+
runInDir buildDir "touch __init__.py" // So relative imports works.
412+
runInDir buildDir "pytest"
413+
414+
371415
let buildLocalPackageWith pkgDir pkgCommand fsproj action =
372416
let version = "3.0.0-local-build-" + DateTime.Now.ToString("yyyyMMdd-HHmm")
373417
action version
@@ -525,9 +569,16 @@ match argsLower with
525569
| "test-react"::_ -> testReact()
526570
| "test-compiler"::_ -> testCompiler()
527571
| "test-integration"::_ -> testIntegration()
572+
| "test-py"::_ -> testPython()
528573
| "quicktest"::_ ->
529574
buildLibraryIfNotExists()
530575
run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --exclude Fable.Core --noCache --runScript"
576+
| "quicktest-py"::_ ->
577+
buildLibraryIfNotExists()
578+
run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Python --exclude Fable.Core --noCache"
579+
| "jupyter" :: _ ->
580+
buildLibraryIfNotExists ()
581+
run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../Fable.Jupyter/src --lang Python --exclude Fable.Core --noCache 2>> /Users/dbrattli/Developer/GitHub/Fable.Jupyter/src/fable.out"
531582

532583
| "run"::_ ->
533584
buildLibraryIfNotExists()
@@ -546,6 +597,7 @@ match argsLower with
546597
| ("watch-library")::_ -> watchLibrary()
547598
| ("fable-library"|"library")::_ -> buildLibrary()
548599
| ("fable-library-ts"|"library-ts")::_ -> buildLibraryTs()
600+
| ("fable-library-py"|"library-py")::_ -> buildLibraryPy()
549601
| ("fable-compiler-js"|"compiler-js")::_ -> buildCompilerJs(minify)
550602
| ("fable-standalone"|"standalone")::_ -> buildStandalone {|minify=minify; watch=false|}
551603
| "watch-standalone"::_ -> buildStandalone {|minify=false; watch=true|}

src/Fable.AST/Plugins.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Verbosity =
1212
type Language =
1313
| JavaScript
1414
| TypeScript
15+
| Python
1516
| Php
1617

1718
type CompilerOptions =

src/Fable.Cli/Entry.fs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ Arguments:
7575
--sourceMapsRoot Set the value of the `sourceRoot` property in generated source maps
7676
7777
--optimize Compile with optimized F# AST (experimental)
78-
--lang|--language Compile to JavaScript (default), "TypeScript" or "Php".
79-
Support for TypeScript and Php is experimental.
78+
--lang|--language Compile to JavaScript (default), TypeScript, Php or Python.
79+
Support for TypeScript, Php and Python is experimental.
8080
8181
Environment variables:
8282
DOTNET_USE_POLLING_FILE_WATCHER
@@ -92,6 +92,7 @@ let defaultFileExt language args =
9292
| None -> CompilerOptionsHelper.DefaultExtension
9393
match language with
9494
| TypeScript -> Path.replaceExtension ".ts" fileExt
95+
| Python -> Path.replaceExtension ".py" fileExt
9596
| Php -> ".php"
9697
| _ -> fileExt
9798

@@ -102,6 +103,7 @@ let argLanguage args =
102103
|> Option.defaultValue "JavaScript"
103104
|> (function
104105
| "ts" | "typescript" | "TypeScript" -> TypeScript
106+
| "py" | "python" | "Python" -> Python
105107
| "php" | "Php" | "PHP" -> Php
106108
| _ -> JavaScript)
107109

src/Fable.Cli/Main.fs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,29 @@ module private Util =
166166
member _.SourceMap =
167167
mapGenerator.Force().toJSON()
168168

169+
type PythonFileWriter(sourcePath: string, targetPath: string, cliArgs: CliArgs, dedupTargetDir) =
170+
let fileExt = ".py"
171+
let targetDir = Path.GetDirectoryName(targetPath)
172+
// PEP8: Modules should have short, all-lowercase names
173+
let fileName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(targetPath))
174+
let fileName = Naming.applyCaseRule Core.CaseRules.SnakeCase fileName
175+
// Note that Python modules cannot contain dots or it will be impossible to import them
176+
let targetPath = Path.Combine(targetDir, fileName + fileExt)
177+
178+
let stream = new IO.StreamWriter(targetPath)
179+
180+
interface PythonPrinter.Writer with
181+
member _.Write(str) =
182+
stream.WriteAsync(str) |> Async.AwaitTask
183+
member _.MakeImportPath(path) =
184+
let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile)
185+
let path = Imports.getImportPath dedupTargetDir sourcePath targetPath projDir cliArgs.OutDir path
186+
if path.EndsWith(".fs") then
187+
let isInFableHiddenDir = Path.Combine(targetDir, path) |> Naming.isInFableHiddenDir
188+
changeFsExtension isInFableHiddenDir path "" // Remove file extension
189+
else path
190+
member _.Dispose() = stream.Dispose()
191+
169192
let compileFile isRecompile (cliArgs: CliArgs) dedupTargetDir (com: CompilerImpl) = async {
170193
try
171194
let fable =
@@ -194,12 +217,18 @@ module private Util =
194217

195218
| Php ->
196219
let php = fable |> Fable2Php.transformFile com
197-
220+
198221
use w = new IO.StreamWriter(outPath)
199222
let ctx = PhpPrinter.Output.Writer.create w
200223
PhpPrinter.Output.writeFile ctx php
201224
w.Flush()
202225

226+
| Python ->
227+
let python = fable |> Fable2Python.Compiler.transformFile com
228+
let map = { new PythonPrinter.SourceMapGenerator with
229+
member _.AddMapping(_,_,_,_,_) = () }
230+
let writer = new PythonFileWriter(com.CurrentFile, outPath, cliArgs, dedupTargetDir)
231+
do! PythonPrinter.run writer map python
203232

204233
"Compiled " + File.getRelativePathFromCwd com.CurrentFile
205234
|> Log.verboseOrIf isRecompile

src/Fable.Core/Fable.Core.PY.fs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Fable.Import
2+
3+
namespace Fable.Core
4+
5+
open System
6+
open System.Text.RegularExpressions
7+
8+
module PY =
9+
type [<AllowNullLiteral>] ArrayConstructor =
10+
[<Emit "$0([None]*$1...)">] abstract Create: size: int -> 'T[]
11+
abstract isArray: arg: obj -> bool
12+
abstract from: arg: obj -> 'T[]
13+
14+
[<RequireQualifiedAccess>]
15+
module Constructors =
16+
17+
let [<Emit("list")>] Array: ArrayConstructor = pyNative
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Fable.Core.PyInterop
2+
3+
open System
4+
open Fable.Core
5+
6+
/// Has same effect as `unbox` (dynamic casting erased in compiled Python code).
7+
/// The casted type can be defined on the call site: `!!myObj?bar(5): float`
8+
let (!!) x: 'T = pyNative
9+
10+
/// Implicit cast for erased unions (U2, U3...)
11+
let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x)
12+
13+
/// Dynamically access a property of an arbitrary object.
14+
/// `myObj?propA` in Python becomes `myObj.propA`
15+
/// `myObj?(propA)` in Python becomes `myObj[propA]`
16+
let (?) (o: obj) (prop: obj): 'a = pyNative
17+
18+
/// Dynamically assign a value to a property of an arbitrary object.
19+
/// `myObj?propA <- 5` in Python becomes `myObj.propA = 5`
20+
/// `myObj?(propA) <- 5` in Python becomes `myObj[propA] = 5`
21+
let (?<-) (o: obj) (prop: obj) (v: obj): unit = pyNative
22+
23+
/// Destructure and apply a tuple to an arbitrary value.
24+
/// E.g. `myFn $ (arg1, arg2)` in Python becomes `myFn(arg1, arg2)`
25+
let ($) (callee: obj) (args: obj): 'a = pyNative
26+
27+
/// Upcast the right operand to obj (and uncurry it if it's a function) and create a key-value tuple.
28+
/// Mostly convenient when used with `createObj`.
29+
/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }`
30+
let (==>) (key: string) (v: obj): string*obj = pyNative
31+
32+
/// Destructure a tuple of arguments and applies to literal Python code as with EmitAttribute.
33+
/// E.g. `emitPyExpr (arg1, arg2) "$0 + $1"` in Python becomes `arg1 + arg2`
34+
let emitPyExpr<'T> (args: obj) (pyCode: string): 'T = pyNative
35+
36+
/// Same as emitPyExpr but intended for Python code that must appear in a statement position
37+
/// E.g. `emitPyExpr aValue "while($0 < 5) doSomething()"`
38+
let emitPyStatement<'T> (args: obj) (pyCode: string): 'T = pyNative
39+
40+
/// Create a literal Python object from a collection of key-value tuples.
41+
/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }`
42+
let createObj (fields: #seq<string*obj>): obj = pyNative
43+
44+
/// Create a literal Python object from a collection of union constructors.
45+
/// E.g. `keyValueList CaseRules.LowerFirst [ MyUnion 4 ]` in Python becomes `{ myUnion: 4 }`
46+
let keyValueList (caseRule: CaseRules) (li: 'T seq): obj = pyNative
47+
48+
/// Create a literal Py object from a mutator lambda. Normally used when
49+
/// the options interface has too many fields to be represented with a Pojo record.
50+
/// E.g. `pyOptions<MyOpt> (fun o -> o.foo <- 5)` in Python becomes `{ "foo": 5 }`
51+
let pyOptions<'T> (f: 'T->unit): 'T = pyNative
52+
53+
/// Create an empty Python object: {}
54+
let createEmpty<'T> : 'T = pyNative
55+
56+
[<Emit("type($0)")>]
57+
let pyTypeof (x: obj): string = pyNative
58+
59+
[<Emit("isinstance($0, $1)")>]
60+
let pyInstanceof (x: obj) (cons: obj): bool = pyNative
61+
62+
/// Works like `ImportAttribute` (same semantics as ES6 imports).
63+
/// You can use "*" or "default" selectors.
64+
let import<'T> (selector: string) (path: string):'T = pyNative
65+
66+
/// F#: let myMember = importMember<string> "myModule"
67+
/// Py: from my_module import my_member
68+
/// Note the import must be immediately assigned to a value in a let binding
69+
let importMember<'T> (path: string):'T = pyNative
70+
71+
/// F#: let myLib = importAll<obj> "myLib"
72+
/// Py: from my_lib import *
73+
let importAll<'T> (path: string):'T = pyNative
74+
75+
/// Imports a file only for its side effects
76+
let importSideEffects (path: string): unit = pyNative

src/Fable.Core/Fable.Core.Util.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Util =
99
try failwith "JS only" // try/catch is just for padding so it doesn't get optimized
1010
with ex -> raise ex
1111

12+
let inline pyNative<'T> : 'T = jsNative
1213
let inline phpNative<'T> : 'T = jsNative
1314

1415
module Experimental =

src/Fable.Core/Fable.Core.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
<Compile Include="Fable.Core.Types.fs" />
1313
<Compile Include="Fable.Core.Util.fs" />
1414
<Compile Include="Fable.Core.JS.fs" />
15+
<Compile Include="Fable.Core.PY.fs" />
1516
<Compile Include="Fable.Core.JsInterop.fs" />
1617
<Compile Include="Fable.Core.PhpInterop.fs" />
18+
<Compile Include="Fable.Core.PyInterop.fs" />
1719
<Compile Include="Fable.Core.Extensions.fs" />
1820
</ItemGroup>
1921
<ItemGroup>

src/Fable.Transforms/Fable.Transforms.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<!-- <Compile Include="Fable2Extended.fs" /> -->
2222
<Compile Include="Fable2Babel.fs" />
2323
<Compile Include="BabelPrinter.fs" />
24+
<Compile Include="Python/Python.fs" />
25+
<Compile Include="Python/Fable2Python.fs" />
26+
<Compile Include="Python/PythonPrinter.fs" />
2427
<Compile Include="Php/Php.fs" />
2528
<Compile Include="Php/Fable2Php.fs" />
2629
<Compile Include="Php/PhpPrinter.fs" />

0 commit comments

Comments
 (0)