├── .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 | --------------------------------------------------------------------------------