Skip to content

Commit b66141b

Browse files
Merge pull request #1616 from alexarchambault/current-dir-python-path
Put workspace and / or current directory in PYTHONPATH in the run and repl commands
2 parents 3d399b5 + 063e237 commit b66141b

File tree

7 files changed

+148
-18
lines changed

7 files changed

+148
-18
lines changed

build.sc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ object dummy extends Module {
194194
Deps.pythonInterface
195195
)
196196
}
197+
object scalaPy extends ScalaModule with Bloop.Module {
198+
def skipBloop = true
199+
def scalaVersion = Scala.defaultInternal
200+
def ivyDeps = Agg(
201+
Deps.scalaPy
202+
)
203+
}
197204
}
198205

199206
trait BuildMacros extends ScalaCliSbtModule

modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import scala.build.internal.{Constants, Runner}
1414
import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, Scope}
1515
import scala.cli.CurrentParams
1616
import scala.cli.commands.publish.ConfigUtil.*
17-
import scala.cli.commands.run.Run.{maybePrintSimpleScalacOutput, orPythonDetectionError}
17+
import scala.cli.commands.run.Run.{
18+
maybePrintSimpleScalacOutput,
19+
orPythonDetectionError,
20+
pythonPathEnv
21+
}
1822
import scala.cli.commands.run.RunMode
1923
import scala.cli.commands.shared.SharedOptions
2024
import scala.cli.commands.{ScalaCommand, WatchUtil}
@@ -264,20 +268,25 @@ object Repl extends ScalaCommand[ReplOptions] {
264268
}
265269
.params
266270

