├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── demo ├── Toy Piece.ipynb ├── demo.html ├── demo.ipynb └── mag_resp.png ├── sckernel ├── __init__.py ├── __main__.py ├── config.py ├── convertNotebookToScd.py ├── install.py ├── kernel.js ├── kernel.py ├── sclangSub.py └── window.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | ignored/ 3 | moveToModule 4 | build/ 5 | dist/ 6 | sckernel.egg-info/ 7 | other_documentation/ 8 | .ipynb_checkpoints/ 9 | .DS_Store 10 | .eggs/ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ANDREW D. DAVIS 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include sckernel/kernel.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sckernel 2 | 3 | sckernel is a Jupyter Notebook kernel for SuperCollider's sclang. sckernel 4 | launches a post window to display output just as the SuperCollider IDE does 5 | while the Notebook front end handles input. 6 | 7 | Syntax highlighting in the Notebook uses codemirror with a mode defined in 8 | kernel.js. 9 | 10 | sckernel has been tested on both MacOS and Windows 10. 11 | 12 | For an overview of Jupyter Notebook and sckernel, please check out the 13 | video linked [here](https://youtu.be/8xYmhyGZz2k). 14 | 15 | ## Requirements 16 | 17 | Users need a working installation of SuperCollider and Jupyter Notebook. 18 | There are several ways to install Jupyter Notebook as detailed at 19 | www.jupyter.org/install. The quickest way is by downloading Anaconda. 20 | 21 | sckernel requires Python 3.5 or higher. Please be sure if you downloaded 22 | the Notebook through Anaconda that it is for Python 3.5 or higher. 23 | 24 | ## Installation 25 | 26 | ### Step 1: Download sckernel 27 | 28 | To download `sckernel` from PyPI: 29 | 30 | ``` 31 | pip install sckernel 32 | ``` 33 | 34 | ### Step 2: Install the kernelspec for sckernel 35 | 36 | To complete the installation, you must select a location for the sckernel 37 | configuration files (called a kernelspec). There are three options: 38 | 39 | 1) To install locally to your user account, run 40 | 41 | ``` 42 | python -m sckernel.install 43 | ``` 44 | 45 | The above line is also equivalent to `python -m sckernel.install --user`. 46 | 47 | 2) To install in the root directory or for an environment like Anaconda or 48 | venv, run 49 | 50 | ``` 51 | python -m sckernel.install --sys-prefix 52 | ``` 53 | 54 | 3) To install to another location (not recommended), run 55 | 56 | ``` 57 | python -m sckernel.install --prefix 58 | ``` 59 | 60 | sckernel's kernelspec will be installed in {PREFIX}/share/jupyter/kernels/. 61 | 62 | ### Step 3: Configure sckernel to find your Python and sclang binaries 63 | 64 | sckernel works by launching two separate subprocesses: a post window implemented 65 | in Python and sclang, the frontend interpreter for SuperCollider. To launch 66 | these processes properly, sckernel needs to know where to find those binaries. 67 | To complete the installation, run the following with those paths: 68 | 69 | ``` 70 | python -m sckernel.config --python /path/to/python --sclang /path/to/sclang 71 | ``` 72 | 73 | You may omit this step entirely. By default, sckernel will attempt to search 74 | through your PATH environment variable for the first instance of `python` and `sclang` 75 | and attempt to run those. Depending upon your personal configuration, you may be 76 | able to rely successfully upon your PATH variable without this step. Additionally, 77 | you can chose to omit just one of the `--python` or `--sclang` flags if you would 78 | like to provide a path to only one. Most users with multiple installations of Python 79 | should run this step to ensure that sckernel uses the correct instance of Python. 80 | 81 | The typical paths for sclang are as follows but may be different on your machine. 82 | 83 | OS X: `"/Applications/SuperCollider/SuperCollider.app/Contents/Resources/sclang"` 84 | Linux: `"/usr/local/bin/sclang"` 85 | Windows: `"C:\Program Files\SuperCollider\sclang.exe"` 86 | 87 | ### Step-By-Step Videos 88 | 89 | Below are two video installation guides for installing sckernel with Anaconda on 90 | a Windows and MacOS machine. This can be especially helpful if you are not 91 | comfortable working on the command line. 92 | 93 | Windows: 94 | MacOS: 95 | 96 | ## Using SuperCollider kernel 97 | 98 | When opening Jupyter notebook, select from the New menu SC_Kernel to create 99 | a new SuperCollider notebook using sclang. 100 | 101 | For the console frontend, you can run `jupyter console --kernel sckernel`. 102 | 103 | ## Converting from Notebooks to SuperCollider files (.scd) 104 | 105 | The sckernel package also comes with a convenience script to translate 106 | from Jupyter notebooks to .scd files (i.e., SuperCollider files). 107 | 108 | ``` 109 | python -m sckernel.convertNotebookToScd /path/to/notebook /path/to/destination 110 | ``` 111 | 112 | Some light formatting is done to make the .scd files readable in a similar way 113 | to Jupyter Notebooks. 114 | 115 | ## Version Log 116 | 117 | ### 0.3.0 118 | 119 | - Created a configuration file for sckernel to read paths to python and sclang 120 | to support different installations 121 | - Reorganized sclangSub.py 122 | - Updated documentation to reflect the need for running sckernel.config 123 | 124 | ### 0.2.0 125 | 126 | - Eliminated window flicker on MacOS 127 | - Added syntax highlighting by implementing CodeMirror 128 | 129 | ### 0.1.0 130 | 131 | - First version of sckernel 132 | -------------------------------------------------------------------------------- /demo/Toy Piece.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Toy Piece" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Start the audio server." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "ServerOptions.devices" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "s.options.outDevice_(\"Out_Soundflower_Built-In\"); // An aggregate device to capture audio during screen recording" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "s.boot;" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "s.meter;" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "FreqScope.new" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "## Instruments" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "Tone generated by sending an impulse through resonant filters." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "SynthDef('tone', {\n", 83 | " arg outBus = 0, freq = 521, amp = 0.2, pos = 0, atk = 0.05;\n", 84 | " var sig;\n", 85 | "\n", 86 | " sig = Klank.ar(`[\n", 87 | " [freq, freq*1.501, freq*1.97], // freqs\n", 88 | " [0.7, 0.45, 0.25], // amps\n", 89 | " [0.2, 0.3, 0.48] // phases\n", 90 | " ], Impulse.ar(0));\n", 91 | " DetectSilence.ar(sig, doneAction: 2);\n", 92 | " sig = sig * EnvGen.kr(Env([0, 1], [atk]));\n", 93 | " sig = Pan2.ar(sig, pos, amp);\n", 94 | "\n", 95 | " Out.ar(outBus, sig);\n", 96 | "}).add;" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "String like sound generated by using a variable triangle/saw wave." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "SynthDef('bleep', {\n", 113 | " arg outBus = 0, freq = 440, amp = 0.2, len = 1, pos = 0;\n", 114 | " var sig, env;\n", 115 | "\n", 116 | " env = EnvGen.kr(\n", 117 | " Env([0, amp, 0], [len * (2/3), len/3], curve: 'cubed'),\n", 118 | " doneAction: 2\n", 119 | " );\n", 120 | "\n", 121 | " sig = VarSaw.ar(\n", 122 | " freq: freq,\n", 123 | " iphase: 0,\n", 124 | " width: env,\n", 125 | " mul: 1\n", 126 | " );\n", 127 | " sig = sig * env;\n", 128 | " sig = Pan2.ar(sig, pos);\n", 129 | "\n", 130 | " Out.ar(outBus, sig);\n", 131 | "}).add;" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "## Reverbs" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": {}, 144 | "source": [ 145 | "A reverberator created through a chain of series allpass filters with dampening from a lowpass filter. `mix` parameter controls the amount of reverb." 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "SynthDef('reverb', {\n", 155 | " arg outBus = 0, inBus, mix = 0.4;\n", 156 | " var dry, wet, sig, numFilters = 8;\n", 157 | "\n", 158 | " dry = In.ar(inBus, 2);\n", 159 | "\n", 160 | " wet = dry;\n", 161 | " numFilters.do({\n", 162 | " wet = AllpassN.ar(\n", 163 | " in: wet,\n", 164 | " maxdelaytime: 0.2,\n", 165 | " delaytime: {Rand(0.05, 0.15)} ! 2,\n", 166 | " decaytime: {Rand(1, 2)} ! 2\n", 167 | " );\n", 168 | " });\n", 169 | "\n", 170 | " wet = LPF.ar(wet, 3500);\n", 171 | " sig = dry.blend(wet, mix);\n", 172 | "\n", 173 | " Out.ar(outBus, sig);\n", 174 | "}).add;" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "A very wet, washy reverb intended to create background drone." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "SynthDef('wash', {\n", 191 | " arg outBus = 0, inBus;\n", 192 | " var sig, numFilters = 16;\n", 193 | "\n", 194 | " sig = In.ar(inBus, 2);\n", 195 | "\n", 196 | " numFilters.do({\n", 197 | " sig = AllpassN.ar(\n", 198 | " in: sig,\n", 199 | " maxdelaytime: 0.2,\n", 200 | " delaytime: {Rand(0.05, 0.15)} ! 2,\n", 201 | " decaytime: {Rand(2, 4)} ! 2\n", 202 | " );\n", 203 | " });\n", 204 | "\n", 205 | " sig = LPF.ar(sig, 3500);\n", 206 | "\n", 207 | " Out.ar(outBus, sig);\n", 208 | "}).add;" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "## Setup" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "Establish a tempo and instantiate both reverbs." 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "~tempo = 60;\n", 232 | "~clock = TempoClock(~tempo/60);\n", 233 | "~washBus = Bus.audio(s, 2);\n", 234 | "~washSynth = Synth('wash', [\\inBus, ~washBus]);\n", 235 | "~reverbBus = Bus.audio(s, 2);\n", 236 | "~reverbSynth = Synth('reverb', [\\inBus, ~reverbBus]);" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "## Workspace" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "Pbindef('tonePattern',\n", 253 | " 'instrument', 'tone',\n", 254 | " 'scale', #[0, 2, 3, 5, 7, 8, 10],\n", 255 | " 'degree', Prand([0, 1, 2, 3, 4, 5, 6], inf),\n", 256 | " 'amp', Pwhite(0.2, 0.3),\n", 257 | " 'dur', Pwhite(0.1, 0.2),\n", 258 | " 'octave', 6,\n", 259 | " 'atk', 0.004,\n", 260 | " 'outBus', ~washBus\n", 261 | ").play(~clock);" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "Pbindef('tonePattern').stop;" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "Pbindef('string',\n", 280 | " 'instrument', 'bleep',\n", 281 | " 'scale', #[0, 2, 3, 5, 7, 8, 10],\n", 282 | " 'degree', Prand([0, 2, 3], inf),\n", 283 | " 'amp', 0.2,\n", 284 | " 'octave', 4,\n", 285 | " 'len', Pwhite(1, 3),\n", 286 | " 'dur', 3,\n", 287 | " 'pos', Pwhite(-0.5, 0.5),\n", 288 | " 'outBus', ~washBus\n", 289 | ").play(~clock);" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": null, 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [ 298 | "Pbindef('string').stop;" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "## Cleanup" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": null, 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "Pbindef.clear;" 315 | ] 316 | } 317 | ], 318 | "metadata": { 319 | "kernelspec": { 320 | "display_name": "SC_Kernel", 321 | "language": "text", 322 | "name": "sckernel" 323 | }, 324 | "language_info": { 325 | "codemirror_mode": "sclang", 326 | "name": "sclang" 327 | } 328 | }, 329 | "nbformat": 4, 330 | "nbformat_minor": 4 331 | } 332 | -------------------------------------------------------------------------------- /demo/demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Jupyter Notebook with sckernel Demo" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "The following short notebook demonstrates a few of the features of integrating SuperCollider with Jupyter Notebook." 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## 1. SuperCollider as a Calculator\n", 22 | "The following expressions can be evaluated by SuperCollider just like a calculator. Most of your intuitions about the following code apply. To see the output of the code, check the post window that was automatically started at the startup of this notebook." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 4, 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "name": "stdout", 32 | "output_type": "stream", 33 | "text": [] 34 | } 35 | ], 36 | "source": [ 37 | "3 + 4" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [] 49 | } 50 | ], 51 | "source": [ 52 | "3 / 2" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [] 64 | } 65 | ], 66 | "source": [ 67 | "2.5 * 1" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "## 2. AM Modulation\n", 75 | "\n", 76 | "First start up the SuperCollider audio server." 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 5, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "name": "stdout", 86 | "output_type": "stream", 87 | "text": [] 88 | } 89 | ], 90 | "source": [ 91 | "s.boot; // Wait for the server to boot up" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "I can write equations and then test out what those sound like in SuperCollider. Jupyter Notebook's can display nicely formatted mathematical equations which is useful when teaching digital signal processing.\n", 99 | "\n", 100 | "$$A_1\\sin(2\\pi f_1t + \\phi_1)A_2\\sin(2\\pi f_2t + \\phi_2)$$" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 6, 106 | "metadata": {}, 107 | "outputs": [ 108 | { 109 | "name": "stdout", 110 | "output_type": "stream", 111 | "text": [] 112 | } 113 | ], 114 | "source": [ 115 | "// A SuperCollider implementation of the above equation\n", 116 | "\n", 117 | "SynthDef(\\am, {\n", 118 | " arg a1 = 0.1, f1 = 100, p1 = 0, a2 = 0.1, f2 = 200, p2 = 0;\n", 119 | " var sig;\n", 120 | " \n", 121 | " sig = SinOsc.ar(f1, p1, a1) * SinOsc.ar(f2, p2, a2);\n", 122 | " Out.ar(0, sig ! 2)\n", 123 | "}).add;" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "Play the AM modulation." 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 7, 136 | "metadata": {}, 137 | "outputs": [ 138 | { 139 | "name": "stdout", 140 | "output_type": "stream", 141 | "text": [] 142 | } 143 | ], 144 | "source": [ 145 | "x = Synth(\\am)" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "I can also still bring up windows from SuperCollider which are useful when diagnosing sound. The frequency scope shows two sine waves at 200Hz and 600Hz." 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 8, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "name": "stdout", 162 | "output_type": "stream", 163 | "text": [] 164 | } 165 | ], 166 | "source": [ 167 | "FreqScope.new" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "Free the sound to stop the audio from playing." 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 9, 180 | "metadata": {}, 181 | "outputs": [ 182 | { 183 | "name": "stdout", 184 | "output_type": "stream", 185 | "text": [] 186 | } 187 | ], 188 | "source": [ 189 | "x.free" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "## 3. One Sample Delay\n", 197 | "\n", 198 | "The following chart shows the magnitude and phase response of a filter with the difference equation: $x[n] + x[n - 1]$." 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "\n" 206 | ] 207 | }, 208 | { 209 | "cell_type": "markdown", 210 | "metadata": {}, 211 | "source": [ 212 | "Here is the code to implement a one sample delay filter in SuperCollider." 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 12, 218 | "metadata": {}, 219 | "outputs": [ 220 | { 221 | "name": "stdout", 222 | "output_type": "stream", 223 | "text": [] 224 | } 225 | ], 226 | "source": [ 227 | "SynthDef(\\oneSampleDelay, {\n", 228 | " arg out = 0, in;\n", 229 | " var sig = In.ar(in, 1);\n", 230 | " sig = sig + Delay1.ar(sig);\n", 231 | " Out.ar(out, sig);\n", 232 | "}).add;" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "One-sample delays make poor filters but are simple to implement and understand." 240 | ] 241 | } 242 | ], 243 | "metadata": { 244 | "kernelspec": { 245 | "display_name": "SC_Kernel", 246 | "language": "text", 247 | "name": "sckernel" 248 | }, 249 | "language_info": { 250 | "codemirror_mode": "smalltalk", 251 | "mimetype": "text", 252 | "name": "sclang" 253 | } 254 | }, 255 | "nbformat": 4, 256 | "nbformat_minor": 4 257 | } 258 | -------------------------------------------------------------------------------- /demo/mag_resp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdavis33/sckernel/a61d2f99a7b94065ed821c199ad1342d4e2716b8/demo/mag_resp.png -------------------------------------------------------------------------------- /sckernel/__init__.py: -------------------------------------------------------------------------------- 1 | """A SuperCollider kernel for Jupyter""" 2 | 3 | __version__ = "0.3.2" 4 | 5 | from .kernel import SCKernel 6 | -------------------------------------------------------------------------------- /sckernel/__main__.py: -------------------------------------------------------------------------------- 1 | from ipykernel.kernelapp import IPKernelApp 2 | from . import SCKernel 3 | 4 | IPKernelApp.launch_instance(kernel_class=SCKernel) 5 | -------------------------------------------------------------------------------- /sckernel/config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | def configure(): 5 | sckernel_dir = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | ap = argparse.ArgumentParser(description= 8 | "Create a configuration file for sckernel to successfully " 9 | "launch subproccesses for sclang and python" 10 | ) 11 | ap.add_argument('-p', '--python', 12 | help="Path to a version of Python 3.5 or higher.", default="python") 13 | ap.add_argument('-s', '--sclang', 14 | help="Path to sclang binary.", default="sclang") 15 | args = ap.parse_args() 16 | 17 | with open(os.path.join(sckernel_dir, "paths.cfg"), "w") as w: 18 | w.write("python=" + args.python + "\n") 19 | w.write("sclang=" + args.sclang + "\n") 20 | 21 | print("Path configuration file written.") 22 | 23 | if __name__ == '__main__': 24 | configure() 25 | -------------------------------------------------------------------------------- /sckernel/convertNotebookToScd.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | LINE_LENGTH = 80 5 | 6 | def createBoundedHeader(line, fd): 7 | borderTop = "/**" + "*" * len(line) + "**\n" 8 | borderBottom = " **" + "*" * len(line) + "**/\n" 9 | fd.write(borderTop) 10 | fd.write(" * " + line + " *\n") 11 | fd.write(borderBottom) 12 | 13 | def linedHeader(line, fd): 14 | asteriskLen = round((LINE_LENGTH - len(line) - 4) / 2) 15 | output = "/" + "*" * asteriskLen + " " + line 16 | output += " " + "*" * asteriskLen + "/\n" 17 | fd.write(output) 18 | 19 | def convertLineOfMarkdown(markdown, fd): 20 | '''Input is a string of markdown''' 21 | while len(markdown) > 0: 22 | if len(markdown) > LINE_LENGTH: 23 | for i in range(LINE_LENGTH, -1, -1): 24 | if markdown[i] == " " or markdown[i] == "\n": break 25 | line = "// " + markdown[:i] + "\n" 26 | markdown = markdown[i + 1:] 27 | else: 28 | line = "// " + markdown + "\n" 29 | markdown = "" 30 | fd.write(line) 31 | 32 | def convertMarkdown(data, fd): 33 | for line in data: 34 | if line == "\n": 35 | fd.write("\n") 36 | elif line.startswith("#### "): 37 | fd.write("// " + line) 38 | elif line.startswith("### "): 39 | createBoundedHeader(line[4:].rstrip(), fd) 40 | elif line.startswith("## "): 41 | linedHeader(line[3:].rstrip(), fd) 42 | else: 43 | convertLineOfMarkdown(line, fd) 44 | fd.write("\n") 45 | 46 | def convertCode(data, fd): 47 | '''Input is a string of code or a list of strings for each line.''' 48 | if len(data) != 1: 49 | fd.write("(\n") 50 | for line in data: 51 | fd.write(line) 52 | fd.write("\n)") 53 | else: 54 | fd.write(data[0]) 55 | fd.write("\n\n") 56 | 57 | def parseJSON(notebook, fd): 58 | cellsList = notebook["cells"] 59 | for cellDict in cellsList: 60 | name = cellDict["cell_type"] 61 | data = cellDict["source"] 62 | if name == "markdown": 63 | convertMarkdown(data, fd) 64 | elif name == "code": 65 | convertCode(data, fd) 66 | else: 67 | raise("Notebook contains data other than markdown" + 68 | " code cells") 69 | 70 | if __name__ == "__main__": 71 | if len(sys.argv) != 3: 72 | print(" usage: " + sys.argv[0] + " ") 73 | exit(1) 74 | else: 75 | inputPath = sys.argv[1] 76 | outputPath = sys.argv[2] 77 | 78 | with open(inputPath, "r") as inputFd: 79 | notebookDict = json.load(inputFd) 80 | with open(outputPath, "w") as outputFd: 81 | parseJSON(notebookDict, outputFd) 82 | -------------------------------------------------------------------------------- /sckernel/install.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | import sckernel 6 | 7 | from jupyter_client.kernelspec import KernelSpecManager 8 | from IPython.utils.tempdir import TemporaryDirectory 9 | 10 | kernel_json = { 11 | "argv": [sys.executable, "-m", "sckernel", "-f", "{connection_file}"], 12 | "display_name": "SC_Kernel", 13 | "language": "text", 14 | } 15 | 16 | def install_my_kernel_spec(user=True, prefix=None): 17 | with TemporaryDirectory() as td: 18 | os.chmod(td, 0o755) # Starts off as 700, not user readable 19 | with open(os.path.join(td, 'kernel.json'), 'w') as f: 20 | json.dump(kernel_json, f, sort_keys=True) 21 | 22 | sckernel_path = os.path.dirname(sckernel.__file__); 23 | with open(os.path.join(td, 'kernel.js'), 'w') as dst: 24 | with open(os.path.join(sckernel_path, 'kernel.js')) as src: 25 | src_content = src.read() 26 | dst.write(src_content) 27 | 28 | print('Installing Jupyter kernel spec') 29 | KernelSpecManager().install_kernel_spec(td, 'sckernel', user=user, prefix=prefix) 30 | 31 | def _is_root(): 32 | try: 33 | return os.geteuid() == 0 34 | except AttributeError: 35 | return False # assume not an admin on non-Unix platforms 36 | 37 | def main(argv=None): 38 | ap = argparse.ArgumentParser() 39 | ap.add_argument('--user', action='store_true', 40 | help="Install to the per-user kernels registry. Default if not root.") 41 | ap.add_argument('--sys-prefix', action='store_true', 42 | help="Install to sys.prefix (e.g. a virtualenv or conda env)") 43 | ap.add_argument('--prefix', 44 | help="Install to the given prefix. " 45 | "Kernelspec will be installed in {PREFIX}/share/jupyter/kernels/") 46 | args = ap.parse_args(argv) 47 | 48 | if args.sys_prefix: 49 | args.prefix = sys.prefix 50 | if not args.prefix and not _is_root(): 51 | args.user = True 52 | 53 | install_my_kernel_spec(user=args.user, prefix=args.prefix) 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /sckernel/kernel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'codemirror/lib/codemirror', 3 | 'base/js/namespace', 4 | ], function( 5 | CodeMirror, 6 | IPython) { 7 | "use strict"; 8 | var onload = function() { 9 | console.log("Loading kernel.js for SC_Kernel") 10 | enableSCMode(CodeMirror); 11 | IPython.CodeCell.options_default["cm_config"]["mode"] = "sclang"; 12 | IPython.CodeCell.options_default['cm_config']['indentUnit'] = 2; 13 | var cells = IPython.notebook.get_cells(); 14 | for (var i in cells){ 15 | var c = cells[i]; 16 | if (c.cell_type === 'code') 17 | c.code_mirror.setOption('indentUnit', 2); 18 | } 19 | } 20 | return {onload:onload}; 21 | }); 22 | 23 | var enableSCMode = function (CodeMirror) { 24 | CodeMirror.defineMode('sclang', function (config) { 25 | var keywords = ["arg", "classvar", "const", "super", "this", "var"]; 26 | var builtIns = ["false", "inf", "nil", "true", "thisFunction", "thisFunctionDef", "thisMethod", "thisProcess", "thisThread", "currentEnvironment", "topEnvironment"]; 27 | 28 | function consumeString(stream, state) { 29 | var curr_char; 30 | while(curr_char = stream.next()) { 31 | if (curr_char === "\\") { 32 | curr_char = stream.next(); 33 | } else if (curr_char === "\"") { 34 | state.state = 0; 35 | return; 36 | }; 37 | } 38 | // End of the line but must be still in a string 39 | return; 40 | }; 41 | 42 | function consumeComment(stream, state) { 43 | var curr_char; 44 | var asterisk = false; 45 | while(curr_char = stream.next()) { 46 | console.log(curr_char); 47 | if (curr_char === "*") { 48 | asterisk = true; 49 | } else if (asterisk && curr_char === "\/") { 50 | state.commentLevel -= 1; 51 | if (state.commentLevel == 0) { 52 | state.state = 0; 53 | return; 54 | } 55 | asterisk = false; 56 | } else { 57 | asterisk = false; 58 | } 59 | }; 60 | 61 | // End of line 62 | return; 63 | }; 64 | 65 | function tokenize(stream, state) { 66 | // Inline Comments with // 67 | if (stream.match(/^\/\//)) { 68 | stream.skipToEnd(); 69 | return "comment"; 70 | }; 71 | 72 | // Multiline comments 73 | if (stream.match(/^\/\*/)) { 74 | state.state = 2; 75 | state.commentLevel += 1; 76 | consumeComment(stream, state); 77 | return "comment"; 78 | }; 79 | 80 | // Symbol 81 | if (stream.match(/^\'/)) { 82 | var curr_char; 83 | while (curr_char = stream.next()) { 84 | if (curr_char === "\\") { 85 | // consume next character because it will be ignored 86 | curr_char = stream.next(); 87 | } else if (curr_char === "'") { 88 | return "string" 89 | }; 90 | } 91 | 92 | // Only get here if it is the end of a line 93 | return "string"; 94 | }; 95 | 96 | // Symbol with a slash 97 | if (stream.match(/^\\\w*/)) { 98 | return "string"; 99 | }; 100 | 101 | // Strings 102 | if (stream.match(/^\"/)) { 103 | state.state = 1; 104 | consumeString(stream, state); 105 | return "string"; 106 | }; 107 | 108 | // White space 109 | if (stream.eatSpace()) { 110 | return null; 111 | }; 112 | 113 | // Radix float 114 | if (stream.match(/^[0-9]+r[0-9a-zA-Z]*(\.[0-9A-Z]*)?/)) { 115 | return "number"; 116 | }; 117 | 118 | // Scale degrees 119 | if (stream.match(/^\d+(s+|b+|[sb]\d+)/)) { 120 | return "number" 121 | }; 122 | 123 | // Hex Float 124 | if (stream.match(/^0x(\d|[a-f]|[A-F])+/)) { 125 | return "number" 126 | }; 127 | 128 | // Floats 129 | if (stream.match(/^(pi)|[0-9]+(\.[0-9]+)?([eE][\-\+]?[0-9]+)?(pi)?/)) { 130 | return "number"; 131 | }; 132 | 133 | // Symbol args 134 | if (stream.match(/^[a-zA-Z]+[:]/)) { 135 | return "builtin" 136 | } 137 | 138 | // Primitive 139 | if (stream.match(/^[_][a-zA-Z]/)) { 140 | return "atom"; 141 | } 142 | 143 | // Char 144 | if (stream.match(/^\$\\?./)) { 145 | return "string"; 146 | } 147 | 148 | // Environment variable 149 | if (stream.match(/^\~[a-z]\w*/)) { 150 | return "variable-3"; 151 | } 152 | 153 | // Class name 154 | if(stream.match(/^[A-Z]\w*/)) { 155 | return "atom"; 156 | }; 157 | 158 | // Builtins, keywords, variable names 159 | // Needs to be after primitive because this captures _ 160 | var m; 161 | if(m = stream.match(/^\w+/)) { 162 | var word = m[0]; 163 | if (builtIns.indexOf(word) > -1) { 164 | return "builtin"; 165 | } else if(keywords.indexOf(word) > -1) { 166 | return "keyword"; 167 | } else { 168 | return "variable-2"; 169 | }; 170 | }; 171 | 172 | // If we can't match anything then consume and move to next char 173 | stream.next(); 174 | return null; 175 | }; 176 | 177 | // States 178 | // 0 : normal 179 | // 1 : inString 180 | // 2 : inComment 181 | return { 182 | startState: function startState() { 183 | return { 184 | state: 0, 185 | commentLevel: 0 186 | } 187 | }, 188 | token: function(stream, state) { 189 | var token_name; 190 | if (state.state == 0 ) { 191 | token_name = tokenize(stream, state); 192 | } else if (state.state == 1) { 193 | consumeString(stream, state); 194 | token_name = "string"; 195 | } else if (state.state == 2) { 196 | consumeComment(stream, state); 197 | token_name = "comment"; 198 | } 199 | return token_name; 200 | } 201 | } 202 | }); 203 | }; 204 | -------------------------------------------------------------------------------- /sckernel/kernel.py: -------------------------------------------------------------------------------- 1 | from ipykernel.kernelbase import Kernel 2 | from .sclangSub import * 3 | 4 | class SCKernel(Kernel): 5 | implementation = "SuperCollider" 6 | implementation_version = "0.1" 7 | language = "SuperCollider" 8 | language_version = "3.10" 9 | language_info = { 10 | 'name':'sclang', 11 | 'codemirror_mode':'sclang' # mode defined in kernel.js 12 | } 13 | banner = "SuperCollider Kernel" 14 | 15 | def __init__(self, **kwargs): 16 | Kernel.__init__(self, **kwargs) 17 | self.sclangSubprocess = SclangSubprocess() 18 | 19 | def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): 20 | # silent should execute code but silence output - need to test 21 | # especially with polling numbers, for example - not sure of 22 | # sclang behavior 23 | 24 | self.sclangSubprocess.send_code(code, silent) 25 | stream_content = {'name':'stdout', 'text':''} 26 | self.send_response(self.iopub_socket, 'stream', stream_content) 27 | 28 | return { 29 | 'status': 'ok', 30 | 'execution_count':self.execution_count, 31 | 'payload': [], 32 | 'user_expressions': {} 33 | } 34 | 35 | def do_complete(self, code, cursor_pos): 36 | pass 37 | 38 | if __name__ == '__main__': 39 | from ipykernel.kernelapp import IPKernelApp 40 | IPKernelApp.launch_instance(kernel_class=SCKernel) 41 | -------------------------------------------------------------------------------- /sckernel/sclangSub.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import _thread 3 | import queue 4 | import sys 5 | import signal 6 | import os 7 | from subprocess import Popen, PIPE, DEVNULL 8 | import subprocess 9 | import time 10 | 11 | class SclangSubprocess: 12 | def __init__(self, encoding="utf-8"): 13 | self.encoding = encoding 14 | 15 | # Ensure window.py is in the same folder 16 | dir_path = os.path.dirname(os.path.realpath(__file__)) 17 | window_path = os.path.join(dir_path, "window.py") 18 | if not os.path.exists(window_path): 19 | raise("window.py is not in the same directory as" + 20 | "sclangSub.py") 21 | 22 | # Get paths to python and sclang binaries 23 | config_path = os.path.join(dir_path, "paths.cfg") 24 | if not os.path.exists(config_path): 25 | py_path = "python" 26 | sc_path = "sclang" 27 | else: 28 | with open(config_path, "r") as c: 29 | py_path = c.readline().split("=")[1].strip() 30 | sc_path = c.readline().split("=")[1].strip() 31 | 32 | # Start window subprocess and sclang subprocess 33 | try: 34 | self.window = Popen( 35 | [py_path, window_path], 36 | stdin=PIPE, stdout=DEVNULL, stderr=DEVNULL, bufsize=0 37 | ) 38 | except: 39 | raise RuntimeError("Could not open window.py. Make sure" 40 | + " that the path to Python is correct using sckernel.config.") 41 | 42 | try: 43 | self.sclang = Popen( 44 | # IDE mode (i.e., -i) suppresses sc3> prompt text and input 45 | # The use of "sckernel" is to provide a name 46 | # See Platform.ideName for more 47 | [sc_path, "-i", "sckernel"], 48 | stdin=PIPE, stdout=PIPE, stderr=DEVNULL, bufsize=0 49 | ) 50 | except: 51 | self.window.kill() 52 | self.window.wait() 53 | raise RuntimeError("Could not open sclang. Check to make " 54 | + "sure that the path of sclang is correct using sckernel.config.") 55 | 56 | self.window_queue = queue.Queue() 57 | self.receiver = threading.Thread(target=self.__receive_output) 58 | self.receiver.daemon = True 59 | self.processor = threading.Thread(target=self.__process_output) 60 | self.processor.daemon = True 61 | self.receiver.start() 62 | self.processor.start() 63 | 64 | self.shuttingDown = False 65 | self.lock = threading.RLock() 66 | 67 | def sig_handler(signum, frame): 68 | self.shutdown() 69 | 70 | signal.signal(signal.SIGINT, sig_handler) 71 | signal.signal(signal.SIGTERM, sig_handler) 72 | 73 | def __receive_output(self): 74 | while True: 75 | try: 76 | outputLine = self.sclang.stdout.readline() 77 | except: 78 | break 79 | if not outputLine: break 80 | outputLine = outputLine.decode(self.encoding) 81 | self.window_queue.put(outputLine) 82 | 83 | # Tell window loop to stop processing 84 | self.window_queue.put(None) 85 | 86 | def __sendToWindow(self, line): 87 | try: 88 | self.window.stdin.write(line.encode(self.encoding)) 89 | except: 90 | # Do not quit process because we may still be 91 | # able to read and write from sclang. 92 | print("Cannot write to window.py. Continuing execution.") 93 | 94 | def __process_output(self): 95 | while True: 96 | line = self.window_queue.get() 97 | 98 | if line is None: break 99 | else: 100 | self.__sendToWindow(line) 101 | 102 | self.window.stdin.close() 103 | 104 | self.lock.acquire() 105 | if not self.shuttingDown: 106 | self.shuttingDown = True 107 | self.lock.release() 108 | 109 | # sclang interpreter must have terminated 110 | print() 111 | print("sclang interpeter shutdown. Exiting " 112 | "from worker thread.") 113 | self.sclang.wait() 114 | print("sclang shutdown") 115 | self.window.wait() 116 | os._exit(1) 117 | else: 118 | pass # let shutdown happen from main thread 119 | 120 | def get_code(self): 121 | try: 122 | code = input("sc3> ") 123 | error = self.send_code(code) 124 | if error: self.shutdown() 125 | except EOFError: 126 | self.shutdown() 127 | 128 | def send_code(self, code, silent = False): 129 | if code == '': return False, code 130 | 131 | code += '\n' 132 | 133 | # attempted to write to sclang, otherwise 134 | # report that sclang has been shutdown 135 | try: 136 | self.sclang.stdin.write(code.encode(self.encoding)) 137 | if silent: 138 | self.sclang.stdin.write(bytearray.fromhex("1b")) 139 | else: 140 | self.sclang.stdin.write(bytearray.fromhex("0c")) 141 | return False 142 | except IOError: 143 | print("Could not write to stdin of sclang.") 144 | return True 145 | except: 146 | print("Some issue other than an IOError when writing to " 147 | + "stdin of sclang") 148 | return True 149 | 150 | def shutdown(self): 151 | self.lock.acquire() 152 | if not self.shuttingDown: 153 | self.shuttingDown = True 154 | self.lock.release() 155 | 156 | print("Shutting down...") 157 | print("Closing input to sclang stdin and waiting for" + 158 | " subprocesses to close.") 159 | 160 | # The line self.sclang.stdin.close() has been replaced 161 | # because sclang on Windows does not exit when stdin 162 | # is closed unlike on MacOS. 163 | 164 | # self.sclang.stdin.close() 165 | self.send_code("1.exit") 166 | self.sclang.wait() 167 | print("Sclang subprocess closed.") 168 | self.window.wait() 169 | print("Window subprocess closed.") 170 | print("Exiting...") 171 | sys.exit(0) 172 | 173 | if __name__ == "__main__": 174 | sclang = SclangSubprocess() 175 | 176 | while True: 177 | sclang.get_code() 178 | -------------------------------------------------------------------------------- /sckernel/window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import scrolledtext 3 | from tkinter import font 4 | from tkinter import ttk 5 | import sys 6 | import threading 7 | import queue 8 | import signal 9 | 10 | WINDOW_INTERVAL = 40 # Time in ms between screen refresh of text 11 | 12 | def addToQueue(q): 13 | while True: 14 | try: 15 | line = sys.stdin.readline() 16 | except: 17 | q.put("") # Trigger quit from main thread 18 | break 19 | 20 | q.put(line) 21 | if line == "": 22 | break 23 | print("Exiting reader thread.") 24 | 25 | def destroyWindow(): 26 | root.destroy() 27 | print("Post window closed.") 28 | 29 | def processText(q): 30 | quit = False 31 | try: 32 | while True: 33 | line = q.get_nowait() 34 | if line == "": quit = True 35 | textbox.insert(tk.END, line) 36 | textbox.see("end") 37 | except queue.Empty: 38 | if not quit: 39 | root.after(WINDOW_INTERVAL, processText, q) 40 | else: 41 | root.after(WINDOW_INTERVAL, destroyWindow) 42 | 43 | def openingLine(q): 44 | textbox.insert(tk.END, "Post Window for SuperCollider:\n") 45 | root.after(WINDOW_INTERVAL, processText, q) 46 | 47 | def zoomIn(): 48 | font.configure(size=font.cget("size") + 2) 49 | 50 | def zoomOut(): 51 | font.configure(size=font.cget("size") - 2) 52 | 53 | # Main code to run 54 | q = queue.Queue() 55 | t = threading.Thread(target=addToQueue, args=(q,)) 56 | t.daemon = True 57 | root = tk.Tk() 58 | root.geometry("600x500") # initial size but can be resizable 59 | root.resizable(True, True) 60 | 61 | # Font buttons 62 | font = font.Font(family="Courier", size=14) 63 | button_frame = tk.Frame(root) 64 | button_frame.pack(fill="x") 65 | 66 | # Top label and buttons 67 | label=tk.Label(button_frame, text="SC Post Window") 68 | zin = tk.Button(button_frame, text="Zoom In", command=zoomIn) 69 | zout = tk.Button(button_frame, text="Zoom Out", command=zoomOut) 70 | label.pack(side="left") 71 | zin.pack(side="left") 72 | zout.pack(side="left") 73 | 74 | # Horizontal Separator 75 | separator = ttk.Separator(root, orient="horizontal") 76 | separator.pack(side="top", fill="x") 77 | 78 | # Textbox 79 | # Adding the width=1 and height=1 is necessary so that resizing font 80 | # does not change the window size 81 | textbox = scrolledtext.ScrolledText(root, width=1, height=1, font=font) 82 | textbox.pack(expand=True, fill="both") 83 | root.after(0, openingLine, q) 84 | 85 | # Install signal handler 86 | def sig_handler(signum, frame): 87 | destroyWindow() 88 | 89 | signal.signal(signal.SIGINT, sig_handler) 90 | signal.signal(signal.SIGTERM, sig_handler) 91 | 92 | # Run display 93 | print("Starting worker thread to process stdin and start main graphics loop...") 94 | t.start() 95 | root.mainloop() 96 | print("Exiting...") 97 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="sckernel", # Replace with your own username 8 | version="0.3.2", 9 | author="Andrew Davis", 10 | author_email="andrewdavis33@gmail.com", 11 | description="A SuperCollider kernel for Jupyter Notebooks", 12 | long_description= long_description, 13 | long_description_content_type = "text/markdown", 14 | url="https://github.com/andrewdavis33/sckernel", 15 | install_requires=[ 16 | 'jupyter_client', 'IPython', 'ipykernel' 17 | ], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | packages=setuptools.find_packages(), 24 | python_requires='>=3.7', 25 | include_package_data=True, 26 | ) 27 | --------------------------------------------------------------------------------