Skip to content

refactor #599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6f769f4
move src -> src-old
Jan 20, 2025
2095996
move file back
Jan 20, 2025
5cd82ce
strip back
Jan 20, 2025
6228555
move src-old/C -> src/C
Jan 20, 2025
4748773
re-enable the internal C module
Jan 20, 2025
7f6580a
explicitly specify public API
Jan 20, 2025
4cbe955
move GIL.jl
Jan 20, 2025
5175f6c
improve imports
Jan 20, 2025
39234ff
add GIL to API
Jan 20, 2025
6508e21
wire up GIL.jl
Jan 20, 2025
7717ae8
move public GIL module
Jan 20, 2025
1741965
move GC.jl
Jan 20, 2025
07e862e
add GC to API
Jan 20, 2025
e195300
introduce place for public types to be defined
Jan 20, 2025
4947c26
move Core back
Jan 20, 2025
c2818b2
move Py typedef
Jan 20, 2025
ac08cf5
move PyException typedef
Jan 20, 2025
4cece64
make CONFIG public
Jan 20, 2025
276781b
change internal module names
Jan 20, 2025
c45e86e
move Utils back
Jan 20, 2025
ea28692
add public bits of API back from comments
Jan 20, 2025
86158c6
more convenient import
Jan 20, 2025
d5e03a7
mroe convenient import
Jan 20, 2025
bfe282a
make API clearer
Jan 20, 2025
7d9664c
get Core working
Jan 20, 2025
5b93f31
Merge remote-tracking branch 'origin/main' into refactor
Jan 22, 2025
9155a88
move source files back from src-old to src
Mar 29, 2025
291d1f4
fix up Convert
Mar 29, 2025
4dec082
patch up remaining refactored modules
Mar 29, 2025
c80766e
fix tests
Mar 29, 2025
69f9c93
fix python tests
Mar 29, 2025
77341af
Merge remote-tracking branch 'origin/main' into refactor
Mar 29, 2025
e7e0eed
switch to julia-actions/cache
Mar 29, 2025
f40e78d
@kwdef not exported in 1.6
Apr 7, 2025
e67e124
split out Utils
Apr 7, 2025
eb0f519
rename some Utils functions
Apr 7, 2025
28b440c
bump minimum julia to 1.8
Apr 7, 2025
194f171
enqueue_all was called wrong
Apr 7, 2025
ae96cb2
split Compat into independent modules
Apr 7, 2025
a086fdf
fix references to moved functions
Apr 7, 2025
55e11de
move wrapper types into separate modules
Apr 8, 2025
b8f6642
simplify imports in Compat
Apr 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,15 @@ jobs:
matrix:
arch: [x64] # x86 unsupported by MicroMamba
os: [ubuntu-latest, windows-latest, macos-latest]
jlversion: ['1','1.6']
jlversion: ['1','1.8']
steps:
- uses: actions/checkout@v2
- name: Set up Julia ${{ matrix.jlversion }}
uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.jlversion }}
arch: ${{ matrix.arch }}
- uses: actions/cache@v1
env:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-test-${{ env.cache-name }}-
${{ runner.os }}-test-
${{ runner.os }}-
- uses: julia-actions/cache@v2
- name: Build package
uses: julia-actions/julia-buildpkg@v1
- name: Run tests
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Tables = "1"
Test = "1"
TestItemRunner = "0 - 999"
UnsafePointers = "1"
julia = "1.6.1"
julia = "1.8"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Bringing [**Python®**](https://www.python.org/) and [**Julia**](https://juliala
- Fast non-copying conversion of numeric arrays in either direction: modify Python arrays (e.g. `bytes`, `array.array`, `numpy.ndarray`) from Julia or Julia arrays from Python.
- Helpful wrappers: interpret Python sequences, dictionaries, arrays, dataframes and IO streams as their Julia counterparts, and vice versa.
- Beautiful stack-traces.
- Supports modern systems: tested on Windows, MacOS and Linux, 64-bit, Julia 1.6.1 upwards and Python 3.8 upwards.
- Supports modern systems: tested on Windows, MacOS and Linux, 64-bit, Julia 1.8 upwards and Python 3.8 upwards.

⭐ If you like this, a GitHub star would be lovely thank you. ⭐

Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ Bringing [**Python®**](https://www.python.org/) and [**Julia**](https://juliala
- Fast non-copying conversion of numeric arrays in either direction: modify Python arrays (e.g. `bytes`, `array.array`, `numpy.ndarray`) from Julia or Julia arrays from Python.
- Helpful wrappers: interpret Python sequences, dictionaries, arrays, dataframes and IO streams as their Julia counterparts, and vice versa.
- Beautiful stack-traces.
- Works anywhere: tested on Windows, MacOS and Linux, 32- and 64-bit, Julia Julia 1.6.1 upwards and Python 3.8 upwards.
- Works anywhere: tested on Windows, MacOS and Linux, 32- and 64-bit, Julia Julia 1.8 upwards and Python 3.8 upwards.
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,14 @@ classifiers = [
requires-python = ">=3.8"
dependencies = ["juliapkg ~=0.1.8"]

[tool.uv]
dev-dependencies = [
"flake8",
"pytest",
"pytest-cov",
"nbval",
"numpy",
]

[tool.hatch.build.targets.wheel]
packages = ["pysrc/juliacall"]
159 changes: 96 additions & 63 deletions pysrc/juliacall/__init__.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,67 @@
# This module gets modified by PythonCall when it is loaded, e.g. to include Core, Base
# and Main modules.

__version__ = '0.9.24'
__version__ = "0.9.24"

_newmodule = None


def newmodule(name):
"A new module with the given name."
global _newmodule
if _newmodule is None:
_newmodule = Main.seval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)")
_newmodule = Main.seval(
"name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)"
)
return _newmodule(name)


_convert = None


def convert(T, x):
"Convert x to a Julia T."
global _convert
if _convert is None:
_convert = PythonCall.JlWrap.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))")
_convert = PythonCall.Internals.JlWrap.seval(
"pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))"
)
return _convert(T, x)


