Skip to content

Commit 69f9c93

Browse files
author
Christopher Doris
committed
fix python tests
1 parent c80766e commit 69f9c93

File tree

5 files changed

+1933
-73
lines changed

5 files changed

+1933
-73
lines changed

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,14 @@ classifiers = [
1515
requires-python = ">=3.8"
1616
dependencies = ["juliapkg ~=0.1.8"]
1717

18+
[tool.uv]
19+
dev-dependencies = [
20+
"flake8",
21+
"pytest",
22+
"pytest-cov",
23+
"nbval",
24+
"numpy",
25+
]
26+
1827
[tool.hatch.build.targets.wheel]
1928
packages = ["pysrc/juliacall"]

pysrc/juliacall/__init__.py

Lines changed: 95 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,67 @@
11
# This module gets modified by PythonCall when it is loaded, e.g. to include Core, Base
22
# and Main modules.
33

4-
__version__ = '0.9.24'
4+
__version__ = "0.9.24"
55

66
_newmodule = None
77

8+
89
def newmodule(name):
910
"A new module with the given name."
1011
global _newmodule
1112
if _newmodule is None:
12-
_newmodule = Main.seval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)")
13+
_newmodule = Main.seval(
14+
"name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)"
15+
)
1316
return _newmodule(name)
1417

18+
1519
_convert = None
1620

21+
1722
def convert(T, x):
1823
"Convert x to a Julia T."
1924
global _convert
2025
if _convert is None:
21-
_convert = PythonCall.JlWrap.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))")
26+
_convert = PythonCall.Internals.JlWrap.seval(
27+
"pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))"
28+
)
2229
return _convert(T, x)
2330

31+
2432
def interactive(enable=True):
2533
"Allow the Julia event loop to run in the background of the Python REPL."
2634
if enable:
27-
PythonCall.Compat._set_python_input_hook()
35+
PythonCall.Internals.Compat._set_python_input_hook()
2836
else:
29-
PythonCall.Compat._unset_python_input_hook()
37+
PythonCall.Internals.Compat._unset_python_input_hook()
38+
3039

3140
class JuliaError(Exception):
3241
"An error arising in Julia code."
42+
3343
def __init__(self, exception, backtrace=None):
3444
super().__init__(exception, backtrace)
45+
3546
def __str__(self):
3647
e = self.exception
3748
b = self.backtrace
3849
if b is None:
3950
return Base.sprint(Base.showerror, e)
4051
else:
4152
return Base.sprint(Base.showerror, e, b)
53+
4254
@property
4355
def exception(self):
4456
return self.args[0]
57+
4558
@property
4659
def backtrace(self):
4760
return self.args[1]
4861

49-
CONFIG = {'inited': False}
62+
63+
CONFIG = {"inited": False}
64+
5065

