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

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


This module provides an implementation of :PEP:`376`.  It was originally
intended to land in :mod:`pkgutil`, but with the inclusion of Packaging in the
standard library, it was thought best to include it in a submodule of
:mod:`packaging`, leaving :mod:`pkgutil` to deal with imports.

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.


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

.. class:: Distribution(path)

   Class representing an installed distribution.  It is different from
   :class:`packaging.dist.Distribution` which holds the list of files, the
   metadata and options during the run of a Packaging command.

   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:`PackagingError` is raised.

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

   .. method:: get_resource_path(relative_path)

      .. TODO

   .. method:: list_distinfo_files(local=False)

      Return an iterator over all files located in the :file:`.dist-info`
      directory.  If *local* is ``True``, each returned path is transformed into
      a local absolute path, otherwise the raw value found in the :file:`RECORD`
      file is returned.

   .. method::  list_installed_files(local=False)

      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.  If *local* is ``True``, the returned path is transformed into a
      local absolute path, otherwise the raw value is returned.

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

   .. method:: uses(path)

      Check whether *path* was installed by this distribution (i.e. if the path
      is present in the :file:`RECORD` file).  *path* can be a local absolute
      path or a relative ``'/'``-separated path.  Returns a boolean.

   Available attributes:

   .. attribute:: metadata

      Instance of :class:`packaging.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(local=False)

   .. method:: uses(path)

   Available attributes:

   .. attribute:: metadata

      Instance of :class:`packaging.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`.  Return ``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:: obsoletes_distribution(name, version=None, use_egg_info=False)

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


.. 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:`packaging.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) a 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
--------

Print all information about a distribution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Given a path to a ``.dist-info`` distribution, we shall print out all
information that can be obtained using functions provided in this module::

   import sys
   import packaging.database

   path = input()
   # first create the Distribution instance
   try:
       dist = packaging.database.Distribution(path)
   except FileNotFoundError:
       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

   $ echo /tmp/choxie/choxie-2.0.0.9.dist-info | python3 print_info.py

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


Find out obsoleted distributions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Now, we take tackle a different problem, we are interested in finding out
which distributions have been obsoleted. This can be easily done as follows::

  import packaging.database

  # iterate over all distributions in the system
  for dist in packaging.database.get_distributions():
      name, version = dist.name, dist.version
      # find out which distributions obsolete this name/version combination
      replacements = packaging.database.obsoletes_distribution(name, version)
      if replacements:
          print('%r %s is obsoleted by' % (name, version),
                ', '.join(repr(r.name) for r in replacements))

This is how the output might look like:

.. code-block:: none

  'strawberry' 0.6 is obsoleted by 'choxie'
  'grammar' 1.0a4 is obsoleted by 'towel-stuff'
