Packages
metastatic
0.21.1
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/adapters/python/subprocess.ex
defmodule Metastatic.Adapters.Python.Subprocess do
@moduledoc """
Subprocess management for Python parser and unparser.
Handles communication with Python scripts via stdin/stdout with JSON serialization.
"""
@parser_path "priv/parsers/python/parser.py"
@unparser_path "priv/parsers/python/unparser.py"
@doc """
Parse Python source code to AST JSON.
## Examples
iex> Metastatic.Adapters.Python.Subprocess.parse("x + 5")
{:ok, %{"_type" => "Module", "body" => [...]}}
iex> Metastatic.Adapters.Python.Subprocess.parse("x +")
{:error, "SyntaxError: ..."}
"""
@spec parse(String.t()) :: {:ok, map()} | {:error, String.t()}
def parse(source) do
case run_python_script(@parser_path, source) do
{:ok, result} ->
case Jason.decode(result) do
{:ok, %{"ok" => true, "ast" => ast}} ->
{:ok, ast}
{:ok, %{"ok" => false, "error" => error}} ->
{:error, format_error(error)}
{:error, reason} ->
{:error, "JSON decode failed: #{inspect(reason)}"}
end
{:error, reason} ->
{:error, "Python process failed: #{inspect(reason)}"}
end
end
@doc """
Unparse AST JSON back to Python source code.
## Examples
iex> ast = %{"_type" => "Module", "body" => [...]}
iex> Metastatic.Adapters.Python.Subprocess.unparse(ast)
{:ok, "x + 5"}
"""
@spec unparse(map()) :: {:ok, String.t()} | {:error, String.t()}
def unparse(ast) do
case Jason.encode(ast) do
{:ok, json} ->
case run_python_script(@unparser_path, json) do
{:ok, result} ->
case Jason.decode(result) do
{:ok, %{"ok" => true, "source" => source}} ->
{:ok, source}
{:ok, %{"ok" => false, "error" => error}} ->
{:error, format_error(error)}
{:error, reason} ->
{:error, "JSON decode failed: #{inspect(reason)}"}
end
{:error, reason} ->
{:error, "Python process failed: #{inspect(reason)}"}
end
{:error, reason} ->
{:error, "JSON encode failed: #{inspect(reason)}"}
end
end
# Private Functions
defp run_python_script(script_path, input) do
script_full_path = Path.join(File.cwd!(), script_path)
# Check if script exists
if File.exists?(script_full_path) do
# Use printf to pipe input to python3
# This avoids stdin: option which doesn't exist in System.cmd
case System.cmd("sh", [
"-c",
"printf '%s' #{shell_escape(input)} | python3 #{script_full_path}"
]) do
{output, 0} ->
{:ok, output}
{error_output, exit_code} ->
{:error, "Exit code #{exit_code}: #{error_output}"}
end
else
{:error, "Python script not found: #{script_full_path}"}
end
rescue
e ->
{:error, "Exception: #{Exception.message(e)}"}
end
defp format_error(%{"type" => type, "msg" => msg, "lineno" => line}) do
"#{type} at line #{line}: #{msg}"
end
defp format_error(%{"type" => type, "msg" => msg}) do
"#{type}: #{msg}"
end
defp format_error(error) when is_binary(error) do
error
end
defp format_error(error) do
inspect(error)
end
defp shell_escape(string) do
# Escape single quotes for shell
escaped = String.replace(string, "'", "'\\''")
"'#{escaped}'"
end
end