defrost 0.4.0
A tool to audit pip freeze outputs and test version requirements
License: MIT License
Keywords: defrost pip freeze output audit version requirements deprecate

defrost
=======

NOTE: this package was originally called ``pipfreeze`` but was renamed to avoid
confusion with the command ``pip freeze``.

Defrost is a command line tool to check if the output of the ``pip freeze``
command complies with a set of package requirements defined in a YAML file.

Usage
-----

.. code-block::

    defrost --help

First define a set of package requirements in a YAML file, ``requirements.yml``.

.. code-block:: yaml

   ---
   requirements:
   - requirement: foobar>=1.0
     reason: foobar pre-1.0 is no longer supported, please upgrade to 1.x

   - requirement: ordereddict<0.0
     reason: OrderedDict is part of Python 2.7 and above. If you are still running Python 2.6, please upgrade!

Then you can pipe the output of ``pip freeze`` to defrost while providing the YAML file.

.. code-block::

    $ pip freeze > freeze.out
    $ defrost requirements.yml freeze.out
    Package(foobar==1.2) does not satisfy Requirement(foobar>=2.0): foobar pre-1.0 is no longer supported, please upgrade to 1.x

Defrost can also take the pip freeze output as stdin by passing a dash sign
``-`` as argument in place of the pip freeze output file.

.. code-block::

    $ pip freeze | defrost requirements.yml -
    Package(foobar==1.2) does not satisfy Requirement(foobar>=2.0): foobar pre-1.0 is no longer supported, please upgrade to 1.x

You can also check whether the YAML file provided is valid with
``defrost-lint``.

.. code-block::

    $ defrost-lint requirements.yml

Install
-------

.. code-block::

    pip install defrost

Library
-------

There are 3 fundamental objects available:

- ``PipFreeze``: a pythonic container of packages that takes a pip freeze
  output as input.
- ``Package``: represents a package instance at an exact version (pinned)
  held by a PipFreeze container (e.g. ``foo==1.2``)
- ``Requirement``: a requirement for a package represents one version
  or a range of versions for a given package, e.g. ``foo>=2.0`` is a
  requirement for all versions of foo greater or equal to 2.0. ``foo`` without
  a version specifier would mean all versions of ``foo``.

PipFreeze
~~~~~~~~~

PipFreeze takes a pip freeze output as input and builds packages internally.

.. code-block:: python

    >>> from defrost import PipFreeze

    >>> pip_freeze_output = """\
    foo==1.2.3
    bar==2.0
    """

    >>> pip_freeze = PipFreeze(pip_freeze_output)
    >>> len(pip_freeze)
    2

    >>> list(pip_freeze)
    [Package(foo==1.2.3), Package(bar==2.0)]

    # test presence of package foo that is less or equal to v2.0
    >>> 'foo<=2.0' in pip_freeze
    True

    # test presence of any version of package zoo
    >>> 'zoo' in pip_freeze
    False

    # test can also be done with a Package instance
    >>> Package('foo==0.1') in pip_freeze
    False

    # ... or with a Requirement
    >>> Requirement('bar>=2.0') in pip_freeze
    True

Package deprecation
~~~~~~~~~~~~~~~~~~~

You can mark packages as deprecated by loading the YAML requirements file and
passing the result of it to ``PipFreeze.load_requirements()``. Packages present
in PipFreeze will be marked as deprecated if they don't satisfy the loaded
requirements. You can also provide an optional reason to why a package is
deprecated.

