Skip to content

Commit

Permalink
* print one package per line by default (see #21)
Browse files Browse the repository at this point in the history
* new --json option to retain old behavior (see #21)
* new --exclude option to ignore some packages (see #17)
* add doc notes about using conda-tree as a conda sub-command
  • Loading branch information
rvalieris committed Dec 27, 2022
1 parent daef140 commit 73b68ab
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 52 deletions.
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,49 @@ conda install -c conda-forge conda-tree 'networkx>=2.5'
```
to make sure the right networkx version is installed.

## Conda sub-command

When conda-tree is installed on the base environment, you can use the "tree" sub-command from any env:

```bash
# normal usage
(base) $ conda tree leaves
package-1
package-2

# activate another env
(base) $ conda activate env2

# no need to install conda-tree on env2
(env2) $ conda tree leaves
package-x
package-y
```

## Features

### Query the dependency tree

```bash
# packages that no other package depends on
$ conda-tree leaves
['samtools','bcftools',...]
samtools
bcftools
etc

# dependencies of a specific package
$ conda-tree depends samtools
['curl', 'xz', 'libgcc', 'zlib']
curl
xz
libgcc
zlib

# which packages depend on a specific package
$ conda-tree whoneeds xz
['samtools', 'bcftools', 'htslib', 'python']
samtools
bcftools
htslib
python

# dependency cycles
$ conda-tree cycles
Expand Down Expand Up @@ -77,11 +104,11 @@ conda-tree==0.0.4
```bash
# query by path
$ conda-tree -p /conda/envs/trinity leaves
['trinity']
trinity

# query by name
$ conda-tree -n trinity leaves
['trinity']
trinity
```

### Query package files
Expand Down
133 changes: 86 additions & 47 deletions conda-tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
import sys
import subprocess
import colorama
from colorama import Fore, Back, Style
from colorama import Style
colorama.init()

import conda.exports
import conda.api
import conda.base.context
import networkx

__version__ = '1.0.5'
__version__ = '1.1.0'

# The number of spaces
TABSIZE = 3
Expand All @@ -40,7 +41,7 @@ def make_cache_graph(cache):
def print_graph_dot(g):
print("digraph {")
for k,v in g.edges():
print(" \"{}\" -> \"{}\"".format(k,v))
print(f" \"{k}\" -> \"{v}\"")
print("}")

def remove_from_graph(g, node, _cache=None):
Expand All @@ -57,22 +58,19 @@ def print_dep_tree(g, pkg, prev, state):
down_search, args = state["down_search"], state["args"]
indent = state["indent"]
empty_cols, is_last = state["empty_cols"], state["is_last"]
tree_exists = state["tree_exists"]

s = "" # String to print
v = g.nodes[pkg].get('version') # Version of package

full_tree = True if ((hasattr(args, "full") and args.full)) else False

# Create list of edges
edges = g.out_edges(pkg) if down_search else g.in_edges(pkg)
e = [i[1] for i in edges] if down_search else [i[0] for i in edges]
# Maybe?: Sort edges in alphabetical order
# e = sorted(e, key=(lambda i: i[1] if down_search else i[0]))

if args.small:
if "conda" in e: state["tree_exists"].add("conda")
if "python" in e: state["tree_exists"].add("python")
if len(args.exclude) > 0:
for p in args.exclude:
state['tree_exists'].add(p)

dependencies_to_hide = (True # We hide dependencies if...
if ((pkg in state["tree_exists"] and not args.full)
Expand Down Expand Up @@ -116,7 +114,7 @@ def print_dep_tree(g, pkg, prev, state):
will_create_subtree = False
# We do not print these lines if:
# python and conda dependencies if '-small' on
if (pkg in ["python", "conda"] and args.small):
if pkg in args.exclude:
pass
else:
br2 = ' ' if is_last else '│'
Expand Down Expand Up @@ -157,7 +155,7 @@ def find_who_owns_file(prefix, target_path):
for p in conda.api.PrefixData(prefix).iter_records():
for f in p['files']:
if target_path in f or f in target_path:
print(p['name'], f)
print(f'{p["name"]}\t{f}')

def find_unowned_files(prefix):
pkg_files = get_pkg_files(prefix)
Expand All @@ -180,51 +178,80 @@ def is_node_reachable(graph, source, target):
else:
return any(networkx.algorithms.simple_paths.all_simple_paths(graph, source, target))

def print_pkgs(pkgs, with_json=False):
if with_json:
print(json.dumps(pkgs))
else:
for p in pkgs:
print(p)

def main():
parser = argparse.ArgumentParser()
parser.add_argument('-p','--prefix', default=None)
parser.add_argument('-n','--name', default=None)
parser.add_argument('-V','--version', action='version', version='%(prog)s '+__version__)
parser.add_argument('-p','--prefix',
help='full path to environment location (i.e. prefix)',
default=None)
parser.add_argument('-n','--name',
help='name of environment',
default=None)
parser.add_argument('-V','--version',
action='version',
version='%(prog)s '+__version__)

subparser = parser.add_subparsers(dest='subcmd')

# Arguments for "package_cmds" commands
# Subcommands that deal with the dependencies of packages
package_cmds = argparse.ArgumentParser(add_help=False)
package_cmds.add_argument('package', help='the target package')

format_args = argparse.ArgumentParser(add_help=False)
# Arguments for "rec_or_tree" commands
# Subcommands that can yield direct dependencies, recursive dependencies, or a tree view
rec_or_tree = package_cmds.add_mutually_exclusive_group(required=False)
rec_or_tree = format_args.add_mutually_exclusive_group(required=False)
rec_or_tree.add_argument('-t', '--tree',
help=('show dependencies of dependencies in tree form'),
help='show dependencies of dependencies in tree form',
action="store_true", default=False)
rec_or_tree.add_argument('--dot',
help='print a graphviz dot graph notation',
action="store_true", default=False)
rec_or_tree.add_argument('--json',
help='print packages in json format',
default=False, action="store_true")
rec_or_tree.add_argument('-r','--recursive',

# Arguments for "package_cmds" commands
# Subcommands that deal with the dependencies of packages
package_cmds = argparse.ArgumentParser(add_help=False, parents=[format_args])
package_cmds.add_argument('package', help='the target package')
package_cmds.add_argument('-r','--recursive',
help='show dependencies of dependencies',
default=False, action='store_true')
action='store_true',
default=False)

# Arguments for "hiding" commands
# Subcommands that enable users to hide a part of the result
hiding_cmds = argparse.ArgumentParser(add_help=False)
hiding_args = hiding_cmds.add_mutually_exclusive_group(required=False)
hiding_args.add_argument('--small',
help=('does not show dependencies of conda or python ' +
'to make the tree easier to understand'),
hiding_cmds.add_argument(
'--exclude',
help='comma separated list of packages to exclude dependencies from tree, ' +
'can be specified multiple times',
default=[],
action='append'
)
hiding_cmds.add_argument('--small',
help="don't include dependencies for conda and python. alias for --exclude conda,python",
default=False, action='store_true')
hiding_args.add_argument('--full',
help=('shows the complete dependency tree,' +
'with all the redundancies that it entails'),
hiding_cmds.add_argument('--full',
help='shows the complete dependency tree, ' +
'with all the redundancies that it entails',
default=False, action='store_true')
hiding_args.add_argument('--dot',
help=('print a graphviz dot graph notation'), action='store_true', default=False)

# Definining the simple subcommands
lv_cmd = subparser.add_parser('leaves',
help='shows leaf packages')
lv_cmd.add_argument('--export', help='export leaves dependencies',
action='store_true', default=False)
lv_cmd.add_argument('--with-cycles', help='include orphan cycles',
action='store_true', default=False)
lv_cmd.add_argument('--export',
help='export leaves dependencies',
default=False, action='store_true')
lv_cmd.add_argument('--with-cycles',
help='include orphan cycles',
default=False, action='store_true')
lv_cmd.add_argument('--json',
help='print packages in json format',
default=False, action="store_true")

subparser.add_parser('cycles', help='shows dependency cycles')

Expand All @@ -237,7 +264,7 @@ def main():
parents=[package_cmds, hiding_cmds])
subparser.add_parser('deptree',
help="shows the complete dependency tree",
parents=[hiding_cmds])
parents=[format_args, hiding_cmds])
subparser.add_parser('unowned-files',
help='shows files that are not owned by any package')
subparser.add_parser('who-owns',
Expand Down Expand Up @@ -296,6 +323,16 @@ def pkgs_with_cycles(graph):
'empty_cols': [], 'is_last': False, 'tree_exists': set(),
'hidden_dependencies': False, 'pkgs_with_cycles': pkgs_with_cycles(g)}

if args.subcmd in ['depends','whoneeds','deptree']:
if len(args.exclude) > 0:
ex = []
for i in args.exclude:
for j in i.split(','):
ex.append(j)
args.exclude = ex
if args.small:
args.exclude.extend(['conda','python'])

if args.subcmd == 'cycles':
print(get_cycles(g), end='')

Expand All @@ -306,23 +343,23 @@ def pkgs_with_cycles(graph):
# The 'depends' subcommand corresponds to a down search.
state["down_search"] = (args.subcmd == "depends")
if args.package not in g:
print("warning: package \"%s\" not found"%(args.package), file=sys.stderr)
print(f"warning: package \"{args.package}\" not found", file=sys.stderr)
sys.exit(1)
if args.recursive:
elif args.dot:
fn = networkx.descendants if state["down_search"] else networkx.ancestors
e = list(fn(g, args.package))
print(e)
print_graph_dot(g.subgraph(e+[args.package]))
elif args.tree:
tree, state = print_dep_tree(g, args.package, None, state)
print(tree, end='')
elif args.dot:
elif args.recursive:
fn = networkx.descendants if state["down_search"] else networkx.ancestors
e = list(fn(g, args.package))
print_graph_dot(g.subgraph(e+[args.package]))
print_pkgs(e, with_json=args.json)
else:
edges = g.out_edges(args.package) if state["down_search"] else g.in_edges(args.package)
e = [i[1] for i in edges] if state["down_search"] else [i[0] for i in edges]
print(e)
print_pkgs(e, with_json=args.json)

elif args.subcmd == 'leaves':
if args.with_cycles:
Expand All @@ -334,16 +371,19 @@ def pkgs_with_cycles(graph):
k = get_package_key(l, p)
print('%s::%s=%s=%s' % (l[k].channel.channel_name, l[k].name, l[k].version, l[k].build))
else:
print(lv)
print_pkgs(lv, with_json=args.json)

elif args.subcmd == 'deptree':
if args.dot:
print_graph_dot(g)
elif args.json:
print_pkgs(list(g), with_json=True)
else:
complete_tree = ""
for pk in get_leaves_plus_cycles(g):
tree, state = print_dep_tree(g, pk, None, state)
complete_tree += tree
if pk not in args.exclude:
tree, state = print_dep_tree(g, pk, None, state)
complete_tree += tree
print(''.join(complete_tree), end='')

elif args.subcmd == 'unowned-files':
Expand Down Expand Up @@ -375,4 +415,3 @@ def pkgs_with_cycles(graph):

if __name__ == "__main__":
main()

0 comments on commit 73b68ab

Please sign in to comment.