pip icon indicating copy to clipboard operation
pip copied to clipboard

Suspected module collision when introducing namespace package as dependency of package

Open connor-mccarthy opened this issue 2 years ago • 2 comments

Description

We extracted a module of our package into a namespace package. When upgrading to the new version without first manually uninstalling the existing version, there is an error.

Specifically...

kfp==2.0.1 kfp has a sub-module dsl. from kfp import dsl works fine.

kfp==2.1.2 kfp takes a dependency on a namespace package kfp-dsl, which is released separately. from kfp import dsl still works fine if the user is not installing kfp==2.1.2 on top of an existing kfp==2.0.1 install (e.g., runs pip uninstall kfp first).

Problem When kfp==2.0.1 is already installed, running pip install kfp==2.1.2 doesn't put the package into a valid state.

Expected behavior

When kfp==2.0.1 is installed, pip install kfp==2.1.2 should put the package into a valid state.

pip version

23.2.1

Python version

3.7

OS

MacOS 13.5

How to Reproduce

Success:

  1. pip install --upgrade pip
  2. pip install kfp==2.0.1
  3. pip uninstall kfp==2.0.1
  4. pip install kfp==2.1.2
  5. python -c 'import kfp'

Failure:

  1. pip install --upgrade pip
  2. pip install kfp==2.0.1
  3. pip install kfp==2.1.2
  4. python -c 'import kfp'

Output

Success: Nothing

Failure:

AttributeError: module 'kfp.dsl.structures' has no attribute 'InputSpec'

Code of Conduct

connor-mccarthy avatar Aug 08 '23 02:08 connor-mccarthy

FYI @chensun

connor-mccarthy avatar Aug 08 '23 02:08 connor-mccarthy

The same happens for top-level files without namespace packages.

Let's say that a package a ships foo.py in version 0.1.0. In version 0.2.0 it splits into a and b, foo.py moves to b (again to the root). Now installing a==0.2.0 produces different results depending on whether a==0.1.0 was previously installed in the environment. If it was, foo.py is gone forever until b is force-reinstalled.

Full repro script:

cd "$(mktemp -d)" && python -m venv .venv && . .venv/bin/activate
mkdir a
# mkdir: created directory 'a'
echo 'FOO = 1' >a/foo.py
echo 'from setuptools import setup; setup(name="a", version="0.1.0")' >| a/setup.py
python -m pip install ./a
# Processing ./a
#   Installing build dependencies ... done
#   Getting requirements to build wheel ... done
#   Preparing metadata (pyproject.toml) ... done
# Building wheels for collected packages: a
#   Building wheel for a (pyproject.toml) ... done
#   Created wheel for a: filename=a-0.1.0-py3-none-any.whl size=1006 sha256=282c74d75d8ccf9053d009b7977b580da6984bdaa84340143caecbddf4d749e7
#   Stored in directory: /tmp/pip-ephem-wheel-cache-1asno282/wheels/a7/45/16/1ca4d44f0d6b35a2c9e7c81a47f28a06ec9172a177bb4e557f
# Successfully built a
# Installing collected packages: a
# Successfully installed a-0.1.0
ls -la .venv/lib/python*/site-packages/
# total 4
# drwxrwxr-x 6 stas stas 140 Oct  6 02:43 .
# drwxrwxr-x 3 stas stas  60 Oct  6 02:42 ..
# drwxrwxr-x 2 stas stas 180 Oct  6 02:43 a-0.1.0.dist-info
# -rw-rw-r-- 1 stas stas   8 Oct  6 02:43 foo.py
# drwxrwxr-x 5 stas stas 180 Oct  6 02:42 pip
# drwxrwxr-x 2 stas stas 220 Oct  6 02:42 pip-24.0.dist-info
# drwxrwxr-x 2 stas stas  60 Oct  6 02:43 __pycache__
mkdir b
# mkdir: created directory 'b'
rm a/foo.py
echo 'FOO = 2' >b/foo.py
echo "from setuptools import setup; setup(name='a', version='0.2.0', install_requires=['b @ file://$(pwd)/b'])" >| a/setup.py
echo 'from setuptools import setup; setup(name="b", version="0.1.0")' >| b/setup.py
python -m pip install ./a
# Processing ./a
#   Installing build dependencies ... done
#   Getting requirements to build wheel ... done
#   Preparing metadata (pyproject.toml) ... done
# Processing ./b (from a==0.2.0)
#   Installing build dependencies ... done
#   Getting requirements to build wheel ... done
#   Preparing metadata (pyproject.toml) ... done
# Building wheels for collected packages: a, b
#   Building wheel for a (pyproject.toml) ... done
#   Created wheel for a: filename=a-0.2.0-py3-none-any.whl size=929 sha256=1f91a5775368d3346f05a97e41ebf1e975c0bc046b8a9f45c76500c489f7b87a
#   Stored in directory: /tmp/pip-ephem-wheel-cache-c5zox3hn/wheels/a7/45/16/1ca4d44f0d6b35a2c9e7c81a47f28a06ec9172a177bb4e557f
#   Building wheel for b (pyproject.toml) ... done
#   Created wheel for b: filename=b-0.1.0-py3-none-any.whl size=1007 sha256=5d7c58fdb8263a95f988a505d3143bbaad99ded208d40ffd742820adb01d7d6f
#   Stored in directory: /tmp/pip-ephem-wheel-cache-c5zox3hn/wheels/38/63/e1/deb3a056150192cf9560e6d17743362c5256bfc6b244b6bdf5
# Successfully built a b
# Installing collected packages: b, a
#   Attempting uninstall: a
#     Found existing installation: a 0.1.0
#     Uninstalling a-0.1.0:
#       Successfully uninstalled a-0.1.0
# Successfully installed a-0.2.0 b-0.1.0
ls -la .venv/lib/python*/site-packages/
# total 0
# drwxrwxr-x 6 stas stas 120 Oct  6 02:44 .
# drwxrwxr-x 3 stas stas  60 Oct  6 02:42 ..
# drwxrwxr-x 2 stas stas 180 Oct  6 02:44 a-0.2.0.dist-info
# drwxrwxr-x 2 stas stas 160 Oct  6 02:44 b-0.1.0.dist-info
# drwxrwxr-x 5 stas stas 180 Oct  6 02:42 pip
# drwxrwxr-x 2 stas stas 220 Oct  6 02:42 pip-24.0.dist-info

This has just hit mypy (python/mypy#20006) - we shipped a shared library with a previous release, and then it was moved to a separate package librt. Installing compiled dev mypy version now produces a broken install if mypy==1.18.2 was present in the environment.

sterliakov avatar Oct 06 '25 00:10 sterliakov