Pyjinn
Contents
- What is Pyjinn?
- Pyjinn in Minescript 5.0
- Java Integration
- Python Language Features
- Python 3.x feature support
What is Pyjinn?
Pyjinn (pronounced like “pidgeon”) is a scripting language with Python syntax that integrates deeply with Java programs. It’s a “pidgin” language that looks and feels like Python while its implementation and runtime behavior are based in Java. (The name “Pyjinn” is a portmanteau of “Python” and “jinn”, which is another word for “genie”.)
While Pyjinn does not support the Python standard library, Pyjinn scripts can access the Java standard library and any publicly accessible Java classes loaded into the Java program in which it’s embedded.
Pyjinn in Minescript 5.0
Pyjinn is integrated into the upcoming release of Minescript 5.0. This allows Python-syntax scripts to be executed directly by the Minescript mod within Minecraft’s Java process without requiring a separate Python installation. Externally executed Python scripts continue to be supported. Minescript Python scripts and Minescript Pyjinn scripts share the same script APIs, with the exception of event handlers. Minescript Python scripts manage their own event queue using EventQueue, whereas Minescript Pyjinn scripts use single-threaded event handling inspired by JavaScript:
add_event_listener(event_type: str, callback: Callable[..., None], **args) -> int
set_interval(callback: Callable[..., None], timer_millis: int, *args) -> int
set_timeout(callback: Callable[..., None], timer_millis: int, *args) -> int
remove_event_listener(listener_id: int) -> bool
The supported event types are:
- “tick”, “render”, “key”, “mouse”, “chat”, “outgoing_chat_intercept”, “add_entity”, “block_update”, “explosion”, “take_item”, “damage”, “chunk”
Scripts can import the Minescript standard library explicitly. For simple IDE integration (e.g. VSCode), you can use imports like these:
from system.pyj.minescript import *
import system.pyj.minescript as m
For consistency with existing Python scripts you can use imports like these:
from minescript import *
import minescript
import minescript as m
If there are no imports of minescript
or system.pyj.minescript
in the main
script, it is imported implicitly as:
from system.pyj.minescript import *
For IDE compatibility, .pyj
files are used only for the main script file and
not imported from other scripts. This is because IDEs like VSCode do not
recognize imports of .pyj
files. To enable Python-syntax support (syntax
highlighting, autocompletion, etc), add this line to the top of your .pyj
file:
#!python
Pyjinn’s search path for imports (the equivalent of PYTHONPATH
, but currently hardcoded) is:
minecraft/minescript
minecraft/minescript/system/pyj
Pyjinn libraries in your minecraft/minescript
directory can be imported by dropping their .py
filename extension. E.g. minecraft/minescript/my_kewl_library.py
can be imported into a Pyjinn
script as:
import my_kewl_library
Pyjinn scripts (.pyj
) and Python scripts (.py
) can import the same .py
library if that library is compatible with both Python and Pyjinn.
Imported .py
libraries can detect if they’re running in a Pyjinn context by checking whether
sys.version
contains the string "Pyjinn"
. E.g. to disallow Python scripts from importing a
Pyjinn-only .py
library, you can add code like this at the top of your library:
import sys
if "Pyjinn" not in sys.version:
raise ImportError(f"Module '{__name__}' requires a Pyjinn interpreter.")
Embedding Pyjinn in Python scripts
Minescript’s pre-installed java
library for Python (minescript/system/lib/java.py
) supports
embedding Pyjinn scripts within Python scripts to access their global variables and functions.
For example, the Python script below uses java.eval_pyjinn_script(...)
to embed a Pyjinn script
that defines:
- global variable
x
- global function
print_x()
that prints the value ofx
- global function
get_fps(units: str)
that returns the current frames per second with the given units
Note: The script below accesses Java code (net.minecraft.client.Minecraft
) which may be
using name mappings. For details see: Mappings. tl;dr: If you’re using the Fabric
mod, install the mappings by running \install_mappings
before running the script below.
# pyjinn_in_python_example.py
import java
# or: import system.lib.java as java
script = java.eval_pyjinn_script(r"""
x = 42
def print_x():
print(f"x from Pyjinn = {x}")
Minecraft = JavaClass("net.minecraft.client.Minecraft")
def get_fps(units: str) -> str:
return f"{Minecraft.getInstance().getFps()} {units}"
""")
get_fps = script.getFunction("get_fps")
print("fps:", get_fps("frames per second"))
print(f"x from Python = {script.getVariable('x')}")
print_x = script.getFunction("print_x")
print_x()
script.setVariable("x", 99)
print_x()
Chat output:
fps: 101 frames per second
x from Python = 42
x from Pyjinn = 42
x from Pyjinn = 99
The Python script accesses globals from the Pyjinn script:
- accesses functions using
script.getFunction(function_name: str)
- gets the value of a global variable using
script.getVariable(variable_name: str)
- sets the value of a global variable using
script.setVariable(variable_name: str, value: Any)
Supported global variable types include bool
, float
, int
, str
, and references to Java
objects.
Java Integration
JavaClass
Import Java classes with JavaClass()
which takes a string literal:
List = JavaClass("java.util.List")
JavaClass
is Pyjinn’s wrapper around a Java Class<?>
and provides access to the class’s
constructors using function-call syntax and static methods using method-call syntax. This is
different from Class<?>
instances which are metaclasses that provide access to reflection
information. Calling type()
on a JavaClass
returns the Java metaclass (e.g. String.class
).
print(JavaClass("java.lang.String"))
# prints: JavaClass("java.lang.String")
print(type("This is a string"))
# prints: JavaClass("java.lang.String")
print("This is a string".getClass())
# prints Class<String>: class java.lang.String
print(type(42))
# prints: JavaClass("java.lang.Integer")
print(type(42).MAX_VALUE)
# prints equivalent of `Integer.MAX_VALUE`: 2147483647
print(type(42)(99))
# prints equivalent of `new Integer(99)`: 99
print(type(Integer))
# prints the metaclass Integer.class: class java.lang.Integer
print(Integer.TYPE)
# prints the int.class primitive type: int
Calling static and non-static Java methods
Call static method of Java class:
List = JavaClass("java.util.List")
java_list = List.of(1, 2, 3)
Call non-static method of Java object:
print(java_list.size())
# Also available in Python-compatible form: len(java_list)
JavaList
Get Java List from Pyjinn list (does not make a copy of the list or its elements):
java_list = JavaList([1, 2, 3])
JavaArray
Get Java array (Object[]
) from Pyjinn tuple:
my_tuple = (1, 2, 3, "foo", "bar")
java_array = JavaArray(my_tuple)
print(len(java_array)) # Equivalent to the java code: java_array.length
Java arrays and iterables are automatically treated as Pyjinn sequences and support slice notation:
int_tuple = (1, 2, 3)
java_array = JavaArray(int_tuple) # type of java_array is Object[]
for x in java_array:
print(x)
# Applies Python slice syntax to a Java array and returns a
# new Java array of the same type:
print(java_array[-2:])
Convert tuple to a Java array of a specific type, e.g. int[]
, by passing a type as the second
argument:
Integer = JavaClass("java.lang.Integer")
# Equivalent to this Java code:
# int[] java_int_array = new int[] { 1, 2, 3 };
java_int_array = JavaArray((1, 2, 3), Integer.TYPE)
print(type(java_int_array)) # prints: JavaClass("[I")
When converting a Pyjinn tuple to a Java array of a specific type, all elements of the tuple need to be assignable to the requested array type.
Implementing Java interfaces in Pyjinn
Pyjinn functions passed to methods with a parameter type that’s a Java interface are automatically
converted to implementations of that interface. In this example, Stream::map
is a Java method that
takes a Function<>
and Stream::filter
takes a Predicate<>
. Here, Pyjinn lambda
expressions
are passed to those Java methods to square the input number and filter for the resulting values that
are even numbers:
java_list = JavaList([x for x in range(10)])
print(
java_list.stream()
.map(lambda x: x * x)
.filter(lambda x: x % 2 == 0)
.toList())
# prints: [0, 4, 16, 36, 64]
Pyjinn functions can also be converted manually to a Java interface by calling the Java interface as if it’s a Python constructor:
Runnable = JavaClass("java.lang.Runnable")
x = Runnable(lambda: print("hello!"))
x.run() # prints: hello!
Strings
String objects in Pyjinn (e.g. "this is a test"
) are instances of java.lang.String
. Some
Python-compatible methods can be called on Pyjinn strings (see Python 3.x feature support:
str), as well as the ~50 methods available to java.lang.String
.
Pyjinn strings support Python slice operations:
s = "foobarbaz"
print(s[0]) # prints: f
print(s[-1]) # prints: z
print(s[3:]) # prints: barbaz
print(s[3:6]) # prints: bar
Although String
is a final
class in Java that cannot be subclassed, Pyjinn strings act as though
they’re a subclass of String
, extended with some Python-specific methods. (They’re not actually a
subclass of String
, there’s just logic built into the interpreter to allow additional methods to
be called on String
objects in Pyjinn scripts.)
# Python-compatible call to str.startswith():
print("foo".startswith("o", 1)) # prints: True
# Java call to String.startsWith() (note the different capitalization):
print("foo".startsWith("f")) # prints: True
Some string methods like split()
have the same name across Python and Java but with different
behavior. For Pyjinn string methods that have Python-compatible behavior and there’s a Java
String
method with the same name, Pyjinn strings by default behave like they do in Python. In rare
cases where a script needs to access Java String
functionality that conflicts with the
Python-compatible str
behavior, the Java behavior can be accessed by wrapping the string in
JavaString()
:
s = "this is a test"
# Call Python-compatible str.split():
print(s.split()) # prints: ["this", "is", "a", "test"]
# Call Java String.split() which takes a regular expression:
print(JavaString(s).split(r'\s+')) # prints: ["this", "is", "a", "test"]
Although the return value of JavaString(...)
gives access to all the Java methods of
java.lang.String
(without any of the Python-compatible methods), its runtime type is
org.pyjinn.interpreter.JavaString
.
Both Pyjinn strings and JavaString
objects can be passed without any explicit conversion to Java
methods that expect java.lang.String
. In the rare case that a JavaString
needs to be converted
explicitly back to String
, a script can call its toString()
method:
s = "foo"
print(s is JavaString(s).toString()) # prints: True
JavaString
is an advanced usage that should not be necessary in most scripts.
Python Language Features
The following are the Python language features supported in Pyjinn.
Names in UPPER_CASE_WITH_UNDERSCORES
are example names.
...
refers to omitted code.
Python expressions:
# Constant expressions:
None # NoneType
True # bool
False # bool
123 # int
3.14 # float
"hi" # str
'bye' # str
r'\o/' # str (raw literal)
"""triple quoted"""
'''triple quoted'''
# Formatted strings (f-string):
X = 1
Y = 2
print(f"{X} + {Y} = {X+Y}") # prints: 1 + 2 = 3
# Tuple literal:
(X, Y, Z)
# Tuple assignment:
X, Y = 1, 2
X, Y = (1, 2)
X, Y = [1, 2]
# List literal:
[X, Y, Z]
# Dict literal:
{K1: V1, K2: V2, ...}
# Binary operators:
X is Y
X is not Y
X == Y
X < Y
X <= Y
X > Y
X >= Y
X != Y
X in Y
X not in Y
# Call function with an iterable sequence into distinct args:
X = [1, 2, 3]
FUNC(*X) # call as FUNC(1, 2, 3)
# Operators:
-X # for numeric types
X and Y
X or Y
not X
X + Y
X - Y
X * Y
X / Y
X ** Y
X % Y
X << Y # since Pyjinn 0.6
X >> Y # since Pyjinn 0.6
# Built-in functions:
abs(X)
bool(X)
chr(INT)
enumerate(ITERABLE)
float(X)
globals()
hex(X)
int(X)
len(ITERABLE)
list(ITERABLE)
max(X, Y, ...)
min(X, Y, ...)
ord(STR)
print(...)
range(...)
round(X)
str(X)
tuple(ITERABLE)
type(X)
# Index/Slice operations:
X[INDEX]
X[FROM:]
X[:TO]
X[:]
X[FROM:TO:STEP]
X[:-1] # Negative indices are relative to the end of the sequence.
# if-else expression:
TRUE_VALUE if CONDITION else FALSE_VALUE
# List comprehension:
[X * 2 for X in range(5)] # evaluates to: [0, 2, 4, 6, 8]
[C for C in "hello"] # evaluates to: ["h", "e", "l", "l", "o"]
# Lambda expression:
lambda: print("no args")
lambda X: print("1 arg:", X)
lambda X, Y: print("2 args:", X, Y)
Python statements:
# Imports:
import MODULE
import MODULE as ALIAS
from MODULE import NAME1 as ALIAS1, NAME2 as ALIAS2, ...
from MODULE import *
# Assignment:
VAR = VALUE
VAR += VALUE
VAR -= VALUE
VAR *= VALUE
VAR /= VALUE
# Deletion:
del OBJECT
del DICT[KEY]
del LIST[INDEX]
# Print to stdout:
print("SOME STRING")
# Print to stderr:
print("SOME STRING", file=sys.stderr)
# Function definitions:
def FUNCTION_NAME(ARG1, ...):
...
return VALUE
def FUNCTION_NAME(ARG1, *VAR_ARG):
...
def FUNCTION_NAME(ARG1, KEYWORD1=VALUE1, KEYWORD2=VALUE2):
...
# Class definitions:
class CLASS_NAME:
def __init__(self, ...):
self.X = VALUE
self.Y = VALUE
...
@classmethod
def METHOD(CLASS, ...):
...
@staticmethod
def METHOD(...):
...
# Construct instance of a class:
OBJECT = CLASS_NAME(...)
# Reference field of object:
print(OBJECT.X)
# Mutable dataclass:
@dataclass
class CLASS_NAME:
FIELD1: TYPE
FIELD2: TYPE = DEFAULT_VALUE
...
# Immutable dataclass:
@dataclass(frozen=True)
class CLASS_NAME:
...
if CONDITION:
...
elif CONDITION:
...
else:
...
for VAR in ITERABLE:
...
break
...
continue # since Pyjinn 0.5
while CONDITION:
...
break
...
continue # since Pyjinn 0.5
try:
...
except EXCEPTION as NAME:
...
except:
pass
raise EXCEPTION(...)
def FUNCTION_NAME(...):
global GLOBAL_VAR
VAR = ...
def INNER_FUNCTION(...):
nonlocal VAR
...
Python 3.x feature support
This section is current through Minescript 5.0b4 and Pyjinn 0.8.
Langauage features
Language features currently NOT supported by Pyjinn:
- Python standard library (except for some basics in
sys
module:sys.argv
,sys.version
,sys.stdout
,sys.stderr
) **kwargs
on caller side and callee side of a function call (but individual keyword args are supported, e.g.print("foo", file=sys.stderr)
)dict(k1=v1, k2=v2, ...)
syntax for constructing a dictionary (but{k1: v1, k2: v2, ...}
is supported)else
blocks followingfor
andwhile
blocks- generators and
yield
statement - threading, asyncio, and async/await syntax (Java
Thread
is supported instead) - inheritance
with
statementmatch
statement (Python 3.10+)- keyword-only arguments to functions (PEP 3102)
- f-string assignment expressions, e.g.
f"{x = }"
(just prints x’s value, not"x = "
) - step increments in slice expressions (parses ok, but step values that aren’t equivalent to 1 result in an exception)
- special class methods with double underscores (except for
__init__()
which is supported) - arbitrary-precision integers (only fixed-size integers like Java
Integer
andLong
are supported) - user-defined decorators
- Python-style metaclasses
- floor division operator:
//
str
class str
(runtime type isjava.lang.String
)endswith(prefix: str, start: int = None, end: int = None) -> bool
find(sub: str, start: int = None, end: int = None) -> int
join(iterable) -> str
lower() -> str
lstrip(chars: str = None) -> str
replace(old: str, new: str, count: int = -1) -> str
rstrip(chars: str = None) -> str
split(sep: str = None, maxsplit: int = -1)
startswith(prefix: str, start: int = None, end: int = None) -> bool
strip(chars: str = None) -> str
upper() -> str
- plus ~50 methods from
java.lang.String
(see Java Integration: Strings)
tuple
class tuple
(runtime type isorg.pyjinn.interpreter.Script.PyTuple
)__add__(value) -> tuple
__contains__(key) -> bool
__eq__(value) -> bool
__ne__(value) -> bool
__len__() -> int
__getitem__(key)
count(value) -> int
index(value) -> int
list
class list
(runtime type isorg.pyjinn.interpreter.Script.PyList
)__add__(value) -> list
__contains__(key) -> bool
__delitem__(key)
__eq__(value) -> bool
__ne__(value) -> bool
__iadd__(value)
__len__() -> int
__getitem__(key)
__setitem__(key, value)
append(object)
clear()
copy() -> list
count(value) -> int
extend(iterable)
index(value) -> int
insert(index: int, object)
pop()
pop(index: int)
remove(value)
reverse()
sort()
dict
class dict
(runtime type isorg.pyjinn.interpreter.Script.PyDict
)items() -> iterable
keys() -> iterable
values() -> iterable
__len__() -> int
get(key)
get(key, default_value)
setdefault(key)
setdefault(key, default_value)
__getitem__(key)
__setitem__(key, value)
__contains__(key) -> bool
__delitem__(key)
Built-in functions
Status of built-in function support in Pyjinn:
abs()
– yesaiter()
– noall()
– noanext()
– noany()
– noascii()
– nobin()
– nobool()
– yesbreakpoint()
– nobytearray()
– nobytes()
– nocallable()
– nochr()
– yesclassmethod()
– yescompile()
– nocomplex()
– nodelattr()
– nodict()
– yesdir()
– nodivmod()
– noenumerate()
– yeseval()
– noexec()
– nofilter()
– nofloat()
– yesformat()
– nofrozenset()
– nogetattr()
– noglobals()
– yeshasattr()
– nohash()
– nohelp()
– nohex()
– yesid()
– noinput()
– noint()
– yesisinstance()
– noissubclass()
– noiter()
– nolen()
– yes (supported for both Python-compatible types and Java iterable types)list()
– yeslocals()
– nomap()
– nomax()
– yesmemoryview()
– nomin()
– yesnext()
– noobject()
– nooct()
– noopen()
– noord()
– yespow()
– noprint()
– yesproperty()
– norange()
– yesrepr()
– noreversed()
– noround()
– yesset()
– nosetattr()
– noslice()
– nosorted()
– nostaticmethod()
– yesstr()
– yessum()
– yessuper()
– notuple()
– yestype()
– yesvars()
– nozip()
– no__import__()
– no