├── .config
└── dotnet-tools.json
├── .flake8
├── .gitignore
├── .paket
└── Paket.Restore.targets
├── Dockerfile
├── Fable.Jupyter.sln
├── LICENSE
├── README.md
├── fable_py
├── __init__.py
├── __main__.py
├── images
│ ├── logo-32x32.png
│ ├── logo-64x64.png
│ └── logo-fable.png
├── kernel.py
└── version.py
├── notebooks
└── F# Exchange 2021.ipynb
├── requirements.txt
├── setup.cfg
└── setup.py
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "paket": {
6 | "version": "6.2.1",
7 | "commands": [
8 | "paket"
9 | ]
10 | },
11 | "fable": {
12 | "version": "4.0.0-theta-006",
13 | "commands": [
14 | "fable"
15 | ]
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E731, T484, T400 # Do not assign a lambda expression, use a def
3 | max-line-length = 121
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | obj/
92 |
93 | .ionide/
94 | .vscode/
95 | .idea/
96 |
97 | bin/
98 | obj/
--------------------------------------------------------------------------------
/.paket/Paket.Restore.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
8 |
9 | $(MSBuildVersion)
10 | 15.0.0
11 | false
12 | true
13 |
14 | true
15 | $(MSBuildThisFileDirectory)
16 | $(MSBuildThisFileDirectory)..\
17 | $(PaketRootPath)paket-files\paket.restore.cached
18 | $(PaketRootPath)paket.lock
19 | classic
20 | proj
21 | assembly
22 | native
23 | /Library/Frameworks/Mono.framework/Commands/mono
24 | mono
25 |
26 |
27 | $(PaketRootPath)paket.bootstrapper.exe
28 | $(PaketToolsPath)paket.bootstrapper.exe
29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\
30 |
31 | "$(PaketBootStrapperExePath)"
32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)"
33 |
34 |
35 |
36 |
37 | true
38 | true
39 |
40 |
41 | True
42 |
43 |
44 | False
45 |
46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/'))
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | $(PaketRootPath)paket
56 | $(PaketToolsPath)paket
57 |
58 |
59 |
60 |
61 |
62 | $(PaketRootPath)paket.exe
63 | $(PaketToolsPath)paket.exe
64 |
65 |
66 |
67 |
68 |
69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json"))
70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"'))
71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | <_PaketCommand>dotnet paket
83 |
84 |
85 |
86 |
87 |
88 | $(PaketToolsPath)paket
89 | $(PaketBootStrapperExeDir)paket
90 |
91 |
92 | paket
93 |
94 |
95 |
96 |
97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)"))
98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"
99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"
100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)"
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | true
122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608
123 | false
124 | true
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)'))
134 |
135 |
136 |
137 |
138 |
139 |
141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``))
142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``))
143 |
144 |
145 |
146 |
147 | %(PaketRestoreCachedKeyValue.Value)
148 | %(PaketRestoreCachedKeyValue.Value)
149 |
150 |
151 |
152 |
153 | true
154 | false
155 | true
156 |
157 |
158 |
162 |
163 | true
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached
183 |
184 | $(MSBuildProjectFullPath).paket.references
185 |
186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references
187 |
188 | $(MSBuildProjectDirectory)\paket.references
189 |
190 | false
191 | true
192 | true
193 | references-file-or-cache-not-found
194 |
195 |
196 |
197 |
198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)'))
199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)'))
200 | references-file
201 | false
202 |
203 |
204 |
205 |
206 | false
207 |
208 |
209 |
210 |
211 | true
212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths)
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | false
224 | true
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length)
236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0])
237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1])
238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4])
239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5])
240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6])
241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7])
242 |
243 |
244 | %(PaketReferencesFileLinesInfo.PackageVersion)
245 | All
246 | runtime
247 | $(ExcludeAssets);contentFiles
248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive
249 | true
250 | true
251 |
252 |
253 |
254 |
255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0])
265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1])
266 |
267 |
268 | %(PaketCliToolFileLinesInfo.PackageVersion)
269 |
270 |
271 |
272 |
276 |
277 |
278 |
279 |
280 |
281 | false
282 |
283 |
284 |
285 |
286 |
287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/>
288 |
289 |
290 |
291 |
292 |
293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile)
294 | true
295 | false
296 | true
297 | false
298 | true
299 | false
300 | true
301 | false
302 | true
303 | false
304 | true
305 | $(PaketIntermediateOutputPath)\$(Configuration)
306 | $(PaketIntermediateOutputPath)
307 |
308 |
309 |
310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/>
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
370 |
371 |
420 |
421 |
466 |
467 |
511 |
512 |
555 |
556 |
557 |
558 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jupyter/scipy-notebook:7aa954ab78d1
2 |
3 | USER root
4 | RUN apt-get update && apt-get install wget -y
5 |
6 | # Make sure the contents of our repo are in ${HOME}
7 | COPY . ${HOME}
8 | WORKDIR ${HOME}
9 |
10 | USER ${NB_USER}
11 | RUN pip install --no-cache-dir notebook
12 | RUN python -m fable_py install --user
13 |
14 | # Install.Net
15 | RUN wget https://dot.net/v1/dotnet-install.sh
16 | RUN chmod 777 ./dotnet-install.sh
17 | RUN ./dotnet-install.sh -c Current
18 | ENV PATH="$PATH:${HOME}/.dotnet"
19 |
20 | RUN dotnet tool restore
21 |
--------------------------------------------------------------------------------
/Fable.Jupyter.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30114.105
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Build", "Build.fsproj", "{A4F9B1E8-4827-48CF-A338-4E61A4A5D881}"
7 | EndProject
8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable", "src\Fable.fsproj", "{666D6703-701E-4292-8B64-674886D97CC2}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Debug|x86 = Debug|x86
15 | Release|Any CPU = Release|Any CPU
16 | Release|x64 = Release|x64
17 | Release|x86 = Release|x86
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|x64.ActiveCfg = Debug|Any CPU
26 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|x64.Build.0 = Debug|Any CPU
27 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|x86.ActiveCfg = Debug|Any CPU
28 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Debug|x86.Build.0 = Debug|Any CPU
29 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|x64.ActiveCfg = Release|Any CPU
32 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|x64.Build.0 = Release|Any CPU
33 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|x86.ActiveCfg = Release|Any CPU
34 | {A4F9B1E8-4827-48CF-A338-4E61A4A5D881}.Release|x86.Build.0 = Release|Any CPU
35 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|x64.ActiveCfg = Debug|Any CPU
38 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|x64.Build.0 = Debug|Any CPU
39 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|x86.ActiveCfg = Debug|Any CPU
40 | {666D6703-701E-4292-8B64-674886D97CC2}.Debug|x86.Build.0 = Debug|Any CPU
41 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|x64.ActiveCfg = Release|Any CPU
44 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|x64.Build.0 = Release|Any CPU
45 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|x86.ActiveCfg = Release|Any CPU
46 | {666D6703-701E-4292-8B64-674886D97CC2}.Release|x86.Build.0 = Release|Any CPU
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Dag Brattli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # F# and Fable (Python) support for Jupyter
2 |
3 | Fable Python is an F# kernel for Jupyter based on [Fable](https://fable.io) and
4 | [IPythonKernel](https://github.com/ipython/ipykernel). Fable is a transpiler
5 | that converts [F#](https://fsharp.org) to Python (and JavaScript).
6 |
7 | This work is work-in-progress and related to
8 |
9 | - https://github.com/fable-compiler/Fable/issues/2339
10 | - https://github.com/fable-compiler/Fable/pull/2345
11 |
12 | ## Install
13 |
14 | Make sure you have a recent version of .NET installed on your machine:
15 | https://dotnet.microsoft.com/download
16 |
17 | You also need to install the latest `fable-py` .NET tool globally (and
18 | make sure it's available in PATH environment)
19 |
20 | ```sh
21 | dotnet tool install -g fable --prerelease
22 |
23 | pip install fable-py
24 | python -m fable_py install
25 | ```
26 |
27 | To use the very latest changes (for development):
28 |
29 | ```sh
30 | git clone https://github.com/dbrattli/Fable.Jupyter.git
31 | cd Fable.Jupyter
32 | python setup.py develop
33 | python -m fable_py install
34 | ```
35 |
36 | ## Usage
37 |
38 | You can use Fable Python in the Jupyter notebook by selecting the "F#
39 | (Fable Python)" kernel. To start Jupyter run e.g:
40 |
41 | ```shell
42 | jupyter notebook
43 |
44 | # or
45 |
46 | jupyter lab
47 | ```
48 |
49 | ## Magic commands
50 |
51 | You can inspect the generated Python code by executing `%python` in a cell:
52 |
53 | ```
54 | %python
55 | ```
56 |
57 | You can inspect the maintained F# program by executing `%fsharp` in a cell:
58 |
59 | ```
60 | %fsharp
61 | ```
62 |
63 | ## F# Program
64 |
65 | The kernel works by maintaining an F# program `Fable.fs` behind the
66 | scenes. This program lives in a separate `tmp` folder for each instance
67 | of the kernel.
68 |
69 | Sometimes the generated F# program might become invalid because of the
70 | submitted code fragments (this can happen with a Python notebook as well).
71 | The way to recover is to reset the kernel. That will reset the F#
72 | program that is running behind the notebook. To reset the kernel select
73 | on the menu: `Kernel -> Restart` or `Kernel -> Restart & Clear Output`.
74 |
75 | or you can use the reset command:
76 |
77 | ```
78 | %reset
79 | ```
80 |
81 | If you need additional package references you currently need to add them
82 | manually to the `Fable.fsproj` project file. TODO: handle `#r nuget "...` commands from within the notebook.
83 |
--------------------------------------------------------------------------------
/fable_py/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .kernel import Fable
3 | from .version import __version__
4 |
5 |
--------------------------------------------------------------------------------
/fable_py/__main__.py:
--------------------------------------------------------------------------------
1 | from fable_py.kernel import Fable
2 |
3 | if __name__ == "__main__":
4 | Fable.run_as_main()
5 |
--------------------------------------------------------------------------------
/fable_py/images/logo-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fable-compiler/Fable.Jupyter/0305e1be3d94ba61b070aec6193fc17fdfb8ed31/fable_py/images/logo-32x32.png
--------------------------------------------------------------------------------
/fable_py/images/logo-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fable-compiler/Fable.Jupyter/0305e1be3d94ba61b070aec6193fc17fdfb8ed31/fable_py/images/logo-64x64.png
--------------------------------------------------------------------------------
/fable_py/images/logo-fable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fable-compiler/Fable.Jupyter/0305e1be3d94ba61b070aec6193fc17fdfb8ed31/fable_py/images/logo-fable.png
--------------------------------------------------------------------------------
/fable_py/kernel.py:
--------------------------------------------------------------------------------
1 | """
2 | An F# Fable (python) kernel for Jupyter based on IPythonKernel.
3 | """
4 | import json
5 | import os
6 | import os.path
7 | import pkgutil
8 | import queue
9 | import re
10 | import subprocess
11 | import sys
12 | import threading
13 | import time
14 | import traceback
15 | from tempfile import TemporaryDirectory
16 | from typing import IO, Any, Dict, List, Optional
17 | from queue import Queue
18 |
19 | try:
20 | import black
21 | except ImportError:
22 | black = None
23 |
24 | from ipykernel.ipkernel import IPythonKernel
25 | from ipykernel.kernelapp import IPKernelApp
26 | from IPython.display import Code
27 | from jupyter_core.paths import jupyter_config_dir, jupyter_config_path
28 | from traitlets.config import Application
29 |
30 | from .version import __version__
31 |
32 |
33 | def format_message(*objects, **kwargs):
34 | """
35 | Format a message like print() does.
36 | """
37 | objects = [str(i) for i in objects]
38 | sep = kwargs.get("sep", " ")
39 | end = kwargs.get("end", "\n")
40 | return sep.join(objects) + end
41 |
42 |
43 | try:
44 | from IPython.utils.PyColorize import NeutralColors
45 |
46 | RED = NeutralColors.colors["header"]
47 | NORMAL = NeutralColors.colors["normal"]
48 | except Exception:
49 | from IPython.core.excolors import TermColors
50 |
51 | RED = TermColors.Red
52 | NORMAL = TermColors.Normal
53 |
54 |
55 | class Fable(IPythonKernel):
56 | """
57 | A Jupyter kernel for F# based on IPythonKernel.
58 | """
59 |
60 | app_name = "Fable Python"
61 | implementation = "Fable Python"
62 | implementation_version = __version__
63 | language = "fs"
64 | language_version = "0.2"
65 | banner = "Fable Python is a compiler designed to make F# a first-class citizen of the Python ecosystem."
66 | language_info = {
67 | "name": "fsharp",
68 | "mimetype": "text/x-fsharp",
69 | "pygments_lexer": "fsharp",
70 | "file_extension": ".fs",
71 | }
72 |
73 | kernel_json = {
74 | "argv": [sys.executable, "-m", "fable_py", "-f", "{connection_file}"],
75 | "display_name": "F# (Fable Python)",
76 | "language": "fsharp",
77 | "codemirror_mode": "fsharp",
78 | "name": "fable-python",
79 | }
80 |
81 | fsproj = """
82 |
83 |
84 | Exe
85 | net5
86 | Major
87 | preview
88 | true
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | """
100 |
101 | # For splitting code blocks into statements (lines that start with identifiers or [)
102 | stmt_regexp = r"\n(?=[\w\[])"
103 | # For parsing a declaration (let, type, open) statement
104 | decl_regex = (
105 | r"^(let)\s+(?P\w+)" # e.g: let a = 10
106 | r"|^(let)\s+``(?P[\w ]+)``" # e.g: let ``end`` = "near"
107 | r"|^(type)\s+(?P\w*)[\s\(]" # e.g: type Test () class end
108 | r"|^(open)\s+(?P[\w.]+)" # e.g: open Fable.Core
109 | r"|^\[<(?P.*)>\]" # e.g: []
110 | )
111 |
112 | magic_prefixes = dict(magic="%", shell="!", help="?")
113 | help_suffix = None
114 |
115 | def __init__(self, *args: Any, **kwargs: Any):
116 | """
117 | Create the Fable (Python) environment
118 | """
119 | super(Fable, self).__init__(*args, **kwargs)
120 |
121 | self.program = dict(module="module Fable.Jupyter")
122 | self.env: Dict[str, str] = {}
123 |
124 | self.tmp_dir = TemporaryDirectory()
125 | self.pyfile = os.path.join(self.tmp_dir.name, "fable.py")
126 | self.fsfile = os.path.join(self.tmp_dir.name, "Fable.fs")
127 | open(self.fsfile, "w+").close() # Make empty file
128 |
129 | self.fable: Optional[subprocess.Popen] = None
130 | self.error_thread: Optional[threading.Thread] = None
131 | self.output_thread: Optional[threading.Thread] = None
132 | self.errors: Queue[str] = Queue()
133 | self.output: Queue[str] = Queue()
134 |
135 | self.start_fable()
136 |
137 | def start_fable(self):
138 | self.log.info("Starting Fable ...")
139 |
140 | sys.path.append(self.tmp_dir.name)
141 | with open(os.path.join(self.tmp_dir.name, "Jupyter.fsproj"), "w") as fd:
142 | fd.write(self.fsproj)
143 | fd.flush()
144 |
145 | env = os.environ.copy()
146 | env["CI"] = "fable-jupyter" # Get simpler CI style compile output from Fable (or vscode will choke)
147 | self.fable = subprocess.Popen(
148 | ["fable", self.tmp_dir.name, "--lang", "py", "--watch", "--outDir", self.tmp_dir.name],
149 | stderr=subprocess.PIPE,
150 | stdout=subprocess.PIPE,
151 | env=env,
152 | )
153 |
154 | def reader(handle: IO[bytes], outq):
155 | for line in iter(handle.readline, b""):
156 | outq.put(line.decode("utf-8"))
157 |
158 | self.error_thread = threading.Thread(target=reader, args=(self.fable.stderr, self.errors))
159 | self.error_thread.start()
160 |
161 | self.output_thread = threading.Thread(target=reader, args=(self.fable.stdout, self.output))
162 | self.output_thread.start()
163 |
164 | def Print(self, *objects, **kwargs):
165 | """Print `objects` to the iopub stream, separated by `sep` and
166 | followed by `end`. Items can be strings or `Widget` instances.
167 | """
168 | message = format_message(*objects, **kwargs)
169 |
170 | stream_content = {"name": "stdout", "text": message}
171 | self.log.debug("Print: %s" % message.rstrip())
172 | self.send_response(self.iopub_socket, "stream", stream_content)
173 |
174 | def Error(self, *objects, **kwargs):
175 | """Print `objects` to stdout, separated by `sep` and followed by
176 | `end`. Objects are cast to strings.
177 | """
178 | message = format_message(*objects, **kwargs)
179 | self.log.debug("Error: %s" % message.rstrip())
180 | stream_content = {"name": "stderr", "text": RED + message + NORMAL}
181 | self.send_response(self.iopub_socket, "stream", stream_content)
182 |
183 | def Code(self, code: Code):
184 | """Print HTML formatted code to stdout."""
185 | content = {"data": {"text/html": code._repr_html_(), "text/plain": repr(code)}, "metadata": {}}
186 | self.send_response(self.iopub_socket, "display_data", content)
187 |
188 | def restart_kernel(self):
189 | self.Print("Restarting kernel...")
190 |
191 | # Clear F# file
192 | open(self.fsfile, "w").close()
193 | self.Print("Done!")
194 |
195 | def do_shutdown(self, restart):
196 | if restart:
197 | self.restart_kernel()
198 |
199 | else:
200 | self.tmp_dir.cleanup()
201 | if self.fable:
202 | self.fable.terminate()
203 |
204 | return super().do_shutdown(restart)
205 |
206 | def set_variable(self, var: str, value: str):
207 | self.env[var] = value
208 |
209 | def get_variable(self, var):
210 | return self.env[var]
211 |
212 | def ok(self) -> Dict[str, Any]:
213 | return {
214 | "status": "ok",
215 | "execution_count": self.execution_count,
216 | "payload": [],
217 | "user_expressions": {},
218 | }
219 |
220 | async def do_magic(
221 | self, code: str, silent: bool, store_history: bool = True, user_expressions=None, allow_stdin: bool = False
222 | ):
223 | # Handle some custom line magics.
224 | if code == r"%python":
225 | with open(self.pyfile, "r") as f:
226 | pycode = f.read()
227 | if black:
228 | pycode = black.format_str(pycode, mode=black.FileMode())
229 | code_ = Code(pycode.strip(), language="python")
230 | self.Code(code_)
231 | return self.ok()
232 | elif code == r"%fsharp":
233 | with open(self.fsfile, "r") as f:
234 | fscode = f.read()
235 | code_ = Code(fscode.strip(), language="fsharp")
236 | self.Code(code_)
237 | return self.ok()
238 |
239 | # Reset command
240 | elif code.startswith(r"%reset"):
241 | self.restart_kernel()
242 | return await super().do_execute(code, silent, store_history, user_expressions, allow_stdin)
243 |
244 | # Send all cell magics straight to the IPythonKernel
245 | elif code.startswith(r"%%"):
246 | # Make sure Python runs in the same context as us
247 | code = code.replace(r"%%python", "")
248 | return await super().do_execute(code, silent, store_history, user_expressions, allow_stdin)
249 |
250 | return
251 |
252 | async def do_execute(
253 | self, code: str, silent: bool, store_history: bool = True, user_expressions=None, allow_stdin: bool = False
254 | ) -> Dict[str, Any]:
255 | """Execute the code, and return result."""
256 |
257 | ret: Dict[str, Any] = await self.do_magic(code, silent, store_history, user_expressions, allow_stdin)
258 | if ret:
259 | return ret
260 |
261 | program = self.program.copy()
262 | lines = code.splitlines()
263 | code = "\n".join([line for line in lines if not line.startswith("%")])
264 | magics = "\n".join([line for line in lines if line.startswith("%")])
265 |
266 | try:
267 | open(self.fsfile, "w+").close() # Clear previous errors
268 | self.errors.queue.clear()
269 | mtime = os.path.getmtime(self.fsfile)
270 |
271 | expr: List[str] = []
272 | decls = []
273 | # Update program declarations redefined in submitted code
274 | stmts = [stmt.lstrip("\n").rstrip() for stmt in re.split(self.stmt_regexp, code, re.M) if stmt]
275 | for stmt in stmts:
276 | match = re.match(self.decl_regex, stmt)
277 | if match:
278 | matches = dict((key, value) for (key, value) in match.groupdict().items() if value)
279 | key = f"{list(matches.keys())[0]} {list(matches.values())[0]}"
280 | program[key] = stmt
281 | decls.append((key, stmt))
282 |
283 | # We need to print single expressions (except for those printing themselves)
284 | else:
285 | expr.append(stmt)
286 |
287 | # Print the result of a single expression.
288 | if len(expr) == 1 and "printf" not in expr[0] and not decls:
289 | expr = [f"""printfn "%A" ({expr[0]})"""]
290 | elif not expr:
291 | # Add an empty do-expression to make sure the program compiles
292 | expr = ["do ()"]
293 |
294 | # Construct the F# program (current and past declarations) and write the program to file.
295 | with open(self.fsfile, "w") as f:
296 | f.write("\n".join(program.values()))
297 | f.write("\n")
298 | f.write("\n".join(expr))
299 |
300 | # Wait for Python file to be compiled
301 | for i in range(20):
302 | self.log.debug("Looping")
303 |
304 | size = self.output.qsize()
305 | lines = [self.output.get(block=False) for _ in range(size)]
306 | self.log.info("\n".join(lines))
307 |
308 | # Detect if the Python file have changed since last compile.
309 | if os.path.exists(self.pyfile) and os.path.getmtime(self.pyfile) > mtime:
310 | with open(self.pyfile, "r") as f:
311 | pycode = f.read()
312 | pycode = magics + "\n" + pycode
313 |
314 | # Only update program if compiled successfully so we don't get stuck with a failing program
315 | self.program = program
316 | return await super().do_execute(pycode, silent, store_history, user_expressions, allow_stdin)
317 | elif magics:
318 | return await super().do_execute(magics, silent, store_history, user_expressions, allow_stdin)
319 |
320 | # Check for compile errors
321 | elif not self.errors.empty():
322 | size = self.errors.qsize()
323 | lines = [self.errors.get(block=False) for _ in range(size)]
324 | self.Error("\n".join(lines))
325 | return self.ok()
326 | time.sleep(i / 10.0)
327 | else:
328 | self.Error("Timeout! Are you sure Fable is running?")
329 | return self.ok()
330 |
331 | except Exception as e:
332 | self.Error(traceback.format_exc())
333 | return {
334 | "status": "error",
335 | "ename": e.__class__.__name__, # Exception name, as a string
336 | "evalue": e.__class__.__name__, # Exception value, as a string
337 | "traceback": [], # traceback frames as strings
338 | }
339 |
340 | return self.ok()
341 |
342 | def get_completions(self, info):
343 | # txt = info["help_obj"]
344 |
345 | return []
346 |
347 | @classmethod
348 | def run_as_main(cls, *args, **kwargs):
349 | """Launch or install the kernel."""
350 |
351 | kwargs["app_name"] = cls.app_name
352 | FableKernelApp.launch_instance(kernel_class=cls, *args, **kwargs)
353 |
354 |
355 | # Borrowed from Metakernel, https://github.com/Calysto/metakernel
356 | class FableKernelApp(IPKernelApp):
357 | """The FableKernel launcher application."""
358 |
359 | config_dir = str()
360 |
361 | def _config_dir_default(self):
362 | return jupyter_config_dir()
363 |
364 | @property
365 | def config_file_paths(self):
366 | path = jupyter_config_path()
367 | if self.config_dir not in path:
368 | path.insert(0, self.config_dir)
369 | path.insert(0, os.getcwd())
370 | return path
371 |
372 | @classmethod
373 | def launch_instance(cls, *args, **kwargs):
374 | cls.name = kwargs.pop("app_name", "metakernel")
375 | super(FableKernelApp, cls).launch_instance(*args, **kwargs)
376 |
377 | @property
378 | def subcommands(self):
379 | # Slightly awkward way to pass the actual kernel class to the install
380 | # subcommand.
381 |
382 | class KernelInstallerApp(Application):
383 | kernel_class = self.kernel_class
384 |
385 | def initialize(self, argv=None):
386 | self.argv = argv
387 |
388 | def start(self):
389 | kernel_spec = self.kernel_class.kernel_json
390 | with TemporaryDirectory() as td:
391 | dirname = os.path.join(td, kernel_spec["name"])
392 | os.mkdir(dirname)
393 | with open(os.path.join(dirname, "kernel.json"), "w") as f:
394 | json.dump(kernel_spec, f, sort_keys=True)
395 | filenames = ["logo-64x64.png", "logo-32x32.png"]
396 | name = self.kernel_class.__module__
397 | for filename in filenames:
398 | try:
399 | data = pkgutil.get_data(name.split(".")[0], "images/" + filename)
400 | except (OSError, IOError):
401 | data = pkgutil.get_data("metakernel", "images/" + filename)
402 | with open(os.path.join(dirname, filename), "wb") as f:
403 | f.write(data) if data else None
404 | try:
405 | subprocess.check_call(
406 | [sys.executable, "-m", "jupyter", "kernelspec", "install"] + self.argv + [dirname]
407 | )
408 | except subprocess.CalledProcessError as exc:
409 | sys.exit(exc.returncode)
410 |
411 | return {"install": (KernelInstallerApp, "Install this kernel")}
412 |
--------------------------------------------------------------------------------
/fable_py/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.7.0"
2 |
--------------------------------------------------------------------------------
/notebooks/F# Exchange 2021.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "b3ef92a1",
6 | "metadata": {
7 | "slideshow": {
8 | "slide_type": "slide"
9 | }
10 | },
11 | "source": [
12 | "
\n",
13 | "\n",
14 | "# Fable Python\n",
15 | "\n",
16 | "|> F# ♥️ Python\n",
17 | "\n",
18 | "Dag Brattli \n",
19 | "Principal Software Engineer \n",
20 | "Cognite, https://cognite.com \n",
21 | "\n",
22 | "
\n",
23 | "
"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "id": "5ada688b",
29 | "metadata": {
30 | "slideshow": {
31 | "slide_type": "slide"
32 | }
33 | },
34 | "source": [
35 | "# Who am I?\n",
36 | "\n",
37 | "- A Python programmer in love with F#\n",
38 | "- Programming Python since 1995\n",
39 | " - [RxPY](https://github.com/ReactiveX/RxPY), Reactive Extensions for Python\n",
40 | " - [Expression](https://github.com/cognitedata/expression), Pragmatic functional programming for Python inspired by F#\n",
41 | "- Programming F# since 2018\n",
42 | " - [Oryx](https://github.com/cognitedata/oryx), Composable middleware for building web request handlers in F# \n",
43 | " - [AsyncRx](https://github.com/dbrattli/asyncrx), for F# and Fable\n",
44 | " - [Fable.Reaction](https://github.com/dbrattli/fable.reaction), AsyncRx for Elmish\n",
45 | "- Microsoft Alumni (FAST, Outlook, Office Division)\n",
46 | "- Work for Cognite, https://cognite.com"
47 | ]
48 | },
49 | {
50 | "cell_type": "markdown",
51 | "id": "acfee1e7",
52 | "metadata": {
53 | "slideshow": {
54 | "slide_type": "slide"
55 | }
56 | },
57 | "source": [
58 | "# Why?\n",
59 | "\n",
60 | "> ... on a quest to bridge the worlds of F# and Python 🌉\n",
61 | "\n",
62 | "- Python and F# look similar in many ways (no braces or semicolons)\n",
63 | "- F# have superior type system and type inference, combined with pipelining, pattern matching, computational expressions\n",
64 | "- I didn’t select this project, the project selected me ✨ It’s a fun weekend project! 😊 \n",
65 | "\n",
66 | "- Can we make F# a better Python? Could we reduce the friction of using F#?"
67 | ]
68 | },
69 | {
70 | "cell_type": "markdown",
71 | "id": "3de45712",
72 | "metadata": {
73 | "slideshow": {
74 | "slide_type": "slide"
75 | }
76 | },
77 | "source": [
78 | "# Python \n",
79 | "\n",
80 | "October 2021: Python ends C and Java's 20-year reign atop the TIOBE index\n",
81 | "\n",
82 | "
"
83 | ]
84 | },
85 | {
86 | "cell_type": "markdown",
87 | "id": "f59e97a1",
88 | "metadata": {
89 | "slideshow": {
90 | "slide_type": "slide"
91 | }
92 | },
93 | "source": [
94 | "# Python (the good parts)\n",
95 | "\n",
96 | "
\n",
97 | "\n",
98 | "- Python is the **top 1** popular language in the world! \n",
99 | "- Python is **easy to use**! The low friction makes Python a **popular choice** for **new developers**\n",
100 | "- Python is also the **de-facto** language for **data science** \n",
101 | "- SciPy stack consisting of libraries such as Pandas, NumPy, SciPy, Matplotlib, and Jupyter\n",
102 | "- Available on **any platform** (pre-installed)\n"
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "id": "87fed5c6",
108 | "metadata": {
109 | "slideshow": {
110 | "slide_type": "slide"
111 | }
112 | },
113 | "source": [
114 | "# Python (the not so good parts)\n",
115 | "\n",
116 | "- ~10 times **slower** than most compiled languages. But in most cases it doesn't really matter\n",
117 | "- Dynamic typing makes it **hard to detect bugs, and refactor** code\n",
118 | "- Optional type hints looks ugly and messes up the language\n",
119 | "- Static **type checkers** like mypy and pyright **break your code** base with every new release\n",
120 | "- Eventually you spend all your time **fixing typing** issues 🤯"
121 | ]
122 | },
123 | {
124 | "cell_type": "markdown",
125 | "id": "85a0db33",
126 | "metadata": {
127 | "slideshow": {
128 | "slide_type": "slide"
129 | }
130 | },
131 | "source": [
132 | "# First Try: Expression\n",
133 | "\n",
134 | "- Pragmatic functional programming for Python inspired by F#\n",
135 | "- Python Library that gives you (a few) F# powers\n",
136 | "- Option, Result, List, Map and Seq modules with type hints\n",
137 | "- Map implementation ported from F#\n",
138 | "- Pipe function, MailboxProcessor, ...\n",
139 | "\n",
140 | "```py\n",
141 | "xs = Seq.of(1, 2, 3)\n",
142 | "ys = pipe(xs,\n",
143 | " seq.map(lambda x: x * 100),\n",
144 | " seq.filter(lambda x: x > 100),\n",
145 | " seq.fold(lambda s, x: s + x, 0),\n",
146 | ")\n",
147 | "```\n",
148 | "\n",
149 | "https://github.com/cognitedata/Expression (138 stars)"
150 | ]
151 | },
152 | {
153 | "cell_type": "markdown",
154 | "id": "27de544a",
155 | "metadata": {
156 | "slideshow": {
157 | "slide_type": "slide"
158 | }
159 | },
160 | "source": [
161 | "# Second Try: Fable Python\n",
162 | "\n",
163 | "- Fable F# to Python transpiler (compiler)\n",
164 | "- Tries to follow [PEP-8](https://www.python.org/dev/peps/pep-0008/) style guide i.e `snake_case` method names\n",
165 | "- Fable.Library makes parts of .NET available\n",
166 | "- Fable.Python makes parts of Python available\n",
167 | "- Python package and module imports (absolute for Exe, relative for Library)\n",
168 | "- Currently produces Python code without type hints"
169 | ]
170 | },
171 | {
172 | "cell_type": "code",
173 | "execution_count": null,
174 | "id": "93b85976",
175 | "metadata": {},
176 | "outputs": [],
177 | "source": [
178 | "let a = Some 10\n",
179 | "match a with\n",
180 | "| Some value -> \n",
181 | " printfn $\"This is {value}\"\n",
182 | "| None -> ()"
183 | ]
184 | },
185 | {
186 | "cell_type": "code",
187 | "execution_count": null,
188 | "id": "38a22240",
189 | "metadata": {},
190 | "outputs": [],
191 | "source": [
192 | "%python"
193 | ]
194 | },
195 | {
196 | "cell_type": "markdown",
197 | "id": "7129ec2b",
198 | "metadata": {
199 | "slideshow": {
200 | "slide_type": "slide"
201 | }
202 | },
203 | "source": [
204 | "# Challenges\n",
205 | "\n",
206 | "Where to start? Translate Babel AST or Fable AST?\n",
207 | "- **Fable AST**: gives more control. Know the intent of the code\n",
208 | "- **Arrow functions**: aka multi-line lambdas\n",
209 | "- **Non-local**: variable scoping. Python have no `let` or `const`\n",
210 | "- **Tail-call**: TC optimization. Closures within loops\n",
211 | "- **Numbers**: integers vs floats – numerics inherited from Fable JS\n",
212 | "- **Imports**: Python packages and modules, relative and absolute imports\n",
213 | "- **Async**: F# Async or Python awaitables, coroutines Futures and Tasks."
214 | ]
215 | },
216 | {
217 | "cell_type": "markdown",
218 | "id": "2f8c4872",
219 | "metadata": {
220 | "slideshow": {
221 | "slide_type": "slide"
222 | }
223 | },
224 | "source": [
225 | " # Type Annotations\n",
226 | " \n",
227 | "Python types and F# types are not fully compatible (yet). Ref:\n",
228 | "https://github.com/microsoft/pyright/issues/1264\n",
229 | "\n",
230 | "F#:\n",
231 | "\n",
232 | "```fs\n",
233 | "let length(xs: 'T list) =\n",
234 | " 42\n",
235 | "```\n",
236 | "\n",
237 | "Python:\n",
238 | "\n",
239 | "```py\n",
240 | "def length(xs: List[_T]) -> int:\n",
241 | " return 42\n",
242 | "```\n",
243 | "Gives error in [Pyright](https://github.com/microsoft/pyright) type checker (used by Pylance):\n",
244 | "\n",
245 | "```\n",
246 | "TypeVar \"_T\" appears only once in generic function signature Pylance(reportInvalidTypeVarUse) (type variable) _T\n",
247 | "```"
248 | ]
249 | },
250 | {
251 | "cell_type": "markdown",
252 | "id": "984767c0",
253 | "metadata": {
254 | "slideshow": {
255 | "slide_type": "slide"
256 | }
257 | },
258 | "source": [
259 | "# Data Type Mapping\n",
260 | "\n",
261 | "| F# | Python | Comment |\n",
262 | "|------------|:----------:|-----------------------------------------------|\n",
263 | "| List | List.fs | F# immutable list |\n",
264 | "| Map | Map.fs | F# immutable map |\n",
265 | "| Array | `list` | TODO: Python has arrays for numeric types |\n",
266 | "| Record | types.py | Custom Record class. Replace with `dict`? |\n",
267 | "| An. Record | `dict` | |\n",
268 | "| Option | Erased | F# `None` will be translated to Python `None` |\n",
269 | "| dict | `dict` | Also used for Dictionary |\n",
270 | "| tuple | `tuple` | |\n",
271 | "| Decimal | `decimal` | |\n",
272 | "| DateTime | `datetime` | |\n",
273 | "| string | `string` | |\n",
274 | "| char | `string` | |\n"
275 | ]
276 | },
277 | {
278 | "cell_type": "markdown",
279 | "id": "eb4073d8",
280 | "metadata": {
281 | "slideshow": {
282 | "slide_type": "slide"
283 | }
284 | },
285 | "source": [
286 | " # Interfaces and Protocols\n",
287 | " \n",
288 | "- .NET uses Interfaces. Python uses protocols (duck typing) or structural sub-typing\n",
289 | "- Trying to translate .NET interfaces to Python magic methods\n",
290 | "- We want the Python code to look like Python (not .NET)\n",
291 | " \n",
292 | "| .NET | Python |\n",
293 | "|:--------------|:------------------:|\n",
294 | "| `IEquatable` | `__eq__` |\n",
295 | "| `IEnumerator` | `__next__` |\n",
296 | "| `IEnumerable` | `__iter__` |\n",
297 | "| `IComparable` | `__lt__`+ `__eq__` |\n",
298 | "| `ToString` | `__str__` |\n",
299 | "\n",
300 | "Calls to `x.ToString` will be translated to `str(x)`."
301 | ]
302 | },
303 | {
304 | "cell_type": "markdown",
305 | "id": "25b1b947",
306 | "metadata": {
307 | "slideshow": {
308 | "slide_type": "slide"
309 | }
310 | },
311 | "source": [
312 | "# Numerics\n",
313 | "\n",
314 | "| F# | .NET | Python |\n",
315 | "|:-----------------|:--------|--------|\n",
316 | "| bool | Boolean | bool |\n",
317 | "| int | Int32 | int |\n",
318 | "| byte | Byte | int |\n",
319 | "| sbyte | SByte | int |\n",
320 | "| int16 | Int16 | int |\n",
321 | "| int64 | Int64 | int |\n",
322 | "| uint16 | Uint16 | int |\n",
323 | "| uint32 | Uint32 | int |\n",
324 | "| uint64 | Uint64 | int |\n",
325 | "| float / double | Double | float |\n",
326 | "| float32 / single | Single | float |\n",
327 | "\n",
328 | "Python integers are unbounded. Max size limited by available memory. "
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "id": "2d309c1b",
334 | "metadata": {
335 | "slideshow": {
336 | "slide_type": "slide"
337 | }
338 | },
339 | "source": [
340 | "# Status: 661 Passing Unit-tests\n",
341 | "\n",
342 | "Tests first run as F# using **xUnit**, then **pytest** after compiling to Python:\n",
343 | "\n",
344 | "```py\n",
345 | "test_arithmetic.py .......................\n",
346 | "test_array.py ............................................\n",
347 | "test_async.py .............\n",
348 | "test_comparison.py .............................\n",
349 | "test_custom_operators.py ..........\n",
350 | "test_date_time.py ........\n",
351 | "test_enum.py ......................\n",
352 | "test_enumerable.py ...\n",
353 | "test_fn.py ..\n",
354 | "test_list.py ......................................................................................\n",
355 | "test_loops.py ...\n",
356 | "test_map.py .......................................\n",
357 | "test_math.py .\n",
358 | "test_option.py ................................\n",
359 | "test_py_interop.py ....\n",
360 | "test_record_type.py ...........\n",
361 | "test_reflection.py .............\n",
362 | "test_result.py ......\n",
363 | "test_seq.py ...............................................................................................\n",
364 | "test_seq_expression.py .............\n",
365 | "test_set.py ...............................................\n",
366 | "test_string.py ......................................................................................................................\n",
367 | "test_sudoku.py .\n",
368 | "test_tail_call.py ................\n",
369 | "test_tuple_type.py ........\n",
370 | "test_union_type.py ..............\n",
371 | "\n",
372 | "========= 661 passed in 18.16s =========\n",
373 | "Build finished successfully\n",
374 | "```"
375 | ]
376 | },
377 | {
378 | "cell_type": "markdown",
379 | "id": "7c3fe5fb",
380 | "metadata": {
381 | "slideshow": {
382 | "slide_type": "slide"
383 | }
384 | },
385 | "source": [
386 | "# Use Case: Jupyter Notebook\n",
387 | "\n",
388 | "
\n",
389 | "\n",
390 | "- What you are seeing right now!\n",
391 | "- Run on `IPythonKernel` and can do anything that Python can do e.g Widgets\n",
392 | "- **Challenge**: Jupyter submit cells. Fable compiles projects and files / modules\n",
393 | "- **TODO:** start F# compilation from within the kernel\n",
394 | "- **TODO:** completion using [FsAutoComplete](https://github.com/fsharp/FsAutoComplete)\n",
395 | "- **TODO:** NuGet handling using `#r \"nuget:...\"`\n",
396 | "\n",
397 | "https://github.com/dbrattli/Fable.Jupyter"
398 | ]
399 | },
400 | {
401 | "cell_type": "code",
402 | "execution_count": null,
403 | "id": "d70a521d",
404 | "metadata": {
405 | "slideshow": {
406 | "slide_type": "slide"
407 | }
408 | },
409 | "outputs": [],
410 | "source": [
411 | "// Example to create some nested functions\n",
412 | "let add(a, b, cont) =\n",
413 | " cont(a + b)\n",
414 | "\n",
415 | "let square(x, cont) =\n",
416 | " cont(x * x)\n",
417 | "\n",
418 | "let sqrt(x, cont) =\n",
419 | " cont(sqrt(x))\n",
420 | "\n",
421 | "let pythagoras(a, b, cont) =\n",
422 | " let mutable state = 0\n",
423 | " square(a, (fun aa ->\n",
424 | " square(b, (fun bb ->\n",
425 | " add(aa, bb, (fun aabb ->\n",
426 | " sqrt(aabb, (fun result ->\n",
427 | " printfn \"result: %A\" result\n",
428 | " state <- 42\n",
429 | " cont(result)\n",
430 | " ))\n",
431 | " ))\n",
432 | " ))\n",
433 | " ))\n",
434 | "\n",
435 | "pythagoras(2.0, 3.5, printfn \"The result is %f\")"
436 | ]
437 | },
438 | {
439 | "cell_type": "code",
440 | "execution_count": null,
441 | "id": "e205437d",
442 | "metadata": {
443 | "slideshow": {
444 | "slide_type": "-"
445 | }
446 | },
447 | "outputs": [],
448 | "source": [
449 | "%python"
450 | ]
451 | },
452 | {
453 | "cell_type": "code",
454 | "execution_count": null,
455 | "id": "268d3885",
456 | "metadata": {
457 | "scrolled": true,
458 | "slideshow": {
459 | "slide_type": "-"
460 | }
461 | },
462 | "outputs": [],
463 | "source": [
464 | "%fsharp"
465 | ]
466 | },
467 | {
468 | "cell_type": "markdown",
469 | "id": "631e2a04",
470 | "metadata": {
471 | "slideshow": {
472 | "slide_type": "slide"
473 | }
474 | },
475 | "source": [
476 | "## Shared Kernel\n",
477 | "\n",
478 | "- F# and Python running together in the same Jupyter kernel\n",
479 | "- Same kernel, same variables"
480 | ]
481 | },
482 | {
483 | "cell_type": "code",
484 | "execution_count": null,
485 | "id": "2e8d002f",
486 | "metadata": {
487 | "scrolled": true,
488 | "slideshow": {
489 | "slide_type": "-"
490 | }
491 | },
492 | "outputs": [],
493 | "source": [
494 | "%%python\n",
495 | "p = 20\n",
496 | "print(\"Python: \", p)"
497 | ]
498 | },
499 | {
500 | "cell_type": "code",
501 | "execution_count": null,
502 | "id": "cb66dabf",
503 | "metadata": {},
504 | "outputs": [],
505 | "source": [
506 | "open Fable.Core\n",
507 | "let [] p : int = nativeOnly\n",
508 | "\n",
509 | "printfn $\"From Python: {p}\""
510 | ]
511 | },
512 | {
513 | "cell_type": "code",
514 | "execution_count": null,
515 | "id": "f3a693fc",
516 | "metadata": {},
517 | "outputs": [],
518 | "source": [
519 | "// F#\n",
520 | "let q = 42"
521 | ]
522 | },
523 | {
524 | "cell_type": "code",
525 | "execution_count": null,
526 | "id": "999bff33",
527 | "metadata": {
528 | "scrolled": true
529 | },
530 | "outputs": [],
531 | "source": [
532 | "%%python\n",
533 | "print(q + 10)"
534 | ]
535 | },
536 | {
537 | "cell_type": "markdown",
538 | "id": "cfca033d",
539 | "metadata": {
540 | "slideshow": {
541 | "slide_type": "slide"
542 | }
543 | },
544 | "source": [
545 | "# Widgets"
546 | ]
547 | },
548 | {
549 | "cell_type": "code",
550 | "execution_count": null,
551 | "id": "ebc71bcd",
552 | "metadata": {
553 | "slideshow": {
554 | "slide_type": "-"
555 | }
556 | },
557 | "outputs": [],
558 | "source": [
559 | "open Fable.Python.IPyWidgets\n",
560 | "\n",
561 | "let handler(x: string) =\n",
562 | " $\"Got: {x}\"\n",
563 | "\n",
564 | "widgets.interact(handler, x=\"test\") |> ignore"
565 | ]
566 | },
567 | {
568 | "cell_type": "code",
569 | "execution_count": null,
570 | "id": "f607b5f1",
571 | "metadata": {},
572 | "outputs": [],
573 | "source": [
574 | "open Fable.Python.IPyWidgets\n",
575 | "\n",
576 | "widgets.FloatSlider()"
577 | ]
578 | },
579 | {
580 | "cell_type": "markdown",
581 | "id": "4ac4ffaa",
582 | "metadata": {
583 | "slideshow": {
584 | "slide_type": "slide"
585 | }
586 | },
587 | "source": [
588 | "# Data Science\n",
589 | "\n",
590 | "Plot a time-series from Cognite Data Fusion (CDF)"
591 | ]
592 | },
593 | {
594 | "cell_type": "code",
595 | "execution_count": null,
596 | "id": "45ce5808",
597 | "metadata": {},
598 | "outputs": [],
599 | "source": [
600 | "open Fable.Core\n",
601 | "open Fable.Python\n",
602 | "\n",
603 | "open CogniteSdk\n",
604 | "open Os\n",
605 | "\n",
606 | "let apiKey () = os.getenv \"API_KEY\"\n",
607 | "let client = CogniteClient(clientName=\"Fable Python\", project=\"publicdata\", apiKey=apiKey().Value)\n",
608 | "\n",
609 | "let ts = client.time_series.retrieve(id=7638223843994790L)\n",
610 | "ts.plot(\"365d-ago\", \"now\", [|\"average\"|], \"1d\")"
611 | ]
612 | },
613 | {
614 | "cell_type": "markdown",
615 | "id": "e34ce26e",
616 | "metadata": {
617 | "slideshow": {
618 | "slide_type": "slide"
619 | }
620 | },
621 | "source": [
622 | "# Use Case: Using Python Standard Library\n",
623 | "\n",
624 | "- F# on Python environment without .NET\n",
625 | "- We need type bindings: [Fable.Python](https://github.com/dbrattli/Fable.Python)\n",
626 | "- Perhaps we want to automate the job using some tool \n",
627 | "- But tools produces code that is hard to use (friction)\n",
628 | "- Hand-crafting and PR’s is needed!\n",
629 | "- But this is not harder than what's lready done with e.g Python [typesched](https://github.com/python/typeshed)\n"
630 | ]
631 | },
632 | {
633 | "cell_type": "code",
634 | "execution_count": null,
635 | "id": "9b07b2b5",
636 | "metadata": {
637 | "slideshow": {
638 | "slide_type": "-"
639 | }
640 | },
641 | "outputs": [],
642 | "source": [
643 | "open Fable.Python.Builtins\n",
644 | "\n",
645 | "print($\"test: {1+2}\")"
646 | ]
647 | },
648 | {
649 | "cell_type": "code",
650 | "execution_count": null,
651 | "id": "20b2de19",
652 | "metadata": {},
653 | "outputs": [],
654 | "source": [
655 | "open Fable.Python.Ast\n",
656 | "let expr = ast.parse(\"lambda x: x + 42\")\n",
657 | "ast.unparse(expr)\n",
658 | "print(ast.dump(expr))"
659 | ]
660 | },
661 | {
662 | "cell_type": "code",
663 | "execution_count": null,
664 | "id": "aaff934d",
665 | "metadata": {},
666 | "outputs": [],
667 | "source": [
668 | "open Fable.Python.Json\n",
669 | "let record = {|A=10; B=20|}\n",
670 | "json.dumps(record)"
671 | ]
672 | },
673 | {
674 | "cell_type": "markdown",
675 | "id": "9ad93be7",
676 | "metadata": {
677 | "slideshow": {
678 | "slide_type": "slide"
679 | }
680 | },
681 | "source": [
682 | "# Use Case: Flask Web Server\n",
683 | "\n",
684 | "
\n",
685 | "\n",
686 | "- Web microframework. Think of it as simpler than Giraffe, more like Express for Node.\n",
687 | "- **Challenge:** Python decorators. Can we use attributes? "
688 | ]
689 | },
690 | {
691 | "cell_type": "markdown",
692 | "id": "8aac8228",
693 | "metadata": {
694 | "slideshow": {
695 | "slide_type": "slide"
696 | }
697 | },
698 | "source": [
699 | "# Use Case: Time Flies Like an Arrow!\n",
700 | "\n",
701 | "
\n",
702 | "\n",
703 | "\n",
704 | "- **Universal code**: What if we could re-use the same F# code for \n",
705 | " - .NET, \n",
706 | " - JavaScript \n",
707 | " - ... and Python\n",
708 | " \n",
709 | "- **Blast From the Past!** FableConf 2018 and Fable.Reaction\n",
710 | "- **AsyncRx** – Inspired by AsyncRx from System.Reactive by Bart de Smet\n",
711 | "\n",
712 | "> If we are careful, we can write universal code that runs anywhere. I.e lock free, ...\n"
713 | ]
714 | },
715 | {
716 | "cell_type": "markdown",
717 | "id": "6f2c70d9",
718 | "metadata": {
719 | "slideshow": {
720 | "slide_type": "slide"
721 | }
722 | },
723 | "source": [
724 | "# Use Case: BBC micro:bit\n",
725 | "\n",
726 | "
\n",
727 | "\n",
728 | "- Uses MicroPython 🙀\n",
729 | "- **Challenge:** Flat file system\n",
730 | "- **Challenge:** No Fable.Library, so we need a micro library replacement, and a bundler\n",
731 | "- **Challenge:** This will be a MicroFSharp, but we can enable features like pattern matching etc"
732 | ]
733 | },
734 | {
735 | "cell_type": "markdown",
736 | "id": "39e953af",
737 | "metadata": {
738 | "slideshow": {
739 | "slide_type": "slide"
740 | }
741 | },
742 | "source": [
743 | "# Current Problems\n",
744 | "\n",
745 | "- Optional F# arguments, i.e `?name: str` \n",
746 | "- Named Python arguments, aka keyword only arguments\n",
747 | "- In Python you have positional (only) or keyword (only) arguments:\n",
748 | "\n",
749 | "```txt\n",
750 | "def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):\n",
751 | " ----------- ---------- ----------\n",
752 | " | | |\n",
753 | " | Positional or keyword |\n",
754 | " | - Keyword only\n",
755 | " -- Positional only\n",
756 | "```\n",
757 | "\n",
758 | "- This is currently not handled in Fable (only positional name-less arguments)\n",
759 | "- PS: But this is something we want to have in F# as well, right?"
760 | ]
761 | },
762 | {
763 | "cell_type": "markdown",
764 | "id": "3760fc9b",
765 | "metadata": {
766 | "slideshow": {
767 | "slide_type": "slide"
768 | }
769 | },
770 | "source": [
771 | "# Final Remarks\n",
772 | "\n",
773 | "- Fable Python is happening! 🎉\n",
774 | "- Big thanks to Alfonso Garcia Caro and @ncave for Fable and for embraching this project 🤗\n",
775 | "- **Alpha!** Not ready for production! ⚠️\n",
776 | "- Still a long road ahead, but we made some progress 🚧\n",
777 | " \n",
778 | "- Need help! \n",
779 | " - Extending the Fable.Python type bindings\n",
780 | " - Documentation, code samples and tutorials\n",
781 | " - Expand Fable Python into Data Science\n",
782 | "\n",
783 | "> Please try it!\n",
784 | "https://github.com/dbrattli/Fable.Python"
785 | ]
786 | },
787 | {
788 | "cell_type": "code",
789 | "execution_count": null,
790 | "id": "56e114df",
791 | "metadata": {
792 | "slideshow": {
793 | "slide_type": "slide"
794 | }
795 | },
796 | "outputs": [],
797 | "source": [
798 | "open Fable.Core\n",
799 | "\n",
800 | "type IDisplay =\n",
801 | " []\n",
802 | " abstract Markdown : data: string -> unit\n",
803 | "\n",
804 | "[]\n",
805 | "let display : IDisplay = nativeOnly\n",
806 | "\n",
807 | "let yna = \n",
808 | " \"ynA\" \n",
809 | " |> Array.ofSeq \n",
810 | " |> Array.rev \n",
811 | " |> Array.map string \n",
812 | " |> String.concat \"\"\n",
813 | " \n",
814 | "display.Markdown $\"\"\"# {yna} {let a = \"questions\" in a}?\"\"\""
815 | ]
816 | },
817 | {
818 | "cell_type": "code",
819 | "execution_count": null,
820 | "id": "df7bde75",
821 | "metadata": {},
822 | "outputs": [],
823 | "source": [
824 | "%python"
825 | ]
826 | },
827 | {
828 | "cell_type": "code",
829 | "execution_count": null,
830 | "id": "e650f0d9",
831 | "metadata": {},
832 | "outputs": [],
833 | "source": []
834 | }
835 | ],
836 | "metadata": {
837 | "celltoolbar": "Slideshow",
838 | "kernelspec": {
839 | "display_name": "F# (Fable Python)",
840 | "language": "fsharp",
841 | "name": "fable-python"
842 | },
843 | "language_info": {
844 | "file_extension": ".fs",
845 | "mimetype": "text/x-fsharp",
846 | "name": "fsharp",
847 | "pygments_lexer": "fsharp"
848 | },
849 | "rise": {
850 | "scroll": true,
851 | "theme": "white",
852 | "transition": "fade"
853 | }
854 | },
855 | "nbformat": 4,
856 | "nbformat_minor": 5
857 | }
858 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jupyter
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | license_file = LICENSE
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | from setuptools import find_packages, setup
4 |
5 | __version__ = None
6 | with io.open("fable_py/version.py", encoding="utf-8") as fid:
7 | for line in fid:
8 | if line.startswith("__version__"):
9 | __version__ = line.strip().split()[-1][1:-1]
10 | break
11 | assert __version__, "__version__ not set"
12 |
13 | with open("README.md") as f:
14 | readme = f.read()
15 |
16 |
17 | setup(
18 | name="fable_py",
19 | version=__version__,
20 | description="A Fable (python) kernel for Jupyter",
21 | long_description=readme,
22 | long_description_content_type="text/markdown",
23 | author="Dag Brattli",
24 | author_email="dag@brattli.net",
25 | url="https://github.com/dbrattli/Fable.Jupyter",
26 | install_requires=["jupyter"],
27 | dependency_links=[],
28 | packages=find_packages(include=["fable_py"]),
29 | package_data={"fable_py": ["images/*.png"]},
30 | classifiers=[
31 | "Framework :: IPython",
32 | "License :: OSI Approved :: MIT License",
33 | "Programming Language :: Python :: 3",
34 | "Programming Language :: F#",
35 | "Topic :: System :: Shells",
36 | ],
37 | )
38 |
--------------------------------------------------------------------------------