def interactive(enable=True):
"Allow the Julia event loop to run in the background of the Python REPL."
if enable:
PythonCall.Compat._set_python_input_hook()
PythonCall.Internals.Compat.GUI._set_python_input_hook()
else:
PythonCall.Compat._unset_python_input_hook()
PythonCall.Internals.Compat.GUI._unset_python_input_hook()


class JuliaError(Exception):
"An error arising in Julia code."

def __init__(self, exception, backtrace=None):
super().__init__(exception, backtrace)

def __str__(self):
e = self.exception
b = self.backtrace
if b is None:
return Base.sprint(Base.showerror, e)
else:
return Base.sprint(Base.showerror, e, b)

@property
def exception(self):
return self.args[0]

@property
def backtrace(self):
return self.args[1]

CONFIG = {'inited': False}

CONFIG = {"inited": False}


def init():
import atexit
Expand All @@ -70,30 +85,29 @@ def option(name, default=None, xkey=None, envkey=None):
Options can be set as command line arguments '-X juliacall-{name}={value}' or as
environment variables 'PYTHON_JULIACALL_{NAME}={value}'.
"""
k = xkey or 'juliacall-'+name.lower().replace('_', '-')
k = xkey or "juliacall-" + name.lower().replace("_", "-")
v = sys._xoptions.get(k)
if v is not None:
return v, f'-X{k}={v}'
k = envkey or 'PYTHON_JULIACALL_'+name.upper()
return v, f"-X{k}={v}"
k = envkey or "PYTHON_JULIACALL_" + name.upper()
v = os.getenv(k)
if v is not None:
return v, f'{k}={v}'
return default, f'<default>={default}'
return v, f"{k}={v}"
return default, f"<default>={default}"

def choice(name, choices, default=None, **kw):
v, s = option(name, **kw)
if v is None:
return default, s
if v in choices:
return v, s
raise ValueError(
f'{s}: expecting one of {", ".join(choices)}')
raise ValueError(f"{s}: expecting one of {', '.join(choices)}")

def path_option(name, default=None, check_exists=False, **kw):
path, s = option(name, **kw)
if path is not None:
if check_exists and not os.path.exists(path):
raise ValueError(f'{s}: path does not exist')
raise ValueError(f"{s}: path does not exist")
return os.path.abspath(path), s
return default, s

Expand All @@ -107,18 +121,20 @@ def int_option(name, *, accept_auto=False, **kw):
int(val)
return val, s
except ValueError:
raise ValueError(f'{s}: expecting an int'+(' or auto' if accept_auto else ""))
raise ValueError(
f"{s}: expecting an int" + (" or auto" if accept_auto else "")
)

def args_from_config(config):
argv = [config['exepath']]
argv = [config["exepath"]]
for opt, val in config.items():
if opt.startswith('opt_'):
if opt.startswith("opt_"):
if val is None:
if opt == 'opt_handle_signals':
val = 'no'
if opt == "opt_handle_signals":
val = "no"
else:
continue
argv.append('--' + opt[4:].replace('_', '-') + '=' + val)
argv.append("--" + opt[4:].replace("_", "-") + "=" + val)
argv = [s.encode("utf-8") for s in argv]

argc = len(argv)
Expand All @@ -127,59 +143,70 @@ def args_from_config(config):
return argc, argv

# Determine if we should skip initialising.
CONFIG['init'] = choice('init', ['yes', 'no'], default='yes')[0] == 'yes'
if not CONFIG['init']:
CONFIG["init"] = choice("init", ["yes", "no"], default="yes")[0] == "yes"
if not CONFIG["init"]:
return

# Parse some more options
CONFIG['opt_home'] = bindir = path_option('home', check_exists=True, envkey='PYTHON_JULIACALL_BINDIR')[0]
CONFIG['opt_check_bounds'] = choice('check_bounds', ['yes', 'no', 'auto'])[0]
CONFIG['opt_compile'] = choice('compile', ['yes', 'no', 'all', 'min'])[0]
CONFIG['opt_compiled_modules'] = choice('compiled_modules', ['yes', 'no'])[0]
CONFIG['opt_depwarn'] = choice('depwarn', ['yes', 'no', 'error'])[0]
CONFIG['opt_inline'] = choice('inline', ['yes', 'no'])[0]
CONFIG['opt_min_optlevel'] = choice('min_optlevel', ['0', '1', '2', '3'])[0]
CONFIG['opt_optimize'] = choice('optimize', ['0', '1', '2', '3'])[0]
CONFIG['opt_procs'] = int_option('procs', accept_auto=True)[0]
CONFIG['opt_sysimage'] = sysimg = path_option('sysimage', check_exists=True)[0]
CONFIG['opt_threads'] = int_option('threads', accept_auto=True)[0]
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no'])[0]
CONFIG['opt_handle_signals'] = choice('handle_signals', ['yes', 'no'])[0]
CONFIG['opt_startup_file'] = choice('startup_file', ['yes', 'no'])[0]
CONFIG['opt_heap_size_hint'] = option('heap_size_hint')[0]
CONFIG["opt_home"] = bindir = path_option(
"home", check_exists=True, envkey="PYTHON_JULIACALL_BINDIR"
)[0]
CONFIG["opt_check_bounds"] = choice("check_bounds", ["yes", "no", "auto"])[0]
CONFIG["opt_compile"] = choice("compile", ["yes", "no", "all", "min"])[0]
CONFIG["opt_compiled_modules"] = choice("compiled_modules", ["yes", "no"])[0]
CONFIG["opt_depwarn"] = choice("depwarn", ["yes", "no", "error"])[0]
CONFIG["opt_inline"] = choice("inline", ["yes", "no"])[0]
CONFIG["opt_min_optlevel"] = choice("min_optlevel", ["0", "1", "2", "3"])[0]
CONFIG["opt_optimize"] = choice("optimize", ["0", "1", "2", "3"])[0]
CONFIG["opt_procs"] = int_option("procs", accept_auto=True)[0]
CONFIG["opt_sysimage"] = sysimg = path_option("sysimage", check_exists=True)[0]
CONFIG["opt_threads"] = int_option("threads", accept_auto=True)[0]
CONFIG["opt_warn_overwrite"] = choice("warn_overwrite", ["yes", "no"])[0]
CONFIG["opt_handle_signals"] = choice("handle_signals", ["yes", "no"])[0]
CONFIG["opt_startup_file"] = choice("startup_file", ["yes", "no"])[0]
CONFIG["opt_heap_size_hint"] = option("heap_size_hint")[0]

# Stop if we already initialised
if CONFIG['inited']:
if CONFIG["inited"]:
return

# we don't import this at the top level because it is not required when juliacall is
# loaded by PythonCall and won't be available
import juliapkg

# Find the Julia executable and project
CONFIG['exepath'] = exepath = juliapkg.executable()
CONFIG['project'] = project = juliapkg.project()
CONFIG["exepath"] = exepath = juliapkg.executable()
CONFIG["project"] = project = juliapkg.project()

# Find the Julia library
cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min',
'-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)']
libpath, default_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0')
cmd = [
exepath,
"--project=" + project,
"--startup-file=no",
"-O0",
"--compile=min",
"-e",
'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)',
]
libpath, default_bindir = subprocess.run(
cmd, check=True, capture_output=True, encoding="utf8"
).stdout.split("\0")
assert os.path.exists(libpath)
assert os.path.exists(default_bindir)
CONFIG['libpath'] = libpath
CONFIG["libpath"] = libpath

# Add the Julia library directory to the PATH on Windows so Julia's system libraries can
# be found. They are normally found because they are in the same directory as julia.exe,
# but python.exe is somewhere else!
if os.name == 'nt':
if os.name == "nt":
libdir = os.path.dirname(libpath)
if 'PATH' in os.environ:
os.environ['PATH'] = libdir + ';' + os.environ['PATH']
if "PATH" in os.environ:
os.environ["PATH"] = libdir + ";" + os.environ["PATH"]
else:
os.environ['PATH'] = libdir
os.environ["PATH"] = libdir

# Open the library
CONFIG['lib'] = lib = c.PyDLL(libpath, mode=c.RTLD_GLOBAL)
CONFIG["lib"] = lib = c.PyDLL(libpath, mode=c.RTLD_GLOBAL)

# parse options
argc, argv = args_from_config(CONFIG)
Expand All @@ -197,8 +224,8 @@ def args_from_config(config):
jl_init.argtypes = [c.c_char_p, c.c_char_p]
jl_init.restype = None
jl_init(
(default_bindir if bindir is None else bindir).encode('utf8'),
None if sysimg is None else sysimg.encode('utf8'),
(default_bindir if bindir is None else bindir).encode("utf8"),
None if sysimg is None else sysimg.encode("utf8"),
)

# call jl_atexit_hook() when python exits to gracefully stop the julia runtime,
Expand All @@ -214,9 +241,11 @@ def at_jl_exit():
jl_eval = lib.jl_eval_string
jl_eval.argtypes = [c.c_char_p]
jl_eval.restype = c.c_void_p

def jlstr(x):
return 'raw"' + x + '"'
script = '''

script = """
try
Base.require(Main, :CompilerSupportLibraries_jll)
import Pkg
Expand All @@ -230,18 +259,18 @@ def jlstr(x):
flush(stderr)
rethrow()
end
'''.format(
""".format(
hex(c.pythonapi._handle),
jlstr(sys.executable or ''),
jlstr(sys.executable or ""),
jlstr(project),
)
res = jl_eval(script.encode('utf8'))
res = jl_eval(script.encode("utf8"))
if res is None:
raise Exception('PythonCall.jl did not start properly')
raise Exception("PythonCall.jl did not start properly")

CONFIG['inited'] = True
CONFIG["inited"] = True

if CONFIG['opt_handle_signals'] is None:
if CONFIG["opt_handle_signals"] is None:
if Base.Threads.nthreads() > 1:
# a warning to help multithreaded users
# TODO: should we set PYTHON_JULIACALL_HANDLE_SIGNALS=yes whenever PYTHON_JULIACALL_THREADS != 1?
Expand All @@ -260,20 +289,22 @@ def jlstr(x):
)

# Next, automatically load the juliacall extension if we are in IPython or Jupyter
CONFIG['autoload_ipython_extension'] = choice('autoload_ipython_extension', ['yes', 'no'])[0]
if CONFIG['autoload_ipython_extension'] in {'yes', None}:
CONFIG["autoload_ipython_extension"] = choice(
"autoload_ipython_extension", ["yes", "no"]
)[0]
if CONFIG["autoload_ipython_extension"] in {"yes", None}:
try:
get_ipython = sys.modules['IPython'].get_ipython
get_ipython = sys.modules["IPython"].get_ipython

if CONFIG['autoload_ipython_extension'] is None:
if CONFIG["autoload_ipython_extension"] is None:
# Only let the user know if it was not explicitly set
print(
"Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython"
)

load_ipython_extension(get_ipython())
except Exception as e:
if CONFIG['autoload_ipython_extension'] == 'yes':
if CONFIG["autoload_ipython_extension"] == "yes":
# Only warn if the user explicitly requested the extension to be loaded
warnings.warn(
"Could not load juliacall extension in Jupyter notebook: " + str(e)
Expand All @@ -283,6 +314,8 @@ def jlstr(x):

def load_ipython_extension(ip):
import juliacall.ipython

juliacall.ipython.load_ipython_extension(ip)


init()
Loading
Loading