5166
def init():
5267
import atexit
@@ -70,30 +85,29 @@ def option(name, default=None, xkey=None, envkey=None):
7085
Options can be set as command line arguments '-X juliacall-{name}={value}' or as
7186
environment variables 'PYTHON_JULIACALL_{NAME}={value}'.
7287
"""
73-
k = xkey or 'juliacall-'+name.lower().replace('_', '-')
88+
k = xkey or "juliacall-" + name.lower().replace("_", "-")
7489
v = sys._xoptions.get(k)
7590
if v is not None:
76-
return v, f'-X{k}={v}'
77-
k = envkey or 'PYTHON_JULIACALL_'+name.upper()
91+
return v, f"-X{k}={v}"
92+
k = envkey or "PYTHON_JULIACALL_" + name.upper()
7893
v = os.getenv(k)
7994
if v is not None:
80-
return v, f'{k}={v}'
81-
return default, f'<default>={default}'
95+
return v, f"{k}={v}"
96+
return default, f"<default>={default}"
8297

8398
def choice(name, choices, default=None, **kw):
8499
v, s = option(name, **kw)
85100
if v is None:
86101
return default, s
87102
if v in choices:
88103
return v, s
89-
raise ValueError(
90-
f'{s}: expecting one of {", ".join(choices)}')
104+
raise ValueError(f"{s}: expecting one of {', '.join(choices)}")
91105

92106
def path_option(name, default=None, check_exists=False, **kw):
93107
path, s = option(name, **kw)
94108
if path is not None:
95109
if check_exists and not os.path.exists(path):
96-
raise ValueError(f'{s}: path does not exist')
110+
raise ValueError(f"{s}: path does not exist")
97111
return os.path.abspath(path), s
98112
return default, s
99113

@@ -107,18 +121,20 @@ def int_option(name, *, accept_auto=False, **kw):
107121
int(val)
108122
return val, s
109123
except ValueError:
110-
raise ValueError(f'{s}: expecting an int'+(' or auto' if accept_auto else ""))
124+
raise ValueError(
125+
f"{s}: expecting an int" + (" or auto" if accept_auto else "")
126+
)
111127

112128
def args_from_config():
113-
argv = [CONFIG['exepath']]
129+
argv = [CONFIG["exepath"]]
114130
for opt, val in CONFIG.items():
115-
if opt.startswith('opt_'):
131+
if opt.startswith("opt_"):
116132
if val is None:
117-
if opt == 'opt_handle_signals':
118-
val = 'no'
133+
if opt == "opt_handle_signals":
134+
val = "no"
119135
else:
120136
continue
121-
argv.append('--' + opt[4:].replace('_', '-') + '=' + val)
137+
argv.append("--" + opt[4:].replace("_", "-") + "=" + val)
122138
argv = [s.encode("utf-8") for s in argv]
123139

124140
argc = len(argv)
@@ -127,58 +143,69 @@ def args_from_config():
127143
return argc, argv
128144

129145
# Determine if we should skip initialising.
130-
CONFIG['init'] = choice('init', ['yes', 'no'], default='yes')[0] == 'yes'
131-
if not CONFIG['init']:
146+
CONFIG["init"] = choice("init", ["yes", "no"], default="yes")[0] == "yes"
147+
if not CONFIG["init"]:
132148
return
133149

134150
# Parse some more options
135-
CONFIG['opt_home'] = bindir = path_option('home', check_exists=True, envkey='PYTHON_JULIACALL_BINDIR')[0]
136-
CONFIG['opt_check_bounds'] = choice('check_bounds', ['yes', 'no', 'auto'])[0]
137-
CONFIG['opt_compile'] = choice('compile', ['yes', 'no', 'all', 'min'])[0]
138-
CONFIG['opt_compiled_modules'] = choice('compiled_modules', ['yes', 'no'])[0]
139-
CONFIG['opt_depwarn'] = choice('depwarn', ['yes', 'no', 'error'])[0]
140-
CONFIG['opt_inline'] = choice('inline', ['yes', 'no'])[0]
141-
CONFIG['opt_min_optlevel'] = choice('min_optlevel', ['0', '1', '2', '3'])[0]
142-
CONFIG['opt_optimize'] = choice('optimize', ['0', '1', '2', '3'])[0]
143-
CONFIG['opt_procs'] = int_option('procs', accept_auto=True)[0]
144-
CONFIG['opt_sysimage'] = sysimg = path_option('sysimage', check_exists=True)[0]
145-
CONFIG['opt_threads'] = int_option('threads', accept_auto=True)[0]
146-
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no'])[0]
147-
CONFIG['opt_handle_signals'] = choice('handle_signals', ['yes', 'no'])[0]
148-
CONFIG['opt_startup_file'] = choice('startup_file', ['yes', 'no'])[0]
151+
CONFIG["opt_home"] = bindir = path_option(
152+
"home", check_exists=True, envkey="PYTHON_JULIACALL_BINDIR"
153+
)[0]
154+
CONFIG["opt_check_bounds"] = choice("check_bounds", ["yes", "no", "auto"])[0]
155+
CONFIG["opt_compile"] = choice("compile", ["yes", "no", "all", "min"])[0]
156+
CONFIG["opt_compiled_modules"] = choice("compiled_modules", ["yes", "no"])[0]
157+
CONFIG["opt_depwarn"] = choice("depwarn", ["yes", "no", "error"])[0]
158+
CONFIG["opt_inline"] = choice("inline", ["yes", "no"])[0]
159+
CONFIG["opt_min_optlevel"] = choice("min_optlevel", ["0", "1", "2", "3"])[0]
160+
CONFIG["opt_optimize"] = choice("optimize", ["0", "1", "2", "3"])[0]
161+
CONFIG["opt_procs"] = int_option("procs", accept_auto=True)[0]
162+
CONFIG["opt_sysimage"] = sysimg = path_option("sysimage", check_exists=True)[0]
163+
CONFIG["opt_threads"] = int_option("threads", accept_auto=True)[0]
164+
CONFIG["opt_warn_overwrite"] = choice("warn_overwrite", ["yes", "no"])[0]
165+
CONFIG["opt_handle_signals"] = choice("handle_signals", ["yes", "no"])[0]
166+
CONFIG["opt_startup_file"] = choice("startup_file", ["yes", "no"])[0]
149167

150168
# Stop if we already initialised
151-
if CONFIG['inited']:
169+
if CONFIG["inited"]:
152170
return
153171

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

158176
# Find the Julia executable and project
159-
CONFIG['exepath'] = exepath = juliapkg.executable()
160-
CONFIG['project'] = project = juliapkg.project()
177+
CONFIG["exepath"] = exepath = juliapkg.executable()
178+
CONFIG["project"] = project = juliapkg.project()
161179

162180
# Find the Julia library
163-
cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min',
164-
'-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)']
165-
libpath, default_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0')
181+
cmd = [
182+
exepath,
183+
"--project=" + project,
184+
"--startup-file=no",
185+
"-O0",
186+
"--compile=min",
187+
"-e",
188+
'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)',
189+
]
190+
libpath, default_bindir = subprocess.run(
191+
cmd, check=True, capture_output=True, encoding="utf8"
192+
).stdout.split("\0")
166193
assert os.path.exists(libpath)
167194
assert os.path.exists(default_bindir)
168-
CONFIG['libpath'] = libpath
195+
CONFIG["libpath"] = libpath
169196

170197
# Add the Julia library directory to the PATH on Windows so Julia's system libraries can
171198
# be found. They are normally found because they are in the same directory as julia.exe,
172199
# but python.exe is somewhere else!
173-
if os.name == 'nt':
200+
if os.name == "nt":
174201
libdir = os.path.dirname(libpath)
175-
if 'PATH' in os.environ:
176-
os.environ['PATH'] = libdir + ';' + os.environ['PATH']
202+
if "PATH" in os.environ:
203+
os.environ["PATH"] = libdir + ";" + os.environ["PATH"]
177204
else:
178-
os.environ['PATH'] = libdir
205+
os.environ["PATH"] = libdir
179206

180207
# Open the library
181-
CONFIG['lib'] = lib = c.PyDLL(libpath, mode=c.RTLD_GLOBAL)
208+
CONFIG["lib"] = lib = c.PyDLL(libpath, mode=c.RTLD_GLOBAL)
182209

183210
# parse options
184211
argc, argv = args_from_config()
@@ -196,8 +223,8 @@ def args_from_config():
196223
jl_init.argtypes = [c.c_char_p, c.c_char_p]
197224
jl_init.restype = None
198225
jl_init(
199-
(default_bindir if bindir is None else bindir).encode('utf8'),
200-
None if sysimg is None else sysimg.encode('utf8'),
226+
(default_bindir if bindir is None else bindir).encode("utf8"),
227+
None if sysimg is None else sysimg.encode("utf8"),
201228
)
202229

203230
# call jl_atexit_hook() when python exits to gracefully stop the julia runtime,
@@ -213,9 +240,11 @@ def at_jl_exit():
213240
jl_eval = lib.jl_eval_string
214241
jl_eval.argtypes = [c.c_char_p]
215242
jl_eval.restype = c.c_void_p
243+
216244
def jlstr(x):
217245
return 'raw"' + x + '"'
218-
script = '''
246+
247+
script = """
219248
try
220249
Base.require(Main, :CompilerSupportLibraries_jll)
221250
import Pkg
@@ -229,18 +258,18 @@ def jlstr(x):
229258
flush(stderr)
230259
rethrow()
231260
end
232-
'''.format(
261+
""".format(
233262
hex(c.pythonapi._handle),
234-
jlstr(sys.executable or ''),
263+
jlstr(sys.executable or ""),
235264
jlstr(project),
236265
)
237-
res = jl_eval(script.encode('utf8'))
266+
res = jl_eval(script.encode("utf8"))
238267
if res is None:
239-
raise Exception('PythonCall.jl did not start properly')
268+
raise Exception("PythonCall.jl did not start properly")
240269

241-
CONFIG['inited'] = True
270+
CONFIG["inited"] = True
242271

243-
if CONFIG['opt_handle_signals'] is None:
272+
if CONFIG["opt_handle_signals"] is None:
244273
if Base.Threads.nthreads() > 1:
245274
# a warning to help multithreaded users
246275
# TODO: should we set PYTHON_JULIACALL_HANDLE_SIGNALS=yes whenever PYTHON_JULIACALL_THREADS != 1?
@@ -259,20 +288,22 @@ def jlstr(x):
259288
)
260289

261290
# Next, automatically load the juliacall extension if we are in IPython or Jupyter
262-
CONFIG['autoload_ipython_extension'] = choice('autoload_ipython_extension', ['yes', 'no'])[0]
263-
if CONFIG['autoload_ipython_extension'] in {'yes', None}:
291+
CONFIG["autoload_ipython_extension"] = choice(
292+
"autoload_ipython_extension", ["yes", "no"]
293+
)[0]
294+
if CONFIG["autoload_ipython_extension"] in {"yes", None}:
264295
try:
265-
get_ipython = sys.modules['IPython'].get_ipython
296+
get_ipython = sys.modules["IPython"].get_ipython
266297

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

273304
load_ipython_extension(get_ipython())
274305
except Exception as e:
275-
if CONFIG['autoload_ipython_extension'] == 'yes':
306+
if CONFIG["autoload_ipython_extension"] == "yes":
276307
# Only warn if the user explicitly requested the extension to be loaded
277308
warnings.warn(
278309
"Could not load juliacall extension in Jupyter notebook: " + str(e)
@@ -282,6 +313,8 @@ def jlstr(x):
282313

283314
def load_ipython_extension(ip):
284315
import juliacall.ipython
316+
285317
juliacall.ipython.load_ipython_extension(ip)
286318

319+
287320
init()

0 commit comments

Comments
 (0)