267-
val scalapyJavaOpts =
271+
val (scalapyJavaOpts, scalapyExtraEnv) =
268272
if (setupPython) {
269273
val props = value {
270274
val python = Python()
271275
val propsOrError = python.scalapyProperties
272276
logger.debug(s"Python Java properties: $propsOrError")
273277
propsOrError.orPythonDetectionError
274278
}
275-
props.toVector.sorted.map {
279+
val props0 = props.toVector.sorted.map {
276280
case (k, v) => s"-D$k=$v"
277281
}
282+
// Putting current dir in PYTHONPATH, see
283+
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
284+
// for context.
285+
val dirs = buildOpt.map(_.inputs.workspace).toSeq ++ Seq(os.pwd)
286+
(props0, pythonPathEnv(dirs: _*))
278287
}
279288
else
280-
Nil
289+
(Nil, Map.empty[String, String])
281290

282291
def additionalArgs = {
283292
val pythonArgs =
@@ -365,7 +374,7 @@ object Repl extends ScalaCommand[ReplOptions] {
365374
maybeAdaptForWindows(replArgs),
366375
logger,
367376
allowExecve = allowExit,
368-
extraEnv = extraEnv
377+
extraEnv = scalapyExtraEnv ++ extraEnv
369378
).waitFor()
370379
if (retCode != 0)
371380
value(Left(new ReplError(retCode)))

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ai.kien.python.Python
44
import caseapp.*
55

66
import java.io.File
7+
import java.util.Locale
78
import java.util.concurrent.CompletableFuture
89

910
import scala.build.EitherCps.{either, value}
@@ -338,6 +339,27 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
338339
value(res)
339340
}
340341

342+
def pythonPathEnv(dirs: os.Path*): Map[String, String] = {
343+
val onlySafePaths = sys.env.exists {
344+
case (k, v) =>
345+
k.toLowerCase(Locale.ROOT) == "pythonsafepath" && v.nonEmpty
346+
}
347+
// Don't add unsafe directories to PYTHONPATH if PYTHONSAFEPATH is set,
348+
// see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH
349+
// and https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1336017760
350+
// for more details.
351+
if (onlySafePaths) Map.empty[String, String]
352+
else {
353+
val (pythonPathEnvVarName, currentPythonPath) = sys.env
354+
.find(_._1.toLowerCase(Locale.ROOT) == "pythonpath")
355+
.getOrElse(("PYTHONPATH", ""))
356+
val updatedPythonPath = (currentPythonPath +: dirs.map(_.toString))
357+
.filter(_.nonEmpty)
358+
.mkString(File.pathSeparator)
359+
Map(pythonPathEnvVarName -> updatedPythonPath)
360+
}
361+
}
362+
341363
private def runOnce(
342364
build: Build.Successful,
343365
mainClass: String,
@@ -397,9 +419,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
397419
value(res)
398420
case Platform.Native =>
399421
val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
400-
val (pythonExecutable, pythonLibraryPaths) =
401-
if (setupPython)
402-
value {
422+
val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) =
423+
if (setupPython) {
424+
val (exec, libPaths) = value {
403425
val python = Python()
404426
val pythonPropertiesOrError = for {
405427
paths <- python.nativeLibraryPaths
@@ -408,8 +430,13 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
408430
logger.debug(s"Python executable and native library paths: $pythonPropertiesOrError")
409431
pythonPropertiesOrError.orPythonDetectionError
410432
}
433+
// Putting the workspace in PYTHONPATH, see
434+
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
435+
// for context.
436+
(exec, libPaths, pythonPathEnv(build.inputs.workspace))
437+
}
411438
else
412-
(None, Nil)
439+
(None, Nil, Map())
413440
// seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308),
414441
// which prevents apps from finding libpython for example, so we update it manually here
415442
val libraryPathsEnv =
@@ -434,7 +461,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
434461
}
435462
val programNameEnv =
436463
pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py))
437-
val extraEnv = libraryPathsEnv ++ programNameEnv
464+
val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv
438465
val maybeResult = withNativeLauncher(
439466
build,
440467
mainClass,
@@ -463,20 +490,24 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
463490
case RunMode.Default =>
464491
val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value)
465492
val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
466-
val pythonJavaProps =
493+
val (pythonJavaProps, pythonExtraEnv) =
467494
if (setupPython) {
468495
val scalapyProps = value {
469496
val python = Python()
470497
val propsOrError = python.scalapyProperties
471498
logger.debug(s"Python Java properties: $propsOrError")
472499
propsOrError.orPythonDetectionError
473500
}
474-
scalapyProps.toVector.sorted.map {
501+
val props = scalapyProps.toVector.sorted.map {
475502
case (k, v) => s"-D$k=$v"
476503
}
504+
// Putting the workspace in PYTHONPATH, see
505+
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
506+
// for context.
507+
(props, pythonPathEnv(build.inputs.workspace))
477508
}
478509
else
479-
Nil
510+
(Nil, Map.empty[String, String])
480511
val allJavaOpts = pythonJavaProps ++ baseJavaProps
481512
if (showCommand) {
482513
val command = Runner.jvmCommand(
@@ -485,6 +516,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
485516
build.fullClassPath,
486517
mainClass,
487518
args,
519+
extraEnv = pythonExtraEnv,
488520
useManifest = build.options.notForBloopOptions.runWithManifest,
489521
scratchDirOpt = scratchDirOpt
490522
)
@@ -499,6 +531,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
499531
args,
500532
logger,
501533
allowExecve = allowExecve,
534+
extraEnv = pythonExtraEnv,
502535
useManifest = build.options.notForBloopOptions.runWithManifest,
503536
scratchDirOpt = scratchDirOpt
504537
)

modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ abstract class ReplTestDefinitions(val scalaVersionOpt: Option[String])
4646
}
4747

4848
test("ammonite scalapy") {
49-
TestInputs.empty.fromRoot { root =>
49+
val inputs = TestInputs(
50+
os.rel / "foo" / "something.py" ->
51+
"""messageStart = 'Hello from'
52+
|messageEnd = 'ScalaPy'
53+
|""".stripMargin
54+
)
55+
inputs.fromRoot { root =>
5056
val ammArgs = Seq(
5157
"-c",
5258
"""println("Hello" + " from Scala " + scala.util.Properties.versionNumberString)
53-
|// py.Dynamic.global.print("Hello from", "ScalaPy") // doesn't work
54-
|println(py"'Hello from '" + py"'ScalaPy'")
59+
|val sth = py.module("foo.something")
60+
|py.Dynamic.global.applyDynamicNamed("print")("" -> sth.messageStart, "" -> sth.messageEnd, "flush" -> py.Any.from(true))
5561
|""".stripMargin
5662
)
5763
.map {
@@ -61,6 +67,24 @@ abstract class ReplTestDefinitions(val scalaVersionOpt: Option[String])
6167
identity
6268
}
6369
.flatMap(arg => Seq("--ammonite-arg", arg))
70+
71+
val errorRes = os.proc(
72+
TestUtil.cli,
73+
"repl",
74+
extraOptions,
75+
"--ammonite",
76+
"--python",
77+
ammArgs
78+
).call(
79+
cwd = root,
80+
env = Map("PYTHONSAFEPATH" -> "foo"),
81+
mergeErrIntoOut = true,
82+
check = false
83+
)
84+
expect(errorRes.exitCode != 0)
85+
val errorOutput = errorRes.out.text()
86+
expect(errorOutput.contains("No module named 'foo'"))
87+
6488
val res = os.proc(
6589
TestUtil.cli,
6690
"repl",

modules/integration/src/test/scala/scala/cli/integration/RunScalaPyTestDefinitions.scala

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,61 @@ trait RunScalaPyTestDefinitions { _: RunTestDefinitions =>
7878
test("scalapy native") {
7979
scalapyNativeTest()
8080
}
81+
82+
def pythonAndScalaSourcesTest(native: Boolean): Unit = {
83+
val tq = "\"\"\""
84+
val inputs = TestInputs(
85+
os.rel / "src" / "helpers.py" ->
86+
s"""class Helper:
87+
| ${tq}Helper class$tq
88+
|
89+
| def message(self):
90+
| return 'Hello from Python'
91+
|""".stripMargin,
92+
os.rel / "src" / "Hello.scala" ->
93+
s"""//> using python
94+
|$maybeScalapyPrefix
95+
|object Hello {
96+
| def main(args: Array[String]): Unit =
97+
| py.local {
98+
| val helpers = py.module("helpers")
99+
| println(helpers.Helper().message())
100+
| }
101+
|}
102+
|""".stripMargin
103+
)
104+
val nativeOpt = if (native) Seq("--native") else Nil
105+
inputs.fromRoot { root =>
106+
107+
// Script dir shouldn't be added to PYTHONPATH if PYTHONSAFEPATH is non-empty
108+
val errorRes = os.proc(TestUtil.cli, "run", extraOptions, nativeOpt, "src")
109+
.call(
110+
cwd = root,
111+
env = Map("PYTHONSAFEPATH" -> "foo"),
112+
mergeErrIntoOut = true,
113+
check = false
114+
)
115+
expect(errorRes.exitCode != 0)
116+
val errorOutput = errorRes.out.text()
117+
expect(errorOutput.contains("No module named 'helpers'"))
118+
119+
val res = os.proc(TestUtil.cli, "run", extraOptions, nativeOpt, "src")
120+
.call(cwd = root)
121+
val output = res.out.trim()
122+
if (native)
123+
expect(output.linesIterator.toVector.endsWith(Seq("Hello from Python")))
124+
else
125+
expect(output == "Hello from Python")
126+
}
127+
}
128+
129+
test("Python and Scala sources") {
130+
pythonAndScalaSourcesTest(native = false)
131+
}
132+
// disabled on Windows for now, for context, see
133+
// https://github.com/VirtusLab/scala-cli/pull/1270#issuecomment-1237904394
134+
if (!Properties.isWin)
135+
test("Python and Scala sources (native)") {
136+
pythonAndScalaSourcesTest(native = true)
137+
}
81138
}

project/deps.sc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ object Deps {
160160
def scalaPackagerCli = ivy"org.virtuslab:scala-packager-cli_2.13:${Versions.scalaPackager}"
161161
// Force using of 2.13 - is there a better way?
162162
def scalaparse = ivy"com.lihaoyi:scalaparse_2.13:2.3.3"
163-
def scalaPy = ivy"me.shadaj::scalapy-core::0.5.2+5-83f1eb68"
163+
def scalaPy = ivy"dev.scalapy::scalapy-core::0.5.3"
164164
def scalaReflect(sv: String) = ivy"org.scala-lang:scala-reflect:$sv"
165165
def semanticDbJavac = ivy"com.sourcegraph:semanticdb-javac:0.7.4"
166166
def semanticDbScalac = ivy"org.scalameta:::semanticdb-scalac:${Versions.scalaMeta}"

website/docs/reference/cli-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ Enable Python support via ScalaPy
10391039

10401040
Aliases: `--scalapy-version`
10411041

1042-
[experimental] Set ScalaPy version (0.5.2+5-83f1eb68 by default)
1042+
[experimental] Set ScalaPy version (0.5.3 by default)
10431043

10441044
## Repl options
10451045

0 commit comments

Comments
 (0)