From 2b5e35c0fc3af9039f8062ce919d40cbe6113f51 Mon Sep 17 00:00:00 2001 From: Phillip Alday Date: Thu, 28 Jul 2022 18:18:14 +0000 Subject: [PATCH] convert package to PythonCall instead of PyCall (#13) * convert package to PythonCall instead of PyCall * cruft + ci * YASG enforcer * CI concurrency * Win32 * Update README.md Co-authored-by: Alex Arslan --- .github/workflows/YASG.yml | 37 +++++++ .github/workflows/ci.yml | 5 +- .gitignore | 2 + CondaPkg.toml | 11 +++ Project.toml | 16 +-- README.md | 182 ++++++---------------------------- deps/build.jl | 12 --- format/Manifest.toml | 197 +++++++++++++++++++++++++++++++++++++ format/Project.toml | 5 + format/run.jl | 20 ++++ src/PyMNE.jl | 41 +------- src/wrappers.jl | 21 ---- test/runtests.jl | 37 +++---- 13 files changed, 340 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/YASG.yml create mode 100644 CondaPkg.toml delete mode 100644 deps/build.jl create mode 100644 format/Manifest.toml create mode 100644 format/Project.toml create mode 100644 format/run.jl delete mode 100644 src/wrappers.jl diff --git a/.github/workflows/YASG.yml b/.github/workflows/YASG.yml new file mode 100644 index 0000000..5eda974 --- /dev/null +++ b/.github/workflows/YASG.yml @@ -0,0 +1,37 @@ +name: YASG-enforcer +on: + push: + branches: + - 'main' + tags: '*' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # note: keep in sync with `format/run.jl` + paths: + - 'src/**' + - 'test/**' + - '.github/workflows/YASG.yml' + - 'format/**' +jobs: + format-check: + name: YASG Enforcement (Julia ${{ matrix.julia-version }} - ${{ github.event_name }}) + # Run on push's or non-draft PRs + if: (github.event_name == 'push') || (github.event.pull_request.draft == false) + runs-on: ubuntu-latest + strategy: + matrix: + julia-version: [1.7] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + - uses: actions/checkout@v1 + - name: Instantiate `format` environment and format + run: | + julia --project=format -e 'using Pkg; Pkg.instantiate()' + julia --project=format 'format/run.jl' + - uses: reviewdog/action-suggester@v1 + if: github.event_name == 'pull_request' + with: + tool_name: JuliaFormatter + fail_on_error: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89b2394..7a0981d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,7 @@ name: CI +concurrency: + group: ${{ github.head_ref }}.ci + cancel-in-progress: true on: push: paths-ignore: @@ -18,7 +21,7 @@ jobs: fail-fast: false matrix: version: - - '1.0' + - '1.6' - '1' - 'nightly' os: diff --git a/.gitignore b/.gitignore index 29126e4..a0b2345 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ docs/site/ # committed for packages, but should be committed for applications that require a static # environment. Manifest.toml + +.CondaPkg/ diff --git a/CondaPkg.toml b/CondaPkg.toml new file mode 100644 index 0000000..0c079a4 --- /dev/null +++ b/CondaPkg.toml @@ -0,0 +1,11 @@ +channels = ["anaconda", "conda-forge"] + +[deps] +# Conda package names and versions +python = ">=3.7,<4" +# see https://mne.tools/stable/install/manual_install.html#installing-mne-python-with-all-dependencies +# CondaPkg uses mamba by default +mne = "=1.0" + +[pip.deps] +# Pip package names and versions diff --git a/Project.toml b/Project.toml index 34d0e04..171703f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,18 +1,22 @@ name = "PyMNE" uuid = "6c5003b2-cbe8-491c-a0d1-70088e6a0fd6" authors = ["Beacon Biosignals, Inc."] -version = "0.1.2" +version = "0.2.0" [deps] -PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" [compat] -PyCall = "1.90" -julia = "1" +CondaPkg = "0.2.11" +PythonCall = "0.9.4" +Reexport = "1" +julia = "1.6" [extras] -PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["PyCall", "Test"] +test = ["Random", "Test"] diff --git a/README.md b/README.md index 3b0f9e5..d737045 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # PyMNE -Julia interface to MNE-Python via PyCall +Julia interface to MNE-Python via PythonCall [![Build Status][build-img]][build-url] [![CodeCov][codecov-img]][codecov-url] @@ -10,55 +10,26 @@ Julia interface to MNE-Python via PyCall ## Installation -This package uses [`PyCall`](https://github.com/JuliaPy/PyCall.jl/) to make +This package uses [`PythonCall`](https://cjdoris.github.io/PythonCall.jl) to make [MNE-Python](https://mne.tools) available from within Julia. Unsurprisingly, MNE-Python and its dependencies need to be installed in order for this to work -and PyMNE will attempt to install when the package is built. - -By default, this installation happens in the "global" path for the Python used -by PyCall. If you're using PyCall via its hidden Miniconda install, your own -Anaconda environment, or a Python virtual environment, this is what you want. -(The "global" path is sandboxed to the Conda/virtual environment.) If you are -however using system Python, then you should set `ENV["PIPFLAGS"] = "--user"` -before `add`ing / `build`ing the package. By default, PyMNE will use the latest -MNE release available on [PyPI](https://pypi.org/project/mne/), but this can also -be changed via the `ENV["MNEVERSION"] = version_number` for your preferred -`version_number`. Note that PyMNE explicitly does not try to abstract out -the rather rapid API changes and deprecation cycle in MNE and as such, it is -incumbent upon the user to manage these versions accordingly. - -Note that MNE-Python uses [scikit-learn](https://scikit-learn.org/stable/) for certain functionality (e.g. ICA and the `decoding` module), but does not install it automatically as a dependency. -If you wish to take advantage of this functionality, the non-exported `install_sklearn` function will install `sklearn`, using the same environment variables as the main installation. - -MNE-Python can also be installed them manually ahead of time. -From the shell, use `python -m pip install mne` for the latest stable release -or `python -m pip install mne==version_number` for a given `version_number`, -ensuring that `python` is the same one that PyCall is using. Alternatively, -you can run this from within Julia: -```julia -using PyCall -pip = pyimport("pip") -pip.main(["install", "mne==version_number"]) # specific version -``` - -If you do not specify a version via `==version`, then the latest versions will be -installed. If you wish to upgrade versions, you can use -`python -m pip install --upgrade mne` or -```julia -using PyCall -pip = pyimport("pip") -pip.main(["install", "--upgrade", "mne"]) -``` - -You can test your setup with `using PyCall; pyimport("mne")`. +and PyMNE will attempt to install when the package is built: this should happen +more or less automatically via [`CondaPkg`](https://github.com/cjdoris/CondaPkg.jl). +You can configure various options via `CondaPkg`. MNE-Python is installed via +Conda, not via pip. ## Usage -In the same philosophy as PyCall, this allows for the transparent use of +In the same philosophy as PythonCall, this allows for the transparent use of MNE-Python from within Julia. The major things the package does are wrap the installation of MNE in the -package `build` step, load all the MNE functionality into the module namespace, -and provide a few accessors. +package installation and load all the MNE functionality into the module +namespace. +After that, it's just a Python package accessible via `using PyMNE` in +Julia. The usual conversion rules and behaviors from PythonCall apply. +The [tests](test/runtests.jl) test a few conversion gotchas, especially +compared to prior versions of this package, which were based on +[PyCall](https://github.com/JuliaPy/PyCall.jl). ### Exposing MNE-Python in Julia @@ -79,118 +50,23 @@ using PyMNE PyMNE.open_docs() ``` -The PyCall infrastructure also means that Python docstrings are available +The PythonCall infrastructure also means that Python docstrings are available in Julia: ```julia help?> PyMNE.open_docs -Launch a new web browser tab with the MNE documentation. - - Parameters - ---------- - kind : str | None - Can be "api" (default), "tutorials", or "examples". - The default can be changed by setting the configuration value - MNE_DOCS_KIND. - version : str | None - Can be "stable" (default) or "dev". - The default can be changed by setting the configuration value - MNE_DOCS_VERSION. -``` - -### Helping with type conversions - -PyCall can be rather aggressive in converting standard types, such as -dictionaries, to their native Julia equivalents, but this can create problems -due to differences in the way inheritance is traditionally used between -languages. -As a concrete example, MNE-Python defines an `Info` type that extends the -Python dictionary. -If an `Info` object is accessed naively from Julia, then it is converted to a -dictionary and the subtyping is lost when passed back to Python, which can -result in type/method errors. -(There is [some discussion](https://github.com/JuliaPy/PyCall.jl/issues/629) -about not automatically converting derived types in PyCall 2.0, exactly -because of this.) -To avoid this problem, PyMNE wraps a few methods to avoid this conversion, -namely Python's `mne.create_info` and the `info` property of many MNE types. - -```julia -julia> using PyMNE -julia> dat = zeros(1, 100); # fake data -julia> PyMNE.mne # direct access to the mne Python module without any wrapping -PyObject -julia> naive_info = PyMNE.mne.create_info([:a], 100) # gets converted to a Julia dictionary -Dict{Any,Any} with 36 entries: - "projs" => Any[] - "utc_offset" => nothing - "dev_head_t" => Dict{Any,Any}("trans"=>[1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0],"to"=>4,"from"=>1) - "experimenter" => nothing - "proj_name" => nothing - "nchan" => 1 - "ctf_head_t" => nothing - "acq_stim" => nothing - "events" => Any[] - "lowpass" => 50.0 - "helium_info" => nothing - "proc_history" => Any[] - "xplotter_layout" => nothing - "dig" => nothing - "kit_system_id" => nothing - "file_id" => nothing - ⋮ => ⋮ -julia> PyMNE.io.RawArray(dat, naive_info) # RawArray requires an Info object and not a 'simple' dictionary -ERROR: PyError ($(Expr(:escape, :(ccall(#= /home/ubuntu/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:43 =# @pysym(:PyObject_Call), PyPtr, (PyPtr, PyPtr, PyPtr), o, pyargsptr, kw))))) -TypeError("info must be an instance of Info, got instead") - File "", line 21, in __init__ - File "/home/ubuntu/.julia/conda/3/lib/python3.8/site-packages/mne/io/array/array.py", line 56, in __init__ - _validate_type(info, 'info', 'info') - File "/home/ubuntu/.julia/conda/3/lib/python3.8/site-packages/mne/utils/check.py", line 379, in _validate_type - raise TypeError('%s must be an instance of %s, got %s instead' - -Stacktrace: - [1] pyerr_check at /home/ubuntu/.julia/packages/PyCall/BcTLp/src/exception.jl:62 [inlined] -. . . - -julia> wrapped_info = PyMNE.create_info([:a], 100) # preserves Python type and show method -PyObject - -julia> raw = PyMNE.io.RawArray(dat, wrapped_info) # now has right type -Creating RawArray with float64 data, n_channels=1, n_times=100 - Range : 0 ... 99 = 0.000 ... 0.990 secs -Ready. -PyObject + Python function open_docs. + + Launch a new web browser tab with the MNE documentation. + + Parameters + ---------- + kind : str | None + Can be "api" (default), "tutorials", or "examples". + The default can be changed by setting the configuration value + MNE_DOCS_KIND. + version : str | None + Can be "stable" (default) or "dev". + The default can be changed by setting the configuration value + MNE_DOCS_VERSION. ``` - -This also leads to the only exported function `get_info`, which is just a -type-preserving accessor the `info` property of many MNE types: -```julia -julia> get_info(raw) -PyObject -``` - -If other automatic type conversions are found to be problematic or there are -particular MNE functions that don't play nice via the default PyCall mechanisms, -then issues and pull requests are welcome. diff --git a/deps/build.jl b/deps/build.jl deleted file mode 100644 index 090d426..0000000 --- a/deps/build.jl +++ /dev/null @@ -1,12 +0,0 @@ -@info "Installing MNE-Python" -using PyCall -pip = pyimport("pip") -flags = split(get(ENV, "PIPFLAGS", "")) -ver = get(ENV, "MNEVERSION", "") -packages = ["""mne$(isempty(ver) ? "" : "==")$(ver)"""] - -@info "Package requirements:" packages -@info "Flags for pip install:" flags -ver = isempty(ver) ? "latest" : ver -@info "MNE version:" ver -pip.main(["install"; flags; packages]) diff --git a/format/Manifest.toml b/format/Manifest.toml new file mode 100644 index 0000000..49f0c9e --- /dev/null +++ b/format/Manifest.toml @@ -0,0 +1,197 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.7.3" +manifest_format = "2.0" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[deps.CSTParser]] +deps = ["Tokenize"] +git-tree-sha1 = "3ddd48d200eb8ddf9cb3e0189fc059fd49b97c1f" +uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" +version = "3.3.6" + +[[deps.CommonMark]] +deps = ["Crayons", "JSON", "URIs"] +git-tree-sha1 = "4cd7063c9bdebdbd55ede1af70f3c2f48fab4215" +uuid = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" +version = "0.8.6" + +[[deps.Compat]] +deps = ["Dates", "LinearAlgebra", "UUIDs"] +git-tree-sha1 = "924cdca592bc16f14d2f7006754a621735280b74" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.1.0" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[deps.Crayons]] +git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.1.1" + +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "d1fff3a548102f48987a52a2e0d114fa97d730f0" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.13" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[deps.JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.3" + +[[deps.JuliaFormatter]] +deps = ["CSTParser", "CommonMark", "DataStructures", "Pkg", "Tokenize"] +git-tree-sha1 = "6f13ba89febc5c12f882902e1f5dcd11a8913fa5" +uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +version = "1.0.7" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" + +[[deps.LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[deps.Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[deps.MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" + +[[deps.OrderedCollections]] +git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.4.1" + +[[deps.Parsers]] +deps = ["Dates"] +git-tree-sha1 = "0044b23da09b5608b4ecacb4e5e6c6332f833a7e" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.3.2" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" + +[[deps.Tokenize]] +git-tree-sha1 = "2b3af135d85d7e70b863540160208fa612e736b9" +uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" +version = "0.5.24" + +[[deps.URIs]] +git-tree-sha1 = "e59ecc5a41b000fa94423a578d29290c7266fc10" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.4.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" diff --git a/format/Project.toml b/format/Project.toml new file mode 100644 index 0000000..71708c8 --- /dev/null +++ b/format/Project.toml @@ -0,0 +1,5 @@ +[deps] +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" + +[compat] +JuliaFormatter = "1" diff --git a/format/run.jl b/format/run.jl new file mode 100644 index 0000000..a1cb709 --- /dev/null +++ b/format/run.jl @@ -0,0 +1,20 @@ +using JuliaFormatter + +function main() + perfect = true + # note: keep in sync with `.github/workflows/format-check.yml` + for d in ["src", "test"] + @info "...linting $d ..." + dir_perfect = format(d; style=YASStyle(), join_lines_based_on_source=true) + perfect = perfect && dir_perfect + end + if perfect + @info "Linting complete - no files altered" + else + @info "Linting complete - files altered" + run(`git status`) + end + return nothing +end + +main() diff --git a/src/PyMNE.jl b/src/PyMNE.jl index d51828c..487e8e4 100644 --- a/src/PyMNE.jl +++ b/src/PyMNE.jl @@ -4,30 +4,20 @@ module PyMNE ##### Dependencies ##### -using PyCall +using Reexport -##### -##### Exports -##### - -export get_info +@reexport using PythonCall ###### ###### Actual functionality ###### -const mne = PyNULL() - -# TODO: examine how to do wrappers for subsubmodules. for now it's not a huge deal -# because the wrapper just gets put in the top-level package namespace and so -# is still accessible - -include("wrappers.jl") +const mne = PythonCall.pynew() function __init__() # all of this is __init__() so that it plays nice with precompilation - # see https://github.com/JuliaPy/PyCall.jl/#using-pycall-from-julia-modules - copy!(mne, pyimport("mne")) + # see https://github.com/cjdoris/PythonCall.jl/blob/5ea63f13c291ed97a8bacad06400acb053829dd4/src/Py.jl#L85-L96 + PythonCall.pycopy!(mne, pyimport("mne")) # don't eval into the module while precompiling; this breaks precompilation # of downstream modules (see #4) if ccall(:jl_generating_output, Cint, ()) == 0 @@ -41,25 +31,4 @@ function __init__() return nothing end -""" - install_sklearn(ver="") - -Install scikit-learn using the specified version. - -The default version is the latest stable version. -""" -function install_sklearn(version="latest"; verbose=false) - verbose && @info "Installing scikit-learn" - pip = pyimport("pip") - flags = split(get(ENV, "PIPFLAGS", "")) - packages = ["scikit-learn" * (version == "latest" ? "" : "==$version")] - if verbose - @info "Package requirements:" packages - @info "Flags for pip install:" flags - @info "scikit-learn version:" version - end - pip.main(["install"; flags; packages]) - return nothing -end - end # module diff --git a/src/wrappers.jl b/src/wrappers.jl deleted file mode 100644 index d392690..0000000 --- a/src/wrappers.jl +++ /dev/null @@ -1,21 +0,0 @@ -""" - create_info(args...; kwargs...) - -A type-preserving wrapper around `mne.create_info`. - -See [`PyMNE.mne.create_info`](@ref) for the associated MNE docstring. -""" -function create_info(args...; kwargs...) - # `mne.create_info(...)` on its own gives us back a Julia `Dict`, but we actually - # want the Python object, so we have to use `pycall` - return pycall(mne.create_info, PyObject, args...; kwargs...) -end - -""" - get_info(::PyObject) - -Extract an `Info` property from an MNE object while preserving Python type. -""" -get_info(o::PyObject) = o."info" - -# TODO: add a converting for Julia Dict to Python Info <: Dict diff --git a/test/runtests.jl b/test/runtests.jl index 88dcf7c..6819f53 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,22 +1,25 @@ using PyMNE -using PyCall +using Random using Test -using PyCall: PyError - -@testset "sklearn" begin - PyMNE.install_sklearn() - # requires sklearn - @test PyMNE.preprocessing.ICA(method="fastica") isa PyObject -end - @testset "create_info and get_info" begin - dat = zeros(1, 100) - naive_info = PyMNE.mne.create_info([:a], 100) - wrapped_info = PyMNE.create_info([:a], 100) - raw = PyMNE.io.RawArray(dat, wrapped_info) - @test raw.get_data() == dat - @test get_info(raw) isa PyObject - @test raw.info isa Dict - @test_throws PyError PyMNE.io.RawArray(dat, naive_info) + dat = rand(1, 100) + info = PyMNE.create_info(pylist(["a"]), 100) + @test info isa Py + @test pyconvert(String, only(info.ch_names)) == "a" + # XXX why all the pyconvert? well comparisons of python objects give you + # a python boolean, so we need to explicitly convert for `@test` + # this is fine because it shows usage and tests out a bit more code + @test pyconvert(Bool, info.ch_names == info["ch_names"]) + # if this ever works after a compat bump then we know we need to change things + @test_broken info.ch_names == info["ch_names"] + @test pyconvert(Bool, info["sfreq"] == 100) + @test pyconvert(Number, info["sfreq"]) == 100 + raw = PyMNE.io.RawArray(dat, info) + @test pyconvert(Number, raw.n_times) == 100 + # XXX Python + Windows means that this may or may not be Int32 even on x64 + @test pyconvert(Number, raw.n_times) isa Integer + @test pyconvert(Number, raw.info["sfreq"]) == 100 + # we want elementwise precise equality + @test all(pyconvert(AbstractArray, raw.get_data()) .== dat) end