Skip to content

Commit

Permalink
feat: support relative paths (#83)
Browse files Browse the repository at this point in the history
Closes #76

### Summary of Changes

The `cwd` of the worker process can now be set via the new `data.cwd`
item of the `program` message.

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lars-reimann and megalinter-bot committed Apr 10, 2024
1 parent c32a4b6 commit a65261b
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 3 deletions.
9 changes: 8 additions & 1 deletion src/safeds_runner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
"""A runner for the Python code generated from Safe-DS programs."""

from .server._pipeline_manager import file_mtime, memoized_dynamic_call, memoized_static_call, save_placeholder
from .server._pipeline_manager import (
absolute_path,
file_mtime,
memoized_dynamic_call,
memoized_static_call,
save_placeholder,
)

__all__ = [
"absolute_path",
"file_mtime",
"memoized_static_call",
"memoized_dynamic_call",
Expand Down
16 changes: 14 additions & 2 deletions src/safeds_runner/server/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,13 @@ class MessageDataProgram:
module name. The values of the inner dictionary is the python code for each module.
main : ProgramMainInformation
Information where the main pipeline (the pipeline to be executed) is located.
cwd:
Current working directory to use for execution. If not set, the default working directory is used.
"""

code: dict[str, dict[str, str]]
main: ProgramMainInformation
cwd: str | None = None

@staticmethod
def from_dict(d: dict[str, Any]) -> MessageDataProgram:
Expand All @@ -107,7 +110,11 @@ def from_dict(d: dict[str, Any]) -> MessageDataProgram:
MessageDataProgram
Dataclass which contains information copied from the provided dictionary.
"""
return MessageDataProgram(d["code"], ProgramMainInformation.from_dict(d["main"]))
return MessageDataProgram(
d["code"],
ProgramMainInformation.from_dict(d["main"]),
d.get("cwd"),
)

def to_dict(self) -> dict[str, Any]:
"""
Expand Down Expand Up @@ -400,7 +407,10 @@ def validate_program_message_data(message_data: dict[str, Any] | str) -> tuple[M
"""
if not isinstance(message_data, dict):
return None, "Message data is not a JSON object"
elif "code" not in message_data:

cwd = message_data.get("cwd", None)

if "code" not in message_data:
return None, "No 'code' parameter given"
elif "main" not in message_data:
return None, "No 'main' parameter given"
Expand All @@ -414,6 +424,8 @@ def validate_program_message_data(message_data: dict[str, Any] | str) -> tuple[M
return None, "Invalid 'main' parameter given"
elif not isinstance(message_data["code"], dict):
return None, "Invalid 'code' parameter given"
elif cwd is not None and not isinstance(cwd, str):
return None, "Invalid 'cwd' parameter given"
else:
code: dict = message_data["code"]
for key in code:
Expand Down
22 changes: 22 additions & 0 deletions src/safeds_runner/server/_pipeline_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import logging
import multiprocessing
import os
import queue
import runpy
import threading
Expand Down Expand Up @@ -265,6 +266,10 @@ def _execute(self) -> None:
main_module = f"gen_{self._pipeline.main.module}_{self._pipeline.main.pipeline}"
# Populate current_pipeline global, so child process can save placeholders in correct location
globals()["current_pipeline"] = self

if self._pipeline.cwd is not None:
os.chdir(self._pipeline.cwd) # pragma: no cover

try:
runpy.run_module(
(
Expand Down Expand Up @@ -408,6 +413,23 @@ def file_mtime(filename: str) -> int | None:
return None


def absolute_path(filename: str) -> str:
"""
Get the absolute path of the provided file.
Parameters
----------
filename:
Name of the file
Returns
-------
absolute_path:
Absolute path of the provided file
"""
return str(Path(filename).resolve())


def get_backtrace_info(error: BaseException) -> list[dict[str, Any]]:
"""
Create a simplified backtrace from an exception.
Expand Down
7 changes: 7 additions & 0 deletions tests/safeds_runner/server/test_memoization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import time
import typing
from datetime import UTC, datetime
from pathlib import Path
from queue import Queue
from typing import Any

Expand All @@ -17,6 +18,7 @@
from safeds_runner.server._messages import MessageDataProgram, ProgramMainInformation
from safeds_runner.server._pipeline_manager import (
PipelineProcess,
absolute_path,
file_mtime,
memoized_dynamic_call,
memoized_static_call,
Expand Down Expand Up @@ -268,6 +270,11 @@ def test_file_mtime_not_exists() -> None:
assert mtime is None


def test_absolute_path() -> None:
result = absolute_path("table.csv")
assert Path(result).is_absolute()


@pytest.mark.parametrize(
argnames="value,expected_size",
argvalues=[
Expand Down
15 changes: 15 additions & 0 deletions tests/safeds_runner/server/test_websocket_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,20 @@ def test_should_fail_message_validation_reason_general(websocket_message: str, e
),
"Invalid 'code' parameter given",
),
(
json.dumps(
{
"type": "program",
"id": "1234",
"data": {
"code": {},
"main": {"modulepath": "1", "module": "2", "pipeline": "3"},
"cwd": 1,
},
},
),
"Invalid 'cwd' parameter given",
),
],
ids=[
"program_invalid_data",
Expand All @@ -297,6 +311,7 @@ def test_should_fail_message_validation_reason_general(websocket_message: str, e
"program_invalid_code1",
"program_invalid_code2",
"program_invalid_code3",
"program_invalid_cwd",
],
)
def test_should_fail_message_validation_reason_program(websocket_message: str, exception_message: str) -> None:
Expand Down

0 comments on commit a65261b

Please sign in to comment.