Skip to content

Instantly share code, notes, and snippets.

@henriquebastos
Last active October 17, 2022 04:22
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save henriquebastos/270cff100cb303f3d74370489022446b to your computer and use it in GitHub Desktop.
Save henriquebastos/270cff100cb303f3d74370489022446b to your computer and use it in GitHub Desktop.
IPython startup script to detect and inject VIRTUAL_ENV's site-packages dirs.
"""IPython startup script to detect and inject VIRTUAL_ENV's site-packages dirs.
IPython can detect virtualenv's path and injects it's site-packages dirs into sys.path.
But it can go wrong if IPython's python version differs from VIRTUAL_ENV's.
This module fixes it looking for the actual directories. We use only old stdlib
resources so it can work with as many Python versions as possible.
References:
http://stackoverflow.com/a/30650831/443564
http://stackoverflow.com/questions/122327/how-do-i-find-the-location-of-my-python-site-packages-directory
https://github.com/ipython/ipython/blob/master/IPython/core/interactiveshell.py#L676
Author: Henrique Bastos <henrique@bastos.net>
License: BSD
"""
import os
import sys
from warnings import warn
virtualenv = os.environ.get('VIRTUAL_ENV')
if virtualenv:
version = os.listdir(os.path.join(virtualenv, 'lib'))[0]
site_packages = os.path.join(virtualenv, 'lib', version, 'site-packages')
lib_dynload = os.path.join(virtualenv, 'lib', version, 'lib-dynload')
if not (os.path.exists(site_packages) and os.path.exists(lib_dynload)):
msg = 'Virtualenv site-packages discovery went wrong for %r' % repr([site_packages, lib_dynload])
warn(msg)
try:
i = sys.path.index("") + 1
except ValueError:
i = 0
sys.path.insert(i, site_packages)
sys.path.insert(i+1, lib_dynload)
@viniciusban
Copy link

viniciusban commented May 5, 2017

I don't know if there's a way to send pull requests for a gist. Anyway, there's a suggestion below.

Keeping the default Python behavior to search for modules in current directory first, change lines 34 and 35 to this:

    try:
        i = sys.path.index("") + 1
    except ValueError:
        i = 0
    sys.path.insert(i, site_packages)
    sys.path.insert(i+1, lib_dynload)

It allows local custom modules override their version even inside the virtualenv.

Good work. Keep walking.

@henriquebastos
Copy link
Author

Nice! Done! I'm trusting you've tested it #lol

@viniciusban
Copy link

hahaha

Yes. I'm using it for a few days and it's working.

@junjiezhujason
Copy link

I was following your Medium post to get something setup on my machine. I started running into some issues with packages that used get_distribution from pkg_resources (in setup tools) in their __init__.py files. The error would look like:

Python 3.6.1 (default, Oct  6 2017, 10:41:43) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.2.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]:  from pkg_resources import get_distribution

In [2]: __version__ = get_distribution('numpy').version
---------------------------------------------------------------------------
DistributionNotFound                      Traceback (most recent call last)
<ipython-input-2-3b18f3abdfeb> in <module>()
----> 1 __version__ = get_distribution('numpy').version

~/.pyenv/versions/3.6.1/envs/jupyter3_install/lib/python3.6/site-packages/pkg_resources/__init__.py in get_distribution(dist)
    560         dist = Requirement.parse(dist)
    561     if isinstance(dist, Requirement):
--> 562         dist = get_provider(dist)
    563     if not isinstance(dist, Distribution):
    564         raise TypeError("Expected string, Requirement, or Distribution", dist)

~/.pyenv/versions/3.6.1/envs/jupyter3_install/lib/python3.6/site-packages/pkg_resources/__init__.py in get_provider(moduleOrReq)
    434     """Return an IResourceProvider for the named module or requirement"""
    435     if isinstance(moduleOrReq, Requirement):
--> 436         return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
    437     try:
    438         module = sys.modules[moduleOrReq]

~/.pyenv/versions/3.6.1/envs/jupyter3_install/lib/python3.6/site-packages/pkg_resources/__init__.py in require(self, *requirements)
    979         included, even if they were already activated in this working set.
    980         """
--> 981         needed = self.resolve(parse_requirements(requirements))
    982 
    983         for dist in needed:

~/.pyenv/versions/3.6.1/envs/jupyter3_install/lib/python3.6/site-packages/pkg_resources/__init__.py in resolve(self, requirements, env, installer, replace_conflicting, extras)
    865                     if dist is None:
    866                         requirers = required_by.get(req, None)
--> 867                         raise DistributionNotFound(req, requirers)
    868                 to_activate.append(dist)
    869             if dist not in req:

DistributionNotFound: The 'numpy' distribution was not found and is required by the application

It turned out that by the time that the script in this gist was called, pkg_resources had already been imported (at least once) before.
Its documentation states that:

add_entry(entry)
Add a path item to the entries, finding any distributions on it. You should use this when you add additional items to sys.path and you want the global working_set to reflect the change. This method is also called by the WorkingSet() constructor during initialization.

This method uses find_distributions(entry,True) to find distributions corresponding to the path entry, and then add() them. entry is always appended to the entries attribute, even if it is already present, however. (This is because sys.path can contain the same value more than once, and the entries attribute should be able to reflect this.)

Thus, the solution that I found (not sure how "correct" it is...) is to:
Add import pkg_resources at the top, then add:

pkg_resources.working_set.add_entry(site_package)
pkg_resources.working_set.add_entry(lib_dynload)

along with the sys.path.insert statements at the end.

@pollackscience
Copy link

Hi,
When using this script, I found that it ignored packages that were installed in 'develop' mode via python setup.py develop or pip install -e .. It seems like when something is installed in develop mode, an egg-link is added to the site-packages which points to the active package directory. I'm not sure how this egg-link is resolved normally in python, but at some point that directory is directly added to sys.path. To mimic this behavior, I added the following anywhere after the variable site_packages is defined:

egg_links = [i for i in os.listdir(site_packages) if 'egg-link' in i]
     for egg in egg_links:
         with open(os.path.join(site_packages,egg), 'r') as fin:
            sys.path.insert(0, fin.readline().rstrip('\n'))

This simply looks for anything that has 'egg-link' in the name, then opens the file and adds the first line to the path. No idea if you could have an edge case in which there could be multiple paths or lines in the egg-link, but this works for my needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment