Improving ScalaPy developer experience and integration with Ammonite and Almond
Introduction
ScalaPy is an excellent library by Shadaj Laddad that provides users access to Python libraries inside Scala which I use a lot. This project sets out to improve developer experience by implementing some quality of life features such as ease of set up, code autocompletion and docstring display, as well as better integration with Ammonite (Scala REPL and scripting) and Almond (Jupyter kernel for Scala).
Ease of setting up
In order to set up ScalaPy to use a specific Python interpreter, users have to specify several properties, namely,
-
for JVM: the directories containing the
libpython
associated with said interpreter as well as the correct name oflibpython
, e.g.libpython3.7
,libpython3.8m
-
for Scala Native: the recommended linker options
Figuring out how to extract these properties in many cases is not straightforward and the rules vary among different platforms, Python versions and installation methods. python-native-libs
was made with an aim to derive these properties in a robust way. Details on how exactly python-native-libs
arrives at these properties are provided here.
Using python-native-libs
to set up ScalaPy either with an Sbt project or with Ammonite or Almond is fairly straightforward,
Sbt
for both JVM and Scala Native, add python-native-libs
to project/plugins.sbt
libraryDependencies += "ai.kien" %% "python-native-libs" % "0.2.1"
then, in build.sbt
,
for JVM,
fork := true
import ai.kien.python.Python
lazy val python = Python("<optional-path-to-a-python-interpreter-executable>")
lazy val javaOpts = python.scalapyProperties.get.map {
case (k, v) => s"""-D$k=$v"""
}.toSeq
javaOptions ++= javaOpts
for Scala Native,
import ai.kien.python.Python
lazy val python = Python("<optional-path-to-a-python-interpreter-executable>")
lazy val pythonLdFlags = python.ldflags.get
nativeLinkingOptions ++= pythonLdFlags
Ammonite and Almond
@
import $ivy.`ai.kien::python-native-libs:0.2.1`
import $ivy.`me.shadaj::scalapy-core:0.5.0+8-7c7a6042`
import ai.kien.python.Python
Python("<optional-path-to-a-python-interpreter-executable>").scalapyProperties.fold(
ex => println(s"Error while getting ScalaPy properties: $ex"),
props => props.foreach { case(k, v) => System.setProperty(k, v) }
)
import me.shadaj.scalapy.py
import me.shadaj.scalapy.py.PyQuote
println(py"'Hello!'")
Future works
The next goal is to integrate python-native-libs
directly into ScalaPy, Ammonite and Almond to make the experience even more seamless. There have already been open PRs in ScalaPy and Ammonite (or coursier if that fails) to move this forward.
Display of rich representations for Python objects in Almond
Jupyter notebooks allow Python users to display plots, images, html, latex by rendering the outputs of the _repr_*_
methods (e.g. _repr_png_
, _repr_svg_
, _repr_html_
, …) for Python objects that implement them. This functionality has just been made available to ScalaPy users using Almond in almond#843 and almond#854 with the help of jvm-repr
. You can see this feature at work here. The implementation was completed even though the PR is still open pending some baffling CI issue which we aim to resolve soonTM.
Future works
This feature only supports displaying static representations of Python objects, not interactive/updatable displays such as progress bar. One possible way to make this happen is to bridge between IPython display APIs and Almond display APIs.
Auto completion and docstring for ScalaPy in Ammonite and Almond
Basic completion for ScalaPy Python objects in Ammonite was made possible by employing the Python standard library rlcompleter
. It supports completion for expressions of the form pyObject.(attr.*)attr|
where pyObject
is an object of type py.Dynamic.global.type
or py.Dynamic
and .attr
’s are invocations of the selectDynamic()
methods, which is equivalent to accessing an attribute of the Python object. Below are some examples of how this feature works in Ammonite.
@ py.Dynamic.global.li|
license( list(
@ import me.shadaj.scalapy.interpreter.CPythonInterpreter
CPythonInterpreter.execManyLines("""class Cls: x = 'string'""")
@ py.Dynamic.global.Cls.x.up|
upper(
@ val p = py"'string'"
@ p.up|
upper(
@ object Obj { val p = py"'string'" }
@ Obj.p.up|
upper(
More advanced completions and docstring display are provided by jedi
Interpreter
@ py.Dynamic.global.range(10).cou|
count|
Here range(10)
being a function call makes it impossible for rlcompleter
to provide completions for cou|
. It is possible in jedi
however, thanks to static analysis.
This feature is in the work at kiendang/Ammonite#2. This PR is not aimed to be merged into Ammonite as is but waits until Ammonite provides an interface for extending the completion functionality. ScalaPy completion would then be its own separate library which is imported and enabled by Ammonite users when they use ScalaPy. This is to avoid adding ScalaPy as a dependency to Ammonite.
Future works
The next step is to continue polishing this feature, hopefully with user feedbacks, and adding Scala 3 support. It is currently only available for Scala 2 (2.12 and 2.13), not Scala 3, mainly due to ScalaPy not being released for Scala 3 yet plus some difference in how the presentation compiler works in Scala 2 and Scala 3 which would require some modifications to the implementation. Works on making autocompletion available for Scala 3 would begin after ScalaPy lands Scala 3 support shadaj/scalapy#209.
Potential features in the work
One feature that is extremely nice to have and we hope to be able to implement if time allows in the future is the ability to start an integrated Python shell inside Ammonite and Almond that allows variable exchange with the Scala shell. Users could drop into a Python shell inside Ammonite and Almond, run some Python code, then return to the Scala shell with all the variables defined inside Python now also available in the Scala shell, similar to polynote
.
Links
Aggregation of links to the works mentioned in this post
-
ScalaPy setup
python-native-libs
, ScalaPy#210, Ammonite#1209 -
Display of rich presentations of Python objects in Almond almond#843, almond#854
-
ScalaPy autocompletion in Ammonite kiendang/Ammonite#2
-
Others ScalaPy#207, ScalaPy#200, ScalaPy#198
Acknowledgements
Special thanks to Alex Archambault, Anatolii Kmetiuk and Shadaj Laddad and the Scala Center for the great support and for answering my various questions!