Skip to content

python3.6 can't import installed namespace package if another package in the same namespace is put locally and pkg_resources are imported #1321

@daa

Description

@daa

Recently I've met an issue with python3.6 and pkg_resources-style namespace packages when for one namespace one package is installed into virtualenv (a) and one is located in current directory (b) - after importing pkg_resources or setuptools package a becomes inaccessible. But no such behaviour is observed with python2.7. To be more clear I have an example:

# package a:
a $ tree
.
├── pkgns
│   ├── __init__.py
│   └── pkga
│       └── __init__.py
├── setup.cfg
└── setup.py

a $ cat pkgns/__init__.py 
__import__('pkg_resources').declare_namespace(__name__)

a $ cat setup.py 
from setuptools import find_packages, setup


setup(
    name='pkgns.pkga',
    version='1.0',
    namespace_packages=[
        'pkgns',
    ],
    packages=find_packages(),
)

# package b:
b $ tree
.
├── pkgns
│   ├── __init__.py
│   └── pkgb
│       └── __init__.py
├── setup.cfg
├── setup.py
└── show-bug.py

b $ cat pkgns/__init__.py 
import pkg_resources

pkg_resources.declare_namespace(__name__)

b $ cat setup.py 
from setuptools import find_packages, setup


setup(
    name='pkgns.pkgb',
    version='1.0',
    namespace_packages=[
        'pkgns',
    ],
    packages=find_packages(),
)

# this file will show a bug, pkg_resources import is necessary to see unexpected behaviour
b $ cat show-bug.py 
import pkg_resources

import pkgns.pkga

# create virtualenv with python2.7 in env27 and python3.6 in env36, install pkgns.pkga to both
# and see problem
b $ ./env36/bin/python show-bug.py 
Traceback (most recent call last):
  File "show-bug.py", line 3, in <module>
    import pkgns.pkga
ModuleNotFoundError: No module named 'pkgns.pkga'

b $ ./env27/bin/python show-bug.py 
b $ echo $?
0

As you can see pkgns.pkga installed in virtualenv was successfully imported under python2.7 and failed under python3.6. For me this is an issue because our code is structured similarly and some our packages contain helper functions to use in setup.py but this bug prevents me from using them. I had a workaround by using older setuptools to build helper packages but this is not a very reliable solution.

I investigated an issue further:

# with python2.7
b $ ./env27/bin/python 
Python 2.7.14 (default, Feb 20 2018, 17:13:09) 
[GCC 6.4.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.modules['pkgns'].__path__
['.../b/env27/lib/python2.7/site-packages/pkgns']
>>> import pkg_resources
>>> sys.modules['pkgns'].__path__
['.../b/pkgns', '.../b/env27/lib/python2.7/site-packages/pkgns']

# and with python3.6
b $ ./env36/bin/python 
Python 3.6.4 (default, Feb 20 2018, 20:29:59) 
[GCC 6.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.modules['pkgns'].__path__
_NamespacePath(['.../b/env36/lib/python3.6/site-packages/pkgns'])
>>> import pkg_resources
>>> sys.modules['pkgns'].__path__
['./pkgns']

So pkg_resources during import changed namespace package __path__ attribute in one case correctly and in second not - old path was lost. Also one may notice that under python3.6 original path is not list but _NamespacePath object and this gives us a key to understanding source of problem. Let's look at nspkg.pth file:

b $ cat env36/lib/python3.6/site-packages/pkgns.pkga-1.0-py2.7-nspkg.pth 
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('pkgns',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('pkgns', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('pkgns', [os.path.dirname(p)])));m = m or sys.modules.setdefault('pkgns', types.ModuleType('pkgns'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)

If python version is greater than 3.5 pkgns module entry is created using importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec(...)) function which gives real pep420 namespace package, pkg_resources on import adjusts namespace packages paths but handles only lists as original path in _rebuild_mod_path() in handle_ns() function (commit 7c0c39e to fix #885):

2141     if not isinstance(orig_path, list):
2142         # Is this behavior useful when module.__path__ is not a list?
2143         return
2144 
2145     orig_path.sort(key=position_in_sys_path)
2146     module.__path__[:] = [_normalize_cached(p) for p in orig_path]

So what happens briefly: pkg_resources tries to fix namespace packages paths on import, and inside _handle_ns() loads module (pkgns.pkgb in my example), after this pkgns' __path__ attribute points to package in current directory, orig_path points to old _NamespacePath object and _rebuild_mod_path does not handle this combination leaving new path not fixed.

Here are versions of software I used: setuptools-39.0.1, pip-9.0.3, python-2.7.14, python-3.6.4 and python-3.6.5.

I can see at least 2 ways to fix the issue - assign module.__path__ to orig_path when the latter is not a list or to convert it to list and proceed with sorting and normalizing, but I cannot currently predict consequences of both decisions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy