diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2014-01-27 11:02:52 -0500 |
---|---|---|
committer | Jason R. Coombs <jaraco@jaraco.com> | 2014-01-27 11:02:52 -0500 |
commit | 2c66805878d8f2b46241d8944e8b5af20eeeb9e1 (patch) | |
tree | a032cd7fb473c5c163e3920162bdfd6e4facc93a | |
parent | f547377479ff30d39a121b3ba1b753dcaf544eb2 (diff) | |
download | external_python_setuptools-2c66805878d8f2b46241d8944e8b5af20eeeb9e1.tar.gz external_python_setuptools-2c66805878d8f2b46241d8944e8b5af20eeeb9e1.tar.bz2 external_python_setuptools-2c66805878d8f2b46241d8944e8b5af20eeeb9e1.zip |
Backed out changeset: ef949e6e6de1, which was itself a backout of the fix for Distribute #323, so this backout restores that fix and also Fixes #141.
-rw-r--r-- | pkg_resources.py | 39 | ||||
-rw-r--r-- | setuptools/dist.py | 5 | ||||
-rw-r--r-- | setuptools/tests/test_easy_install.py | 141 |
3 files changed, 135 insertions, 50 deletions
diff --git a/pkg_resources.py b/pkg_resources.py index 4bc05e57..bde30989 100644 --- a/pkg_resources.py +++ b/pkg_resources.py @@ -504,7 +504,7 @@ class WorkingSet(object): seen[key]=1 yield self.by_key[key] - def add(self, dist, entry=None, insert=True): + def add(self, dist, entry=None, insert=True, replace=False): """Add `dist` to working set, associated with `entry` If `entry` is unspecified, it defaults to the ``.location`` of `dist`. @@ -512,8 +512,9 @@ class WorkingSet(object): set's ``.entries`` (if it wasn't already present). `dist` is only added to the working set if it's for a project that - doesn't already have a distribution in the set. If it's added, any - callbacks registered with the ``subscribe()`` method will be called. + doesn't already have a distribution in the set, unless `replace=True`. + If it's added, any callbacks registered with the ``subscribe()`` method + will be called. """ if insert: dist.insert_on(self.entries, entry) @@ -522,7 +523,7 @@ class WorkingSet(object): entry = dist.location keys = self.entry_keys.setdefault(entry,[]) keys2 = self.entry_keys.setdefault(dist.location,[]) - if dist.key in self.by_key: + if not replace and dist.key in self.by_key: return # ignore hidden distros self.by_key[dist.key] = dist @@ -532,7 +533,8 @@ class WorkingSet(object): keys2.append(dist.key) self._added_new(dist) - def resolve(self, requirements, env=None, installer=None): + def resolve(self, requirements, env=None, installer=None, + replace_conflicting=False): """List all distributions needed to (recursively) meet `requirements` `requirements` must be a sequence of ``Requirement`` objects. `env`, @@ -542,6 +544,12 @@ class WorkingSet(object): will be invoked with each requirement that cannot be met by an already-installed distribution; it should return a ``Distribution`` or ``None``. + + Unless `replace_conflicting=True`, raises a VersionConflict exception if + any requirements are found on the path that have the correct name but + the wrong version. Otherwise, if an `installer` is supplied it will be + invoked to obtain the correct version of the requirement and activate + it. """ requirements = list(requirements)[::-1] # set up the stack @@ -558,10 +566,18 @@ class WorkingSet(object): if dist is None: # Find the best distribution and add it to the map dist = self.by_key.get(req.key) - if dist is None: + if dist is None or (dist not in req and replace_conflicting): + ws = self if env is None: - env = Environment(self.entries) - dist = best[req.key] = env.best_match(req, self, installer) + if dist is None: + env = Environment(self.entries) + else: + # Use an empty environment and workingset to avoid + # any further conflicts with the conflicting + # distribution + env = Environment([]) + ws = WorkingSet([]) + dist = best[req.key] = env.best_match(req, ws, installer) if dist is None: #msg = ("The '%s' distribution was not found on this " # "system, and is required by this application.") @@ -1811,6 +1827,7 @@ def register_namespace_handler(importer_type, namespace_handler): def _handle_ns(packageName, path_item): """Ensure that named package includes a subpath of path_item (if needed)""" + importer = get_importer(path_item) if importer is None: return None @@ -1825,12 +1842,14 @@ def _handle_ns(packageName, path_item): elif not hasattr(module,'__path__'): raise TypeError("Not a package:", packageName) handler = _find_adapter(_namespace_handlers, importer) - subpath = handler(importer,path_item,packageName,module) + subpath = handler(importer, path_item, packageName, module) if subpath is not None: path = module.__path__ path.append(subpath) loader.load_module(packageName) - module.__path__ = path + for path_item in path: + if path_item not in module.__path__: + module.__path__.append(path_item) return subpath def declare_namespace(packageName): diff --git a/setuptools/dist.py b/setuptools/dist.py index 3126cb96..0801ae74 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -260,9 +260,10 @@ class Distribution(_Distribution): """Resolve pre-setup requirements""" from pkg_resources import working_set, parse_requirements for dist in working_set.resolve( - parse_requirements(requires), installer=self.fetch_build_egg + parse_requirements(requires), installer=self.fetch_build_egg, + replace_conflicting=True ): - working_set.add(dist) + working_set.add(dist, replace=True) def finalize_options(self): _Distribution.finalize_options(self) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index a90ae23f..8c797d55 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -19,8 +19,10 @@ from setuptools.command.easy_install import ( from setuptools.command.easy_install import PthDistributions from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution +from pkg_resources import working_set, VersionConflict from pkg_resources import Distribution as PRDistribution import setuptools.tests.server +import pkg_resources class FakeDist(object): def get_entry_map(self, group): @@ -230,47 +232,15 @@ class TestUserInstallTest(unittest.TestCase): SandboxViolation. """ - test_setup_attrs = { - 'name': 'test_pkg', 'version': '0.0', - 'setup_requires': ['foobar'], - 'dependency_links': [os.path.abspath(self.dir)] - } - - test_pkg = os.path.join(self.dir, 'test_pkg') + test_pkg = create_setup_requires_package(self.dir) test_setup_py = os.path.join(test_pkg, 'setup.py') - os.mkdir(test_pkg) - - f = open(test_setup_py, 'w') - f.write(textwrap.dedent("""\ - import setuptools - setuptools.setup(**%r) - """ % test_setup_attrs)) - f.close() - foobar_path = os.path.join(self.dir, 'foobar-0.1.tar.gz') - make_trivial_sdist( - foobar_path, - textwrap.dedent("""\ - import setuptools - setuptools.setup( - name='foobar', - version='0.1' - ) - """)) - - old_stdout = sys.stdout - old_stderr = sys.stderr - sys.stdout = StringIO() - sys.stderr = StringIO() try: - try: + with quiet_context(): with reset_setup_stop_context(): run_setup(test_setup_py, ['install']) - except SandboxViolation: - self.fail('Installation caused SandboxViolation') - finally: - sys.stdout = old_stdout - sys.stderr = old_stderr + except SandboxViolation: + self.fail('Installation caused SandboxViolation') class TestSetupRequires(unittest.TestCase): @@ -290,8 +260,10 @@ class TestSetupRequires(unittest.TestCase): # Some platforms (Jython) don't find a port to which to bind, # so skip this test for them. return - # create an sdist that has a build-time dependency. - with TestSetupRequires.create_sdist() as dist_file: + with quiet_context(): + # TODO: correct indentation here + # create an sdist that has a build-time dependency. + with TestSetupRequires.create_sdist() as dist_file: with tempdir_context() as temp_install_dir: with environment_context(PYTHONPATH=temp_install_dir): ei_params = ['--index-url', p_index.url, @@ -330,6 +302,81 @@ class TestSetupRequires(unittest.TestCase): """).lstrip()) yield dist_path + def test_setup_requires_overrides_version_conflict(self): + """ + Regression test for issue #323. + + Ensures that a distribution's setup_requires requirements can still be + installed and used locally even if a conflicting version of that + requirement is already on the path. + """ + + pr_state = pkg_resources.__getstate__() + fake_dist = PRDistribution('does-not-matter', project_name='foobar', + version='0.0') + working_set.add(fake_dist) + + def setup_and_run(temp_dir): + test_pkg = create_setup_requires_package(temp_dir) + test_setup_py = os.path.join(test_pkg, 'setup.py') + try: + stdout, stderr = quiet_context( + lambda: reset_setup_stop_context( + # Don't even need to install the package, just running + # the setup.py at all is sufficient + lambda: run_setup(test_setup_py, ['--name']) + )) + except VersionConflict: + self.fail('Installing setup.py requirements caused ' + 'VersionConflict') + + lines = stdout.splitlines() + self.assertTrue(len(lines) > 0) + self.assertTrue(lines[-1].strip(), 'test_pkg') + + try: + tempdir_context(setup_and_run) + finally: + pkg_resources.__setstate__(pr_state) + + +def create_setup_requires_package(path): + """Creates a source tree under path for a trivial test package that has a + single requirement in setup_requires--a tarball for that requirement is + also created and added to the dependency_links argument. + """ + + test_setup_attrs = { + 'name': 'test_pkg', 'version': '0.0', + 'setup_requires': ['foobar==0.1'], + 'dependency_links': [os.path.abspath(path)] + } + + test_pkg = os.path.join(path, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + + f = open(test_setup_py, 'w') + f.write(textwrap.dedent("""\ + import setuptools + setuptools.setup(**%r) + """ % test_setup_attrs)) + f.close() + + foobar_path = os.path.join(path, 'foobar-0.1.tar.gz') + make_trivial_sdist( + foobar_path, + textwrap.dedent("""\ + import setuptools + setuptools.setup( + name='foobar', + version='0.1' + ) + """)) + + return test_pkg + def make_trivial_sdist(dist_path, setup_py): """Create a simple sdist tarball at dist_path, containing just a @@ -392,3 +439,21 @@ def reset_setup_stop_context(): distutils.core._setup_stop_after = None yield distutils.core._setup_stop_after = setup_stop_after + + +@contextlib.contextmanager +def quiet_context(): + """ + Redirect stdout/stderr to StringIO objects to prevent console output from + distutils commands. + """ + + old_stdout = sys.stdout + old_stderr = sys.stderr + new_stdout = sys.stdout = StringIO.StringIO() + new_stderr = sys.stderr = StringIO.StringIO() + try: + yield new_stdout, new_stderr + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr |