pipdeptree-2.2.0/0000755000175000017500000000000014131341150013173 5ustar nileshnileshpipdeptree-2.2.0/README.rst0000644000175000017500000003625014131341150014670 0ustar nileshnileshpipdeptree ========== .. image:: https://github.com/naiquevin/pipdeptree/workflows/check/badge.svg :target: https://github.com/naiquevin/pipdeptree/actions ``pipdeptree`` is a command line utility for displaying the installed python packages in form of a dependency tree. It works for packages installed globally on a machine as well as in a virtualenv. Since ``pip freeze`` shows all dependencies as a flat list, finding out which are the top level packages and which packages do they depend on requires some effort. It's also tedious to resolve conflicting dependencies that could have been installed because older version of ``pip`` didn't have true dependency resolution [1]_. ``pipdeptree`` can help here by identifying conflicting dependencies installed in the environment. To some extent, ``pipdeptree`` is inspired by the ``lein deps :tree`` command of `Leiningen `_. Installation ------------ .. code-block:: bash $ pip install pipdeptree pipdeptree has been tested with Python versions ``2.7``, ``3.5``, ``3.6``, ``3.7``, ``3.8``, ``3.9`` as well as ``pypy2`` and ``pypy3``. Python ``2.6`` is way past it's end of life but if you ever find yourself stuck on a legacy environment, version ``0.9.0`` *might* work. Running in virtualenvs ---------------------- `New in ver. 2.0.0` If you want to run pipdeptree in the context of a particular virtualenv, you can specify the ``--python`` option. Note that this capability has been recently added in version ``2.0.0``. Alternately, you may also install pipdeptree inside the virtualenv and then run it from there. Usage and examples ------------------ To give you a brief idea, here is the output of ``pipdeptree`` compared with ``pip freeze``: .. code-block:: bash $ pip freeze Flask==0.10.1 itsdangerous==0.24 Jinja2==2.11.2 -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy MarkupSafe==0.22 pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl Werkzeug==0.11.2 And now see what ``pipdeptree`` outputs, .. code-block:: bash $ pipdeptree Warning!!! Possibly conflicting dependencies found: * Jinja2==2.11.2 - MarkupSafe [required: >=0.23, installed: 0.22] ------------------------------------------------------------------------ Flask==0.10.1 - itsdangerous [required: >=0.21, installed: 0.24] - Jinja2 [required: >=2.4, installed: 2.11.2] - MarkupSafe [required: >=0.23, installed: 0.22] - Werkzeug [required: >=0.7, installed: 0.11.2] Lookupy==0.1 pipdeptree==2.0.0b1 - pip [required: >=6.0.0, installed: 20.1.1] setuptools==47.1.1 wheel==0.34.2 Is it possible to find out why a particular package is installed? ----------------------------------------------------------------- `New in ver. 0.5.0` Yes, there's a ``--reverse`` (or simply ``-r``) flag for this. To find out which packages depend on a particular package(s), it can be combined with ``--packages`` option as follows: .. code-block:: bash $ pipdeptree --reverse --packages itsdangerous,MarkupSafe Warning!!! Possibly conflicting dependencies found: * Jinja2==2.11.2 - MarkupSafe [required: >=0.23, installed: 0.22] ------------------------------------------------------------------------ itsdangerous==0.24 - Flask==0.10.1 [requires: itsdangerous>=0.21] MarkupSafe==0.22 - Jinja2==2.11.2 [requires: MarkupSafe>=0.23] - Flask==0.10.1 [requires: Jinja2>=2.4] What's with the warning about conflicting dependencies? ------------------------------------------------------- As seen in the above output, ``pipdeptree`` by default warns about possible conflicting dependencies. Any package that's specified as a dependency of multiple packages with different versions is considered as a conflicting dependency. Conflicting dependencies are possible if older version of pip<=20.2 (`without the new resolver `_ [1]_) was ever used to install dependencies at some point. The warning is printed to stderr instead of stdout and it can be completely silenced by specifying the ``-w silence`` or ``--warn silence`` option. On the other hand, it can be made mode strict with ``--warn fail``, in which case the command will not only print the warnings to stderr but also exit with a non-zero status code. This is useful if you want to fit this tool into your CI pipeline. **Note**: The ``--warn`` option is added in version ``0.6.0``. If you are using an older version, use ``--nowarn`` flag to silence the warnings. Warnings about circular dependencies ------------------------------------ In case any of the packages have circular dependencies (eg. package A depends on package B and package B depends on package A), then ``pipdeptree`` will print warnings about that as well. .. code-block:: bash $ pipdeptree --exclude pip,pipdeptree,setuptools,wheel Warning!!! Cyclic dependencies found: - CircularDependencyA => CircularDependencyB => CircularDependencyA - CircularDependencyB => CircularDependencyA => CircularDependencyB ------------------------------------------------------------------------ wsgiref==0.1.2 argparse==1.2.1 Similar to the warnings about conflicting dependencies, these too are printed to stderr and can be controlled using the ``--warn`` option. In the above example, you can also see ``--exclude`` option which is the opposite of ``--packages`` ie. these packages will be excluded from the output. Using pipdeptree to write requirements.txt file ----------------------------------------------- If you wish to track only top level packages in your ``requirements.txt`` file, it's possible by grep-ing [2]_. only the top-level lines from the output, .. code-block:: bash $ pipdeptree --warn silence | grep -E '^\w+' Flask==0.10.1 gnureadline==8.0.0 Lookupy==0.1 pipdeptree==2.0.0b1 setuptools==47.1.1 wheel==0.34.2 There is a problem here though - The output doesn't mention anything about ``Lookupy`` being installed as an *editable* package (refer to the output of ``pip freeze`` above) and information about its source is lost. To fix this, ``pipdeptree`` must be run with a ``-f`` or ``--freeze`` flag. .. code-block:: bash $ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+' Flask==0.10.1 gnureadline==8.0.0 -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl setuptools==47.1.1 wheel==0.34.2 $ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+' > requirements.txt The freeze flag will not prefix child dependencies with hyphens, so you could dump the entire output of ``pipdeptree -f`` to the requirements.txt file thus making it human-friendly (due to indentations) as well as pip-friendly. .. code-block:: bash $ pipdeptree -f | tee locked-requirements.txt Flask==0.10.1 itsdangerous==0.24 Jinja2==2.11.2 MarkupSafe==0.23 Werkzeug==0.11.2 gnureadline==8.0.0 -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl pip==20.1.1 setuptools==47.1.1 wheel==0.34.2 On confirming that there are no conflicting dependencies, you can even treat this as a "lock file" where all packages, including the transient dependencies will be pinned to their currently installed versions. Note that the ``locked-requirements.txt`` file could end up with duplicate entries. Although ``pip install`` wouldn't complain about that, you can avoid duplicate lines (at the cost of losing indentation) as follows, .. code-block:: bash $ pipdeptree -f | sed 's/ //g' | sort -u > locked-requirements.txt Using pipdeptree with external tools ------------------------------------ `New in ver. 0.5.0` It's also possible to have ``pipdeptree`` output json representation of the dependency tree so that it may be used as input to other external tools. .. code-block:: bash $ pipdeptree --json Note that ``--json`` will output a flat list of all packages with their immediate dependencies. This is not very useful in itself. To obtain nested json, use ``--json-tree`` `New in ver. 0.11.0` .. code-block:: bash $ pipdeptree --json-tree Visualizing the dependency graph -------------------------------- .. image:: https://raw.githubusercontent.com/naiquevin/pipdeptree/master/docs/twine-pdt.png The dependency graph can also be visualized using `GraphViz `_: .. code-block:: bash $ pipdeptree --graph-output dot > dependencies.dot $ pipdeptree --graph-output pdf > dependencies.pdf $ pipdeptree --graph-output png > dependencies.png $ pipdeptree --graph-output svg > dependencies.svg Note that ``graphviz`` is an optional dependency ie. required only if you want to use ``--graph-output``. Since version ``2.0.0b1``, ``--package`` and ``--reverse`` flags are supported for all output formats ie. text, json, json-tree and graph. In earlier versions, ``--json``, ``--json-tree`` and ``--graph-output`` options override ``--package`` and ``--reverse``. Usage ----- .. code-block:: bash usage: pipdeptree.py [-h] [-v] [-f] [--python PYTHON] [-a] [-l] [-u] [-w [{silence,suppress,fail}]] [-r] [-p PACKAGES] [-e PACKAGES] [-j] [--json-tree] [--graph-output OUTPUT_FORMAT] Dependency tree of the installed python packages optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit -f, --freeze Print names so as to write freeze files --python PYTHON Python to use to look for packages in it (default: where installed) -a, --all list all deps at top level -l, --local-only If in a virtualenv that has global access do not show globally installed packages -u, --user-only Only show installations in the user site dir -w [{silence,suppress,fail}], --warn [{silence,suppress,fail}] Warning control. "suppress" will show warnings but return 0 whether or not they are present. "silence" will not show warnings at all and always return 0. "fail" will show warnings and return 1 if any are present. The default is "suppress". -r, --reverse Shows the dependency tree in the reverse fashion ie. the sub-dependencies are listed with the list of packages that need them under them. -p PACKAGES, --packages PACKAGES Comma separated list of select packages to show in the output. If set, --all will be ignored. -e PACKAGES, --exclude PACKAGES Comma separated list of select packages to exclude from the output. If set, --all will be ignored. -j, --json Display dependency tree as json. This will yield "raw" output that may be used by external tools. This option overrides all other options. --json-tree Display dependency tree as json which is nested the same way as the plain text output printed by default. This option overrides all other options (except --json). --graph-output OUTPUT_FORMAT Print a dependency graph in the specified output format. Available are all formats supported by GraphViz, e.g.: dot, jpeg, pdf, png, svg Known issues ------------ 1. ``pipdeptree`` relies on the internal API of ``pip``. I fully understand that it's a bad idea but it mostly works! On rare occasions, it breaks when a new version of ``pip`` is out with backward incompatible changes in internal API. So beware if you are using this tool in environments in which ``pip`` version is unpinned, specially automation or CD/CI pipelines. Limitations & Alternatives -------------------------- ``pipdeptree`` merely looks at the installed packages in the current environment using pip, constructs the tree, then outputs it in the specified format. If you want to generate the dependency tree without installing the packages, then you need a dependency resolver. You might want to check alternatives such as `pipgrip `_ or `poetry `_. Runing Tests (for contributors) ------------------------------- There are 2 test suites in this repo: 1. Unit tests that use mock objects. These are configured to run on every push to the repo and on every PR thanks to Github Actions. 2. End-to-end tests that are run against actual packages installed in virtualenvs Unit tests can be run against all version of python using `tox `_ as follows: .. code-block:: bash $ make test-tox-all This assumes that you have python versions specified in the ``tox.ini`` file. If you don't want to install all the versions of python but want to run tests quickly against ``Python3.6`` only: .. code-block:: bash $ make test Unit tests are written using ``pytest`` and you can also run the tests with code coverage as follows, .. code-block:: bash $ make test-cov On the other hand, end-to-end tests actually create virtualenvs, install packages and then run tests against them. These tests are more reliable in the sense that they also test ``pipdeptree`` with the latest version of ``pip`` and ``setuptools``. The downside is that when new versions of ``pip`` or ``setuptools`` are released, these need to be updated. At present the process is manual but I have plans to setup nightly builds for these for faster feedback. The end-to-end tests can be run as follows, .. code-block:: bash $ make test-e2e # starts with a clean virtualenvs $ # or $ make test-e2e-quick # reuses existing virtualenvs By default the e2e tests uses python executable ``python3.6``. To use an alternate version set the environment var ``E2E_PYTHON_EXE``. .. code-block:: bash $ E2E_PYTHON_EXE=python2.7 make test-e2e Release checklist ----------------- #. Make sure that tests pass on Github Actions. #. Create a commit with following changes and push it to github #. Update the `__version__` in the `pipdeptree.py` file. #. Add Changelog in `CHANGES.md` file. #. Also update `README.md` if required. #. Create an annotated tag on the above commit and push the tag to github #. Upload new version to PyPI. License ------- MIT (See `LICENSE <./LICENSE>`_) Footnotes --------- .. [1] pip version 20.3 has been released in Nov 2020 with the dependency resolver _ .. [2] If you are on windows (powershell) you can run ``pipdeptree --warn silence | Select-String -Pattern '^\w+'`` instead of grep pipdeptree-2.2.0/pipdeptree.py0000644000175000017500000007417214131341150015721 0ustar nileshnileshfrom __future__ import print_function import os import inspect import sys import subprocess from itertools import chain from collections import defaultdict, deque import argparse import json from importlib import import_module import tempfile try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict try: from collections.abc import Mapping except ImportError: from collections import Mapping from pip._vendor import pkg_resources # inline: # from graphviz import backend, Digraph __version__ = '2.2.0' flatten = chain.from_iterable def sorted_tree(tree): """Sorts the dict representation of the tree The root packages as well as the intermediate packages are sorted in the alphabetical order of the package names. :param dict tree: the pkg dependency tree obtained by calling `construct_tree` function :returns: sorted tree :rtype: collections.OrderedDict """ return OrderedDict([(k, sorted(v)) for k, v in sorted(tree.items())]) def guess_version(pkg_key, default='?'): """Guess the version of a pkg when pip doesn't provide it :param str pkg_key: key of the package :param str default: default version to return if unable to find :returns: version :rtype: string """ try: m = import_module(pkg_key) except ImportError: return default else: v = getattr(m, '__version__', default) if inspect.ismodule(v): return getattr(v, '__version__', default) else: return v def frozen_req_from_dist(dist): # The `pip._internal.metadata` modules were introduced in 21.1.1 # and the `pip._internal.operations.freeze.FrozenRequirement` # class now expects dist to be a subclass of # `pip._internal.metadata.BaseDistribution`, however the # `pip._internal.utils.misc.get_installed_distributions` continues # to return objects of type # pip._vendor.pkg_resources.DistInfoDistribution. # # This is a hacky backward compatible (with older versions of pip) # fix. try: from pip._internal import metadata except ImportError: pass else: dist = metadata.pkg_resources.Distribution(dist) try: return FrozenRequirement.from_dist(dist) except TypeError: return FrozenRequirement.from_dist(dist, []) class Package(object): """Abstract class for wrappers around objects that pip returns. This class needs to be subclassed with implementations for `render_as_root` and `render_as_branch` methods. """ def __init__(self, obj): self._obj = obj self.project_name = obj.project_name self.key = obj.key def render_as_root(self, frozen): return NotImplementedError def render_as_branch(self, frozen): return NotImplementedError def render(self, parent=None, frozen=False): if not parent: return self.render_as_root(frozen) else: return self.render_as_branch(frozen) @staticmethod def frozen_repr(obj): return str(obj.as_requirement()) def __getattr__(self, key): return getattr(self._obj, key) def __repr__(self): return '<{0}("{1}")>'.format(self.__class__.__name__, self.key) def __lt__(self, rhs): return self.key < rhs.key class DistPackage(Package): """Wrapper class for pkg_resources.Distribution instances :param obj: pkg_resources.Distribution to wrap over :param req: optional ReqPackage object to associate this DistPackage with. This is useful for displaying the tree in reverse """ def __init__(self, obj, req=None): super(DistPackage, self).__init__(obj) self.version_spec = None self.req = req def render_as_root(self, frozen): if not frozen: return '{0}=={1}'.format(self.project_name, self.version) else: return self.__class__.frozen_repr(self._obj) def render_as_branch(self, frozen): assert self.req is not None if not frozen: parent_ver_spec = self.req.version_spec parent_str = self.req.project_name if parent_ver_spec: parent_str += parent_ver_spec return ( '{0}=={1} [requires: {2}]' ).format(self.project_name, self.version, parent_str) else: return self.render_as_root(frozen) def as_requirement(self): """Return a ReqPackage representation of this DistPackage""" return ReqPackage(self._obj.as_requirement(), dist=self) def as_parent_of(self, req): """Return a DistPackage instance associated to a requirement This association is necessary for reversing the PackageDAG. If `req` is None, and the `req` attribute of the current instance is also None, then the same instance will be returned. :param ReqPackage req: the requirement to associate with :returns: DistPackage instance """ if req is None and self.req is None: return self return self.__class__(self._obj, req) def as_dict(self): return {'key': self.key, 'package_name': self.project_name, 'installed_version': self.version} class ReqPackage(Package): """Wrapper class for Requirements instance :param obj: The `Requirements` instance to wrap over :param dist: optional `pkg_resources.Distribution` instance for this requirement """ UNKNOWN_VERSION = '?' def __init__(self, obj, dist=None): super(ReqPackage, self).__init__(obj) self.dist = dist @property def version_spec(self): specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<' return ','.join([''.join(sp) for sp in specs]) if specs else None @property def installed_version(self): if not self.dist: return guess_version(self.key, self.UNKNOWN_VERSION) return self.dist.version @property def is_missing(self): return self.installed_version == self.UNKNOWN_VERSION def is_conflicting(self): """If installed version conflicts with required version""" # unknown installed version is also considered conflicting if self.installed_version == self.UNKNOWN_VERSION: return True ver_spec = (self.version_spec if self.version_spec else '') req_version_str = '{0}{1}'.format(self.project_name, ver_spec) req_obj = pkg_resources.Requirement.parse(req_version_str) return self.installed_version not in req_obj def render_as_root(self, frozen): if not frozen: return '{0}=={1}'.format(self.project_name, self.installed_version) elif self.dist: return self.__class__.frozen_repr(self.dist._obj) else: return self.project_name def render_as_branch(self, frozen): if not frozen: req_ver = self.version_spec if self.version_spec else 'Any' return ( '{0} [required: {1}, installed: {2}]' ).format(self.project_name, req_ver, self.installed_version) else: return self.render_as_root(frozen) def as_dict(self): return {'key': self.key, 'package_name': self.project_name, 'installed_version': self.installed_version, 'required_version': self.version_spec} class PackageDAG(Mapping): """Representation of Package dependencies as directed acyclic graph using a dict (Mapping) as the underlying datastructure. The nodes and their relationships (edges) are internally stored using a map as follows, {a: [b, c], b: [d], c: [d, e], d: [e], e: [], f: [b], g: [e, f]} Here, node `a` has 2 children nodes `b` and `c`. Consider edge direction from `a` -> `b` and `a` -> `c` respectively. A node is expected to be an instance of a subclass of `Package`. The keys are must be of class `DistPackage` and each item in values must be of class `ReqPackage`. (See also ReversedPackageDAG where the key and value types are interchanged). """ @classmethod def from_pkgs(cls, pkgs): pkgs = [DistPackage(p) for p in pkgs] idx = {p.key: p for p in pkgs} m = {p: [ReqPackage(r, idx.get(r.key)) for r in p.requires()] for p in pkgs} return cls(m) def __init__(self, m): """Initialize the PackageDAG object :param dict m: dict of node objects (refer class docstring) :returns: None :rtype: NoneType """ self._obj = m self._index = {p.key: p for p in list(self._obj)} def get_node_as_parent(self, node_key): """Get the node from the keys of the dict representing the DAG. This method is useful if the dict representing the DAG contains different kind of objects in keys and values. Use this method to lookup a node obj as a parent (from the keys of the dict) given a node key. :param node_key: identifier corresponding to key attr of node obj :returns: node obj (as present in the keys of the dict) :rtype: Object """ try: return self._index[node_key] except KeyError: return None def get_children(self, node_key): """Get child nodes for a node by it's key :param str node_key: key of the node to get children of :returns: list of child nodes :rtype: ReqPackage[] """ node = self.get_node_as_parent(node_key) return self._obj[node] if node else [] def filter(self, include, exclude): """Filters nodes in a graph by given parameters If a node is included, then all it's children are also included. :param set include: set of node keys to include (or None) :param set exclude: set of node keys to exclude (or None) :returns: filtered version of the graph :rtype: PackageDAG """ # If neither of the filters are specified, short circuit if include is None and exclude is None: return self # Note: In following comparisons, we use lower cased values so # that user may specify `key` or `project_name`. As per the # documentation, `key` is simply # `project_name.lower()`. Refer: # https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects if include: include = set([s.lower() for s in include]) if exclude: exclude = set([s.lower() for s in exclude]) else: exclude = set([]) # Check for mutual exclusion of show_only and exclude sets # after normalizing the values to lowercase if include and exclude: assert not (include & exclude) # Traverse the graph in a depth first manner and filter the # nodes according to `show_only` and `exclude` sets stack = deque() m = {} seen = set([]) for node in self._obj.keys(): if node.key in exclude: continue if include is None or node.key in include: stack.append(node) while True: if len(stack) > 0: n = stack.pop() cldn = [c for c in self._obj[n] if c.key not in exclude] m[n] = cldn seen.add(n.key) for c in cldn: if c.key not in seen: cld_node = self.get_node_as_parent(c.key) if cld_node: stack.append(cld_node) else: # It means there's no root node # corresponding to the child node # ie. a dependency is missing continue else: break return self.__class__(m) def reverse(self): """Reverse the DAG, or turn it upside-down In other words, the directions of edges of the nodes in the DAG will be reversed. Note that this function purely works on the nodes in the graph. This implies that to perform a combination of filtering and reversing, the order in which `filter` and `reverse` methods should be applied is important. For eg. if reverse is called on a filtered graph, then only the filtered nodes and it's children will be considered when reversing. On the other hand, if filter is called on reversed DAG, then the definition of "child" nodes is as per the reversed DAG. :returns: DAG in the reversed form :rtype: ReversedPackageDAG """ m = defaultdict(list) child_keys = set(r.key for r in flatten(self._obj.values())) for k, vs in self._obj.items(): for v in vs: # if v is already added to the dict, then ensure that # we are using the same object. This check is required # as we're using array mutation try: node = [p for p in m.keys() if p.key == v.key][0] except IndexError: node = v m[node].append(k.as_parent_of(v)) if k.key not in child_keys: m[k.as_requirement()] = [] return ReversedPackageDAG(dict(m)) def sort(self): """Return sorted tree in which the underlying _obj dict is an OrderedDict, sorted alphabetically by the keys :returns: Instance of same class with OrderedDict """ return self.__class__(sorted_tree(self._obj)) # Methods required by the abstract base class Mapping def __getitem__(self, *args): return self._obj.get(*args) def __iter__(self): return self._obj.__iter__() def __len__(self): return len(self._obj) class ReversedPackageDAG(PackageDAG): """Representation of Package dependencies in the reverse order. Similar to it's super class `PackageDAG`, the underlying datastructure is a dict, but here the keys are expected to be of type `ReqPackage` and each item in the values of type `DistPackage`. Typically, this object will be obtained by calling `PackageDAG.reverse`. """ def reverse(self): """Reverse the already reversed DAG to get the PackageDAG again :returns: reverse of the reversed DAG :rtype: PackageDAG """ m = defaultdict(list) child_keys = set(r.key for r in flatten(self._obj.values())) for k, vs in self._obj.items(): for v in vs: try: node = [p for p in m.keys() if p.key == v.key][0] except IndexError: node = v.as_parent_of(None) m[node].append(k) if k.key not in child_keys: m[k.dist] = [] return PackageDAG(dict(m)) def render_text(tree, list_all=True, frozen=False): """Print tree as text on console :param dict tree: the package tree :param bool list_all: whether to list all the pgks at the root level or only those that are the sub-dependencies :param bool frozen: whether or not show the names of the pkgs in the output that's favourable to pip --freeze :returns: None """ tree = tree.sort() nodes = tree.keys() branch_keys = set(r.key for r in flatten(tree.values())) use_bullets = not frozen if not list_all: nodes = [p for p in nodes if p.key not in branch_keys] def aux(node, parent=None, indent=0, chain=None): chain = chain or [] node_str = node.render(parent, frozen) if parent: prefix = ' '*indent + ('- ' if use_bullets else '') node_str = prefix + node_str result = [node_str] children = [aux(c, node, indent=indent+2, chain=chain+[c.project_name]) for c in tree.get_children(node.key) if c.project_name not in chain] result += list(flatten(children)) return result lines = flatten([aux(p) for p in nodes]) print('\n'.join(lines)) def render_json(tree, indent): """Converts the tree into a flat json representation. The json repr will be a list of hashes, each hash having 2 fields: - package - dependencies: list of dependencies :param dict tree: dependency tree :param int indent: no. of spaces to indent json :returns: json representation of the tree :rtype: str """ tree = tree.sort() return json.dumps([{'package': k.as_dict(), 'dependencies': [v.as_dict() for v in vs]} for k, vs in tree.items()], indent=indent) def render_json_tree(tree, indent): """Converts the tree into a nested json representation. The json repr will be a list of hashes, each hash having the following fields: - package_name - key - required_version - installed_version - dependencies: list of dependencies :param dict tree: dependency tree :param int indent: no. of spaces to indent json :returns: json representation of the tree :rtype: str """ tree = tree.sort() branch_keys = set(r.key for r in flatten(tree.values())) nodes = [p for p in tree.keys() if p.key not in branch_keys] def aux(node, parent=None, chain=None): if chain is None: chain = [node.project_name] d = node.as_dict() if parent: d['required_version'] = node.version_spec if node.version_spec else 'Any' else: d['required_version'] = d['installed_version'] d['dependencies'] = [ aux(c, parent=node, chain=chain+[c.project_name]) for c in tree.get_children(node.key) if c.project_name not in chain ] return d return json.dumps([aux(p) for p in nodes], indent=indent) def dump_graphviz(tree, output_format='dot', is_reverse=False): """Output dependency graph as one of the supported GraphViz output formats. :param dict tree: dependency graph :param string output_format: output format :returns: representation of tree in the specified output format :rtype: str or binary representation depending on the output format """ try: from graphviz import backend, Digraph except ImportError: print('graphviz is not available, but necessary for the output ' 'option. Please install it.', file=sys.stderr) sys.exit(1) if output_format not in backend.FORMATS: print('{0} is not a supported output format.'.format(output_format), file=sys.stderr) print('Supported formats are: {0}'.format( ', '.join(sorted(backend.FORMATS))), file=sys.stderr) sys.exit(1) graph = Digraph(format=output_format) if not is_reverse: for pkg, deps in tree.items(): pkg_label = '{0}\\n{1}'.format(pkg.project_name, pkg.version) graph.node(pkg.key, label=pkg_label) for dep in deps: edge_label = dep.version_spec or 'any' if dep.is_missing: dep_label = '{0}\\n(missing)'.format(dep.project_name) graph.node(dep.key, label=dep_label, style='dashed') graph.edge(pkg.key, dep.key, style='dashed') else: graph.edge(pkg.key, dep.key, label=edge_label) else: for dep, parents in tree.items(): dep_label = '{0}\\n{1}'.format(dep.project_name, dep.installed_version) graph.node(dep.key, label=dep_label) for parent in parents: # req reference of the dep associated with this # particular parent package req_ref = parent.req edge_label = req_ref.version_spec or 'any' graph.edge(dep.key, parent.key, label=edge_label) # Allow output of dot format, even if GraphViz isn't installed. if output_format == 'dot': return graph.source # As it's unknown if the selected output format is binary or not, try to # decode it as UTF8 and only print it out in binary if that's not possible. try: return graph.pipe().decode('utf-8') except UnicodeDecodeError: return graph.pipe() def print_graphviz(dump_output): """Dump the data generated by GraphViz to stdout. :param dump_output: The output from dump_graphviz """ if hasattr(dump_output, 'encode'): print(dump_output) else: with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream: bytestream.write(dump_output) def conflicting_deps(tree): """Returns dependencies which are not present or conflict with the requirements of other packages. e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed :param tree: the requirements tree (dict) :returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage :rtype: dict """ conflicting = defaultdict(list) for p, rs in tree.items(): for req in rs: if req.is_conflicting(): conflicting[p].append(req) return conflicting def render_conflicts_text(conflicts): if conflicts: print('Warning!!! Possibly conflicting dependencies found:', file=sys.stderr) # Enforce alphabetical order when listing conflicts pkgs = sorted(conflicts.keys()) for p in pkgs: pkg = p.render_as_root(False) print('* {}'.format(pkg), file=sys.stderr) for req in conflicts[p]: req_str = req.render_as_branch(False) print(' - {}'.format(req_str), file=sys.stderr) def cyclic_deps(tree): """Return cyclic dependencies as list of tuples :param PackageDAG pkgs: package tree/dag :returns: list of tuples representing cyclic dependencies :rtype: list """ index = {p.key: set([r.key for r in rs]) for p, rs in tree.items()} cyclic = [] for p, rs in tree.items(): for r in rs: if p.key in index.get(r.key, []): p_as_dep_of_r = [x for x in tree.get(tree.get_node_as_parent(r.key)) if x.key == p.key][0] cyclic.append((p, r, p_as_dep_of_r)) return cyclic def render_cycles_text(cycles): if cycles: print('Warning!! Cyclic dependencies found:', file=sys.stderr) # List in alphabetical order of the dependency that's cycling # (2nd item in the tuple) cycles = sorted(cycles, key=lambda xs: xs[1].key) for a, b, c in cycles: print('* {0} => {1} => {2}'.format(a.project_name, b.project_name, c.project_name), file=sys.stderr) def get_parser(): parser = argparse.ArgumentParser(description=( 'Dependency tree of the installed python packages' )) parser.add_argument('-v', '--version', action='version', version='{0}'.format(__version__)) parser.add_argument('-f', '--freeze', action='store_true', help='Print names so as to write freeze files') parser.add_argument('--python', default=sys.executable, help='Python to use to look for packages in it (default: where' ' installed)') parser.add_argument('-a', '--all', action='store_true', help='list all deps at top level') parser.add_argument('-l', '--local-only', action='store_true', help=( 'If in a virtualenv that has global access ' 'do not show globally installed packages' )) parser.add_argument('-u', '--user-only', action='store_true', help=( 'Only show installations in the user site dir' )) parser.add_argument('-w', '--warn', action='store', dest='warn', nargs='?', default='suppress', choices=('silence', 'suppress', 'fail'), help=( 'Warning control. "suppress" will show warnings ' 'but return 0 whether or not they are present. ' '"silence" will not show warnings at all and ' 'always return 0. "fail" will show warnings and ' 'return 1 if any are present. The default is ' '"suppress".' )) parser.add_argument('-r', '--reverse', action='store_true', default=False, help=( 'Shows the dependency tree in the reverse fashion ' 'ie. the sub-dependencies are listed with the ' 'list of packages that need them under them.' )) parser.add_argument('-p', '--packages', help=( 'Comma separated list of select packages to show ' 'in the output. If set, --all will be ignored.' )) parser.add_argument('-e', '--exclude', help=( 'Comma separated list of select packages to exclude ' 'from the output. If set, --all will be ignored.' ), metavar='PACKAGES') parser.add_argument('-j', '--json', action='store_true', default=False, help=( 'Display dependency tree as json. This will yield ' '"raw" output that may be used by external tools. ' 'This option overrides all other options.' )) parser.add_argument('--json-tree', action='store_true', default=False, help=( 'Display dependency tree as json which is nested ' 'the same way as the plain text output printed by default. ' 'This option overrides all other options (except --json).' )) parser.add_argument('--graph-output', dest='output_format', help=( 'Print a dependency graph in the specified output ' 'format. Available are all formats supported by ' 'GraphViz, e.g.: dot, jpeg, pdf, png, svg' )) return parser def _get_args(): parser = get_parser() return parser.parse_args() def handle_non_host_target(args): of_python = os.path.abspath(args.python) # if target is not current python re-invoke it under the actual host if of_python != os.path.abspath(sys.executable): # there's no way to guarantee that graphviz is available, so refuse if args.output_format: print("graphviz functionality is not supported when querying" " non-host python", file=sys.stderr) raise SystemExit(1) argv = sys.argv[1:] # remove current python executable for py_at, value in enumerate(argv): if value == "--python": del argv[py_at] del argv[py_at] elif value.startswith("--python"): del argv[py_at] # feed the file as argument, instead of file # to avoid adding the file path to sys.path, that can affect result file_path = inspect.getsourcefile(sys.modules[__name__]) with open(file_path, 'rt') as file_handler: content = file_handler.read() cmd = [of_python, "-c", content] cmd.extend(argv) # invoke from an empty folder to avoid cwd altering sys.path cwd = tempfile.mkdtemp() try: return subprocess.call(cmd, cwd=cwd) finally: os.removedirs(cwd) return None def main(): args = _get_args() result = handle_non_host_target(args) if result is not None: return result pkgs = list(pkg_resources.working_set) tree = PackageDAG.from_pkgs(pkgs) is_text_output = not any([args.json, args.json_tree, args.output_format]) return_code = 0 # Before any reversing or filtering, show warnings to console # about possibly conflicting or cyclic deps if found and warnings # are enabled (ie. only if output is to be printed to console) if is_text_output and args.warn != 'silence': conflicts = conflicting_deps(tree) if conflicts: render_conflicts_text(conflicts) print('-'*72, file=sys.stderr) cycles = cyclic_deps(tree) if cycles: render_cycles_text(cycles) print('-'*72, file=sys.stderr) if args.warn == 'fail' and (conflicts or cycles): return_code = 1 # Reverse the tree (if applicable) before filtering, thus ensuring # that the filter will be applied on ReverseTree if args.reverse: tree = tree.reverse() show_only = set(args.packages.split(',')) if args.packages else None exclude = set(args.exclude.split(',')) if args.exclude else None if show_only is not None or exclude is not None: tree = tree.filter(show_only, exclude) if args.json: print(render_json(tree, indent=4)) elif args.json_tree: print(render_json_tree(tree, indent=4)) elif args.output_format: output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) print_graphviz(output) else: render_text(tree, args.all, args.freeze) return return_code if __name__ == '__main__': sys.exit(main()) pipdeptree-2.2.0/tox.ini0000644000175000017500000000061314131341150014506 0ustar nileshnilesh# http://tox.readthedocs.org/ - sets up and runs the test suite based on a declarative configuration [tox] envlist = py39 py38 py37 py36 py35 py34 py27 pypy3 pypy2 [testenv] description = run test suite under {basepython} commands = pytest {posargs:-vv} deps = graphviz pip>=8.0.2 pytest pytest-cov virtualenv>=20,<21 mock;python_version<"3" extras = graphviz pipdeptree-2.2.0/Makefile0000644000175000017500000000157514131341150014643 0ustar nileshnilesh.PHONY: clean test-env test test-cov test-tox-all test-e2e TOX_ENV ?= py36 E2E_PYTHON_EXE ?= python3.6 clean: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rmdir {} + test-env: pip install -r dev-requirements.txt test: tox -e $(TOX_ENV) test-cov: tox -e $(TOX_ENV) -- -x -vv --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing # Requires all the versions of python executables to be present (See # tox.ini for list of python versions) test-tox-all: tox clean-e2e: rm -rf tests/profiles/*/.env_$(E2E_PYTHON_EXE)* test-e2e: clean-e2e cd tests && ./e2e-tests webapp cd tests && ./e2e-tests conflicting cd tests && ./e2e-tests cyclic test-e2e-quick: cd tests && ./e2e-tests webapp cd tests && ./e2e-tests conflicting cd tests && ./e2e-tests cyclic pipdeptree-2.2.0/setup.py0000644000175000017500000000272614131341150014714 0ustar nileshnileshimport re import ast from setuptools import setup _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('pipdeptree.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) with open('./README.rst') as f: long_desc = f.read() install_requires = ["pip >= 6.0.0"] setup( name='pipdeptree', version=version, author='Vineet Naik', author_email='naikvin@gmail.com', url='https://github.com/naiquevin/pipdeptree', license='MIT License', license_file='LICENSE', description='Command line utility to show dependency tree of packages', long_description=long_desc, install_requires=install_requires, extras_require={'graphviz': ['graphviz']}, python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', py_modules=['pipdeptree'], entry_points={ 'console_scripts': [ 'pipdeptree = pipdeptree:main' ] }, classifiers=[ 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9' ] ) pipdeptree-2.2.0/tests/0000755000175000017500000000000014131341150014335 5ustar nileshnileshpipdeptree-2.2.0/tests/profiles/0000755000175000017500000000000014131341150016160 5ustar nileshnileshpipdeptree-2.2.0/tests/profiles/webapp/0000755000175000017500000000000014131341150017436 5ustar nileshnileshpipdeptree-2.2.0/tests/profiles/webapp/test_spec.json0000644000175000017500000000220214131341150022316 0ustar nileshnilesh[ { "id": "default_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}}", "expected_output_file": "default.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "reverse_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -r", "expected_output_file": "reverse.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "--all_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} --all", "expected_output_file": "all_flag.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "--packages_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} --packages pexpect,ipython", "expected_output_file": "packages_opt.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "--packages--reverse_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -r -p decorator,click", "expected_output_file": "packages_reverse.out", "expected_err_file": null, "expected_returncode": 0 } ] pipdeptree-2.2.0/tests/profiles/webapp/default.out0000644000175000017500000000300614131341150021612 0ustar nileshnileshFlask-Script==2.0.6 - Flask [required: Any, installed: 1.1.2] - click [required: >=5.1, installed: 8.0.3] - importlib-metadata [required: Any, installed: 4.8.1] - typing-extensions [required: >=3.6.4, installed: 3.10.0.2] - zipp [required: >=0.5, installed: 3.6.0] - itsdangerous [required: >=0.24, installed: 2.0.1] - Jinja2 [required: >=2.10.1, installed: 3.0.2] - MarkupSafe [required: >=2.0, installed: 2.0.1] - Werkzeug [required: >=0.15, installed: 2.0.2] - dataclasses [required: Any, installed: 0.8] gnureadline==8.0.0 ipython==7.13.0 - appnope [required: Any, installed: 0.1.2] - backcall [required: Any, installed: 0.2.0] - decorator [required: Any, installed: 5.1.0] - jedi [required: >=0.10, installed: 0.18.0] - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2] - pexpect [required: Any, installed: 4.8.0] - ptyprocess [required: >=0.5, installed: 0.7.0] - pickleshare [required: Any, installed: 0.7.5] - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20] - wcwidth [required: Any, installed: 0.2.5] - pygments [required: Any, installed: 2.10.0] - setuptools [required: >=18.5, installed: 58.2.0] - traitlets [required: >=4.2, installed: 4.3.3] - decorator [required: Any, installed: 5.1.0] - ipython-genutils [required: Any, installed: 0.2.0] - six [required: Any, installed: 1.16.0] pipdeptree==2.1.0 - pip [required: >=6.0.0, installed: 21.3] pymongo==3.10.1 redis==3.4.1 slugify==0.0.1 wheel==0.37.0 pipdeptree-2.2.0/tests/profiles/webapp/packages_reverse.out0000644000175000017500000000037014131341150023500 0ustar nileshnileshclick==8.0.3 - Flask==1.1.2 [requires: click>=5.1] - Flask-Script==2.0.6 [requires: Flask] decorator==5.1.0 - ipython==7.13.0 [requires: decorator] - traitlets==4.3.3 [requires: decorator] - ipython==7.13.0 [requires: traitlets>=4.2] pipdeptree-2.2.0/tests/profiles/webapp/packages_opt.out0000644000175000017500000000151214131341150022626 0ustar nileshnileshipython==7.13.0 - appnope [required: Any, installed: 0.1.2] - backcall [required: Any, installed: 0.2.0] - decorator [required: Any, installed: 5.1.0] - jedi [required: >=0.10, installed: 0.18.0] - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2] - pexpect [required: Any, installed: 4.8.0] - ptyprocess [required: >=0.5, installed: 0.7.0] - pickleshare [required: Any, installed: 0.7.5] - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20] - wcwidth [required: Any, installed: 0.2.5] - pygments [required: Any, installed: 2.10.0] - setuptools [required: >=18.5, installed: 58.2.0] - traitlets [required: >=4.2, installed: 4.3.3] - decorator [required: Any, installed: 5.1.0] - ipython-genutils [required: Any, installed: 0.2.0] - six [required: Any, installed: 1.16.0] pipdeptree-2.2.0/tests/profiles/webapp/requirements.txt0000644000175000017500000000016014131341150022717 0ustar nileshnileshFlask==1.1.2 Flask-Script==2.0.6 gnureadline==8.0.0 pymongo==3.10.1 redis==3.4.1 slugify==0.0.1 ipython==7.13.0 pipdeptree-2.2.0/tests/profiles/webapp/reverse.out0000644000175000017500000000375014131341150021647 0ustar nileshnileshappnope==0.1.2 - ipython==7.13.0 [requires: appnope] backcall==0.2.0 - ipython==7.13.0 [requires: backcall] dataclasses==0.8 - Werkzeug==2.0.2 [requires: dataclasses] - Flask==1.1.2 [requires: Werkzeug>=0.15] - Flask-Script==2.0.6 [requires: Flask] decorator==5.1.0 - ipython==7.13.0 [requires: decorator] - traitlets==4.3.3 [requires: decorator] - ipython==7.13.0 [requires: traitlets>=4.2] gnureadline==8.0.0 ipython-genutils==0.2.0 - traitlets==4.3.3 [requires: ipython-genutils] - ipython==7.13.0 [requires: traitlets>=4.2] itsdangerous==2.0.1 - Flask==1.1.2 [requires: itsdangerous>=0.24] - Flask-Script==2.0.6 [requires: Flask] MarkupSafe==2.0.1 - Jinja2==3.0.2 [requires: MarkupSafe>=2.0] - Flask==1.1.2 [requires: Jinja2>=2.10.1] - Flask-Script==2.0.6 [requires: Flask] parso==0.8.2 - jedi==0.18.0 [requires: parso>=0.8.0,<0.9.0] - ipython==7.13.0 [requires: jedi>=0.10] pickleshare==0.7.5 - ipython==7.13.0 [requires: pickleshare] pip==21.3 - pipdeptree==2.1.0 [requires: pip>=6.0.0] ptyprocess==0.7.0 - pexpect==4.8.0 [requires: ptyprocess>=0.5] - ipython==7.13.0 [requires: pexpect] pygments==2.10.0 - ipython==7.13.0 [requires: pygments] pymongo==3.10.1 redis==3.4.1 setuptools==58.2.0 - ipython==7.13.0 [requires: setuptools>=18.5] six==1.16.0 - traitlets==4.3.3 [requires: six] - ipython==7.13.0 [requires: traitlets>=4.2] slugify==0.0.1 typing-extensions==3.10.0.2 - importlib-metadata==4.8.1 [requires: typing-extensions>=3.6.4] - click==8.0.3 [requires: importlib-metadata] - Flask==1.1.2 [requires: click>=5.1] - Flask-Script==2.0.6 [requires: Flask] wcwidth==0.2.5 - prompt-toolkit==3.0.20 [requires: wcwidth] - ipython==7.13.0 [requires: prompt-toolkit>=2.0.0,<3.1.0,!=3.0.1,!=3.0.0] wheel==0.37.0 zipp==3.6.0 - importlib-metadata==4.8.1 [requires: zipp>=0.5] - click==8.0.3 [requires: importlib-metadata] - Flask==1.1.2 [requires: click>=5.1] - Flask-Script==2.0.6 [requires: Flask] pipdeptree-2.2.0/tests/profiles/webapp/all_flag.out0000644000175000017500000000607614131341150021741 0ustar nileshnileshappnope==0.1.2 backcall==0.2.0 click==8.0.3 - importlib-metadata [required: Any, installed: 4.8.1] - typing-extensions [required: >=3.6.4, installed: 3.10.0.2] - zipp [required: >=0.5, installed: 3.6.0] dataclasses==0.8 decorator==5.1.0 Flask==1.1.2 - click [required: >=5.1, installed: 8.0.3] - importlib-metadata [required: Any, installed: 4.8.1] - typing-extensions [required: >=3.6.4, installed: 3.10.0.2] - zipp [required: >=0.5, installed: 3.6.0] - itsdangerous [required: >=0.24, installed: 2.0.1] - Jinja2 [required: >=2.10.1, installed: 3.0.2] - MarkupSafe [required: >=2.0, installed: 2.0.1] - Werkzeug [required: >=0.15, installed: 2.0.2] - dataclasses [required: Any, installed: 0.8] Flask-Script==2.0.6 - Flask [required: Any, installed: 1.1.2] - click [required: >=5.1, installed: 8.0.3] - importlib-metadata [required: Any, installed: 4.8.1] - typing-extensions [required: >=3.6.4, installed: 3.10.0.2] - zipp [required: >=0.5, installed: 3.6.0] - itsdangerous [required: >=0.24, installed: 2.0.1] - Jinja2 [required: >=2.10.1, installed: 3.0.2] - MarkupSafe [required: >=2.0, installed: 2.0.1] - Werkzeug [required: >=0.15, installed: 2.0.2] - dataclasses [required: Any, installed: 0.8] gnureadline==8.0.0 importlib-metadata==4.8.1 - typing-extensions [required: >=3.6.4, installed: 3.10.0.2] - zipp [required: >=0.5, installed: 3.6.0] ipython==7.13.0 - appnope [required: Any, installed: 0.1.2] - backcall [required: Any, installed: 0.2.0] - decorator [required: Any, installed: 5.1.0] - jedi [required: >=0.10, installed: 0.18.0] - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2] - pexpect [required: Any, installed: 4.8.0] - ptyprocess [required: >=0.5, installed: 0.7.0] - pickleshare [required: Any, installed: 0.7.5] - prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20] - wcwidth [required: Any, installed: 0.2.5] - pygments [required: Any, installed: 2.10.0] - setuptools [required: >=18.5, installed: 58.2.0] - traitlets [required: >=4.2, installed: 4.3.3] - decorator [required: Any, installed: 5.1.0] - ipython-genutils [required: Any, installed: 0.2.0] - six [required: Any, installed: 1.16.0] ipython-genutils==0.2.0 itsdangerous==2.0.1 jedi==0.18.0 - parso [required: >=0.8.0,<0.9.0, installed: 0.8.2] Jinja2==3.0.2 - MarkupSafe [required: >=2.0, installed: 2.0.1] MarkupSafe==2.0.1 parso==0.8.2 pexpect==4.8.0 - ptyprocess [required: >=0.5, installed: 0.7.0] pickleshare==0.7.5 pip==21.3 pipdeptree==2.1.0 - pip [required: >=6.0.0, installed: 21.3] prompt-toolkit==3.0.20 - wcwidth [required: Any, installed: 0.2.5] ptyprocess==0.7.0 Pygments==2.10.0 pymongo==3.10.1 redis==3.4.1 setuptools==58.2.0 six==1.16.0 slugify==0.0.1 traitlets==4.3.3 - decorator [required: Any, installed: 5.1.0] - ipython-genutils [required: Any, installed: 0.2.0] - six [required: Any, installed: 1.16.0] typing-extensions==3.10.0.2 wcwidth==0.2.5 Werkzeug==2.0.2 - dataclasses [required: Any, installed: 0.8] wheel==0.37.0 zipp==3.6.0 pipdeptree-2.2.0/tests/profiles/webapp/.gitignore0000644000175000017500000000000514131341150021421 0ustar nileshnilesh.env*pipdeptree-2.2.0/tests/profiles/cyclic/0000755000175000017500000000000014131341150017426 5ustar nileshnileshpipdeptree-2.2.0/tests/profiles/cyclic/test_spec.json0000644000175000017500000000125114131341150022311 0ustar nileshnilesh[ { "id": "default_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}}", "expected_output_file": "default.out", "expected_err_file": "default.err", "expected_returncode": 0 }, { "id": "warning_silenced", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -w silence", "expected_output_file": "default.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "fail_if_cyclic", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -w fail", "expected_output_file": "default.out", "expected_err_file": "default.err", "expected_returncode": 1 } ] pipdeptree-2.2.0/tests/profiles/cyclic/default.err0000644000175000017500000000036614131341150021571 0ustar nileshnileshWarning!! Cyclic dependencies found: * CircularDependencyB => CircularDependencyA => CircularDependencyB * CircularDependencyA => CircularDependencyB => CircularDependencyA ------------------------------------------------------------------------ pipdeptree-2.2.0/tests/profiles/cyclic/default.out0000644000175000017500000000014014131341150021576 0ustar nileshnileshpipdeptree==2.1.0 - pip [required: >=6.0.0, installed: 21.3] setuptools==58.2.0 wheel==0.37.0 pipdeptree-2.2.0/tests/profiles/cyclic/requirements.txt0000644000175000017500000000005014131341150022705 0ustar nileshnileshCircularDependencyA CircularDependencyB pipdeptree-2.2.0/tests/profiles/cyclic/.gitignore0000644000175000017500000000000514131341150021411 0ustar nileshnilesh.env*pipdeptree-2.2.0/tests/profiles/conflicting/0000755000175000017500000000000014131341150020457 5ustar nileshnileshpipdeptree-2.2.0/tests/profiles/conflicting/test_spec.json0000644000175000017500000000161714131341150023350 0ustar nileshnilesh[ { "id": "default_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}}", "expected_output_file": "default.out", "expected_err_file": "default.err", "expected_returncode": 0 }, { "id": "reverse_output", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -r", "expected_output_file": "reverse.out", "expected_err_file": "default.err", "expected_returncode": 0 }, { "id": "warning_silenced", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -w silence", "expected_output_file": "default.out", "expected_err_file": null, "expected_returncode": 0 }, { "id": "fail_if_conflicting", "method": "cmp_with_file_contents", "command": "{{pipdeptree}} -w fail", "expected_output_file": "default.out", "expected_err_file": "default.err", "expected_returncode": 1 } ] pipdeptree-2.2.0/tests/profiles/conflicting/default.err0000644000175000017500000000027614131341150022622 0ustar nileshnileshWarning!!! Possibly conflicting dependencies found: * Jinja2==3.0.2 - MarkupSafe [required: >=2.0, installed: 0.22] ------------------------------------------------------------------------ pipdeptree-2.2.0/tests/profiles/conflicting/default.out0000644000175000017500000000051214131341150022632 0ustar nileshnileshargparse==1.4.0 Flask==0.10.1 - itsdangerous [required: >=0.21, installed: 0.24] - Jinja2 [required: >=2.4, installed: 3.0.2] - MarkupSafe [required: >=2.0, installed: 0.22] - Werkzeug [required: >=0.7, installed: 0.11.2] pipdeptree==2.1.0 - pip [required: >=6.0.0, installed: 20.2.3] setuptools==58.2.0 wheel==0.37.0 pipdeptree-2.2.0/tests/profiles/conflicting/requirements.txt0000644000175000017500000000012314131341150023737 0ustar nileshnileshFlask==0.10.1 itsdangerous==0.24 Jinja2 MarkupSafe==0.22 Werkzeug==0.11.2 argparse pipdeptree-2.2.0/tests/profiles/conflicting/reverse.out0000644000175000017500000000052614131341150022666 0ustar nileshnileshargparse==1.4.0 itsdangerous==0.24 - Flask==0.10.1 [requires: itsdangerous>=0.21] MarkupSafe==0.22 - Jinja2==3.0.2 [requires: MarkupSafe>=2.0] - Flask==0.10.1 [requires: Jinja2>=2.4] pip==20.2.3 - pipdeptree==2.1.0 [requires: pip>=6.0.0] setuptools==58.2.0 Werkzeug==0.11.2 - Flask==0.10.1 [requires: Werkzeug>=0.7] wheel==0.37.0 pipdeptree-2.2.0/tests/profiles/conflicting/.gitignore0000644000175000017500000000000514131341150022442 0ustar nileshnilesh.env*pipdeptree-2.2.0/tests/e2e_tests.py0000644000175000017500000000354314131341150016611 0ustar nileshnileshimport json import os import shlex import subprocess from jinja2 import Environment, BaseLoader import pytest ## Uncomment following lines for running in shell # os.environ['TEST_PROFILE_DIR'] = 'profiles/webapp' # os.environ['PIPDEPTREE_EXE'] = 'profiles/webapp/.env_python3.6_pip-latest/bin/pipdeptree' test_profile_dir = os.environ['TEST_PROFILE_DIR'] pipdeptree_path = os.environ['PIPDEPTREE_EXE'] def load_test_spec(): test_spec_path = os.path.join(test_profile_dir, 'test_spec.json') with open(test_spec_path) as f: return json.load(f) test_spec = load_test_spec() def final_command(s): tmpl = Environment(loader=BaseLoader).from_string(s) return tmpl.render(pipdeptree=pipdeptree_path) def _test_cmp_with_file_contents(spec): p = subprocess.Popen(shlex.split(spec['command']), stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() assert spec['expected_returncode'] == p.returncode if spec['expected_output_file'] is not None: exp_output_file = os.path.join(test_profile_dir, spec['expected_output_file']) with open(exp_output_file, 'rb') as f: expected_output = f.read() assert expected_output == out else: assert out == b'' if spec['expected_err_file'] is not None: exp_err_file = os.path.join(test_profile_dir, spec['expected_err_file']) with open(exp_err_file, 'rb') as f: expected_err = f.read() assert expected_err == err else: assert err == b'' @pytest.mark.parametrize('spec', test_spec) def test_all_tests_in_profile(spec): spec['command'] = final_command(spec['command']) if spec['method'] == 'cmp_with_file_contents': _test_cmp_with_file_contents(spec) pipdeptree-2.2.0/tests/e2e-tests0000755000175000017500000000142614131341150016101 0ustar nileshnilesh#!/usr/bin/env bash set -e PROFILE=$1 PYTHON_EXE=${PYTHON_EXE:-python3.6} PIP_VERSION=${PIP_VERSION:-latest} if [ "$PROFILE" == "conflicting" ]; then PIP_VERSION=20.2.3 fi cd profiles/$PROFILE echo "Profile dir: $(pwd)" env_dir=".env_$(basename $PYTHON_EXE)_pip-${PIP_VERSION}" echo "Profile env: $env_dir" if [ ! -d $env_dir ]; then virtualenv -p $PYTHON_EXE $env_dir fi pip=$env_dir/bin/pip if [ "$PIP_VERSION" == "latest" ]; then $pip install -U pip else $pip install pip==$PIP_VERSION fi # Install requirements $pip install -r requirements.txt # Install pipdeptree $pip install -e ../../../ pip_deptree=$env_dir/bin/pipdeptree export TEST_PROFILE_DIR="profiles/$PROFILE" export PIPDEPTREE_EXE=$TEST_PROFILE_DIR/$pip_deptree cd - pytest -v e2e_tests.py pipdeptree-2.2.0/tests/test_pipdeptree.py0000644000175000017500000004431714131341150020120 0ustar nileshnileshfrom contextlib import contextmanager import platform import sys from tempfile import NamedTemporaryFile try: from unittest import mock except ImportError: import mock import pytest import virtualenv import pipdeptree as p # Tests for DAG classes def mock_pkgs(simple_graph): for node, children in simple_graph.items(): nk, nv = node p = mock.Mock(key=nk, project_name=nk, version=nv) as_req = mock.Mock(key=nk, project_name=nk, specs=[('==', nv)]) p.as_requirement = mock.Mock(return_value=as_req) reqs = [] for child in children: ck, cv = child r = mock.Mock(key=ck, project_name=ck, specs=cv) reqs.append(r) p.requires = mock.Mock(return_value=reqs) yield p def mock_PackageDAG(simple_graph): pkgs = list(mock_pkgs(simple_graph)) return p.PackageDAG.from_pkgs(pkgs) # util for comparing tree contents with a simple graph def dag_to_dict(g): return {k.key: [v.key for v in vs] for k, vs in g._obj.items()} def sort_map_values(m): return {k: sorted(v) for k, v in m.items()} t = mock_PackageDAG({ ('a', '3.4.0'): [('b', [('>=', '2.0.0')]), ('c', [('>=', '5.7.1')])], ('b', '2.3.1'): [('d', [('>=', '2.30'), ('<', '2.42')])], ('c', '5.10.0'): [('d', [('>=', '2.30')]), ('e', [('>=', '0.12.1')])], ('d', '2.35'): [('e', [('>=', '0.9.0')])], ('e', '0.12.1'): [], ('f', '3.1'): [('b', [('>=', '2.1.0')])], ('g', '6.8.3rc1'): [('e', [('>=', '0.9.0')]), ('f', [('>=', '3.0.0')])] }) def test_PackageDAG__get_node_as_parent(): assert 'b' == t.get_node_as_parent('b').key assert 'c' == t.get_node_as_parent('c').key def test_PackageDAG_filter(): # When both show_only and exclude are not specified, same tree # object is returned assert t.filter(None, None) is t # when show_only is specified g1 = dag_to_dict(t.filter(set(['a', 'd']), None)) expected = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d', 'e'], 'd': ['e'], 'e': []} assert expected == g1 # when exclude is specified g2 = dag_to_dict(t.filter(None, ['d'])) expected = {'a': ['b', 'c'], 'b': [], 'c': ['e'], 'e': [], 'f': ['b'], 'g': ['e', 'f']} assert expected == g2 # when both show_only and exclude are specified g3 = dag_to_dict(t.filter(set(['a', 'g']), set(['d', 'e']))) expected = {'a': ['b', 'c'], 'b': [], 'c': [], 'f': ['b'], 'g': ['f']} assert expected == g3 # when conflicting values in show_only and exclude, AssertionError # is raised with pytest.raises(AssertionError): dag_to_dict(t.filter(set(['d']), set(['D', 'e']))) def test_PackageDAG_reverse(): t1 = t.reverse() expected = {'a': [], 'b': ['a', 'f'], 'c': ['a'], 'd': ['b', 'c'], 'e': ['c', 'd', 'g'], 'f': ['g'], 'g': []} assert isinstance(t1, p.ReversedPackageDAG) assert sort_map_values(expected) == sort_map_values(dag_to_dict(t1)) assert all([isinstance(k, p.ReqPackage) for k in t1.keys()]) assert all([isinstance(v, p.DistPackage) for v in p.flatten(t1.values())]) # testing reversal of ReversedPackageDAG instance expected = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d', 'e'], 'd': ['e'], 'e': [], 'f': ['b'], 'g': ['e', 'f']} t2 = t1.reverse() assert isinstance(t2, p.PackageDAG) assert sort_map_values(expected) == sort_map_values(dag_to_dict(t2)) assert all([isinstance(k, p.DistPackage) for k in t2.keys()]) assert all([isinstance(v, p.ReqPackage) for v in p.flatten(t2.values())]) # Tests for Package classes # # Note: For all render methods, we are only testing for frozen=False # as mocks with frozen=True are a lot more complicated def test_DistPackage__render_as_root(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') dp = p.DistPackage(foo) is_frozen = False assert 'foo==20.4.1' == dp.render_as_root(is_frozen) def test_DistPackage__render_as_branch(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) dp = p.DistPackage(foo).as_parent_of(rp) is_frozen = False assert 'foo==20.4.1 [requires: bar>=4.0]' == dp.render_as_branch(is_frozen) def test_DistPackage__as_parent_of(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') dp = p.DistPackage(foo) assert dp.req is None bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) dp1 = dp.as_parent_of(rp) assert dp1._obj == dp._obj assert dp1.req is rp dp2 = dp.as_parent_of(None) assert dp2 is dp def test_DistPackage__as_dict(): foo = mock.Mock(key='foo', project_name='foo', version='1.3.2b1') dp = p.DistPackage(foo) result = dp.as_dict() expected = {'key': 'foo', 'package_name': 'foo', 'installed_version': '1.3.2b1'} assert expected == result def test_ReqPackage__render_as_root(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) is_frozen = False assert 'bar==4.1.0' == rp.render_as_root(is_frozen) def test_ReqPackage__render_as_branch(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) is_frozen = False assert 'bar [required: >=4.0, installed: 4.1.0]' == rp.render_as_branch(is_frozen) def test_ReqPackage__as_dict(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) result = rp.as_dict() expected = {'key': 'bar', 'package_name': 'bar', 'installed_version': '4.1.0', 'required_version': '>=4.0'} assert expected == result # Tests for render_text # # @NOTE: These tests use mocked tree and it's not easy to test for # frozen=True with mocks. Hence those tests are covered only in # end-to-end tests. Check the ./e2e-tests script. @pytest.mark.parametrize( "list_all,reverse,expected_output", [ ( True, False, [ 'a==3.4.0', ' - b [required: >=2.0.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - c [required: >=5.7.1, installed: 5.10.0]', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'b==2.3.1', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', 'c==5.10.0', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'd==2.35', ' - e [required: >=0.9.0, installed: 0.12.1]', 'e==0.12.1', 'f==3.1', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', 'g==6.8.3rc1', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - f [required: >=3.0.0, installed: 3.1]', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]' ] ), ( True, True, [ 'a==3.4.0', 'b==2.3.1', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', 'c==5.10.0', ' - a==3.4.0 [requires: c>=5.7.1]', 'd==2.35', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', 'e==0.12.1', ' - c==5.10.0 [requires: e>=0.12.1]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - d==2.35 [requires: e>=0.9.0]', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - g==6.8.3rc1 [requires: e>=0.9.0]', 'f==3.1', ' - g==6.8.3rc1 [requires: f>=3.0.0]', 'g==6.8.3rc1' ] ), ( False, False, [ 'a==3.4.0', ' - b [required: >=2.0.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - c [required: >=5.7.1, installed: 5.10.0]', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'g==6.8.3rc1', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - f [required: >=3.0.0, installed: 3.1]', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ] ), ( False, True, [ 'e==0.12.1', ' - c==5.10.0 [requires: e>=0.12.1]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - d==2.35 [requires: e>=0.9.0]', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - g==6.8.3rc1 [requires: e>=0.9.0]', ] ) ] ) def test_render_text(capsys, list_all, reverse, expected_output): tree = t.reverse() if reverse else t p.render_text(tree, list_all=list_all, frozen=False) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.out.strip() # Tests for graph outputs def test_render_pdf(): output = p.dump_graphviz(t, output_format='pdf') @contextmanager def redirect_stdout(new_target): old_target, sys.stdout = sys.stdout, new_target try: yield new_target finally: sys.stdout = old_target with NamedTemporaryFile(delete=True) as f: with redirect_stdout(f): p.print_graphviz(output) rf = open(f.name, 'rb') assert b'%PDF' == rf.read()[:4] # @NOTE: rf is not closed to avoid "bad filedescriptor" error def test_render_svg(capsys): output = p.dump_graphviz(t, output_format='svg') p.print_graphviz(output) out, _ = capsys.readouterr() assert out.startswith('') # Test for conflicting deps @pytest.mark.parametrize( "mpkgs,expected_keys,expected_output", [ ( { ('a', '1.0.1'): [('b', [('>=', '2.3.0')])], ('b', '1.9.1'): [] }, {'a': ['b']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - b [required: >=2.3.0, installed: 1.9.1]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '7.0')])], ('c', '8.0.1'): [] }, {'a': ['c']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - c [required: >=9.4.1, installed: 8.0.1]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '9.4.0')])] }, {'a': ['c'], 'b': ['c']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - c [required: >=9.4.1, installed: ?]', '* b==2.3.0', ' - c [required: >=9.4.0, installed: ?]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '7.0')])], ('c', '9.4.1'): [] }, {}, [] ) ] ) def test_conflicting_deps(capsys, mpkgs, expected_keys, expected_output): tree = mock_PackageDAG(mpkgs) result = p.conflicting_deps(tree) result_keys = {k.key: [v.key for v in vs] for k, vs in result.items()} assert expected_keys == result_keys p.render_conflicts_text(result) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.err.strip() # Tests for cyclic deps @pytest.mark.parametrize( "mpkgs,expected_keys,expected_output", [ ( { ('a', '1.0.1'): [('b', [('>=', '2.0.0')])], ('b', '2.3.0'): [('a', [('>=', '1.0.1')])], ('c', '4.5.0'): [('d', [('==', '2.0')])], ('d', '2.0'): [] }, [('a', 'b', 'a'), ('b', 'a', 'b')], [ 'Warning!! Cyclic dependencies found:', '* b => a => b', '* a => b => a' ] ), ( # if a dependency isn't installed, cannot verify cycles { ('a', '1.0.1'): [('b', [('>=', '2.0.0')])], }, [], [] # no output expected ) ] ) def test_cyclic_deps(capsys, mpkgs, expected_keys, expected_output): tree = mock_PackageDAG(mpkgs) result = p.cyclic_deps(tree) result_keys = [(a.key, b.key, c.key) for (a, b, c) in result] assert sorted(expected_keys) == sorted(result_keys) p.render_cycles_text(result) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.err.strip() # Tests for the argparse parser def test_parser_default(): parser = p.get_parser() args = parser.parse_args([]) assert not args.json assert args.output_format is None def test_parser_j(): parser = p.get_parser() args = parser.parse_args(['-j']) assert args.json assert args.output_format is None def test_parser_json(): parser = p.get_parser() args = parser.parse_args(['--json']) assert args.json assert args.output_format is None def test_parser_json_tree(): parser = p.get_parser() args = parser.parse_args(['--json-tree']) assert args.json_tree assert not args.json assert args.output_format is None def test_parser_pdf(): parser = p.get_parser() args = parser.parse_args(['--graph-output', 'pdf']) assert args.output_format == 'pdf' assert not args.json def test_parser_svg(): parser = p.get_parser() args = parser.parse_args(['--graph-output', 'svg']) assert args.output_format == 'svg' assert not args.json @pytest.mark.parametrize("args_joined", [True, False]) def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined): result = virtualenv.cli_run([str(tmp_path), "--activators", ""]) cmd = [sys.executable] cmd += ["--python={}".format(result.creator.exe)] if args_joined else ["--python", str(result.creator.exe)] monkeypatch.setattr(sys, "argv", cmd) p.main() out, _ = capfd.readouterr() found = {i.split("==")[0] for i in out.splitlines()} implementation = platform.python_implementation() if implementation == "CPython": expected = {"pip", "setuptools", "wheel"} elif implementation == "PyPy": expected = {"cffi", "greenlet", "pip", "readline", "setuptools", "wheel"} else: raise ValueError(implementation) assert found == expected, out monkeypatch.setattr(sys, "argv", cmd + ["--graph-output", "something"]) with pytest.raises(SystemExit) as context: p.main() out, err = capfd.readouterr() assert context.value.code == 1 assert not out assert err == "graphviz functionality is not supported when querying" " non-host python\n" pipdeptree-2.2.0/CHANGES.md0000644000175000017500000001376614131341150014602 0ustar nileshnileshChangelog ========= 2.2.0 ----- * Fix pipdeptree to work with pip version 21.3. The _internal pip api that was being used earlier is now replaced with new functions. (PR #154) 2.1.0 ----- * JSON output is sorted alphabetically to make it deterministic * Fix --freeze option due to breaking changes in pip's internal api in version > 21.1.1 * Include license file in dist package 2.0.0 ----- * Support for running in the context of a virtualenv (without installing pipdeptree inside the virtualenv) * Avoid crash when rendering cyclic dependencies * Fix graphviz (dot file) output * Handle a (rare) case while guessing version of a package * Migrate from travisCI to Github workflows * Improve integration tests 2.0.0b1 (beta version) ---------------------- * In this first beta release targeting `2.0.0`, the underlying code is heavily refactored to make different CLI options work well with each other. This was a serious limitation in older version `<=1.0.0` which made it difficult to extend the tool. For more information about the plans for 2.0.0 release, please check `docs/v2beta-opts.org` file. * The `--reverse`, `--packages` and `--exclude` flags now work with `--json-tree` and `--graph-output` * Dropped support for python `3.3` and added support for python `3.7` and `3.8` * Another problem with older version was that tests setup was convoluted and involved loading packages pickled from one env into the current env (in which tests are run). Moreover there was no separation between unit tests and integration tests (flaky) * Tests have been separated into 2 suites (1) unit tests that totally rely on mock objects and run on every commit ( travis.ci) and (2) end-to-end tests that need to be run manually. * The test setup for end-to-end tests has been greatly simplified although the "flakyness"" still remains because these tests are run against unpinned versions of `pip`. However this is by design because we want to know when `pipdeptree` fails with a new version of `pip`. * Move continuous integration from Travis to Github Actions. 1.0.0 ----- * Use `pkg_resources` vendored with `pip`. * Besides this, there's no other change in this major version release. 0.13.2 ------ * Fixed call to `FrozenRequirement.to_dist` to handle changes to the internal api of pip version 19.0. The api change is because dependency links support has been removed in pip 19.0 See more: - https://github.com/pypa/pip/pull/6060 - https://github.com/pypa/pip/pull/5881/commits/46ffb13f13f69c509fd253329da49889008f8e23 0.13.1 ------ * Fixed import after changes in pip._internal introduced in pip version 18.1 0.13.0 ------ * Added `--exclude` option to exclude packages specified as CSV * In case of multiple version specs eg. =y, fix the order to ensure consistent output. The sorting is naive - puts the '>' prior to '<', and '!'. * [Developer affecting] Updated dependencies in test environments, thereby fixing the long standing issue of inconsistent test behaviour. 0.12.1 ------ * Fix import of 'FrozenRequirement' for pip 10.0.0 0.12.0 ------ * Changes to make pipdeptree work with pip 10.0.0. This change is backward compatible. 0.11.0 ------ * Added support for nested json output (`--json-tree` flag). Behaviour of `--json` stays the same. * Test environments have been updated to fix the builds. 0.10.1 ------ * Fixed change of behaviour due to support for ``--json`` and ``--packages`` together. PR #65 was reverted for this. 0.10.0 ------ * Dropped support for Python 2.6. * ``--json`` and ``--packages`` options can now be used together. * Fixed binary graphviz output on Python 3 0.9.0 ----- * Support for visualizing dependency tree of packages using Graphviz in various formats. * Support to consider only packages installed in the user directory. * Fix the output to use a better term, "Any" instead of "None" if a dependency doesn't need to be of a specific version. * CLI option to print version. 0.8.0 ----- * Use pip's list of excluded default packages. This means that the ``pipdeptree`` package itself is no longer excluded and will appear in the output tree. * Fix the bug that caused a package to appear in conflicting deps although it's installed version could be guessed. 0.7.0 ----- * Fix for a bug in reverse mode. * Alphabetical sorting of packages in the output. * Fallback to guess installed version of packages "skipped" by pip. 0.6.0 ----- * Better checking for possibly "confusing" dependencies, hence the word "confusing" in the warning message is now replaced with "coflicting" [PR#37] * Fix a bug when rendering dependencies of packages [PR#38] * The ``--nowarn`` flag is now replaced with ``--warn`` with 'silence', 'suppress' and 'fail' as possible values, thus giving more control over what should happen when there are warnings. The default behaviour (ie. when the flag is not specified) remains the same. [PR#39] * Fixes for Python 3.5 support [PR#40] 0.5.0 ----- * Add `--reverse` flag to show the dependency tree upside down. * Add `--packages` flag to show only select packages in output. * Add `--json` flag to output dependency tree as json that may be used by external tools. 0.4.3 ----- * Add python support classifiers to setup.py * Include license and changelog in distribution tar ball * Removed bullets from output of pipdeptree if the `freeze` (-f) flag is set. * Changes related to test setup and travis-ci integration. 0.4.2 ----- * Fix Python 3.x incompatibility (`next()` instead of `.next()`) * Suppress error if a dep is in skipped packages 0.4.1 ----- * Fix: Show warning about cyclic deps only if found 0.4 --- * Python 2.6 compatibility * Fix infinite recursion in case of cyclic dependencies * Show warnings about cyclic dependencies * Travis integration and other improvements 0.3 --- * Add `--freeze` flag * Warn about possible confusing dependencies * Some minor help text and README fixes 0.2 --- * Minor fixes 0.1 --- First version pipdeptree-2.2.0/pytest.ini0000644000175000017500000000106514131341150015226 0ustar nileshnilesh[pytest] norecursedirs = build docs/_build *.egg .tox *.venv profiles addopts = # --verbose --tb=short # Turn on --capture to have brief, less noisy output # You will only see output if the test fails # Use --capture no if you want to see it all or have problems debugging # --capture=fd # --capture=no # show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed. -rfEsxX # --junitxml=junit.xml # --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing pipdeptree-2.2.0/.gitignore0000644000175000017500000000021014131341150015154 0ustar nileshnilesh*.pyc __pycache__ *~ *.egg-info/ build/ dist/ .tox/ tests/virtualenvs/equimapper/ .coverage coverage.xml htmlcov/ .cache/ .pytest_cache/pipdeptree-2.2.0/LICENSE0000644000175000017500000000206314131341150014201 0ustar nileshnileshCopyright (c) 2015 Vineet Naik (naikvin@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pipdeptree-2.2.0/.github/0000755000175000017500000000000014131341150014533 5ustar nileshnileshpipdeptree-2.2.0/.github/workflows/0000755000175000017500000000000014131341150016570 5ustar nileshnileshpipdeptree-2.2.0/.github/workflows/check.yml0000644000175000017500000000264614131341150020400 0ustar nileshnileshname: check on: push: pull_request: schedule: - cron: "0 8 * * *" jobs: test: name: Test on ${{ matrix.py }} under ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - Ubuntu-latest py: - 3.9 - 3.8 - 3.7 - 3.6 - 3.5 - 2.7 - pypy3 - pypy2 steps: - name: Setup graphviz uses: ts-graphviz/setup-graphviz@v1 - name: Setup python for tox uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install tox run: python -m pip install tox - name: Setup python for test ${{ matrix.py }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.py }} - uses: actions/checkout@v2 - name: Pick tox environment to run run: | import os import platform import sys major, minor, impl = sys.version_info[0], sys.version_info[1], platform.python_implementation() toxenv = ("py" if impl == "CPython" else "pypy") + ("{}{}".format(major, minor) if impl == "CPython" else ("3" if major == 3 else "")) env_file = open(os.environ['GITHUB_ENV'], "a") env_file.write("TOXENV={}\n".format(toxenv)) env_file.close() shell: python - name: Setup test suite run: tox -vv --notest - name: Run test suite run: tox --skip-pkg-install pipdeptree-2.2.0/MANIFEST.in0000644000175000017500000000003214131341150014724 0ustar nileshnileshinclude LICENSE CHANGES.mdpipdeptree-2.2.0/docs/0000755000175000017500000000000014131341150014123 5ustar nileshnileshpipdeptree-2.2.0/docs/v2beta-opts.org0000644000175000017500000000761414131341150017012 0ustar nileshnilesh* Options in version 0.x v/s 2.x (upcoming release) Until version 0.13.2, the some of the options that pipdeptree supports didn't work in combination with other options. In fact this was the primary reason behind refactoring the code. The upcoming version 2.x plans to fix this as shown in the tables below. Note: The changes for upcoming 2.x release can be found in the ~v2beta~ branch. ** Until version 0.13.2 | *Features* | all | local-only | user-only | freeze | warn | reverse | packages | json | json-tree | graph-output | |--------------+-----+------------+-----------+--------+------+---------+----------+------+-----------+--------------| | | | | | | | | | | | | | all | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | local-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | user-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | freeze | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | | | | warn | ✓ | ✓ | ✓ | | | | | ✗ | ✗ | | | reverse | ✓ | ✓ | ✓ | ✓ | | | ✓ | | ✗ | ✗ | | packages | ✓ | ✓ | ✓ | ✓ | | ✓ | | ✗ | ✗ | ✗ | | json | ✓ | ✓ | ✓ | | ✗ | | ✗ | | | | | json-tree | ✓ | ✓ | ✓ | | ✗ | ✗ | ✗ | | | | | graph-output | ✓ | ✓ | ✓ | | | ✗ | ✗ | | | | ** Plan for version 2.0.0 (work in progress) | *Features* | all | local-only | user-only | freeze | warn | reverse | packages | json | json-tree | graph-output | | |--------------+-----+------------+-----------+--------+----------+----------+----------+----------+-----------+--------------+---| | | | | | | | | | | | | | | all | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | local-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | user-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | freeze | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | | | | | warn | ✓ | ✓ | ✓ | | | | | ✓ (todo) | ✓ (todo) | | | | reverse | ✓ | ✓ | ✓ | ✓ | | | ✓ | | ✓ (done) | ✓ (done) | | | packages | ✓ | ✓ | ✓ | ✓ | | ✓ | | ✓ (done) | ✓ (done) | ✓ (done) | | | json | ✓ | ✓ | ✓ | | ✓ (todo) | | ✓ (done) | | | | | | json-tree | ✓ | ✓ | ✓ | | ✓ (todo) | ✓ (done) | ✓ (done) | | | | | | graph-output | ✓ | ✓ | ✓ | | | ✓ (done) | ✓ (done) | | | | | pipdeptree-2.2.0/docs/twine-pdt.png0000644000175000017500000027644014131341150016561 0ustar nileshnileshPNG  IHDRcsRGB@IDATx USI%Rf 2 ,)C^3![̳D""C!C(ʜ!CB2}={9 9wa(S tA]' K@U0R#" " "L>sox?իWׯo/O55\ӧ &oRE@D@D@D@D@D +$ve*$O?oQ2[6|/*iRK-V,lխ[DBCJ,Z]v>oݺ-2RƘVژ4oƦNj_}=z +vh" " " " " O@bWcPD $q.rjY޽[nvn]졇~ږ]vY޽.|⫶$ +?Z-" ex뮳|VZi%coaѕou3,p$1cGy5i$Wv ]9S$*\/"{l-?ʷn s9ǚ7oo]Q{E@D@D@D@D@j@ U)" F`ʔ).nHÙ3gK/dD(te˖vWxk]_};餓lѢE^,ؕeND@A첋ִiSlJsbw7ƹs5\cA /?9F5BD@D@D@D@D Hʽ}@y{ڷooM>{1СC?֭͛ggu]|Ŷf[MH?]g-@F̞=:vb׬YlmH]_loֹsgwm?rj@-UKUT {N;4[feYUVSOѣ}BO @H?-" %GaO>Hأb6`{W<~n&M0]:D@D G ֥K{GaÆ5Wyh =zݻM7Tޢ]D@D@D@D@DH)~" "Wkڵ-Z^|Ehl,nvx1?l fT@ؕC;CM|nEE-hB`R pZƍOtOa--"" " " " "Ph$vUD@/xQеju?4h`~5k̎;l7A2]Toׯ[v͘1$td6?C/N:ZlaնE@D@D@D@D@Rz " "P(gFiSNڪPUk q3kӦm営{ۘ1cuڙgi=[~ѐ_.[wuc fsε-pG{1M8Zk-6lX_/3gkذ|֭[=+V]ѣG" " " " " H@]S% 0k,õpȐ!mp c:pmlX;!L!lmfn?ozr Ȉ /voѷ?am\4_ Aİk뭷^:,m/o"E]:a +7Z!"PztM LUW]ڵkUlvqG7X!M6RK٩꟱ UݲK/s+0 b< ֬YK1ǶΝ;_v9< d/a"" " " " "P$vUD@rTBG}tV[+Sp_e~y;[n(namO?v~g46M-d |n܍sԨQغ6)" " " " "+$vʞP;D@1OlDzc[nf`w}g?O۷VeYk>sݲ ٳgۇ~h^z%fA-@l-" " " " "P=Mk@VZF۩bㅕVX_XZ#F(ĪZW@R>\~7ꫯ}O4$ 0z%"}YҥKZ\&&1V_}uk'{cU=vWݢ+ ?7h#[c5 *" " " " " I@bWaWJD ,X-pVAl޽ 6Jr?r%-~|' +/"wڴim~뭷2v2?tIֺuk_~IT֛>}Fz%\b[mg9)S[om 6Ç|`l-X7|}jC" " " " " 9C@bW 5DDXj]f͚.v1XNTWe_v饗ڼyl„ 6c ;#]L뮻ʭk…֧Ok߾y֢E k}=~F;h*" " " " " I@bWaWJD p7\3]u6`[{TUeau.q۷lٲeqo,_F\C0{K~\vqLl^r`' " Gɓ'ŋļСCرcGڵkå~O%믿N= P'ڠ&@g}<`ĉݪ)|g6tP[ FbxWYuU~Zt ]v7J+نnp|fztׯ퉀dĮ3V " "V[̓|y-v\9sxo-O>y>,>ǿsNB^z-{=;wO|2 QY:u\Xc+Yږ@ HJ3PmND@R%p{agۀ뭷͜9ӭ=\xgQriܸ}w.(%?=2S+t.]qӈ7o`Oz&KGiڴo'Q3ʖYf@-XM*E@D Wg4g[_|=s XE%uY>#K+=]_ˑ#GUvׯcrFp0,L 22WR]vYkݺu+d+W~H3]i͉@uEV뮻ΦMfm]ra֬YVFmd> ^b}jӦMFvgeu `͛7iv+ Xbꪫ?eq[-%mVTE"" " " " "])aB" "PsZ&M2\~mÒ_ꫯZPǘr=JvygkРAO|:clĈ,/km:uJ<=5_Yjذm>%|IIL8>C=d_}/JfH@&'g]U㥥E@DJ}vwhZxaԭ[7ݲm۶ /гV ~KAvvڹk[J*o?Ј9{ځhM4~څ'1cK/}[nŰZc5wޱz]9WXaUk:o?޽{W `ժU+͖X~7g\C$[ WHX[]}0إ@D@H6D!(.b_u'VO,\: eӽ;zUJTC҈X`Q*[_34ȶb dM7ߴUVYŷ!"K;4,W[z_~I#tWn!]Lv8&j+7`_U" аůx(֨QM-Bݨh{@ p 3g [\/ֱcG‚ Q#,}쳏Є{ڀH/_ЂXG쫸 !ᾘBs9DžGD|.]0LÈ 쒱8>gFj$v$4 |w.<~`Cr#nKX7_խ/VZiTV2e{[ `~VTew?qE0~E<Ę`a%;j$ؕD_E@D,o=cٌ3Bim)j߾})o_|2XA!a5R5SNzg@[WviӦUh.Md|D XkѢEZnhęj֬J%RMwqj -=%bX{=C1ȜXǷ~{ kCJÆ 29TD@D@D@D@D HʥdAMVbEo5WVwgY0am۶6u$l:ꫯNIk̘1pBik}5M?~a  Q f\ c 0&ة@mUU@=5yde qim:uZˣAyl%ěӧ۫j7t[.jrrO4ɮ ;BgrL`j tkM7e5$[믿vM4q6bƍ "SD@D@D@DH%VD {^u8q=#+x>xbܹP̙c'twka <ŜUW]L.60sLs5n5"~:tp K1SËE9xBC `X+aJƐ_-%" " " "P9]3" yF?0bn!n!r}g۞{ɻtS/ֈs5ʶrKk}}vQGyoFO2k<\.S$Hwy3gHmSmnoK`s5J⫑4#2fU@ؕ;MMX\SO/V9=zpcǎ9'P`uСCRK.qKwm}q+5\sI C=䮊'6p.kk-[z@{OM? "r-ڵksk(Įw' ?Ӌ/豶pOٳ[=OhG٩n+B̟x ׿e_~{챾*ԩSݚ p62T;.zyn^s.lab kį7x|x,bU9rj@ H1Bm@D [Ⱥ/\Zym=˺up-MGC<޽{n7'{~%'Vl￿]}slE3P.{3U&@ 3p}pz^I- W_u!D,3H2(f ,XĂ_UVY-k;CQSY~1bU-5l0+=}.úg3R!ܭ4D8]M 0 _xo+aW,l A@]e@O"Pxim{1ܦxQg} K.E1}ݏUFp #R;qm?R`\ T.&U]rZO4&MrGL` \"CvI,cKv9ؙg_no{ad?D[-\rI.61/D?Hq̙nw衇:׵^;/Ff_|aJ믿vHsb咫sfh" " " J@bWYK!0c 뮻e\ǵI&U?F<+&} ȀV(?v7A\Tr饗E]dϷƍj3]dKX; vͭzYtyӲP5b2"`iӦ֮];7Y?ND@D@Dz$vU"@7ۈCӿ`fs̱>\xv '\38?}䖫$IbFXrbOz:ȭ7dT Ŝx`?-r־}{r-K&z6ނٱ]E"A5k"2KVDL[o=ZjUpakvy|n&UwYV~Nf? b{5j ż'@VZ p/ 9\[v#[l5\37]z(E{̳1g~}uLWšwuO<`^?pWZc5r \ꫯ>:ۛ Dxg_m]vK\x blܹFW3&,U#" " "$v@ w\il֫W/[a Y.22d7B.db硇ʛn8ЦMb\2~OV#.k$j@^m2PpC~[!}g% ;ud[m1'W:u @ؕ{D-,\1cA7`ãke#  .N=Ԃv#^ĉ{e_9%[=r$b!z,X{18Q=S=^K/F/\I_u^vE@D@DH*}1}QlҤIְaC/ *1bvi+6ڨ ?'v'|wŋmԩG+c?~ _<󌭺.#|5dk)#~͜9縕cuKlD/eX-\ + E'.F{キիW_}"ݙgb8J`c9oGO?m첋꫶f[ g-mmuk/b|I(]]+_1¾ '9$aRZETD@D@ĮejpcرnUA r!Cgu) 5'.p~;wmxV]Ծ}{S}M>]n=׺uk,R9b5AD@D@D $v!) M7䱸1rx:)ԩS>r=x-ߧMfo͙3]b ߻a a?T=#8.ڸ9n٤j V__`ܛHlҤ'1!($`POsyBnᨣrq *'ې!C w=,E߸qoe 5R&L>CN;p{.D/VZ){+X#|)B`^VE@D@Įbs |.pd#PJ |WrLF.t! ?7b\tEv ' uYUJ@XI//$vyg0%d}DrjTE@D@D H*佫eҧLb_ZHVŦMf-\/dAXzFB;>lm+Jaݽk̙n1|H÷kF'Ջ&@\H=%-qbkvi^e5^D@DH*=d/j`>|͝;x=m /l=jAV2!VVٳuҥoթ܍/<R+v|=x*ot[>fdY2tTyu׵;ζbڵ=*}=Uaɲ$o".ۼy T=[;u;nuwڷ~k[{qM7<ܹJ" dn}d݊UVvrխ駟˅o: رcm%o_~1,Κ5~wg^+ U`<[V S▤8@7 jf jE!VRRV9z7 Uu&MrNcǎhȑ=dWuc!ɀ{S[w-Q<Ŀ6U3]Qf . e]7~E#Fʦ~ tEmڴBvYf~2^Q+ŧVqPq"" "PeW/ѣGRGa4ŗ [Hߢ_~e:GmYFGB|ߒ?\r%Q㏒)XNE!SZxU{u+sc)[oEat_B Jb|B$!9`1"%/c=ֳ=V\yGCt Ō^rKp2xT$@`рLk?ex h\j62E3fxB!S_Id[hܸq2Wj؅װ?\3Q2q~1IJ^E X=+5m&]ٸw)O,q[c%kג hLXyw=ӳ>;X.b/Ve'Ԥ*o&S޲0 jRr8I_@ R  &T09 W-V6s=QȒS4Zy#b^Dp|e9$翇?Wʪ\ +R"yb%XE-& @ƕq%[H3_z ]Q1KA֯(fW L ?<ײ?-|K3i*b9ТEw~-1fW`:!T\mբO?cfN:ϯ^_c)li}$ %. \Un\cemf7bi~Ȑ!Q\,_uc$$$"ȕ{/#6_ӦM`Wfw+$oAr]D@Djjt@!nv' X.Qܒ`!vFJ'lNC娣Jm vQT骯J !%e+E9[Q\[^KU_xᅈD:bWs13L k%7o[E,Eg-]uEEdc@22ӆw%P=: oɋ{Mϯ\_>EZ Dq/+Ԥ>I" &KeW_{WpY0BINkbsJ>״%!NYE? `o c;nK*$;ߏ@*"PƒS"PT m 7LΝ;{Hc+dH&h}q jj=Gg{!d$IW}6lH,פQ#9q(ijM1O=, =7xW'1*+`ٳޏcTNT沖񖎾.~ žǩI&`+%,%Ïs2>H@IDAT*w檂UСC<kj1̮Ԥ>6h܅w fT&@`[bLc2!dM6ZD!bs,kݺTv!x~*+d h,XKN.YmR@%(YPY,m'2`qq|beY*gde2f?+[#D92ݢp7 ?x_ 켔WVx%Apm)wto5[ Xng _,ex/-2X"e@ Af ɞ[o{GbZY1X zJQk }?n*clrhq'"P)`TF<Rf=ŕm$WUE,зfRM_%^;{Wu2 aP!bⲞ:2Uug1"⸎T pw [0Ľ7$Id)|/" "PwzuX!cW'#̵QpW\3 Eb"VdދPG sq緊):W]?՛(/$dx<8y"YȨ*iW: V?'[zCznrF۳&/O^/&L%9w=#  VbGE뭷# 3gt kyH~*KJ^STqÃ|&MK/4d㭤~(o+XLbLbYs̫@M wY{pĵ眫%SWX0ҭ[(İX$R)XcAVJIsBF*X(XH,X!CҔd>y->֒R=EA"-}#]7(]*) %iM .^mTnxHh}łMOEJYR_‹bQ1-`q=vFϫ"{xm-[yR㦜E31؂*:]V,:Ċ Fl6屏G|`E<2}~r}-"9Ifwa}WzG\ ڛo\-e֓ꏕ՝xdS]vνՓ^2՗l1|B@L}?)L½7 JZFD@ ĮܯE٫YfY}6`.`xI!^HGqgz/{NS @!mrgP9Y R#2ڧ` 8Z$щ`Ik!NZ*ԖD@D@D C$ve6=!n\Hq*=Y׻Le!΋˞5+Π=`\R.ncjvKF.a0쳏 _X;%Zf;c! f;Twv `]B={1EP2jP1EF@bWjT4i]x;Em!yz0}Q ArqywB`9 w5 }ku9NW{׭B>w:C-ĈЍ<ǻ@2e_ 0bVZ.X̘nG @6HeՑV? :peKr=uhc5'?]f\YjQmT)f.VXV?k.'.wyn¹ٳgO Yq3^*$#/ӦM3 #Srj-\m6Qu1]E F7n>o(7-ds CG ?>42u g@VG,V뮶3)PD@D@Lb%u"p rj\%7 DQF]vRåL%3y뭷쥗^LyU,k6mjC Q<dSNuk{ϭpRb!@B3fO<֭n_`,Aؕ=֪)E rk^{Es%Ѐ¸qlԨQ6`ml\xyog}4ݲ&5#Uرc]ކ;̃>m]k@~Xh[}!|xw/ 5j(:֊]9KA\Xe̙3DR?L֫W/#+Ƥ9kݺu*-?֧O[p[ya" =[o۸~nżN:E@@]<"L$]Yfe<'V_lV'HĮAi ù1{oj߾}*ԖF/5d/mhп/>}c/w"ǯ"u]gW\&@ &MFQ,R \sj[& `s!V_̿k[{]BAl-" "P $vȎn;D[`[-[UD _Xn ы_ /{QL7HN`~vm} k7Nwڞ@U ;2/%#ٌ]p֩S|Bѷ_MȖ٤Ig ;lW0߲3Ǝ֭[WD@D@Į"\wU\{gD't|vWRK--k3en?C:߳>ؾ/gIzX".X@xKwkڴiZkN|-[qtHX|!~͝;5jd]vuX_͚5K_ڒ@Nؕ% СCmѶŋđݯ$6l[~ۑJ}W<{lOvꫯ*E >p;027Gs?>ȅ//#!m.|1L:RD@D0 0k"Xz6e7=Z%5dmE ]5F* Eၽy)oXOed,=^^˵yF0 9qg*" fZA!2G~~M7YIl#د*d" " B@]#kE䪫`g}uQfTwg 2`0a7kAieZveYƈ{ʒh3g\E ^Xz18A" _8[i T@E. 30Bfi2M슕$_LP[%" @@bW.-xS.bNp ~n%׌#wp*Q9'c>} SH6'y睗(lF`ѢE ы$YRBXj)ra/Yp=.w}Bg<ۏ4}n3s?#8?9V5rx{K-ELG57xRחu_~eVx=2W(;u`ˢ""`ow鱋vm7{)+Q "PwyE/į3fx(- "" "[$v<3kf￿gXԍ=gwW CdM<w/h-Zt;Z s' Ń>F hw}1"""?dדO>i+tA.|i r; by#±@D@DV(c`JZn]tUVYŮK#X˓߉g(eY_~i'OΓn31"tvnܳ]wխP͛-ks"֭q駟zM}"[nxEK^2CI&ֿw駟67wuV[m5U?}@*$vBzW'(/²Q,b<7.2fVdlXĨd<8'^{-zkkРUja-Xksα?_I !bӦM֩jm$}꫍\O9{wކ0F/,X ;։@cʺC*-~'[ݺ l]tE6|p[gu~M14Z\E}m%vm>R\>Dx*" lرH5"X*2\EV_='/b "CňE}eWP~E1oyc>T~6l'nn}U.m- {l.R#{#/"3syl]v7r"Pވu$#݄7oͧLbSH/Zxǥ/73#.?Jrye]3&C<+Lyy5f ̪Pi!C/vkN;N=TSm7A<쫯 CL}bY>8~իW2UW]G9#鸖p<դ^ׯ.,&ۺ?0pccP<`85j1nE guRum[oyoAk@'''X|aXN6&mGHګR1ޏT8` SJ"VVc9׍X C,A1Eh7qv[WfWq㿜;Ol+ ~枔8ʽ =eq9 |1#{*̎LI08ǕW^AS]/ז}*8y(|Y&:kPs%s13]|N&y׊Ͽ|'d;hgCሇaju Vܮ _"P\E*9$»ϖȜsIuLJs(M o.1 'G^&dݮo7Ə/  L;OSe ܈ԱcG۷#\q?8 k>@~@H뮻 .p+X@JX=i UX_c@E9/X31!b+W-Zn5 (pFlz<Ĵ礨8vwD} wva F)jp\$i$}pOK<'yI4D*9yGAMi(0丈IE7\oUD C'N(<`OΝ]/5,{Xp"ֵkWѣ.))GJp|7kfJ3 w7FX<ۂFu+?uL_T3Cy9$K%.D*eۃjI0a.^x\Cڈk2Ts +U_K+ VA\fLSu{%%y\yCYoJTRu'^pSx 4QI.щ'[X G1 qs=7 F7(X F!) #QQ0.:v8ȱ2+ȈB d@.܂09}R*$"$/!dg!PERdeY.Tst|,O>_0GSYnXBv֭|C\C CSHpi Z(d(^XSLQ1.FXNRS~o"xy%~$.V*" 'V^yNlqj4 &-%X{1SL/d4L;w.`:GU%.;/[h ,?xa( DED8p[l뮓X1j `pD:ugۮ]Ff`iD.sFo K[WXI=bȩ@6T[Dqu,ӧOF[UG L8M8dB\D2(.\v̙3}$vɗ,q]$IJ{I^ MV"%@h ,pIA'>k>XRK_{w-(6$%  WłڻJŹ9cƌz3[K,iBv\h"{ 0R[˩%6&Xt^{ebia4w98>B< .,;-1Ӧ^zYޱ.(d)͚5B  > ds7v `%c7p~-~]/Yi*qy% Ypv)b!au7q98XY0<~ۂmAΉ"P[Hzeճc!+}孢EF,vN\,67D0B^h@8 q,dS{eI[nNAc<>~M4{P^Ƃ VlcCED+V0 (*+GNryٝݝٝ=ywfܹ?s۹:9A:z?3'qL9ꨣjf̘1N@U /P"kW_݉|5Tu2,\(C$ڨ*H PY]I(8eu%tNU +ωbI}@[ @.QG${7^{妞z(;R믿 Y9V+ӆ-\ANx]W_$p[b%2/i}ԅݻ;cv})O+ l1?pU҂[Nf,&v­0MR߁K]#Gt ᳻k~tsbjYW,gĉVgqFb5%6U+bR|$*Pf1.2!\%:AfEB vGE|V*|*btUO"%VLM׋˫~+JښKNY[Q={qXOQUIhwI'~:Y8O$.r9]`B}<^y7h 'AjLI3-&}Dq %{ō_Zܘ6$ `X*3^+U1bl B=#9Xk!Qxt\e˽;qmt`u5&H ltz#Lt}#/'Kb,ӗ1|O%64/<NiZ(|>JDUU:Ç||zbTUZtW*(p q]}P")o,$z.ߖwyg'E-ଳr6L1:Z|26MyӞ{~mG7OgY P@P|Y@H3 @xdW]uU "b d«@S@BJͱ}~zL&6n-H1i0QZƦ/,A=Լ_ּBC0ɧ!Ss\!9p%>Op0Z>eHPzҪ; ϑ:҅*'So( Հw-ʸB쫮bn~WDADwqZ^Q)j1kĺNUQHV B8跑;Ԅ2'~+kz/A2!P#EY Ӿv032жI[( Qy6R84(}+R ub5໠c9>xԴOWj.(p71QniEO,QT>} c1>gY4E}D 3IB)Z<) DT XgƷmܸ6idd@qƝ11sND/ JɹxLWB Sud j#ELu|<RKd.D5|Q>) zܧeWQ'|~oBa z(5%Rp oF \].dh2K0B@:qPqrRݢn2Fd\Xxuh5r|.!P :G;zǣžzP at_̍q u^/sFi$0XXc~ĵ~DH!nCQ@46Uhb- hUFza+ M_ay8L&ȈD_ h)bէ S=HAŖAn֒]tft,yk}B]ȲP1c^.KdC=ƀ f)aAAʽ|4Cz x,ꩌGI}#;!ݢye~wL.9.( B1&xbdS ?e(]f,7{9,@-EgbRK2ˢ&B-\.-F'jx#NX(DPL]ahLV^q!0tr_V7_xᅅc₭y;Ֆُ;c&? I].,R7Q/I+_ 9$VK!y67n.W^uJ21T(L13Gp#_R70aj\{ĂK R!XGܧ/|NZCz2HFYjÖ{p+j$~ωa^/yVϮc+={%L´{fMo2ō%nK;Lᦊk~d)uRSs*q?Ϥ} rEn _p3mħK\'֜z |\`}h.]:ځ%,V k`yopD[ (O) D).e8rb.J{* Ƅb-c,nݺSNI$c3߶R[ÕV,ˋO+i_˸?/gFJFYsB xLo}y/-\ոSxp%rI̥~Ba</eѰP92ǽwŏ#1nµT_eyn~~xnIR$m#62RO͛Vo?(RnR)!P Ew+<8GӰ\AXF9YMKu-D:Qxl>  Qx@uLNQp (=#R|V腡oUAU> }w)/\f HAw+Q]U|łS 2ԥ.r=~'!MzNG snT+sMFτ+Nx?b5> G)9ic g .(*Of/^[leWď'ǀ3ebV-NZrbBdlnُߒ/z)+^i%kwW$¢^%c|y5 JPyxo=Mq-TX8|#/xHI2-jˬ7#$)wG$q}R/Q\Ȫv0@IDAT RX% W{-hVˈ%ĪP`k"1eTb0,idXmC>+L:Y*`uK'BajXׄ!7Gqi5Rg֫ 5M%qIVP t챂*-+^X08.NG0ǕLcʊS5$W(--_@@X^3 >Z,E%Je=@Ib")9qy*IB(:]Tr"@RY(5%k,(2!>}-aL\ɘ:Ct",<0, Z0GI6&; _W7ۤaϫ>d)DKed 6/o_nPᖈEfX(nֻyپmȃ0 Et73H<ɢy%HWd'"Ȣ'jTkKcW ǝòŬ(E\}.?)5&/剺&Pս^ aƊ]gA<$+D~1AAwwAd\HN}c$o*M3|}~r'\Ѹ}jn"ԛFmKNqRQ"/2E&馛HhHePzIJJ+{4Q!$׉2E##r3D# P !J_J*[hrDŽsLy&}R@0M3O<-FJ%V;P:5ځlH$gHQL*! j3}N8$ߓ2|-ӓ5Ê@)&B~wM/7 P@:H=F/IC}[v NY|Di_*= hOdLj( ?OPOP ~A"q(pQe_ HZ9&H&Qj: [ͣJ_h b$ټo-l;O? 2z-ai5AL !Hˉ!.īⱣߗ(] CgAO(ut|DP~Q>(__(udaD˅#v-3}+yBY9 $-wt$u4|tܟx)[#V(1Pl0LFR4*ibAc!tH^ ĎIhu+aUj@E)+zWvp?J.K\cƌDEA1hР) F:Ҋx&7а(A4GBjrQ"G1#.8ء\aaf']q{aM6J+fiPFSryB[xt10;f;2"JY€PV3@XhOXo寥}}h竒Dсk%Z-'ިQN|?20W t82I)0Z.'V\C,P>%D#,~qG8eywHU{+ ,],E« ̈́]A$-{VY`Xn|U?l{=]NJ\SjL^|=uma-"?(+Xhv+6'\y%uuP ײ_*զVI]QVlx=9.WYri7"WcBR`'|&Si:s,#l婧ow,ו;NUky>aKk9EmZvNY +\P`]9 (){AV*XIxA^\N"]֒($qFTZ.*yqe5ηRʮV,)]P㭑Vџ(jbnq_\.]]FP^Ƞj u%oWXPmȊ#`",M3/J3aŕOC,:ESoc*'S1OX'"U^E:`1"{75U }85ʏ(*J#/7ŌeB숻銲Uɽ!&:%A*CDWǢ~# xrzu#/(IȊw(ߥo׈k/MG\B§:ӶrJ³SD:Qu:E4[/ 1N8aM*ɿoA[JZjs\9EAĝo b" pN*-@y+)M kƍ+pkCxonHB϶K+.'nz+T|o ǝO鹙 ooD&\(9c[j22-ӂ+SB$LYطC XVKt$W|c=<[W`$+6-Rz U|U3AEM@Z앖 Q+ޟz\pU=iGaEW^seabǤ{BJf.X 7jvPҝD,QE3¹{ /fb.4uLQPy7|>}:+_xqb'N,>jx%]xR ƸP"\Z(,ʽ{8RQĝn(wq+Y-r^ RU/zvi ,l1> )z:-qi$7(Z#I+b+$ vJC ѕ^X䤔0`" r-u^I.0&-<^#=e%L3+Jv,ac;}Ie)ޟW VrK*boӺS2$m ۫ZA;`P9r&9L Qf40`ZmՔw[ /lu] D4vv[V\?o5UqLp]0i,,XuØz;l͜s`%Z"o es +O&arEAQ=G1qtq|nqTW;9f|m2h%`R R(#1, /ᘋyT2֫_IBek$s"*DsԵy;2Dɥ Bp ' #DAc QD"|JLJ>L#w kBh_?"q;ȅվ7xadҥ6V}+bzBȿWYݩ5' $3 MiD)Lَ;dd<Õ۰[r})>BN(ѣGkX[<#r+j}ĉQPx~^ \b]ȂUK%Z @ Ey5p_DtEQ,#LN+8c} ׿82x.(=Ʉjr2ډ{ϡK"w_z%=tv}+K~B^f@wYNٕ-b#?Yfm4}qR}qUs?A=ZEPD^S)\K;(Xb"HJ/[X PBb Zj@,yEba 'к`#VyL|q\(b"* oӉ'sw|KTBjk렸_?~D%J!Q@T.ܤP2;/|I:TPx\W;>I $blڋ/s1>Z X! -9Ǧ^u?˶#F9}ص.74 ISv$XD.]T+ ꣄+.130zʼn j!6 OʰKDlk{ɡZ}_YxQK/lk`饗(Z]-br2Sv%iଳ-FIZT{_\zVjŋzCB:K?0XG0 0>`R| ֭[D O#}%?ChUW]YF_5D#R2iAqPSO=! 1qn;)~#@[¼@/J xDV[mi> )~F$Ȏ2޺+('_*,.9n堄K%cj~y_qZ&m2"tA.s2D^KGHyY9S"BJ=drP(Ez-v='=~G5=C-@[,!2SeqRy{9 \wA9'6Vduש؂xBVv5TvLrb=:g".%r\q4>[QJM74"+eg%V&*D],pJI[V".[qĈeF!-c,0D>K"W{_db%5.$$W"\{K 'ֽ_2%"6*>U˕qH9!rp"Pm$=.D۷o =3,ir!N5jzNmz뭝X$Sѭ) ":5BN<9G/O>-h߅?H3  +6Bd&kE]4ru܇sNE=E+ 7pZ-"Y/WZU-Z"TLnQG$[k:xa"˚AXrʡBHoPHJT[T ӨQ6Ip.&Xv1-gj.W k5irUN'|[!PK]xԀ,6l3dW ӡpW֎W'ЌWT3֫ڶ<·. nXk)@ @DpJf ,I&VHϝD;M X,qʦ?BV 'JwL0iUY Y_b dI7fyK8U/ VV(xp nذa?NuرJ‹? I"QmQ~AJ={OܯƜDa+t^2.&f‡p.o!C +ط_9ʵՌG+ϡݑ :Yguf i:^Ѹq>)ܞ&iuA)RXئ X Xxoi[;mtF5q=„ Qalj=%"dUVQ!&Q⋫#naA+>r0u/ kF)޽{$~}aد_?UL뮑 JVpRN&nȑ$% 9rj 26DEjI4<^W}IҎN Fu%q}DY@ >>H?O[j ZJǣreWU'bD4T;t)Jopm(dA/[~cZ~]s㕨jlBL.]jNhSd|&RA42MFf[t^&HNbѤ։E: /mfs)h 4RDN=QJ Q_3r N\>צ/jT.38c +tWI;EZ+! IY nD7"ph8mBKT@܍:esL0ŕ1WNj+ 1r@8!4,էOp ^珳dH5ZqS/\KFJM1뚺f{GUwy mᆁ>=b!8*/h|K{_^Y1K; 2DB0j.mXj-ŸߌXXޓWHƝ>8@H\kL)߷֣k|grgdX$CtZMltѹ/rvhDԅ;sYg-G.Ijղ% ۀN? egG+ÇpM222sH/E nbo}Øx3F#3w gL z#5&M?|c+799Vy5N0tIW^9n邋.(D '`u['D5 tN[bb1rvm文ή:,b+w1xGLn2yȑZkc?ц11@W^Q+.xٳ@Lh[IHUO+9J/:^{͟mxgukW_}A9Z]veX[Ԃ42r Y-#}%[u@.O#+)CVl~_,2B ?X~CGl0!B.oNvP~^q<xv,)}xve lJC PBѴ?࣏>*}5 y"JU, q51@&._0Vw^f呩ߖU7߼йC $Tnk{w`}o,Ě VYy7=bu5TJ ^5$2ũKƽu̵2s=Wyig #r@;?)9a裏~xЭ[7tQ-8Bo~{Iqk3F21  x  .Ad tTDYnB`*ztnP:!L+iu4`NܑWܣnuzJЉV4uoAo[d>~&`2#@[YgU_& ZWnvSuƱ-.ʘ8պG@;S_%\2ֹT-/ׯ~+xyE`*2&m]dر .p3G'NVLaÆTmzn5lǖO8_N;4' 'j4NVcij ĢˉbTĽa@n}q;0wG:Qt)O<=vv30C˗nN^N`M3 ">S'''N͝L(3, 6KEOYXp¿${x8qbq3RԉH+C' N"ɓ';QH;_:I}/ Ϭe s1Nu|J7Uj0}'\}^Bܒ19wwdHJ4J+ͻsj C Dw,<N&<2,^{mXYxcR͆W_} /L>3+<7,4[Z&m0P@=~x H>xbo[I0sbN"{n'\\fmt_"VF!.($#F>VrPLYɑ`RۆxwuUv&LF"91vlZ0.(ZU|~o(PEpi8\O.:J; Qf矿ӵv* =([qN ˯:qti5-؃ oF5X0zu1UVY+h|YlaQ CPh.+{0*ɓN:I N(X01@C]aP./U~':N"a W6:|K3XʠxYgunCɅrpgl<((=B_mO34߭"=Z\%3ӧ!/"R̚ˁ΅^讼J3tw} V;s^伊 z뭧F-!P?\S*/CkxZNQR hj5L3%װkjxH:o L&NimgO.?[ce?v+($P__joB 7ʮp OG9{Z'oEa '3H袋b 8/w+QpÇ׎tLl͚ExcЉvm(>+B t{έdGXQƤzp&!8P| : ѽa,Nn$Ke~'Jz?&q6faБǂʫW( QxX$~ɤu@?,p XaɟWt—K_^j:_'uRӫ+<aV%$mݦ.ڂ=O6Ϭt^TvI;p-Ư0t$t&Mr(,X%ϙNo3 "XoUH^$_~ᇕ_+vXFEϤ2( &Qf( +B0| 6]]MZ}bHP@v^-5T^"uh2"?c#c,1#7n?ǂ7U$2Y( ;@ւ"nLl_W,[q uNK()QP}DwO*iPz7qU 0:U%^I0F阕0g;)SECBҭPvN+^I+:Tv&4~ˊ_#D-J-\| ʕy睷ٵgH(P$E$VYM~1c-dz|?32_$k}k ѢP.ҹRF(_w MA?N9(p}|%o#xk@9r;՚H(R#`-m?Qg X8 7<a%l(JA='d5<^ %+"4{&_, C}֕*Q^b!$#܈yK_7_3*Uh{hQ'qώ(\XJA(P/D}fZ»K|ЁjAV,D)]Qƀ BVhHi4i8ih0Ar +*$qTZV)l1 81p@o< |G(PNΤ>PQ8aUvo:{;`U[i]3HXP qk y `&"{KJoC(a"-h{`F[~IeYX;2dq?Ə~o1~{|I*uBEfR v1%ƧlÂK!c]Ba:1ce}:cS+J!f~@oEFm/v&P Θ@EͷRNhh3Š2;JqY .;wwj 0дwf4+^cA>اvZCi҆3AcFL/RN{eGMǤ3䯑 ibaie vtƪIEf娇:I3_'%_ڂ"Ex>p]N2NWPP<0ތVpdMaÆ)w\ s@3([p-V^+C%(ߵ X zk/ckZ3@\7oo֔|3& w|fre41L"(z3hI20 [ظ?&: (q#ƒWeEd=6첋=`͸<\؏|dſc^t?l!VSIgyf.WL+g!`D"ЫW/}ݑ!`TVprAn+J.8M C`ِ!C~HIO" 8`7sy睧6Xuѭx*G03<.fl On$+^{*Jƞ={*+h 믿m/Vv)~eW'!`4) ؀0*CSxq~mׯ_?StU]mʆ=C Ų+l9EK/kTpK3hI?$D>|x޽{wuc;HSve!вu=(wW fDZ|Uńw駟>'YR!pQ'y7j(7ar%AbuY'y_hE]=P.ex/'Nt'pp Պx7wzrp+|ywP20 C0@?gyG`uS|w)Z[_~ŝy普{wu&Lp]w)fMLQtM箿zuGˤ#;찃Zy;nVSEt~Svٗ`!`TQ!O*.7qJ+vygꪫ:H/"h-@"裏[aiyqǹ':L7ިVPDmvYa '&OI= .{ u 2u[ ASvK"!`UDLݞ-(p7`G_'Xt5[ꆀ!P>nuqܫJw%ko-c}keuRn7J)O*O(M )<5 C0"fmnwe@~jO"dh`D21 "pM7)=ܣDA5tw/+90Kmk b+y+͐!C/m)[ C0@\)_}UY@6(. EZkU C#Db-.<(*2FE]TߢhΣK,F=ܳ>ۜCguV?ݱ렛od{D0eWߎ0 C 0 au̘1ͣeB/t~[lTa{ת[nu3 :"/?+tLM݈#Tyߊ z9n*D&%nkgҾ}߽0 Cfi&w׭ZC*v!P?{"n|>}'&!XpW[c518K/^jyq=hVExX}mTB{Ayn5tGn/ldSv C0 n<Tv]m .&Z"Q@ p t-#`]GCC0 "0+2qnr˹=ӭ:>s) ,.;8ꪫ:pi8oJ~S__;CV^Ǐ,g1iLJi!De46#|-xt0I&kcb@>VZ]p /Tnݺ#sM  =sJ-n&z[*VX<۸-ov -C0 6F`yuD[+zxnDby1Sn!n3Pr#V;S:?Vʜ|.{챎xIgUW-~.iު/I|"Bzy98\qbQ7רQB;0p@j~@~JUtYfmfa~5L>ӕ~#DҾ뮻V, >gŶ4d&!`!P2 fy@x4jHn5#gM1fiqW n@'P2 w$+{ r뮻n'6Uh 6pc ;Np 'OPV n:}( T>h p6l]13~m%k J(Qr7|dr_Ddn"T1Y .y$swqU("+@u.g֛zo&\~k&L~!ŭ? մ#Q}сXUjuގ5?!`@ 0e$5b@sǫi8`P/arV,8&LQ1)3fLl6Z5'tc /pi*NV;(RBs%Kf-Xa nj X} NJw8;kL;XtTYZ3(EAo N;By>`ꩧVx9{Z.SeJv?Ϻj2ZwPK?pP5kx!`ʮ{gcC0 "E&@\2dH Urg?cF$_}U5`+ځtUctR7JPaŅS %1J2,k*Pp 6,8>(9LkN΄;,'N>\دB4\yX3f`񝡨Z@4,ص7,ҺhSP~i#ѣU!B1iKZqC0 CVn;ww:(՚o⋻OyFz8eDh ҲcD?w_#'ᜋr)>/QJ8|wG]X%͉]+>2OLO{'E뮻*봶"pn 6p_^#H=qhsIcӖ\#pm)ogUv) i.D_|ovpGq*r gFo 'j\6B"@2yqqQy/'FrZ( ZX pOkGA123c9!tqǹ.]82g"<^ڞ+qMFo'.B,:-䒪 ދŦfg5($*N/Mk/›Z NKhtzJk&M@YI C0 " (% {z#@fBkܑ#T^r=|x?"Yj D?pEipa(s=W|.F[N4`xnݺ,(v/=~饗1_Erp/|y7-I|%G_[,m7~4L&!`!J ٱ$ \m Isnɤ`EK9Љb$wuשœL]?,J,p\Q2uĻN7S \8eb섓D]GIDKSO1sKuYwH\yCGme-ܢXI(B{px9ܨZv ^:J.:W Kȏ?ؽKji(*.{%7q pƢ kLQ: ,]w^7[ZNx)St+6:-D1,D-Xzꩱit2'ՏH@ 'LO<‰V&gv:+of!T7*ߖ" %gpjfPId [o1c2X^bI|&qoR^5C5oQ8w"n7Tr?r I(e"aD:|j+N~7g}$2w4 7 nowۻM4) b@Ȣ^pchnuQR\ 8 ~( Cqv 78^뾸4U9qurrZj`ş/CHd9%8 V.'ww u)|\[+{Xub)mr6U}Vo ^)?R&ƍSeOKwQyx©{|%[, meW\Jҭ <}Vm<4?磒z#Xk脥5%V͢V,Z [ C0~2zZD4VX$VRRPbyDrϥ \~Pt!_~8SϻⲴ;I>{!b^x>.AS6#jʌ հǒ &;VO Rm_(J /UK&/|_GuT͊.|; lKXs񭡰y:9\>kA:CzT[;$(QիpE=M]pb ev>`|^+2I4`ذac-<;/A!C wSi?0 mj" /0 C0RB@&'Jz+RdZHb ɚ,CD EEN *ojR"'$} L4[g ĊI ŵ/@&!RB /J± 'dG"iy3|X!b#t:DT4x#GT~Z- LR^{m .'&xe $zW'2jD_nEjп~DB}NCjKIEIR84|3jV"{G@]eD@tMQKt[m}~ІpJ]Oq +&?lދ(>؀X˛n+䄀'y`j׋H84 P@/QRHҨ#h(M C0 C E0'+)hI53 ؅&@,"U P#K(citҥ0de] A1@D8 L\4BXBNQuߓO>oJ .$<"aXPtQuT,atQT )x VkWo}G[QYҬj+N~7 h6SJ9JҔjOJ6N\b+dǩ(P"h*jsq:L}Xv8'n} ߼3r‡rKĽ]1ީuH( X pGFI'I?ƨߘXgS4T&!`!p\|rKJٲdC,;P>~9"A㣎eqha rdL+\##% k܁sկ<+'3<Dn>\RAq=jKώqmrE$Շ0DCU}E!SJs⇊%#Q|'\qϰ9D݅w FJA}, lź3GX)#`ʮ C0 xV7'+H" /TAn OXE!+X (=]H+{R"tFR-:ڬl| %#K;C\TGljvs 5͈,hRbUcOLٕb2 Ch\[eC6"hds=׽JB:%y^{m *2qD:vtz_i{ھ),RnĈD~Knʮ˙!`@#]!96in~gwGy׿744w,@"b^.{ mqG IL7ts7`wǺgy a95 <|np~+Z>r}QGƎksAeYq62koAU^{5wꩧ6eiGƍby_ǔ]܆!`uA[nAIx饗Dn0ɓ'W^Y\pse> i6mѯuYg}IKl<< #`ʮƿˁ!`@#uQ1l뮪 p_urZ CFuyYgUM6٤ڌ駟_)X6n.`I,gcGq[uUUQ 'Ic0eWc!`( C9\in&[gY#,g}.MK.{用gQ~\f2e?VE눊 &.lmՈ[/q+r&'aA駟jD!Rc1 |?t*F` C0 C"~Kc9F92~g >_}(٤~|ʩ1tP%\릞چE{ $C ;>c8i$Ub`O/wܡ/hޚdB -."{"lC[ ]vu/PoVnuixfٕwc93 ChbPhu]n]v}2¬H= bM2C q,I'~m>bd*% ҧ]fǻO@!㏵=.cEcX8qb'@-R?ad)@4 Ch{]vY]%=zt IrlMqSW~<]GynyjuGvoZRZj{`e|ꩧܞ{^D4+B9C|M*(_8n;ꪫ.\uM)=0 C5ZW\Sv!q)vxN;h)Bڛ@k w߹?Os(XdiZp-^RV[^ 5 /H4xs9C9.bףG/q&|sϭ/q5Ƽh{!`!кнj_*-,L);V>r=nvr6n[ "0yd68^7|Ekw&L9t{qmY5v"=vygE^MAu*mR`Zk9,̟|I+K=0 CAnA2ˮeb,x;ޮʖ-g^ fʮ˗!`@#$HMٕk~74+~k[.X*!PWP Dۈַ;ȵ o?{X:) ΃,X %.FYgMN#;衇m֔)S/3#`ʮ! C0 _Ek@ў)O$w_essx@ŋ@WG\g5K|S9?<Ř oR,{챇fO2_2mU2J (<&2qgxIAEǮ]VHrSGzL̘T,2@<㝉.j`Z10Yp ]lԩB9^&";N>dg[;/ZED[ `\y饗<7u& S{6m_|qVnFo9^J6;#)o<{Fo +&w{Ĭ 1ڀgoE~qki.CJDdHJj(s܀?> &7p'yLVZiR`ɓ,"s&&`tӾ}zxr% 1[sϼ9ն"PL}?Gb+N~GbW c 7'qX[?nlLc T \쳏[ijO<~ۛğk#bW,|q⺅0j<7KE$U/TwuW{aw޽}.ws.W~#W_}u^1 immq4?;$v埩j(}]&AYD0c"S] <95D&܊:(X~8餓[ſúk*"PpCK< 7X?"U)QFX{fW) ϟ~ѣGB!Y\[Uhcd[ ݥAoٶeG=VPzh\ !/첀E$óf,dq%(J M'`~oGf :4"17 zdOC#{Hd.JY\ݲ4e²֥"PU8 ő,]dyd #ʡk$L,(2fYN\dL"yc1"Gfu\NL/]pŇ+"2'}yG\Ml"0:#sNs6l"{dI75x5ަV`~Oe ?Z|*" " "P,k [#`ʑGd`A#Y`U4UVd1VC~VeVYtUaf-hUGʅc-;kd"v̝\~X@ܧ"skT9b0cV7c/.@!:C-[cbX8X‚'E{E!0+[-[dYSYFErީĮr>@~ /7&! 1QEo*_c/`1,rq]FAlOYjŚ,Q[aՅE?<,mdb ~2D|J(f}T&I&yv$p c fyQ0k2ӋԾ}LH,3WfI@iςxԃ=/Ƨ6 cǎP4`ҲM B6AfHSN &zhw3W &Lt K-Tu`7Eګ۳vO'H|B$h{o'j @)*Oq#NJ &knҵkh̙ͯT5@[R;G'N,֧[B @ѬYf6QϞ=W_}i \L芮7r)7p# ҭk GƝBZCW2ĽR%?-{5O - M_*Jkߦ0@Fҽb7:m'%ElfXqNիWI! {.|W>~tڨ6Y0r`n̔&e(y]G#<2̘136b馛7x#)teX_cIvE2>SaW,t3Jb5\{ᮻ 7|sfm9{q[%?$v凣j('auE߾}KեDD.,&&7n"*"P}]), f[lrnQեKo?>`T ;wzj]KMс{\SN~,_Z[SC Β^u> %!XжmUȵy`bI|YFnkU>nPxR/MN;TV+X;n: ] GMm̅_,,Nnlu@ ,i|Í'x" 4aIܩW]uզUVx`M0<rcl?m-" "bбaʔ)駟i9M1f5L?gKEJ\g0dwU#ڱcGW^b,zi-aJH]:, ]i=Xul\ЇzEձfcFEKv$t5z+ɂ'$wSuK쪈ìN@:.nY殀YhӦ]}CÇ~Xi" |MqE7f36]m?@m6IhS%~/vء)h xM7.`]FXJ[!*nպYz݂qݲxҙԲΫ1cdVC@nuE@D@J2 zg= }I4ɓJauk)7*"PJSxw^$Yä[lN:$wsǏGuTvm~o'?N;NJp|k Ȫ2sLˇGy$n:BheyBjRƽL2{u)b]s5+vew>蠃iDbU=n!_WfJDx*_{z![-ɂc 2;v䓩UdtvRD@D xĂ T(_xHRͦ;buG7\p}+oXx0% &͈b#&Gyd!v>ކ+m#fR.+"x~;+g6mZm-qŬ~a9;/.; ʇEg`9x[WAŠPBT'Fke7.3$;Y;#,TBv@{Ev_6}J#6"s& /JNk%(6s[~dE#L,XsJ?s->3wd"[d֘,&ve~l<ӫ-/ 戀/G')-Mk}fVUY7Gfy-!Hrװ]w^zih&E<=wYE]j7tS?\1+X g!ܯǞTʄ,/ԧFeW%(To4:_O5-OGq /Pէ2#@@^{-}&}ni`;;:rfv*T̂X <8qaW0ׯ%\2]/b'!b͘1: װ߀:\>zzk6d;'.7_Ncȵm9u ǕtÅ /qƯ׿5s=Xk|Jc -V@WX \y9ݴUJ0@IDAT 5싘 N8ЪU͝;7\~n=4ܬ!]uU.Bww * F"R߾>Soɓ}bNl&L|ۓJI 7V7~C6 L~U(y Y ,PcU\=gqArm.k>gY36DXЅ Mww9x}d C4hP.k_qJELB&.B|ฤ ve2#ȑ(/ÛK }o}Qh߾CnF~-p =Y4y\ϯcpb 6ZL]\{99uygaK5,>|xW︞!wމ'@O xmBcs|QLe&U}Y(n".m̙Pb{D"bM~#AvdE[.&E-Zt_U=߲Չv#˞٤5 `dqU߫YTꫯ,{4jԨDu#L$ZgёԏMwa|2֘f|E=dj=/(ָ;;, \L$,nL7Fvn|9PHq3,zjdIT9?:'=s]\2axys1z\#M % mMXe}.?K4S~]D{w`&ԩSۧ~\,Bd"ˬqkG}yWZ8L2gZԶm[_M,,gd"U\EX99-΄GR{;`;.]D™%2s*" " "PYA+{R{{SdY=jmb뮻ΗYrlO٫։cbdNز,9$U}bg}v~,vܿ?3(䆅Lͺ$aަMȂ&-M"ߓY4ilb",HtB ՊV[0;@.dwoQ,<2ЈM6'U &NjH*B1^,(j Xb E<묳"VCEBȊH QKmܸq^](ya|1%68efiJf p,\?˒x-t <"Txq~h6fK,;kdVUM,̾ ^< cZksbab1.#%1Chxe@i Y#F#Sqt̳_nyWn=MTvXp]o_$$qY;k՚e7B<%'Ľ)D,J@,ܭ7g!a׮UNkM4 qk $?xQ lr[{1?Fsz/q@SWC&(stW Qwqss5vZG&\78eZ0a ]X! l/^{yYs-n;z eIxS.}QԄ5 cw w%ke|_D\8޸~rnpqP ƌejDZ cws2&Yp&<L0"io՗}" "Pt=+FA:Lg+.f^xW^䩺x$54/&M`cmDX?*,OA(fid0%:6"Jd:Ŀ,hxE#%Ɍ/=w.fSq\bSWANUR-YbKӚ9VԷ^=bf=2z²kb92 I Pu# [v5Zvs  Q%-Yb|lM!xbX{Um5lG>>sDp}$Q,?4$/:%vUD@D YƊYȤH!;OD-VW?\KIm42sBM<ł{CB\cR&3m|O2ˢlndFdܼeuyT4ᰘhETcH9j-ɅW[r$W؊%kƜƽXzLHv,Rqц7$رc=.u!S_b[ qaa;=+(VkujCC׮.nKaCZ>_c [o̪fhXf+"3dqΒœ̎g].5ϥ\Ʋl.ت7$ HbTĮr:T n.xX'I>i{O͉M;e8L>Q4hI:#2,<D*\c}&Wih_LWح&v9cӅ pw_N&Ԥ 7I,Gy`yY KU~ Ld-ф/C~'b\0[;-F YQrkXI2d5.X7Q &܈|IGy$."%luϞ=s,mxesݰ|Y,-{^@`q/|(HNku-X[r튯[usݣmH5 *Βc6$?3>o^WqB\e>O ~7B{",t,a eqw%8q>.l2˶Nx@ ezf$;b7lў{Y_Cl٫ڥYqEvٍCDJn{YMDۣnݺEH%J5,&G~I2.[$wQUQ :̉fSV"k UNJܯl+G)*sެL|>UXƘHFwȘf - $E4q:h!&F==2p*" " "PljZH3\VbmfI<q3ҞĮpyUJ#܆xb;M+Ե/\؆n37uF 1xJKX4 BhxK'n'1,$>K,~Zq$o,]~g`. bye+5dje ݌ՓmY,n𖭍]xO]1bR 墋.rlA[ˋsw<,斺]s:mװcMR,*XB ƃV[-M]"0=X,X%f5?~CcY: 5,I+7LbaK!qMSe[bCrRIDY=IZA4TM1x.̟Ʃm" " "lR2y3֩M]bRu-D:.qVbLq1W6&x3'`"b uv饗v8}]24UNø+S$v:FXDAd{b͂e]<3^H\R׵1u~\$YxpVk%E昑kUmٶ96 e-2C삛>qe i̧csN0\O<:C0L{M,:x\5\o##u]<0a ]\~]ySD5JD@D@ 8-l7EٷHR(5ǤV:g?7X04eR[ZP8<\+(qlVZ%Q}^ZX@o^lID!V_L|ҋEșg>ZhAv}/%"J p8IKglB}/>&uYUujAcL0^ae_I죹u2%'nL`~Nsw<0lX%7=]24wr,s!1&?VX$B]-ID[>LzdwfDroH)v[eHJQsD@D@r'ǰIiZ-jܚF!"SZ%87^B}/>Ɨ,~kԂ"F5 ~T,VuV0MuX7x#pҸܸ3~I[]!X*q%m HIzwm7J6 71pG$k&] l)}"fL?ΊKe)]pF'{z=J)^ +DZ@ioW^8ihRQ^.0$9[L`c+qUi·hZ o^N[h`D Ťe:u2}׻+n |'cW_,߷fY5u$vA" " !@u2E謳 vo6hC̔\  >Xsl]Xq#T>TJ>(Mq\vi$ϥQ 9u`!$ kR(Xf&w۶mPYk굫5iXN ^;ѦgKtGTα'fmzFY5fi=g8$HfY0qM\/|Qb\v7;,X2:"" " %K.0HM/Zo"{ [G6q'=Qm|Z?Fmׯu-hvu՗CFL5U4Ew'[-fyK'M812 7،\ƜJ: ?JΝ#LH,2WDtMv5u朷IhnWtM75z8JF-C2F{Wd I=H9c#KPח]/뻧4яGYq>}Dr4bqlzhW?QO}TD@D@Dd X`bD/}8Xo Ə_o_hڴiQn|b76Ldz!>Wѡ@ͪRFs25mmuI'_F,kWhh̩Y xL9ٮ>#p}`dA I"zjv9o7bVͬ-W_ń;#Xt1&Dxgca,0ҬH[:>ܷ#?$*E;.9j=Y2 UsiBbW$" "3;mZ;K^Kxy\ʾm-ǿUjvI+Cda$۠E&D#ĭvpocOs) 9q-D%^w)]X40:dΤXCskWSy~'=', .GX:U~Z˷r˨SN׵ m|7 YLhƌ٬*fI*" " KݻwfᕊN;VVWT4ȍ8g*"{ 3<&Ukɓ7|u_4$1^?0L𪶟&WX ͢+l:d1aÏW^n׮lW6Q`uT_YÁX[L[Z*mDM;S-:uz0kj! \sceru]=+c沚UW7:c ^z\ZnRYbW'" " g2:xZZ3Q$ 4Jm>2@ZI&֞7;[-O~]tQXa?>0a„pA{.54 qc>_|a _Avٿ`b3 wuWb-QA B8 I{{.XL`OOjW++1XD@DP̭&L ˜c1cx^Yfq?\4:b(O}xcϤ0!Ye_}U̲ͯ2gK92,rB v$aRGS]lWf +Ç/؃,, +x~!zΘ+g@X' n! /ͭ0E5S ӑԮWbWbhU@`mKfӧO/nsiO=ܜ֯,GPiWO`=m۶ lZ/%v5lgLXZ^xظ˛ZEQCtZ BCXY#b׮\KML=[v`A*D#p ,n3pwSW_]m~H9s+)׈B{^,l ~^YVBv/oؕ7HD@DpUi߾}8ꨣٌj&?V!LTjF8Z") ڈiݵ*8_~)P\rIne  pĺ ሗ% JkZK!l,k=j 80Z"kW%}Y¤I¨Q Y컻K ZkoV wŰhBo2S-Sa0TD x òYk.~\ߜB =7oW;=wqGsOd[Dx \uUŚl= ;+kWcK-4s N9ХKBޓQ >cYhX Z6FS536|tn>:}C]~? GO^z)?G L]N$Yw>lbM7c:U$I8}]8#MO>1 ɸ $"\3h2ky<b/GU0[wBEڶn[`MwK4{ަ!/~zxsʫXȚH]v٥Z;ۥ$$(VAj׮]ue,V_˞"" " eCDwFiwe7?Mv/e0z3Zj"sm6=:2Wnjn#GO?tXrgl.[]I, E[l.T$B`ܸqM"HMȂG&TmӹsȬWd#űj944dlnz>P_oxY7w%4r=osi{/^"UfTk<-R,N=hF\5;WmSǞۥyw"z,B~ヅ{)&j/(mfYP[nY73mv'Q5_{WU[A?jً/72L\" [=piP{nj-Xdiǫ\VI3BSO=^T$C2k. >ZkmƑDBYj5!!3IE\Ɯj؇4]z-lr>#2NNj֠:vkWSy/cs?-ŲF,Hd7ͽ̚k\<Y:dۮG]wU4SLA@a*" " "PVpj ^RR耱9sg P.7j$nTCjLVJҲۓVE u6˩0~xH]=uQI7KYX3fUR\m[o =Đ \4t3]pFf@W^ -O2QUI)-+ ĺPr"@%/DHd0cIǎ_^*eeu D,BI%:`>Ü9s/:t66K,3uLzү*ZZT- P+ߞڇuZFP!YW_*uĮJ=TeY&@ܼ#m@]v *.zOQA ib >shX˅SO=-tyѾ}!Ahe A%tJ#t{cR+ ?|CaXo7upFE\WFɩT' :}(cud„ tȨ1ix|Yg;]֫&h:m6pn3x`i^"K/:}e]A7h#*ˑvyZbC>`F+%9F1̌ ^W_}uy6?#ҽ{w[H?OD@DBҸlvdw߅>:l&Ĺgy١jXa£>Gɺ Sh? .Zj)JAD,,uXKdT<e,yZk-м[BúwXb{o6۪ O?mc ?v8XjG ĉ,^<#,A>2䩗ސ'cǎ d[$:묓=(={0]XVF#Gq;I Bc'8߿_\$G?8\Z@Ɗ qNnEZ!za-iԨQVx0 l2\~UVE:Rb-.P?@[oqGmۺE20 /tf|A8蠃dU?:}[!_~pw5`[>}Ui ht7v8# "*J<4og{ROz7 s\s . ̚5-\pz-/{.o쳏1E뭷J[Mj?z_Υj=ڭ[7$庑Ȫ?#'z=eZhps f|K/Y c,+ZWD Yŀy3e[▙;Ud1W\12WD_|Eqa:Ns=W(O\S̞=[L1[T `o12wq7Bf͔V˄LF<ȞVT@ĉ# fcFY<^zUal2 '!T'5\# ,R/J0'˺̚)2h8֭K{od\2,YHE]lZҢ_-.EqY'JjM_,Mrm^{0'E9+qM:v..w7r>[:KYxve[;[)G20 6-n_3gtv~{{cា m_ZYH 1n8ń ,}COY܍bqcbt۷oɏI% r$a1ˮ0Jti.]^A~-+< 2YWB`R8VjcO:W^y~L{6`[G8cqܺN;$<̭`w}>q!~x12BB!6lvygJ*g <>U՝z꩞t's1np T, y߯_`1Re9UeZnm8e$VK5s2nIʝZ̙:&EL00'OdM\@*h͏u뭗_~-H?d2Ą}" %ob|q[6l3[Xy ۰{n5륗^ 3f6xcWis<ŒȤKbAl\AA@EHؿ%*e䢽,z\xir߼SL) fmSLqb#o QFաCޠ6^*@؂ċX0k׮Ƹkk.qq&Ɣ 6qLTA8VF֬Y8,V]uU6p}_# qqͧh_XB-DX26 ` P)m0 0/9Zk-nxayeYV},jy%qK>^92X `DDⅈ}5R~uu?h%v%WT:nTĂS;q!;/&-Zp*~{KpsǶ[e=Ĵx3A @i[>` O#UChGwhxlAǓ̱XbYqUZ,1J`|Jchyx`FܫX!MK;ܹsy;l,pqe{lkFw*0V~,&m kXA,;d /cQyL|_ ;"O\`'k 0h ĝҋĮJ?ȴxs#ȤDL>b+w+)ʡNEGbL_VL&1%X?w,2gإR8/)_X sd YW|F Cůw\^ůj _s{B c9 ъa,-%[ipD* w^͵TW\ѣ=&b6\+H䣯T$SN9%\|Ş͒i(+RpQG,J,BKM?Z]?c>}z+)-%vTE@D@D@D@D@D@D C=4n:{پNݻKjh<嫯ڌZJ{S]}zE &QF-[> vۀU;3fm .g|SƤ&+iª_D@D@D@D@D@D@ H믿C Gydr- jM7 ,L?~|~*Z:tx ,b:." " " " " "P=ذ袋s9$7޽{KjرĮfӦ" " " " " " " )!@n-\y.xYn.&OFo B~fͪHîNs;,*Ӄ?å܍ XU+Įzڱ^>3_ũWܮ_dEk*5HĮ7JD@D@D@D@D@D@RCw?btp aWNMӐ>}I&~9T축^bWŞ@8K,bWiv _.]*h?LR}egr$hSѣG /0,M! ;HgSު 6 |KyK<]gE@D@D@D@D@D@D`رcs= Bhd3gNvY6A좼[eӧ\;"+WRZOD@D@D@D@D@D@RF 'N {nZ;l}رjYqÒK.)*" " " " " " %OओN lMٳg%[ZhЯ_0f̘l_kYeW@:އeY˹tA^ j9w3IJ*H"WC.]>5uv5kMMJ!]ov47oT̮TE" " " " " " "P/n>|xavXo wygܒ=b?>jx3[+\]몄y'zE<0k֬/O?=aw_~yT7|s8p`_EQ\8JULgH13gz(wq)ne~⋇]"45_~e2dHXkP mM7XcwqG6lX8;~:7?C߾}];Cm#FsY{コV)S" " " " " " JUW]5ӧ\_XTM6ͅ+$/vXlrYgKK,6dpg{,\s5usi'|2}ǎC^Gٶeu֑XhZ_D@D@D@D@D@D@D0~Gwo;ꨣ|Wd/auyem=}Yo:vڅW^9j"mVmӹsg,[nᆰˇvکj>oJ~.DJ*쪤41cx^x![@c\^{p's=7L2ŭzGX-zTʗs19 p|饗3:Z0w'x|.zZl>Ƌ'OzJ+Zj&NpslLi߾}Yv@ZWD@D@D@D@D@D@Dx!h9[mU5jT8s±}]Eܸ +x6C,x# SjРAa 7 g&LOr,~i>t%t:3!=c0t%sφ_~ڲl?p}W߲X#ԩS3cup\z饫U@FfYfeŒ385 .͙3',r5-S" " " " " " FwީNl\pAE+ݱ"K.QWċBy\iݺu8S}ٸq"ebj.E%~}x̀lXֿ4jݶmzPeOmeauOD@D@D@D@D@D@JRBfEbwpĈɓ}F;fA"=zYT} r*"P(Z]x}6,'^-qDIJ?l ~EU~pYgb Rs; )bA)w~en$)c! g*#4cǺEk믿_d_\mY7N/g^d!5ZČ"7gb#XkAq=5kV b6ZsK/H@  ~A+[ZW˖-k>ǙmS2DVX! ` T юXf>h Sc1R֭?q/2۬2W/\lʱ̬k3b=Þ2r_bW9UID@D@D@D@D@D,T+_>:J(p0a"PuNnI5eʔТEBCaÆ!CT$6WFD@D@D@D@D@D@ʀ۴ijzs׺؆n;p'*F#g.J^,fΜYȥruD@D@D@D@D@D@D>8|%wS|F.,5FFHh/]-qf̘QڝȱrcVB ߪ&Nz쎉,^}0u԰RK%ҫs^dUGX(Iƍ=zHĹ*u H7t:gVj;Xc5Փ^$vVD@D@D@D@D@D@J~ dhƌ /b4!\}=Kg}6aER@/g {SSMn.{Cu{j fͪ2VbWDuAD@D@D@D@D@DYuƲ*6TI&vء."tqg 󽋒]*æ@i6mmvݑ~5 Ǐ>h2dH ZT&dEQqϟT@z-ϸءCV[-"L<9jsFCdh0`@XeU\ve{lmUk!?OpׯwRRbW)-UD@D@D@D@D@D4Q>1v TG7tS7n\':ujxF 935zh ]R˫* 80/<3oXknFl 7rĮlG[D@D@D@D@D@D@D@V^y:H1x`P@kw{O?t+)-FϬ-z뭳~W %vSE@D@D@D@D@D@JꫯղQus->~.xGM[֞.]xl.$D B]vD"@&XVq_â.Z;-h>sϰ뮻z ךlM7Mg*]̀ME@D@D@D@D@D@D2 BL߾}(s/b5*x̅wqG~wzof` B˖-3{Y]R'D@D@D@D@D@D@Dt vNYZh<EpG}'\r%{qpm?{peEuFD@D@D@D@D@D@K+ \|a\&pEVX! :4|aȑYc%܌ĪUm۶a̙nI[$bV}z D.D,RĢLM6UVWV.`X\=l_d[{$$N>d\M7U[\r%N #Ff~Gzꩧ5ydB_뭷[o-#֌|I 1-~qh"t%t__uNbW6*Z&" " " " " "P!;n>I2%Zk"﫯?@di4fraq?toƌ!!nCp8Kd=WW8p.$=3CBBg~=vD-^.l>w`]駫D7>Rn-ܲ(b0 " " " " " " i$@$Ĭ_~9K^ n!B $TG5ݰډ_c`XFΝ;ZE+Dw݃c5S,vaՄ% ,2mڴaaq@HBPBMS<{w_X>/x[%vP[D@D@D@D@D@D@$0_\I"653pK;UIH 9DDXc V;jЮ]&Âp7wFk_sd'N W^yexk1èP7.^> X2#cSeҤIawq<ֻR/q>|x@,n\ qK7psυM6${~z^?J+NU&" " " " " "*>hxL')M8R!mI,L1L}{v⟿^֮ wM*XΘ!~O,N?tG]ur, ;]cĮ$N |t{ ?|],ZxoK%~3τ &A0|{cLu-WO>ᡇW袏K&,<=N;9OPk?pGooժUswLdı"ck:px̯R̙3#<BXr ,@fA",)sX^X| :41" VbA>b߮F!tKijI} M7,d#BXbDJ)So9vmcwv ݻynAqƍ>wa$\]+[wE]4s=a7nqܗZl門}kNzVD@D@D@D@D@DN~aKZcy 8Cn`*"W_9/ ps\pKPlE\/D({X0-%$N>\_~pyHj*9m'" " " " " "njdZ;=GjgťF+"~ )B\N8:` G.s1uYyZߊ@nf͚w_z7O:(Y䆰boԨQk@G\k&kRբE wm?r9r]wʲ+;-ȑ1;Ygvm뢊4_ց]Ļ҇V<@Pcǖdɛ8=cu94)j.;() (ArQ$YP`%˰JEpYDrra1 A%(9YrΒAD@o=3=3={ϩ[޺9      gƉL{&#۷ &'OucCB5jH6mI&P)Çsŋ)tyҐb̙2rH!(v%ے (cǎIÆ K.ꮈS! 7oX{=jN\{ ֎'Nf͚Tzn? 2Dz)111I+Ů$bC      3g+WN._,۷oW!Ԃϟ>}Z.7oP|eիJ,)8G~gXwaZ GTT?~<(/66V "~݃rK.ŋ :M7ߔ?OOkL7Kn)vE $@$@$@$@$@$@~g!<ćʕ+;7 SN_|!˗/TPj/.]:yWeݺu~z}3fzK~a b}Yw'%Kvyu8pdϞq YynhcǎwUYʳ>qի[l :ϏfZ ?^6l 1(o޼zVKpD0>}H|dڵ.hp[fZ}ᢉ .T/đZhdɒEC$;w~m۶< +{L*T}1' Ȓ7VWX1xavtC<_j _.gֽ]{ڷo^ݺu+Ww $~BZ t_tRV-tv>XHHHHHHH >#4YFhLs)/رcaaxazOccիWO댘_mtVZg@:uX7nf̘åt<˧~c+(Dӧ:#JGfM:ڴiUn]>k,o[F:# YFtx =oѢcn׮]Vj $"֪UHͥKvڥs6Bav;we7&x)S&mrd\Lݻ,\>Mr\:,#Z?x#9׮]6lYr3gNG@2§իWL{OFյ믿jaX`ed9֭kNYXHHHHHHH 7oZmH3, (A |'ʂewqSLwƍkcդue,Xm>3mcT2_ɬ績e^@G3|XjԨ˗/3N޽]ݴAN]R Sk*XWp4fj2${@c(x5 `+pSݻsU(v:     cxMBRxQ‚pF(e\" 9l+eEGoulNh,W^1Y^2G,#{c1c8p@!]q~14޹ %ƀtƍpPp{C={Cy#yX;(`ҤI>gXdemA [fĸИӁ;wniժUj.IH{2V{ Bbct .WTXG$@$@$@$@$@aJa`Զ (*W,ժUӏ[|ª ˔)S"Ş+,+~b[!(? 2P/I)&6q+aaԩSh޹AB4_| 'W)Lpo4iL/ư2Yeذab bi!n,PΝ;{X`p]Dp2tPݛ`j-1Tca\J#^{51Qd fwlk3;-y 0`}[: hXNx&0;vԓ "Y6lؠ-**ʟjlO) ~!b-ʕKp`^V_ >666Eg= @xضmfhC&`-g\3Uti̙3:XqYƪJ| [F$.FhfXZVJ,#X&n:uh4R2lmc,cy qf>5FYQ>Ç0,]ʓ'e^z2e4[ƈP c#k"2LܸyZȪhD ]vQ1.0}&fe,n6h,Ft 1dX^:Ws=yxnVdd?o؞ĉ-M-2yUubb}|'zOcUh!(~GȀLkw`/ YHHHHHHœo)s )pC. k$*EfAXM!m`EkwUn޼aer.:^ v?6"(<ඈx^,_(Fdd3BWrwE#￯1ӧO+Sg:_Wc,[L\>nQ`a7:}S1BPȼpC8lFO< CR<p%F0[婠M=5jylE1` Cbr]v;gЅ   LŘ~p\ze;v[$Zjm!@{r_)R&vߒbW|"<'     0%XU[ӧc#? a 6ڑ#Gĸfj찏>( Aֵʕ+W4縞 qLЈbW$      $հa OUnE`firwR̴z^ ,!f[u.g<&     0&v|'B6lp*Y(AqXCƿs      "ϟgE0`+>C]Bxb] 98L(f~+ @ yx& jvB-Zm&IZrXq;w2N>QG-/(Q]3] _@̘Y]y+^` ŮDxN$@$@$@$@$@aJVZkٳ)Ϗ 6mŋej ץKٹs >cgXpW8ݗ_~< F%ӦMSBgIªTR7xBTK:Hk~duaJ0$ a 'zJ|ѢE O=dYf[RreiڴKWZqKJ9ԩSҠA`ңGQvG'}jlbYVܛuX?o״|VeG'o ~\r>1!e֭rY o״|ׯU HHHHHH ̙S?ØDG3T"br]JJ .cȬ8p@9X6kծ];@ J&M4ڔ)SR>k~ɓjժRbECRr$@$@$@$@$@$j׮-&LPA>o{zGyj~!3DBW~oSNZqxloߖ-[ … %mڴpeӧ&/t钷k~xH^v8]n 7={瞓?0a/Uw9r$ Xu ΒSla,sph{-ݴgYr,X0{!CywR)k~)wСcǎvU=ŮHXA$@$@$@$@$@$`1bL}ĉvuXuֲsN9w]_~:8pʂ $}Ҵi8q'wvZ$Yd ^rOr4\xݻ'wm6G%a)vE $@$@$@$@$@$@ Gշo_ѣ‘믿]Gv=!J !h|w5W9sGvEWСC؅Oon̙3u6 Xro'w@`(˿͛7 .)Y?+Wȯ*sX؋pEƤXQ2 @8@L!CkceڴijaL'9XfϞ]fϞ-uMĠf͚ ,auVH/FaԬYS̝;׮/ #_W9p 0@{Xr7I;vH~R .#GL2ykׯ_Wٴid˖-s ',&&F:$9+)؆HHHHHH U Lx:B)%p1uEzXsB q qߪe˖r T$!&܄?$ ]ŮdfS      /O> pGdϒF Y*}~zPBVEUkK Y4bN<)Hp-e+Ww6-)v%  "ƍSzIn4{'?.#8~ygdɒj* FiJ'ʕ 6cӡ4`Z$C ar Ůc{      a:c تU+Upa߿9rm?^O|gҰaC0WEѵkW L޼%w7n<+\׭[0agɒ%`vZɟ?WM+lD$@$@$@$@$@$2M0AN:%,X@"""qz,KCŋK6mԊ\ȼ׹sgI6 _df\β?3ar:vBWr]l4xL$@$@$@$@$@$En_rȑCSNOH4ir_<]&->@kɛ7]^gرclSNbŊA>޽{Պᅲ)S+kJL+ٟHHHHHH Y`5o< =Ԯ];ua,AײeTZbεE3H%]tA=Mn˖-*mĈ֝#GQFIŊe֬Y |Ac xE!B SO=BH%SL^N#pQ嫯҉u4a",<2f[nXYt 4HubŮXF|H     ~;wŋ DN8'8Gh|ƍt/,퓬YJFe˖Qr`݈YoKjw+p_w!k֬QqB bAe˖M{1ݪW bhj(wV:uTɗ/,~ڵRtiݻtEdSoԈ5ydAlf͚+"X/.M ~pGA\rRhѠ|ȅ T#b!92BBP{ıÖ+W.:ڵK7B|%gΜO Zn-NH@/ +=nN8nݺ/pك}5^}̙՚ e`e{g{n͹Ν;0lQ>o[ V>*,X!(QBA@VVUnӋ/AxYpap gƌlĂPAH q'O` "0l 4P`)vk      #XFQK\NGJ6Yd1C|-p:k,E`}Tg k49al a5`wؕ7Ͷ$@$@$@$@$@$@$,m ArвKضJ>YZ>7xX`7oĶs1V B+TM$@$@$@$@$@$@$@$} jXA$@$@$@$@$@$@$@$@!JbW8NHHHHHHHH !] HHHHHHHH D  bw]IENDB` pipdeptree-2.2.0/docs/example-requirements.txt0000644000175000017500000000031614131341150021040 0ustar nileshnileshFlask==0.10.1 itsdangerous==0.24 Jinja2 MarkupSafe==0.22 Werkzeug==0.11.2 argparse -e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy-master gnureadline==8.0.0