:mod:`distlib.database` --- Database of installed distributions
=================================================================

.. module:: distlib.database
   :synopsis: Functions to query and manipulate installed distributions.

This module provides an implementation of :PEP:`376`.

.. warning:: This documentation has not been updated since being copied over
   from ``distutils2`` and may not be up to date.

Installed Python distributions are represented by instances of
:class:`Distribution`, or :class:`EggInfoDistribution` for legacy egg formats.
Most functions also provide an extra argument ``use_egg_info`` to take legacy
distributions into account.

For the purpose of this module, "installed" means that the distribution's
:file:`.dist-info` or :file:`.egg-info` or :file:`egg` directory is found
on :data:`sys.path`.  For example, if the parent directory of a
:file:`dist-info` directory is added to :envvar:`PYTHONPATH`, then it will be
available in the database.

Classes representing installed distributions
--------------------------------------------

.. class:: InstalledDistribution(path)

   Class representing an installed distribution.

   Instantiate with the *path* to a ``.dist-info`` directory.  Instances can be
   compared and sorted.  Other available methods are:

   .. XXX describe how comparison works

   .. method:: get_distinfo_file(path, binary=False)

      Return a read-only file object for a file located at
      :file:`{project}-{version}.dist-info/{path}`.  *path* should be a
      ``'/'``-separated path relative to the ``.dist-info`` directory or an
      absolute path; if it is an absolute path and doesn't start with the path
      to the :file:`.dist-info` directory, a :class:`DistlibException` is
      raised.

      If *binary* is ``True``, the file is opened in binary mode.

   .. method:: get_resource_path(relative_path)

      .. TODO

   .. method:: list_distinfo_files()

      Return an iterator over all files located in the :file:`.dist-info`
      directory.  Each returned path is transformed into a local absolute path.

   .. method::  list_installed_files()

      Iterate over the files installed with the distribution and registered in
      the :file:`RECORD` file and yield a tuple ``(path, md5, size)`` for each
      line.  Each returned path is transformed into a local absolute path.

      A local absolute path is an absolute path in which occurrences of ``'/'``
      have been replaced by :data:`os.sep`.

   Available attributes:

   .. attribute:: metadata

      Instance of :class:`distlib.metadata.Metadata` filled with the contents
      of the :file:`{project}-{version}.dist-info/METADATA` file.

   .. attribute:: name

      Shortcut for ``metadata['Name']``.

   .. attribute:: version

      Shortcut for ``metadata['Version']``.

   .. attribute:: requested

      Boolean indicating whether this distribution was requested by the user of
      automatically installed as a dependency.

.. class:: EggInfoDistribution(path)

   Class representing a legacy distribution.  It is compatible with distutils'
   and setuptools' :file:`.egg-info` and :file:`.egg` files and directories.

   .. FIXME should be named EggDistribution

   Instantiate with the *path* to an egg file or directory. Instances can be
   compared and sorted. Other available methods are:

   .. method:: list_installed_files()

   Available attributes:

   .. attribute:: metadata

      Instance of :class:`distlib.metadata.Metadata` filled with the contents
      of the :file:`{project-version}.egg-info/PKG-INFO` or
      :file:`{project-version}.egg` file.

   .. attribute:: name

      Shortcut for ``metadata['Name']``.

   .. attribute:: version

      Shortcut for ``metadata['Version']``.

Functions to work with the database
-----------------------------------

.. function:: get_distribution(name, use_egg_info=False, paths=None)

   Return an instance of :class:`Distribution` or :class:`EggInfoDistribution`
   for the first installed distribution matching *name*.  Egg distributions are
   considered only if *use_egg_info* is true; if both a dist-info and an egg
   file are found, the dist-info prevails.  The directories to be searched are
   given in *paths*, which defaults to :data:`sys.path`. Returns ``None`` if no
   matching distribution is found.

   .. FIXME param should be named use_egg

.. function:: get_distributions(use_egg_info=False, paths=None)

   Return an iterator of :class:`Distribution` instances for all installed
   distributions found in *paths* (defaults to :data:`sys.path`).  If
   *use_egg_info* is true, also return instances of :class:`EggInfoDistribution`
   for legacy distributions found.

.. function:: get_file_users(path)

   Return an iterator over all distributions using *path*, a local absolute path
   or a relative ``'/'``-separated path.

   .. XXX does this work with prefixes or full file path only?