.. code-block:: python

    >>> pip_freeze = PipFreeze("""\
    foobar==0.8
    bar==2.0
    ordereddict==1.1
    """)

    >>> import yaml
    >>> reqs = yaml.load(open('my-reqs.yaml'))
    >>> pip_freeze.load_requirements(reqs)
    >>> pip_freeze.deprecated
    [Package(foobar==0.8), Package(ordereddict==1.1)]
    >>> for package in pip_freeze.deprecated:
    ...     print("%s: deprecated=%s, deprecated_by=%s, reason=%s" % (
                package, package.deprecated, package.deprecated_by, package.deprecation_reason
            ))
    ...
    Package(foobar==0.8): deprecated=True, deprecated_by=Requirement(foobar>=1.0), reason=foobar pre-1.0 is no longer supported, please upgrade to 1.x
    Package(ordereddict==1.1): deprecated=True, deprecated_by=Requirement(ordereddict<0.0), reason=ordereddict is part of Python 2.7 and above. If you are still running Python 2.6, please upgrade!

Package
~~~~~~~

Packages take an exact package version as input.

.. code-block:: python

    >>> from defrost import Package

    >>> package = Package('foo==1.2')
    >>> package.name
    'foo'
    >>> package.version
    '1.2'

If you don't pass an exact version in your requirement it will raise a ``ValueError``.

.. code-block:: python

    >>> package = Package('foo')
    >>> Package('foo')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        ...
    ValueError: foo does not represent an exact package version; the format should be foo==1.0

You can also manually deprecate packages:

.. code-block:: python

    >>> package = Package('foo==1.2')
    >>> package.deprecated
    False
    >>> package.deprecate(reason='because')
    >>> package.deprecated
    True
    >>> package.deprecation_reason
    'because'

Requirement
~~~~~~~~~~~

A requirement represents a range of package versions.

.. code-block:: python

    >>> from defrost import Requirement

    >>> req = Requirement('foo>=1.0,<2.0')
    >>> req.name
    'foo'
    >>> req.specifier
    [('>=', '1.0'), ('<', '2.0')]

Requirements play well with packages. Using the Python operator ``in``, you
can check if a package satifies a requirement.

.. code-block:: python

    >>> req = Requirement('foo>=1.0')
    >>> Package('foo==1.0') in req
    True
    >>> Package('foo==2.0') in req
    True
    >>> Package('foo==0.1') in req
    False


0.4.0
=====

* implement ``PipFreeze.__getitem__`` and ``PipFreeze.get()``.

0.3.2
=====

* gracefully handle blank lines and lines starting with dash
  (https://github.com/SurveyMonkey/defrost/pull/14)

0.3.1
=====

* gracefully handle comments that may be present in pip freeze output

0.3.0
=====

* introduce ``defrost-lint`` command to test the validity of requirement files.

0.2.0
=====

* remove attributes ``Package.raw`` and ``Requirement.raw``, instead use
  ``Package.__str__()`` and ``Requirement.__str__()``.
* ignore links found in pip freeze output (-f or -e lines)
* Introduce the notion of deprecation severity. ``Package.deprecate()`` now
  takes a severity kwarg which defaults to ``"error"`` and a requirement entry
  in the YAML file now accepts an optional ``severity`` which can be set to
  ``error`` or ``warn``. This affects the exit status code for the command line
  interface.
* ``PipFreeze.load_requirements()`` would choke if a reason was not provided

0.1.0
=====

* add attribute ``Package.deprecated_by``
* method ``Package.deprecate()`` takes an optional ``deprecated_by`` argument.
* add command line utility to list deprecated packages given a requirement file
  and a pip freeze output file.
* rename project pipfreeze to defrost to avoid confusion with the command
  ``pip freeze``.

0.0.4
=====

* sort packages on name and then version such that ``foo==2.0`` comes before
  ``foo-bar==1.0`` which is not the case when both are treated as plain
  strings.

0.0.3
=====

* implement ``Requirement``
* implement ``Package``
* implement ``PipFreeze.__contains__()``
* implement ``PipFreeze.__len__()``
* drop py26 support
* remove ``PipFreeze.satisfies_requirement()``
* implement ``PipFreeze.load_requirements()``

0.0.2
=====

* implement ``PipFreeze.__bool__()`` (py3) and ``PipFreeze.__nonzero__()`` (py2)
* implement ``PipFreeze.__iter__()``

0.0.1
=====

* implement ``pipfreeze.PipFreeze``