Skip to content

First try at accommodating "modern" editable python packages #52

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

Merged
merged 14 commits into from
May 17, 2025

Conversation

dingraha
Copy link
Contributor

The pyjuliapkg docs explain that the package will search for juliapkg.json files in the following locations:

  • {project}/pyjuliapkg where project is as above (depending on your environment).
  • Every directory and direct sub-directory in sys.path.

Unfortunately, when a Python package is installed in --editable mode, it appears that its source location is not, sometimes, added to sys.path. (The source location does appear to be added to sys.path if the --config-settings editable_mode=compat option is used with pip, but that option is deprecated: https://setuptools.pypa.io/en/latest/userguide/development_mode.html#legacy-behavior) Instead, some strange-looking files starting with an __editable__ prefix are added to site-packages, which Python somehow uses to find the editable package.

This PR looks for those __editable__ files in each entry in sys.path, and then uses the _EditableFinder thingies in sys.meta_path to find the install location of the editable Python packages, and finally uses those locations when looking for juliapkg.json files.

@dingraha
Copy link
Contributor Author

dingraha commented May 7, 2025

@cjdoris A polite ping re: this PR.

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

Thanks for this. I've just been experimenting locally and for me, I don't get the __editable prefix. For example when I install juliacall in editable mode I just get a _juliacall.pth file. I tried this with both pip and uv.

Reading the docs for .pth files, it looks like we should just read ALL these files, loop over each line, which should be a dir, and search for juliapkg.json within that dir.

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

Actually I'm extra confused now - the dirs in the .pth files already get added to sys.path, so the juliapkg.json files for editable packages should already get found. Do you have an instance where they are not found?

@dingraha
Copy link
Contributor Author

Actually I'm extra confused now - the dirs in the .pth files already get added to sys.path, so the juliapkg.json files for editable packages should already get found. Do you have an instance where they are not found?

Interesting! Here's what I see, sorry for the extremely long post:

dingraha@GRLRL2024060112 ~/p/juliapkg_editable> conda create --name juliapkg_editable
Retrieving notices: done
Channels:
 - conda-forge
Platform: linux-64
Collecting package metadata (repodata.json): done
Solving environment: done


==> WARNING: A newer version of conda exists. <==
    current version: 25.3.0
    latest version: 25.3.1

Please update conda by running

    $ conda update -n base -c conda-forge conda



## Package Plan ##

  environment location: /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable



Proceed ([y]/n)? y


Downloading and Extracting Packages:

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate juliapkg_editable
#
# To deactivate an active environment, use
#
#     $ conda deactivate

dingraha@GRLRL2024060112 ~/p/juliapkg_editable> conda activate juliapkg_editable
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable [4]> conda install pip
Channels:
 - conda-forge
Platform: linux-64
Collecting package metadata (repodata.json): done
Solving environment: done


==> WARNING: A newer version of conda exists. <==
    current version: 25.3.0
    latest version: 25.3.1

Please update conda by running

    $ conda update -n base -c conda-forge conda



## Package Plan ##

  environment location: /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable

  added / updated specs:
    - pip


The following NEW packages will be INSTALLED:

  _libgcc_mutex      conda-forge/linux-64::_libgcc_mutex-0.1-conda_forge 
  _openmp_mutex      conda-forge/linux-64::_openmp_mutex-4.5-2_gnu 
  bzip2              conda-forge/linux-64::bzip2-1.0.8-h4bc722e_7 
  ca-certificates    conda-forge/noarch::ca-certificates-2025.4.26-hbd8a1cb_0 
  ld_impl_linux-64   conda-forge/linux-64::ld_impl_linux-64-2.43-h712a8e2_4 
  libexpat           conda-forge/linux-64::libexpat-2.7.0-h5888daf_0 
  libffi             conda-forge/linux-64::libffi-3.4.6-h2dba641_1 
  libgcc             conda-forge/linux-64::libgcc-15.1.0-h767d61c_2 
  libgcc-ng          conda-forge/linux-64::libgcc-ng-15.1.0-h69a702a_2 
  libgomp            conda-forge/linux-64::libgomp-15.1.0-h767d61c_2 
  liblzma            conda-forge/linux-64::liblzma-5.8.1-hb9d3cd8_1 
  libmpdec           conda-forge/linux-64::libmpdec-4.0.0-h4bc722e_0 
  libsqlite          conda-forge/linux-64::libsqlite-3.49.2-hee588c1_0 
  libuuid            conda-forge/linux-64::libuuid-2.38.1-h0b41bf4_0 
  libzlib            conda-forge/linux-64::libzlib-1.3.1-hb9d3cd8_2 
  ncurses            conda-forge/linux-64::ncurses-6.5-h2d0b736_3 
  openssl            conda-forge/linux-64::openssl-3.5.0-h7b32b05_1 
  pip                conda-forge/noarch::pip-25.1.1-pyh145f28c_0 
  python             conda-forge/linux-64::python-3.13.3-hf636f53_101_cp313 
  python_abi         conda-forge/noarch::python_abi-3.13-7_cp313 
  readline           conda-forge/linux-64::readline-8.2-h8c095d6_2 
  tk                 conda-forge/linux-64::tk-8.6.13-noxft_h4845f30_101 
  tzdata             conda-forge/noarch::tzdata-2025b-h78e105d_0 


Proceed ([y]/n)? y


Downloading and Extracting Packages:

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> git clone [email protected]:dingraha/OpenMDAO.jl.git
Cloning into 'OpenMDAO.jl'...
remote: Enumerating objects: 2249, done.
remote: Counting objects: 100% (292/292), done.
remote: Compressing objects: 100% (136/136), done.
remote: Total 2249 (delta 196), reused 207 (delta 149), pack-reused 1957 (from 2)
Receiving objects: 100% (2249/2249), 1.37 MiB | 15.38 MiB/s, done.
Resolving deltas: 100% (1107/1107), done.
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> pip install -e ./OpenMDAO.jl/python
Obtaining file:///home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
Collecting openmdao~=3.26.0 (from omjlcomps==0.2.0)
  Using cached openmdao-3.26.0.tar.gz (5.6 MB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Collecting juliapkg~=0.1.10 (from omjlcomps==0.2.0)
  Downloading juliapkg-0.1.17-py3-none-any.whl.metadata (6.2 kB)
Collecting juliacall~=0.9.13 (from omjlcomps==0.2.0)
  Downloading juliacall-0.9.25-py3-none-any.whl.metadata (4.5 kB)
Collecting filelock<4.0,>=3.16 (from juliapkg~=0.1.10->omjlcomps==0.2.0)
  Using cached filelock-3.18.0-py3-none-any.whl.metadata (2.9 kB)
Collecting semver<4.0,>=3.0 (from juliapkg~=0.1.10->omjlcomps==0.2.0)
  Using cached semver-3.0.4-py3-none-any.whl.metadata (6.8 kB)
Collecting networkx>=2.0 (from openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached networkx-3.4.2-py3-none-any.whl.metadata (6.3 kB)
Collecting numpy (from openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (62 kB)
Collecting scipy (from openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB)
Collecting requests (from openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached requests-2.32.3-py3-none-any.whl.metadata (4.6 kB)
Collecting packaging (from openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting charset-normalizer<4,>=2 (from requests->openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (35 kB)
Collecting idna<4,>=2.5 (from requests->openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting urllib3<3,>=1.21.1 (from requests->openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached urllib3-2.4.0-py3-none-any.whl.metadata (6.5 kB)
Collecting certifi>=2017.4.17 (from requests->openmdao~=3.26.0->omjlcomps==0.2.0)
  Using cached certifi-2025.4.26-py3-none-any.whl.metadata (2.5 kB)
Downloading juliacall-0.9.25-py3-none-any.whl (12 kB)
Downloading juliapkg-0.1.17-py3-none-any.whl (16 kB)
Using cached filelock-3.18.0-py3-none-any.whl (16 kB)
Using cached semver-3.0.4-py3-none-any.whl (17 kB)
Using cached networkx-3.4.2-py3-none-any.whl (1.7 MB)
Using cached numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.1 MB)
Using cached packaging-25.0-py3-none-any.whl (66 kB)
Using cached requests-2.32.3-py3-none-any.whl (64 kB)
Using cached charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (148 kB)
Using cached idna-3.10-py3-none-any.whl (70 kB)
Using cached urllib3-2.4.0-py3-none-any.whl (128 kB)
Using cached certifi-2025.4.26-py3-none-any.whl (159 kB)
Using cached scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (37.3 MB)
Building wheels for collected packages: omjlcomps, openmdao
  Building editable for omjlcomps (pyproject.toml) ... done
  Created wheel for omjlcomps: filename=omjlcomps-0.2.0-0.editable-py3-none-any.whl size=3109 sha256=b753acaabd957c659f0678d596fd806b98724c35df03b498190f4920d2a83178
  Stored in directory: /tmp/pip-ephem-wheel-cache-zqh2jqub/wheels/dc/ab/41/242c30ee1ec6e6ee2a8c5072fe8b76252ace6d9eac935aceb3
  Building wheel for openmdao (pyproject.toml) ... done
  Created wheel for openmdao: filename=openmdao-3.26.0-py3-none-any.whl size=6105619 sha256=64254b614f388fee25b38b2704f006b34f960286627b9a43f35c962655247726
  Stored in directory: /home/dingraha/.cache/pip/wheels/6e/c2/b7/e905bb4caef399b9ae6e38045b37a93b1766c9070ea5344579
Successfully built omjlcomps openmdao
Installing collected packages: urllib3, semver, packaging, numpy, networkx, idna, filelock, charset-normalizer, certifi, scipy, requests, juliapkg, openmdao, juliacall, omjlcomps
Successfully installed certifi-2025.4.26 charset-normalizer-3.4.2 filelock-3.18.0 idna-3.10 juliacall-0.9.25 juliapkg-0.1.17 networkx-3.4.2 numpy-2.2.5 omjlcomps-0.2.0 openmdao-3.26.0 packaging-25.0 requests-2.32.3 scipy-1.15.3 semver-3.0.4 urllib3-2.4.0
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> 
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> python
Python 3.13.3 | packaged by conda-forge | (main, Apr 14 2025, 20:44:03) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import juliacall
[juliapkg] Found dependencies: /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/juliacall/juliapkg.json
[juliapkg] Found dependencies: /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/juliapkg/juliapkg.json
[juliapkg] Locating Julia 1.6.1 - 1.10.0, ^1.10.3
[juliapkg] Querying Julia versions from https://julialang-s3.julialang.org/bin/versions.json
[juliapkg] WARNING: About to install Julia 1.11.5 to /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/pyjuliapkg/install.
[juliapkg]   If you use juliapkg in more than one environment, you are likely to
[juliapkg]   have Julia installed in multiple locations. It is recommended to
[juliapkg]   install JuliaUp (https://github.com/JuliaLang/juliaup) or Julia
[juliapkg]   (https://julialang.org/downloads) yourself.
[juliapkg] Downloading Julia from https://julialang-s3.julialang.org/bin/linux/x64/1.11/julia-1.11.5-linux-x86_64.tar.gz
             download complete
[juliapkg] Verifying download
[juliapkg] Installing Julia 1.11.5 to /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/pyjuliapkg/install
[juliapkg] Using Julia 1.11.5 at /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/pyjuliapkg/install/bin/julia
[juliapkg] Using Julia project at /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env
[juliapkg] Writing Project.toml:
             [deps]
             PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
             OpenSSL_jll = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
             [compat]
             PythonCall = "=0.9.25"
             OpenSSL_jll = "3.0.0 - 3.5"
[juliapkg] Installing packages:
             import Pkg
             Pkg.Registry.update()
             Pkg.add([
               Pkg.PackageSpec(name="PythonCall", uuid="6099a3de-0909-46bc-b1f4-468b9a2dfc0d"),
               Pkg.PackageSpec(name="OpenSSL_jll", uuid="458c3c95-2e84-50aa-8efc-19380b2a3a95"),
             ])
             Pkg.resolve()
             Pkg.precompile()
    Updating registry at `~/.julia/registries/General.toml`
   Resolving package versions...
    Updating `~/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/Project.toml`
  [6099a3de] + PythonCall v0.9.25
  [458c3c95] + OpenSSL_jll v3.5.0+0
    Updating `~/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/Manifest.toml`
  [992eb4ea] + CondaPkg v0.2.28
  [9a962f9c] + DataAPI v1.16.0
  [e2d170a0] + DataValueInterfaces v1.0.0
  [82899510] + IteratorInterfaceExtensions v1.0.0
  [692b3bcd] + JLLWrappers v1.7.0
  [0f8b85d8] + JSON3 v1.14.2
  [1914dd2f] + MacroTools v0.5.16
  [0b3b1443] + MicroMamba v0.1.14
  [bac558e1] + OrderedCollections v1.8.0
  [69de0a69] + Parsers v2.8.3
  [fa939f87] + Pidfile v1.3.0
⌅ [aea7be01] + PrecompileTools v1.2.1
  [21216c6a] + Preferences v1.4.3
  [6099a3de] + PythonCall v0.9.25
  [ae029012] + Requires v1.3.1
  [6c6a2e73] + Scratch v1.2.1
  [856f2bd8] + StructTypes v1.11.0
  [3783bdb8] + TableTraits v1.0.1
  [bd369af6] + Tables v1.12.0
  [e17b2a0c] + UnsafePointers v1.0.0
  [458c3c95] + OpenSSL_jll v3.5.0+0
  [f8abcde7] + micromamba_jll v1.5.8+0
  [4d7b5844] + pixi_jll v0.41.3+0
  [0dad84c5] + ArgTools v1.1.2
  [56f22d72] + Artifacts v1.11.0
  [2a0f44e3] + Base64 v1.11.0
  [ade2ca70] + Dates v1.11.0
  [f43a241f] + Downloads v1.6.0
  [7b1f6079] + FileWatching v1.11.0
  [b77e0a4c] + InteractiveUtils v1.11.0
  [4af54fe1] + LazyArtifacts v1.11.0
  [b27032c2] + LibCURL v0.6.4
  [76f85450] + LibGit2 v1.11.0
  [8f399da3] + Libdl v1.11.0
  [56ddb016] + Logging v1.11.0
  [d6f4376e] + Markdown v1.11.0
  [a63ad114] + Mmap v1.11.0
  [ca575930] + NetworkOptions v1.2.0
  [44cfe95a] + Pkg v1.11.0
  [de0858da] + Printf v1.11.0
  [9a3f8284] + Random v1.11.0
  [ea8e919c] + SHA v0.7.0
  [9e88b42a] + Serialization v1.11.0
  [fa267f1f] + TOML v1.0.3
  [a4e569a6] + Tar v1.10.0
  [8dfed614] + Test v1.11.0
  [cf7118a7] + UUIDs v1.11.0
  [4ec0a83e] + Unicode v1.11.0
  [deac9b47] + LibCURL_jll v8.6.0+0
  [e37daf67] + LibGit2_jll v1.7.2+0
  [29816b5a] + LibSSH2_jll v1.11.0+1
  [c8ffd9c3] + MbedTLS_jll v2.28.6+0
  [14a3606d] + MozillaCACerts_jll v2023.12.12
  [83775a58] + Zlib_jll v1.2.13+1
  [8e850ede] + nghttp2_jll v1.59.0+0
  [3f19e933] + p7zip_jll v17.4.0+2
        Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated -m`
  No Changes to `~/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/Project.toml`
  No Changes to `~/local/miniforge3-2025-04-29/envs/juliapkg_editable/julia_env/Manifest.toml`
>>> import omjlcomps
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    import omjlcomps
  File "/home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python/omjlcomps/__init__.py", line 9, in <module>
    jl.seval("using OpenMDAOCore: OpenMDAOCore")
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dingraha/.julia/packages/PythonCall/L4cjh/src/JlWrap/module.jl", line 27, in seval
    return self._jl_callmethod($(pyjl_methodnum(pyjlmodule_seval)), expr)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
juliacall.JuliaError: ArgumentError: Package OpenMDAOCore not found in current path.
- Run `import Pkg; Pkg.add("OpenMDAOCore")` to install the OpenMDAOCore package.
Stacktrace:
  [1] macro expansion
    @ ./loading.jl:2296 [inlined]
  [2] macro expansion
    @ ./lock.jl:273 [inlined]
  [3] __require(into::Module, mod::Symbol)
    @ Base ./loading.jl:2271
  [4] #invoke_in_world#3
    @ ./essentials.jl:1089 [inlined]
  [5] invoke_in_world
    @ ./essentials.jl:1086 [inlined]
  [6] require(into::Module, mod::Symbol)
    @ Base ./loading.jl:2260
  [7] eval
    @ ./boot.jl:430 [inlined]
  [8] eval
    @ ./Base.jl:130 [inlined]
  [9] pyjlmodule_seval(self::Module, expr::Py)
    @ PythonCall.JlWrap ~/.julia/packages/PythonCall/L4cjh/src/JlWrap/module.jl:13
 [10] _pyjl_callmethod(f::Any, self_::Ptr{PythonCall.C.PyObject}, args_::Ptr{PythonCall.C.PyObject}, nargs::Int64)
    @ PythonCall.JlWrap ~/.julia/packages/PythonCall/L4cjh/src/JlWrap/base.jl:67
 [11] _pyjl_callmethod(o::Ptr{PythonCall.C.PyObject}, args::Ptr{PythonCall.C.PyObject})
    @ PythonCall.JlWrap.Cjl ~/.julia/packages/PythonCall/L4cjh/src/JlWrap/C.jl:63
>>> 
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> cat OpenMDAO.jl/python/omjlcomps/juliapkg.json 
{"packages": {
    "OpenMDAOCore": {
        "uuid": "24d19c10-6eee-420f-95df-4537264b2753",
        "version": "0.3.0"},
    "LinearAlgebra": {
        "uuid": "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"},
    "PythonCall": {
        "uuid": "6099a3de-0909-46bc-b1f4-468b9a2dfc0d",
        "version": "0.9.0"}}}
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> python
Python 3.13.3 | packaged by conda-forge | (main, Apr 14 2025, 20:44:03) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print(sys.path)
['', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python313.zip', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/lib-dynload', '/home/din
graha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages']
>>> 
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> ls ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/
certifi                             __editable___omjlcomps_0_2_0_finder.py  idna-3.10.dist-info         networkx                  omjlcomps-0.2.0.dist-info  pip                   requests-2.32.3.dist-info  semver-3.0.4.dist-info
certifi-2025.4.26.dist-info         __editable__.omjlcomps-0.2.0.pth        juliacall                   networkx-3.4.2.dist-info  openmdao                   pip-25.1.1.dist-info  scipy                      urllib3
charset_normalizer                  filelock                                juliacall-0.9.25.dist-info  numpy                     openmdao-3.26.0.dist-info  __pycache__           scipy-1.15.3.dist-info     urllib3-2.4.0.dist-info
charset_normalizer-3.4.2.dist-info  filelock-3.18.0.dist-info               juliapkg                    numpy-2.2.5.dist-info     packaging                  README.txt            scipy.libs
conda-site.pth                      idna                                    juliapkg-0.1.17.dist-info   numpy.libs                packaging-25.0.dist-info   requests              semver
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> 

TLDR: I get these __editable___ files, and the directories they point to don't get added to sys.path, and so the juliapkg.json files aren't found.

@dingraha
Copy link
Contributor Author

I see the same thing you do when installing juliacall, though:

(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable [128]> git clone [email protected]:JuliaPy/PythonCall.jl.git
Cloning into 'PythonCall.jl'...
remote: Enumerating objects: 9592, done.
remote: Counting objects: 100% (1735/1735), done.
remote: Compressing objects: 100% (516/516), done.
remote: Total 9592 (delta 1483), reused 1248 (delta 1179), pack-reused 7857 (from 4)
Receiving objects: 100% (9592/9592), 5.09 MiB | 8.93 MiB/s, done.
Resolving deltas: 100% (6350/6350), done.
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> pip install -e ./PythonCall.jl
Obtaining file:///home/dingraha/projects/juliapkg_editable/PythonCall.jl
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Installing backend dependencies ... done
  Preparing editable metadata (pyproject.toml) ... done
Requirement already satisfied: juliapkg~=0.1.17 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (from juliacall==0.9.25) (0.1.17)
Requirement already satisfied: filelock<4.0,>=3.16 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (from juliapkg~=0.1.17->juliacall==0.9.25) (3.18.0)
Requirement already satisfied: semver<4.0,>=3.0 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (from juliapkg~=0.1.17->juliacall==0.9.25) (3.0.4)
Building wheels for collected packages: juliacall
  Building editable for juliacall (pyproject.toml) ... done
  Created wheel for juliacall: filename=juliacall-0.9.25-py3-none-any.whl size=3689 sha256=49b26b920237bd7f3c054470f53adb62f138e62cdf5f79808f474781fc11890a
  Stored in directory: /tmp/pip-ephem-wheel-cache-_8vy6l5j/wheels/ff/3c/d8/c799268e59d65ff3ad8138d5fec70893f4e92c5574ec5e9523
Successfully built juliacall
Installing collected packages: juliacall
  Attempting uninstall: juliacall
    Found existing installation: juliacall 0.9.25
    Uninstalling juliacall-0.9.25:
      Successfully uninstalled juliacall-0.9.25
Successfully installed juliacall-0.9.25
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> ls ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/
certifi                             __editable___omjlcomps_0_2_0_finder.py  idna-3.10.dist-info         networkx                  omjlcomps-0.2.0.dist-info  pip                   requests-2.32.3.dist-info  semver-3.0.4.dist-info
certifi-2025.4.26.dist-info         __editable__.omjlcomps-0.2.0.pth        juliacall-0.9.25.dist-info  networkx-3.4.2.dist-info  openmdao                   pip-25.1.1.dist-info  scipy                      urllib3
charset_normalizer                  filelock                                _juliacall.pth              numpy                     openmdao-3.26.0.dist-info  __pycache__           scipy-1.15.3.dist-info     urllib3-2.4.0.dist-info
charset_normalizer-3.4.2.dist-info  filelock-3.18.0.dist-info               juliapkg                    numpy-2.2.5.dist-info     packaging                  README.txt            scipy.libs
conda-site.pth                      idna                                    juliapkg-0.1.17.dist-info   numpy.libs                packaging-25.0.dist-info   requests              semver
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> cat ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/_juliacall.pth 
/home/dingraha/projects/juliapkg_editable/PythonCall.jl/pysrc⏎                                                                                                                                                                                                                            
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> python
Python 3.13.3 | packaged by conda-forge | (main, Apr 14 2025, 20:44:03) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print(sys.path)
['', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python313.zip', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/lib-dynload', '/home/din
graha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages', '/home/dingraha/projects/juliapkg_editable/PythonCall.jl/pysrc']
>>> 

Something must be different about the way the python package I'm working with is set up, compared to juliacall.

@dingraha
Copy link
Contributor Author

Ah, OK, so if I make it use hatchling everything works again:

(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/j/O/python ((09e78212…) *)> cat pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = 'omjlcomps'
description = "Create OpenMDAO Components using the Julia programming language"
readme = "README.md"
keywords = ["openmdao_component"]
license = {text = "MIT"}
version = "0.2.5"

dependencies = [
    "openmdao~=3.36",
    "juliapkg~=0.1.10",
    "juliacall~=0.9.13",
]

[project.optional-dependencies]
test = ["om-aviary"]

[project.entry-points.openmdao_component]
juliaexplicitcomp = "omjlcomps:JuliaExplicitComp"
juliaimplicitcomp = "omjlcomps:JuliaImplicitComp"
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/j/O/python ((09e78212…) *)> pwd
/home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/j/O/python ((09e78212…))> pip install -e .
Obtaining file:///home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Installing backend dependencies ... done
  Preparing editable metadata (pyproject.toml) ... done
Requirement already satisfied: juliacall~=0.9.13 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-pa
ckages (from omjlcomps==0.2.5) (0.9.25)
Requirement already satisfied: juliapkg~=0.1.10 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-pac
kages (from omjlcomps==0.2.5) (0.1.17)
Requirement already satisfied: openmdao~=3.36 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packa
ges (from omjlcomps==0.2.5) (3.38.0)
Requirement already satisfied: filelock<4.0,>=3.16 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-
packages (from juliapkg~=0.1.10->omjlcomps==0.2.5) (3.18.0)
Requirement already satisfied: semver<4.0,>=3.0 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-pac
kages (from juliapkg~=0.1.10->omjlcomps==0.2.5) (3.0.4)
Requirement already satisfied: networkx>=2.0 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packag
es (from openmdao~=3.36->omjlcomps==0.2.5) (3.4.2)
Requirement already satisfied: numpy in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (from
 openmdao~=3.36->omjlcomps==0.2.5) (2.2.5)
Requirement already satisfied: packaging in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (
from openmdao~=3.36->omjlcomps==0.2.5) (25.0)
Requirement already satisfied: requests in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (f
rom openmdao~=3.36->omjlcomps==0.2.5) (2.32.3)
Requirement already satisfied: scipy in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages (from
 openmdao~=3.36->omjlcomps==0.2.5) (1.15.3)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/
site-packages (from requests->openmdao~=3.36->omjlcomps==0.2.5) (3.4.2)
Requirement already satisfied: idna<4,>=2.5 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-package
s (from requests->openmdao~=3.36->omjlcomps==0.2.5) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-p
ackages (from requests->openmdao~=3.36->omjlcomps==0.2.5) (2.4.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-p
ackages (from requests->openmdao~=3.36->omjlcomps==0.2.5) (2025.4.26)
Building wheels for collected packages: omjlcomps
  Building editable for omjlcomps (pyproject.toml) ... done
  Created wheel for omjlcomps: filename=omjlcomps-0.2.5-py2.py3-none-any.whl size=2762 sha256=d90cb948ba223239db80bbdbf19aff6cae5ecd6fd2c1b6
c318a2db6ee4627be5
  Stored in directory: /tmp/pip-ephem-wheel-cache-47or0c4b/wheels/dc/ab/41/242c30ee1ec6e6ee2a8c5072fe8b76252ace6d9eac935aceb3
Successfully built omjlcomps
Installing collected packages: omjlcomps
  Attempting uninstall: omjlcomps
    Found existing installation: omjlcomps 0.2.5
    Uninstalling omjlcomps-0.2.5:
      Successfully uninstalled omjlcomps-0.2.5
Successfully installed omjlcomps-0.2.5
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/j/O/python ((09e78212…) *)> ls ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/pyt
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/j/O/python ((09e78212…) *)> python
Python 3.13.3 | packaged by conda-forge | (main, Apr 14 2025, 20:44:03) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print(sys.path)
['', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python313.zip', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13', '/home/dingraha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/lib-dynload', '/home/din
graha/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages', '/home/dingraha/projects/juliapkg_editable/PythonCall.jl/pysrc', '/home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python']
>>> 

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

Thanks for the details, that's really helpful.

What is the content of the two __editable__* files?

I think the difference in behaviour between those packages is that they use different build backends. Yours uses setuptools whereas mine uses hatchling which is more modern and easy to use (IMO). So an easy fix for you would be to upgrade that package to build with hatchling (or some other modern backend of your choice).

@dingraha
Copy link
Contributor Author

What is the content of the two editable* files?

The .pth file is very short:

(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> cat ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/__editable__.omjlcomps-0.2.5.pth 
import __editable___omjlcomps_0_2_5_finder; __editable___omjlcomps_0_2_5_finder.install()⏎                                                                                                                                                                                                

but the .py file is a longer script:

(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> cat ~/local/miniforge3-2025-04-29/envs/juliapkg_editable/lib/python3.13/site-packages/__editable___omjlcomps_0_2_5_finder.py 
from __future__ import annotations
import sys
from importlib.machinery import ModuleSpec, PathFinder
from importlib.machinery import all_suffixes as module_suffixes
from importlib.util import spec_from_file_location
from itertools import chain
from pathlib import Path

MAPPING: dict[str, str] = {'omjlcomps': '/home/dingraha/projects/juliapkg_editable/OpenMDAO.jl/python/omjlcomps'}
NAMESPACES: dict[str, list[str]] = {}
PATH_PLACEHOLDER = '__editable__.omjlcomps-0.2.5.finder' + ".__path_hook__"


class _EditableFinder:  # MetaPathFinder
    @classmethod
    def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None:  # type: ignore
        # Top-level packages and modules (we know these exist in the FS)
        if fullname in MAPPING:
            pkg_path = MAPPING[fullname]
            return cls._find_spec(fullname, Path(pkg_path))

        # Handle immediate children modules (required for namespaces to work)
        # To avoid problems with case sensitivity in the file system we delegate
        # to the importlib.machinery implementation.
        parent, _, child = fullname.rpartition(".")
        if parent and parent in MAPPING:
            return PathFinder.find_spec(fullname, path=[MAPPING[parent]])

        # Other levels of nesting should be handled automatically by importlib
        # using the parent path.
        return None

    @classmethod
    def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:
        init = candidate_path / "__init__.py"
        candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
        for candidate in chain([init], candidates):
            if candidate.exists():
                return spec_from_file_location(fullname, candidate)
        return None


class _EditableNamespaceFinder:  # PathEntryFinder
    @classmethod
    def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:
        if path == PATH_PLACEHOLDER:
            return cls
        raise ImportError

    @classmethod
    def _paths(cls, fullname: str) -> list[str]:
        paths = NAMESPACES[fullname]
        if not paths and fullname in MAPPING:
            paths = [MAPPING[fullname]]
        # Always add placeholder, for 2 reasons:
        # 1. __path__ cannot be empty for the spec to be considered namespace.
        # 2. In the case of nested namespaces, we need to force
        #    import machinery to query _EditableNamespaceFinder again.
        return [*paths, PATH_PLACEHOLDER]

    @classmethod
    def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None:  # type: ignore
        if fullname in NAMESPACES:
            spec = ModuleSpec(fullname, None, is_package=True)
            spec.submodule_search_locations = cls._paths(fullname)
            return spec
        return None

    @classmethod
    def find_module(cls, _fullname) -> None:
        return None


def install():
    if not any(finder == _EditableFinder for finder in sys.meta_path):
        sys.meta_path.append(_EditableFinder)

    if not NAMESPACES:
        return

    if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):
        # PathEntryFinder is needed to create NamespaceSpec without private APIS
        sys.path_hooks.append(_EditableNamespaceFinder._path_hook)
    if PATH_PLACEHOLDER not in sys.path:
        sys.path.append(PATH_PLACEHOLDER)  # Used just to trigger the path hook
(juliapkg_editable) dingraha@GRLRL2024060112 ~/p/juliapkg_editable> 

@dingraha
Copy link
Contributor Author

So an easy fix for you would be to upgrade that package to build with hatchling (or some other modern backend of your choice).

Yes, makes sense. The solution I came up with for this PR is perhaps a bit inelegant. If you'd prefer not to accept it, I guess some documentation re: the limitations of setuptools and editable packages would be helpful (which I'd be happy to contribute).

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

Yeah a PR to add a little "known limitations" section to the readme would be appreciated.

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

I'm not against adding this functionality but doing it robustly seems hard.

Here's a possibly nicer way:

  • loop over each finder in sys.meta_path
  • get its __module__
  • ignore if it's not "__editable__*_finder"
  • get the actual module from sys.modules
  • get MAPPING from this module
  • it's values are directories, add these to our list

This avoids looking at the filesystem again, since the finders are already loaded when python starts. It does rely on internals (MAPPING and the name of the finder module) but so does your version and I doubt there's a way to avoid internals.

Edit: Equally if you're happy with hatchling I'm happy to park this PR.

@cjdoris
Copy link
Collaborator

cjdoris commented May 14, 2025

Ah just noticed you already tried hatchling, I didn't see that message!

@dingraha
Copy link
Contributor Author

Here's a possibly nicer way:

  • loop over each finder in sys.meta_path
  • get its __module__
  • ignore if it's not "__editable__*_finder"
  • get the actual module from sys.modules
  • get MAPPING from this module
  • it's values are directories, add these to our list

This avoids looking at the filesystem again, since the finders are already loaded when python starts. It does rely on internals (MAPPING and the name of the finder module) but so does your version and I doubt there's a way to avoid internals.

That does seem nicer! I took a stab at implementing it and it seems to work well. I updated this PR.

Equally if you're happy with hatchling I'm happy to park this PR.

The hatchling switch works fine for my case, but I'm hoping to get others using Julia+Python in my organization, so it would be great if juliapkg could also support setuptools, etc..

@cjdoris cjdoris merged commit 3c66a04 into JuliaPy:main May 17, 2025
6 checks passed
@cjdoris
Copy link
Collaborator

cjdoris commented May 17, 2025

OK I've added some tests and things and merged this, thanks!

@dingraha
Copy link
Contributor Author

@cjdoris Wonderful, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants