From 7a1150e7e6dcc5b569189e237356ebc53345bd3b Mon Sep 17 00:00:00 2001 From: Christopher Doris Date: Wed, 17 Jul 2024 12:40:39 +0100 Subject: [PATCH 1/2] check for null in multimedia show functions --- docs/src/releasenotes.md | 1 + src/Compat/multimedia.jl | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/src/releasenotes.md b/docs/src/releasenotes.md index 8e4de0be..3abe029e 100644 --- a/docs/src/releasenotes.md +++ b/docs/src/releasenotes.md @@ -5,6 +5,7 @@ * `numpy.bool_` can now be converted to `Bool` and other number types. * `datetime.timedelta` can now be converted to `Dates.Nanosecond`, `Microsecond`, `Millisecond` and `Second`. This behaviour was already documented. * In JuliaCall, the Julia runtime is now properly terminated when Python exits. This means all finalizers should always run. +* NULL Python objects (such as from `pynew()`) can be safely displayed in multimedia contexts (VSCode/Pluto/etc.) ## 0.9.20 (2024-05-01) * The IPython extension is now automatically loaded upon import if IPython is detected. diff --git a/src/Compat/multimedia.jl b/src/Compat/multimedia.jl index 3049f359..419472fe 100644 --- a/src/Compat/multimedia.jl +++ b/src/Compat/multimedia.jl @@ -9,16 +9,20 @@ end function pyshow(io::IO, mime::MIME, x) x_ = Py(x) - for rule in PYSHOW_RULES - rule(io, string(mime), x_) && return + if !pyisnull(x_) + for rule in PYSHOW_RULES + rule(io, string(mime), x_) && return + end end throw(MethodError(show, (io, mime, x_))) end function pyshowable(mime::MIME, x) x_ = Py(x) - for rule in PYSHOW_RULES - rule(devnull, string(mime), x_) && return true + if !pyisnull(x_) + for rule in PYSHOW_RULES + rule(devnull, string(mime), x_) && return true + end end return false end From 8749745fb3f73f7fee5353a518531790ca3f5157 Mon Sep 17 00:00:00 2001 From: Christopher Doris Date: Sat, 20 Jul 2024 21:37:28 +0100 Subject: [PATCH 2/2] add tests for show/showable on null --- test/Core.jl | 80 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/test/Core.jl b/test/Core.jl index 822e4afa..03ea6867 100644 --- a/test/Core.jl +++ b/test/Core.jl @@ -130,7 +130,7 @@ @test pylen(x) == 0 end @testset "pydir" begin - x = pytype("Foo", (), ["foo"=>1, "bar"=>2])() + x = pytype("Foo", (), ["foo" => 1, "bar" => 2])() d = pydir(x) @test pycontains(d, "__class__") @test pycontains(d, "foo") @@ -256,7 +256,7 @@ end end @testset "pyinv" begin for n in -2:2 - @test pyeq(Bool, pyinv(pyint(n)), pyint(-n-1)) + @test pyeq(Bool, pyinv(pyint(n)), pyint(-n - 1)) end end @testset "pyindex" begin @@ -267,21 +267,21 @@ end @testset "pyadd" begin for x in -2:2 for y in -2:2 - @test pyeq(Bool, pyadd(pyint(x), pyint(y)), pyint(x+y)) + @test pyeq(Bool, pyadd(pyint(x), pyint(y)), pyint(x + y)) end end end @testset "pysub" begin for x in -2:2 for y in -2:2 - @test pyeq(Bool, pysub(pyint(x), pyint(y)), pyint(x-y)) + @test pyeq(Bool, pysub(pyint(x), pyint(y)), pyint(x - y)) end end end @testset "pymul" begin for x in -2:2 for y in -2:2 - @test pyeq(Bool, pymul(pyint(x), pyint(y)), pyint(x*y)) + @test pyeq(Bool, pymul(pyint(x), pyint(y)), pyint(x * y)) end end end @@ -299,7 +299,7 @@ end if y == 0 @test_throws PyException pytruediv(pyint(x), pyint(y)) else - @test pyeq(Bool, pytruediv(pyint(x), pyint(y)), pyfloat(x/y)) + @test pyeq(Bool, pytruediv(pyint(x), pyint(y)), pyfloat(x / y)) end end end @@ -409,7 +409,7 @@ end @test pyeq(Bool, sys.__name__, "sys") @test pyeq(Bool, os.__name__, "os") sysos = pyimport("sys", "os") - @test sysos isa Tuple{Py, Py} + @test sysos isa Tuple{Py,Py} @test pyis(sysos[1], sys) @test pyis(sysos[2], os) ver = pyimport("sys" => "version") @@ -417,7 +417,7 @@ end path = pyimport("sys" => "path") @test pyis(path, sys.path) verpath = pyimport("sys" => ("version", "path")) - @test verpath isa Tuple{Py, Py} + @test verpath isa Tuple{Py,Py} @test pyis(verpath[1], ver) @test pyis(verpath[2], path) end @@ -438,12 +438,12 @@ end end @testitem "bytes" begin - @test pyisinstance(pybytes(UInt8[1,2,3]), pybuiltins.bytes) - @test pyeq(Bool, pybytes(pylist([1,2,3])), pybytes(UInt8[1,2,3])) + @test pyisinstance(pybytes(UInt8[1, 2, 3]), pybuiltins.bytes) + @test pyeq(Bool, pybytes(pylist([1, 2, 3])), pybytes(UInt8[1, 2, 3])) @test pyeq(Bool, pybytes(b"foo"), pystr("foo").encode("ascii")) @test pyeq(Bool, pybytes(codeunits(SubString("foobarbaz", 4:6))), pystr("bar").encode("ascii")) - @test pybytes(Vector, pylist([1,2,3])) == UInt8[1,2,3] - @test pybytes(Vector{UInt8}, pylist([1,2,3])) == UInt8[1,2,3] + @test pybytes(Vector, pylist([1, 2, 3])) == UInt8[1, 2, 3] + @test pybytes(Vector{UInt8}, pylist([1, 2, 3])) == UInt8[1, 2, 3] @test pybytes(Base.CodeUnits, pystr("foo").encode("ascii")) == b"foo" @test pybytes(Base.CodeUnits{UInt8,String}, pystr("bar").encode("ascii")) == b"bar" end @@ -452,36 +452,36 @@ end z = pytuple() @test pyisinstance(z, pybuiltins.tuple) @test pylen(z) == 0 - x = pytuple((1,2,3)) + x = pytuple((1, 2, 3)) @test pyisinstance(x, pybuiltins.tuple) @test pylen(x) == 3 @test pyeq(Bool, pygetitem(x, 0), 1) @test pyeq(Bool, pygetitem(x, 1), 2) @test pyeq(Bool, pygetitem(x, 2), 3) - @test pyeq(Bool, pytuple([1,2,3]), x) - @test pyeq(Bool, pytuple(i+1 for i in 0:10 if i<3), x) - @test pyeq(Bool, pytuple(pytuple((1,2,3))), x) - @test pyeq(Bool, pytuple(pylist([1,2,3])), x) + @test pyeq(Bool, pytuple([1, 2, 3]), x) + @test pyeq(Bool, pytuple(i + 1 for i in 0:10 if i < 3), x) + @test pyeq(Bool, pytuple(pytuple((1, 2, 3))), x) + @test pyeq(Bool, pytuple(pylist([1, 2, 3])), x) end @testitem "list" begin z = pylist() @test pyisinstance(z, pybuiltins.list) @test pylen(z) == 0 - x = pylist((1,2,3)) + x = pylist((1, 2, 3)) @test pyisinstance(x, pybuiltins.list) @test pylen(x) == 3 @test pyeq(Bool, pygetitem(x, 0), 1) @test pyeq(Bool, pygetitem(x, 1), 2) @test pyeq(Bool, pygetitem(x, 2), 3) - @test pyeq(Bool, pylist([1,2,3]), x) - @test pyeq(Bool, pylist(i+1 for i in 0:10 if i<3), x) - @test pyeq(Bool, pylist(pylist((1,2,3))), x) - @test pyeq(Bool, pylist(pytuple([1,2,3])), x) - @test pyeq(Bool, pycollist([1,2,3]), pylist([1,2,3])) - @test pyeq(Bool, pycollist([1 2; 3 4]), pylist((pylist([1,3]), pylist([2,4])))) - @test pyeq(Bool, pyrowlist([1,2,3]), pylist([1,2,3])) - @test pyeq(Bool, pyrowlist([1 2; 3 4]), pylist((pylist([1,2]), pylist([3,4])))) + @test pyeq(Bool, pylist([1, 2, 3]), x) + @test pyeq(Bool, pylist(i + 1 for i in 0:10 if i < 3), x) + @test pyeq(Bool, pylist(pylist((1, 2, 3))), x) + @test pyeq(Bool, pylist(pytuple([1, 2, 3])), x) + @test pyeq(Bool, pycollist([1, 2, 3]), pylist([1, 2, 3])) + @test pyeq(Bool, pycollist([1 2; 3 4]), pylist((pylist([1, 3]), pylist([2, 4])))) + @test pyeq(Bool, pyrowlist([1, 2, 3]), pylist([1, 2, 3])) + @test pyeq(Bool, pyrowlist([1 2; 3 4]), pylist((pylist([1, 2]), pylist([3, 4])))) end @testitem "dict" begin @@ -493,9 +493,9 @@ end @test pylen(x) == 2 @test pyeq(Bool, pygetitem(x, "foo"), 1) @test pyeq(Bool, pygetitem(x, "bar"), 2) - @test pyeq(Bool, pydict(["foo"=>1, "bar"=>2]), x) - @test pyeq(Bool, pydict([("foo"=>1), ("bar"=>2)]), x) - @test pyeq(Bool, pydict(Dict("foo"=>1, "bar"=>2)), x) + @test pyeq(Bool, pydict(["foo" => 1, "bar" => 2]), x) + @test pyeq(Bool, pydict([("foo" => 1), ("bar" => 2)]), x) + @test pyeq(Bool, pydict(Dict("foo" => 1, "bar" => 2)), x) @test pyeq(Bool, pydict((foo=1, bar=2)), x) @test pyeq(Bool, pydict(x), x) end @@ -508,7 +508,7 @@ end @test pyis(pybool(-1.2), pybuiltins.True) @test pyis(pybool(pybuiltins.None), pybuiltins.False) @test pyis(pybool(pylist()), pybuiltins.False) - @test pyis(pybool(pylist([1,2,3])), pybuiltins.True) + @test pyis(pybool(pylist([1, 2, 3])), pybuiltins.True) end @testitem "int" begin @@ -546,7 +546,7 @@ end y = pyfloat(x) @test pyisinstance(y, pybuiltins.float) @test pyeq(Bool, y, pytruediv(1, 4)) - x = 1//4 + x = 1 // 4 y = pyfloat(x) @test pyisinstance(y, pybuiltins.float) @test pyeq(Bool, y, pyfloat(float(x))) @@ -584,7 +584,7 @@ end @test pyisinstance(yf, pybuiltins.frozenset) @test pylen(yf) == 0 @test pyeq(Bool, y, yf) - x = [1,2,3,2,1] + x = [1, 2, 3, 2, 1] y = pyset(x) yf = pyfrozenset(x) @test pyisinstance(y, pybuiltins.set) @@ -649,7 +649,7 @@ end x = pytype(pybuiltins.type) @test pyisinstance(x, pybuiltins.type) @test pyis(x, pybuiltins.type) - x = pytype("Foo", (), ["foo"=>1, "bar"=>2]) + x = pytype("Foo", (), ["foo" => 1, "bar" => 2]) @test pyisinstance(x, pybuiltins.type) @test pyeq(Bool, x.__name__, "Foo") @test pyeq(Bool, x.foo, 1) @@ -782,6 +782,20 @@ end # but now tries to do `1 + [1, 2]` which properly fails @test_throws PyException [1 2; 3 4] .+ pylist([1, 2]) end + @testset "showable" begin + @test showable(MIME("text/plain"), Py(nothing)) + @test showable(MIME("text/plain"), Py(12)) + # https://github.com/JuliaPy/PythonCall.jl/issues/522 + @test showable(MIME("text/plain"), PythonCall.pynew()) + @test !showable(MIME("text/html"), PythonCall.pynew()) + end + @testset "show" begin + @test sprint(show, MIME("text/plain"), Py(nothing)) == "Python: None" + @test sprint(show, MIME("text/plain"), Py(12)) == "Python: 12" + # https://github.com/JuliaPy/PythonCall.jl/issues/522 + @test sprint(show, MIME("text/plain"), PythonCall.pynew()) == "Python: NULL" + @test_throws MethodError sprint(show, MIME("text/html"), PythonCall.pynew()) + end end @testitem "pywith" begin