.. function:: provides_distribution(name, version=None, use_egg_info=False)

   Return an iterator over all distributions that declare they provide *name*.
   *version* is an optional argument to match only specific releases (see
   :mod:`distlib.version`).  If *use_egg_info* is true, legacy egg
   distributions will be considered as well.

Utility functions
-----------------

.. function:: distinfo_dirname(name, version)

   Escape *name* and *version* into a filename-safe form and return the
   directory name built from them, for example
   :file:`{safename}-{safeversion}.dist-info`. In *name*, runs of
   non-alphanumeric characters are replaced with one ``'_'``; in *version*,
   spaces become dots, and runs of other non-alphanumeric characters (except
   dots) are replaced by one ``'-'``.

   .. XXX wth spaces in version numbers?

For performance purposes, the list of distributions is being internally
cached. Caching is enabled by default, but you can control it with these
functions:

.. function:: clear_cache()

   Clear the cache.

.. function:: disable_cache()

   Disable the cache, without clearing it.

.. function:: enable_cache()

   Enable the internal cache, without clearing it.

Examples
--------

Printing all information about a distribution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Given the name of an installed distribution, we shall print out all
information that can be obtained using functions provided in this module::

    import sys
    import distlib.database

    try:
        name = sys.argv[1]
    except ValueError:
        sys.exit('Not enough arguments')

    # first create the Distribution instance
    dist = distlib.database.Distribution(path)
    if dist is None:
        sys.exit('No such distribution')

    print('Information about %r' % dist.name)
    print()

    print('Files')
    print('=====')
    for path, md5, size in dist.list_installed_files():
        print('* Path: %s' % path)
        print('  Hash %s, Size: %s bytes' % (md5, size))
    print()

    print('Metadata')
    print('========')
    for key, value in dist.metadata.items():
        print('%20s: %s' % (key, value))
    print()

    print('Extra')
    print('=====')
    if dist.requested:
        print('* It was installed by user request')
    else:
        print('* It was installed as a dependency')

If we save the script above as ``print_info.py``, we can use it to extract
information from a :file:`.dist-info` directory. By typing in the console:

.. code-block:: sh

    python print_info.py choxie

we get the following output:

.. code-block:: none

   Information about 'choxie'

   Files
   =====
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9/truffles.py
     Hash 5e052db6a478d06bad9ae033e6bc08af, Size: 111 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py
     Hash ac56bf496d8d1d26f866235b95f31030, Size: 214 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py
     Hash 416aab08dfa846f473129e89a7625bbc, Size: 25 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9.dist-info/INSTALLER
     Hash d41d8cd98f00b204e9800998ecf8427e, Size: 0 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA
     Hash 696a209967fef3c8b8f5a7bb10386385, Size: 225 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9.dist-info/REQUESTED
     Hash d41d8cd98f00b204e9800998ecf8427e, Size: 0 bytes
   * Path: ../tmp/distutils2/tests/fake_dists/choxie-2.0.0.9.dist-info/RECORD
     Hash None, Size: None bytes

   Metadata
   ========
       Metadata-Version: 1.2
                   Name: choxie
                Version: 2.0.0.9
               Platform: []
     Supported-Platform: UNKNOWN
                Summary: Chocolate with a kick!
            Description: UNKNOWN
               Keywords: []
              Home-page: UNKNOWN
                 Author: UNKNOWN
           Author-email: UNKNOWN
             Maintainer: UNKNOWN
       Maintainer-email: UNKNOWN
                License: UNKNOWN
             Classifier: []
           Download-URL: UNKNOWN
         Obsoletes-Dist: ['truffles (<=0.8,>=0.5)', 'truffles (<=0.9,>=0.6)']
            Project-URL: []
          Provides-Dist: ['truffles (1.0)']
          Requires-Dist: ['towel-stuff (0.1)']
        Requires-Python: UNKNOWN
      Requires-External: []

   Extra
   =====
   * It was installed as a dependency

Getting metadata about a distribution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sometimes you're not interested about the packaging information contained in a
full :class:`Distribution` object but just want to do something with its
:attr:`~Distribution.metadata`::

   >>> from distlib.database import get_distribution
   >>> info = get_distribution('chocolate').metadata
   >>> info['Keywords']
   ['cooking', 'happiness']
   >>>
