ยปCore Development>Code coverage>Lib/packaging/install.py

Python code coverage for Lib/packaging/install.py

#countcontent
1n/a"""Building blocks for installers.
2n/a
3n/aWhen used as a script, this module installs a release thanks to info
4n/aobtained from an index (e.g. PyPI), with dependencies.
5n/a
6n/aThis is a higher-level module built on packaging.database and
7n/apackaging.pypi.
8n/a"""
9n/aimport os
10n/aimport sys
11n/aimport stat
12n/aimport errno
13n/aimport shutil
14n/aimport logging
15n/aimport tempfile
16n/afrom sysconfig import get_config_var, get_path, is_python_build
17n/a
18n/afrom packaging import logger
19n/afrom packaging.dist import Distribution
20n/afrom packaging.util import (_is_archive_file, ask, get_install_method,
21n/a egginfo_to_distinfo)
22n/afrom packaging.pypi import wrapper
23n/afrom packaging.version import get_version_predicate
24n/afrom packaging.database import get_distributions, get_distribution
25n/afrom packaging.depgraph import generate_graph
26n/a
27n/afrom packaging.errors import (PackagingError, InstallationException,
28n/a InstallationConflict, CCompilerError)
29n/afrom packaging.pypi.errors import ProjectNotFound, ReleaseNotFound
30n/afrom packaging import database
31n/a
32n/a
33n/a__all__ = ['install_dists', 'install_from_infos', 'get_infos', 'remove',
34n/a 'install', 'install_local_project']
35n/a
36n/a
37n/adef _move_files(files, destination):
38n/a """Move the list of files in the destination folder, keeping the same
39n/a structure.
40n/a
41n/a Return a list of tuple (old, new) emplacement of files
42n/a
43n/a :param files: a list of files to move.
44n/a :param destination: the destination directory to put on the files.
45n/a """
46n/a
47n/a for old in files:
48n/a filename = os.path.split(old)[-1]
49n/a new = os.path.join(destination, filename)
50n/a # try to make the paths.
51n/a try:
52n/a os.makedirs(os.path.dirname(new))
53n/a except OSError as e:
54n/a if e.errno != errno.EEXIST:
55n/a raise
56n/a os.rename(old, new)
57n/a yield old, new
58n/a
59n/a
60n/adef _run_distutils_install(path):
61n/a # backward compat: using setuptools or plain-distutils
62n/a cmd = '%s setup.py install --record=%s'
63n/a record_file = os.path.join(path, 'RECORD')
64n/a os.system(cmd % (sys.executable, record_file))
65n/a if not os.path.exists(record_file):
66n/a raise ValueError('failed to install')
67n/a else:
68n/a egginfo_to_distinfo(record_file, remove_egginfo=True)
69n/a
70n/a
71n/adef _run_setuptools_install(path):
72n/a cmd = '%s setup.py install --record=%s --single-version-externally-managed'
73n/a record_file = os.path.join(path, 'RECORD')
74n/a
75n/a os.system(cmd % (sys.executable, record_file))
76n/a if not os.path.exists(record_file):
77n/a raise ValueError('failed to install')
78n/a else:
79n/a egginfo_to_distinfo(record_file, remove_egginfo=True)
80n/a
81n/a
82n/adef _run_packaging_install(path):
83n/a # XXX check for a valid setup.cfg?
84n/a dist = Distribution()
85n/a dist.parse_config_files()
86n/a try:
87n/a dist.run_command('install_dist')
88n/a name = dist.metadata['Name']
89n/a return database.get_distribution(name) is not None
90n/a except (IOError, os.error, PackagingError, CCompilerError) as msg:
91n/a raise ValueError("Failed to install, " + str(msg))
92n/a
93n/a
94n/adef _install_dist(dist, path):
95n/a """Install a distribution into a path.
96n/a
97n/a This:
98n/a
99n/a * unpack the distribution
100n/a * copy the files in "path"
101n/a * determine if the distribution is packaging or distutils1.
102n/a """
103n/a where = dist.unpack()
104n/a
105n/a if where is None:
106n/a raise ValueError('Cannot locate the unpacked archive')
107n/a
108n/a return _run_install_from_archive(where)
109n/a
110n/a
111n/adef install_local_project(path):
112n/a """Install a distribution from a source directory.
113n/a
114n/a If the source directory contains a setup.py install using distutils1.
115n/a If a setup.cfg is found, install using the install_dist command.
116n/a
117n/a Returns True on success, False on Failure.
118n/a """
119n/a path = os.path.abspath(path)
120n/a if os.path.isdir(path):
121n/a logger.info('Installing from source directory: %r', path)
122n/a return _run_install_from_dir(path)
123n/a elif _is_archive_file(path):
124n/a logger.info('Installing from archive: %r', path)
125n/a _unpacked_dir = tempfile.mkdtemp()
126n/a try:
127n/a shutil.unpack_archive(path, _unpacked_dir)
128n/a return _run_install_from_archive(_unpacked_dir)
129n/a finally:
130n/a shutil.rmtree(_unpacked_dir)
131n/a else:
132n/a logger.warning('No project to install.')
133n/a return False
134n/a
135n/a
136n/adef _run_install_from_archive(source_dir):
137n/a # XXX need a better way
138n/a for item in os.listdir(source_dir):
139n/a fullpath = os.path.join(source_dir, item)
140n/a if os.path.isdir(fullpath):
141n/a source_dir = fullpath
142n/a break
143n/a return _run_install_from_dir(source_dir)
144n/a
145n/a
146n/ainstall_methods = {
147n/a 'packaging': _run_packaging_install,
148n/a 'setuptools': _run_setuptools_install,
149n/a 'distutils': _run_distutils_install}
150n/a
151n/a
152n/adef _run_install_from_dir(source_dir):
153n/a old_dir = os.getcwd()
154n/a os.chdir(source_dir)
155n/a install_method = get_install_method(source_dir)
156n/a func = install_methods[install_method]
157n/a try:
158n/a func = install_methods[install_method]
159n/a try:
160n/a func(source_dir)
161n/a return True
162n/a except ValueError as err:
163n/a # failed to install
164n/a logger.info(str(err))
165n/a return False
166n/a finally:
167n/a os.chdir(old_dir)
168n/a
169n/a
170n/adef install_dists(dists, path, paths=None):
171n/a """Install all distributions provided in dists, with the given prefix.
172n/a
173n/a If an error occurs while installing one of the distributions, uninstall all
174n/a the installed distribution (in the context if this function).
175n/a
176n/a Return a list of installed dists.
177n/a
178n/a :param dists: distributions to install
179n/a :param path: base path to install distribution in
180n/a :param paths: list of paths (defaults to sys.path) to look for info
181n/a """
182n/a
183n/a installed_dists = []
184n/a for dist in dists:
185n/a logger.info('Installing %r %s...', dist.name, dist.version)
186n/a try:
187n/a _install_dist(dist, path)
188n/a installed_dists.append(dist)
189n/a except Exception as e:
190n/a logger.info('Failed: %s', e)
191n/a
192n/a # reverting
193n/a for installed_dist in installed_dists:
194n/a logger.info('Reverting %r', installed_dist)
195n/a remove(installed_dist.name, paths)
196n/a raise e
197n/a return installed_dists
198n/a
199n/a
200n/adef install_from_infos(install_path=None, install=[], remove=[], conflicts=[],
201n/a paths=None):
202n/a """Install and remove the given distributions.
203n/a
204n/a The function signature is made to be compatible with the one of get_infos.
205n/a The aim of this script is to povide a way to install/remove what's asked,
206n/a and to rollback if needed.
207n/a
208n/a So, it's not possible to be in an inconsistant state, it could be either
209n/a installed, either uninstalled, not half-installed.
210n/a
211n/a The process follow those steps:
212n/a
213n/a 1. Move all distributions that will be removed in a temporary location
214n/a 2. Install all the distributions that will be installed in a temp. loc.
215n/a 3. If the installation fails, rollback (eg. move back) those
216n/a distributions, or remove what have been installed.
217n/a 4. Else, move the distributions to the right locations, and remove for
218n/a real the distributions thats need to be removed.
219n/a
220n/a :param install_path: the installation path where we want to install the
221n/a distributions.
222n/a :param install: list of distributions that will be installed; install_path
223n/a must be provided if this list is not empty.
224n/a :param remove: list of distributions that will be removed.
225n/a :param conflicts: list of conflicting distributions, eg. that will be in
226n/a conflict once the install and remove distribution will be
227n/a processed.
228n/a :param paths: list of paths (defaults to sys.path) to look for info
229n/a """
230n/a # first of all, if we have conflicts, stop here.
231n/a if conflicts:
232n/a raise InstallationConflict(conflicts)
233n/a
234n/a if install and not install_path:
235n/a raise ValueError("Distributions are to be installed but `install_path`"
236n/a " is not provided.")
237n/a
238n/a # before removing the files, we will start by moving them away
239n/a # then, if any error occurs, we could replace them in the good place.
240n/a temp_files = {} # contains lists of {dist: (old, new)} paths
241n/a temp_dir = None
242n/a if remove:
243n/a temp_dir = tempfile.mkdtemp()
244n/a for dist in remove:
245n/a files = dist.list_installed_files()
246n/a temp_files[dist] = _move_files(files, temp_dir)
247n/a try:
248n/a if install:
249n/a install_dists(install, install_path, paths)
250n/a except:
251n/a # if an error occurs, put back the files in the right place.
252n/a for files in temp_files.values():
253n/a for old, new in files:
254n/a shutil.move(new, old)
255n/a if temp_dir:
256n/a shutil.rmtree(temp_dir)
257n/a # now re-raising
258n/a raise
259n/a
260n/a # we can remove them for good
261n/a for files in temp_files.values():
262n/a for old, new in files:
263n/a os.remove(new)
264n/a if temp_dir:
265n/a shutil.rmtree(temp_dir)
266n/a
267n/a
268n/adef _get_setuptools_deps(release):
269n/a # NotImplementedError
270n/a pass
271n/a
272n/a
273n/adef get_infos(requirements, index=None, installed=None, prefer_final=True):
274n/a """Return the informations on what's going to be installed and upgraded.
275n/a
276n/a :param requirements: is a *string* containing the requirements for this
277n/a project (for instance "FooBar 1.1" or "BarBaz (<1.2)")
278n/a :param index: If an index is specified, use this one, otherwise, use
279n/a :class index.ClientWrapper: to get project metadatas.
280n/a :param installed: a list of already installed distributions.
281n/a :param prefer_final: when picking up the releases, prefer a "final" one
282n/a over a beta/alpha/etc one.
283n/a
284n/a The results are returned in a dict, containing all the operations
285n/a needed to install the given requirements::
286n/a
287n/a >>> get_install_info("FooBar (<=1.2)")
288n/a {'install': [<FooBar 1.1>], 'remove': [], 'conflict': []}
289n/a
290n/a Conflict contains all the conflicting distributions, if there is a
291n/a conflict.
292n/a """
293n/a # this function does several things:
294n/a # 1. get a release specified by the requirements
295n/a # 2. gather its metadata, using setuptools compatibility if needed
296n/a # 3. compare this tree with what is currently installed on the system,
297n/a # return the requirements of what is missing
298n/a # 4. do that recursively and merge back the results
299n/a # 5. return a dict containing information about what is needed to install
300n/a # or remove
301n/a
302n/a if not installed:
303n/a logger.debug('Reading installed distributions')
304n/a installed = list(get_distributions(use_egg_info=True))
305n/a
306n/a infos = {'install': [], 'remove': [], 'conflict': []}
307n/a # Is a compatible version of the project already installed ?
308n/a predicate = get_version_predicate(requirements)
309n/a found = False
310n/a
311n/a # check that the project isn't already installed
312n/a for installed_project in installed:
313n/a # is it a compatible project ?
314n/a if predicate.name.lower() != installed_project.name.lower():
315n/a continue
316n/a found = True
317n/a logger.info('Found %r %s', installed_project.name,
318n/a installed_project.version)
319n/a
320n/a # if we already have something installed, check it matches the
321n/a # requirements
322n/a if predicate.match(installed_project.version):
323n/a return infos
324n/a break
325n/a
326n/a if not found:
327n/a logger.debug('Project not installed')
328n/a
329n/a if not index:
330n/a index = wrapper.ClientWrapper()
331n/a
332n/a if not installed:
333n/a installed = get_distributions(use_egg_info=True)
334n/a
335n/a # Get all the releases that match the requirements
336n/a try:
337n/a release = index.get_release(requirements)
338n/a except (ReleaseNotFound, ProjectNotFound):
339n/a raise InstallationException('Release not found: %r' % requirements)
340n/a
341n/a if release is None:
342n/a logger.info('Could not find a matching project')
343n/a return infos
344n/a
345n/a metadata = release.fetch_metadata()
346n/a
347n/a # we need to build setuptools deps if any
348n/a if 'requires_dist' not in metadata:
349n/a metadata['requires_dist'] = _get_setuptools_deps(release)
350n/a
351n/a # build the dependency graph with local and required dependencies
352n/a dists = list(installed)
353n/a dists.append(release)
354n/a depgraph = generate_graph(dists)
355n/a
356n/a # Get what the missing deps are
357n/a dists = depgraph.missing[release]
358n/a if dists:
359n/a logger.info("Missing dependencies found, retrieving metadata")
360n/a # we have missing deps
361n/a for dist in dists:
362n/a _update_infos(infos, get_infos(dist, index, installed))
363n/a
364n/a # Fill in the infos
365n/a existing = [d for d in installed if d.name == release.name]
366n/a if existing:
367n/a infos['remove'].append(existing[0])
368n/a infos['conflict'].extend(depgraph.reverse_list[existing[0]])
369n/a infos['install'].append(release)
370n/a return infos
371n/a
372n/a
373n/adef _update_infos(infos, new_infos):
374n/a """extends the lists contained in the `info` dict with those contained
375n/a in the `new_info` one
376n/a """
377n/a for key, value in infos.items():
378n/a if key in new_infos:
379n/a infos[key].extend(new_infos[key])
380n/a
381n/a
382n/adef remove(project_name, paths=None, auto_confirm=True):
383n/a """Removes a single project from the installation.
384n/a
385n/a Returns True on success
386n/a """
387n/a dist = get_distribution(project_name, use_egg_info=True, paths=paths)
388n/a if dist is None:
389n/a raise PackagingError('Distribution %r not found' % project_name)
390n/a files = dist.list_installed_files(local=True)
391n/a rmdirs = []
392n/a rmfiles = []
393n/a tmp = tempfile.mkdtemp(prefix=project_name + '-uninstall')
394n/a
395n/a def _move_file(source, target):
396n/a try:
397n/a os.rename(source, target)
398n/a except OSError as err:
399n/a return err
400n/a return None
401n/a
402n/a success = True
403n/a error = None
404n/a try:
405n/a for file_, md5, size in files:
406n/a if os.path.isfile(file_):
407n/a dirname, filename = os.path.split(file_)
408n/a tmpfile = os.path.join(tmp, filename)
409n/a try:
410n/a error = _move_file(file_, tmpfile)
411n/a if error is not None:
412n/a success = False
413n/a break
414n/a finally:
415n/a if not os.path.isfile(file_):
416n/a os.rename(tmpfile, file_)
417n/a if file_ not in rmfiles:
418n/a rmfiles.append(file_)
419n/a if dirname not in rmdirs:
420n/a rmdirs.append(dirname)
421n/a finally:
422n/a shutil.rmtree(tmp)
423n/a
424n/a if not success:
425n/a logger.info('%r cannot be removed.', project_name)
426n/a logger.info('Error: %s', error)
427n/a return False
428n/a
429n/a logger.info('Removing %r: ', project_name)
430n/a
431n/a for file_ in rmfiles:
432n/a logger.info(' %s', file_)
433n/a
434n/a # Taken from the pip project
435n/a if auto_confirm:
436n/a response = 'y'
437n/a else:
438n/a response = ask('Proceed (y/n)? ', ('y', 'n'))
439n/a
440n/a if response == 'y':
441n/a file_count = 0
442n/a for file_ in rmfiles:
443n/a os.remove(file_)
444n/a file_count += 1
445n/a
446n/a dir_count = 0
447n/a for dirname in rmdirs:
448n/a if not os.path.exists(dirname):
449n/a # could
450n/a continue
451n/a
452n/a files_count = 0
453n/a for root, dir, files in os.walk(dirname):
454n/a files_count += len(files)
455n/a
456n/a if files_count > 0:
457n/a # XXX Warning
458n/a continue
459n/a
460n/a # empty dirs with only empty dirs
461n/a if os.stat(dirname).st_mode & stat.S_IWUSR:
462n/a # XXX Add a callable in shutil.rmtree to count
463n/a # the number of deleted elements
464n/a shutil.rmtree(dirname)
465n/a dir_count += 1
466n/a
467n/a # removing the top path
468n/a # XXX count it ?
469n/a if os.path.exists(dist.path):
470n/a shutil.rmtree(dist.path)
471n/a
472n/a logger.info('Success: removed %d files and %d dirs',
473n/a file_count, dir_count)
474n/a
475n/a return True
476n/a
477n/a
478n/adef install(project):
479n/a """Installs a project.
480n/a
481n/a Returns True on success, False on failure
482n/a """
483n/a if is_python_build():
484n/a # Python would try to install into the site-packages directory under
485n/a # $PREFIX, but when running from an uninstalled code checkout we don't
486n/a # want to create directories under the installation root
487n/a message = ('installing third-party projects from an uninstalled '
488n/a 'Python is not supported')
489n/a logger.error(message)
490n/a return False
491n/a
492n/a logger.info('Checking the installation location...')
493n/a purelib_path = get_path('purelib')
494n/a
495n/a # trying to write a file there
496n/a try:
497n/a with tempfile.NamedTemporaryFile(suffix=project,
498n/a dir=purelib_path) as testfile:
499n/a testfile.write(b'test')
500n/a except OSError:
501n/a # FIXME this should check the errno, or be removed altogether (race
502n/a # condition: the directory permissions could be changed between here
503n/a # and the actual install)
504n/a logger.info('Unable to write in "%s". Do you have the permissions ?'
505n/a % purelib_path)
506n/a return False
507n/a
508n/a logger.info('Getting information about %r...', project)
509n/a try:
510n/a info = get_infos(project)
511n/a except InstallationException:
512n/a logger.info('Cound not find %r', project)
513n/a return False
514n/a
515n/a if info['install'] == []:
516n/a logger.info('Nothing to install')
517n/a return False
518n/a
519n/a install_path = get_config_var('base')
520n/a try:
521n/a install_from_infos(install_path,
522n/a info['install'], info['remove'], info['conflict'])
523n/a
524n/a except InstallationConflict as e:
525n/a if logger.isEnabledFor(logging.INFO):
526n/a projects = ('%r %s' % (p.name, p.version) for p in e.args[0])
527n/a logger.info('%r conflicts with %s', project, ','.join(projects))
528n/a
529n/a return True