pipgrip 0.11.1__py2.py3-none-any.whl → 0.12.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pipgrip/_repo_version.py CHANGED
@@ -1 +1 @@
1
- version = "0.11.1"
1
+ version = "0.12.0"
pipgrip/cli.py CHANGED
@@ -42,7 +42,7 @@ from multiprocessing import cpu_count
42
42
  from subprocess import CalledProcessError
43
43
 
44
44
  import click
45
- from anytree import AsciiStyle, ContStyle, Node, RenderTree
45
+ from anytree import AsciiStyle, ContStyle, Node, PreOrderIter, RenderTree
46
46
  from anytree.exporter import DictExporter
47
47
  from packaging.markers import default_environment
48
48
  from packaging.requirements import InvalidRequirement
@@ -107,6 +107,47 @@ class DepTreeDictExporter(DictExporter):
107
107
  return data
108
108
 
109
109
 
110
+ class ReversedDepTreeDictExporter(DictExporter):
111
+ """Export reversed tree in full detail, children renamed to dependents."""
112
+
113
+ def __init__(
114
+ self, dictcls=OrderedDict, attriter=None, childiter=list, maxlevel=None
115
+ ):
116
+ DictExporter.__init__(
117
+ self,
118
+ dictcls=dictcls,
119
+ attriter=attriter,
120
+ childiter=childiter,
121
+ maxlevel=maxlevel,
122
+ )
123
+
124
+ @classmethod
125
+ def customsort(cls, tup):
126
+ order = ["name", "extras_name", "version", "pip_string", "requires"]
127
+ k, v = tup
128
+ if k in order:
129
+ return (str(order.index(k)), 0)
130
+ return tup
131
+
132
+ def export(self, node):
133
+ """Export tree starting at `node`."""
134
+ attriter = self.attriter or partial(sorted, key=self.customsort)
135
+ return self.__export(node, self.dictcls, attriter, self.childiter)
136
+
137
+ def __export(self, node, dictcls, attriter, childiter, level=1):
138
+ attr_values = attriter(self._iter_attr_values(node))
139
+ data = dictcls(attr_values)
140
+ maxlevel = self.maxlevel
141
+ if maxlevel is None or level < maxlevel:
142
+ children = [
143
+ self.__export(child, dictcls, attriter, childiter, level=level + 1)
144
+ for child in childiter(node.children)
145
+ ]
146
+ if children:
147
+ data["dependents"] = children
148
+ return data
149
+
150
+
110
151
  def flatten(tree_dict):
111
152
  """Flatten tree_dict to a shallow OrderedDict with all unique exact pins."""
112
153
  out = OrderedDict()
@@ -221,16 +262,18 @@ def render_tree(tree_root, max_depth, tree_ascii=False):
221
262
  for fill, _, node in RenderTree(child, style=style):
222
263
  if max_depth and node.depth > max_depth:
223
264
  continue
224
- lines.append(
225
- # fmt: off
226
- u"{}{} ({}{})".format(
227
- fill,
228
- node.pip_string,
229
- node.version,
230
- ", cyclic" if hasattr(node, "cyclic") else "",
231
- )
232
- # fmt: on
233
- )
265
+ # Build the parenthetical part
266
+ requires = getattr(node, "requires", None)
267
+ # fmt: off
268
+ cyclic = u", cyclic" if hasattr(node, "cyclic") else u""
269
+ if requires:
270
+ # Reversed tree dependent: name (version requires spec)
271
+ paren = u"{} requires {}{}".format(node.version, requires, cyclic)
272
+ else:
273
+ # Normal node: name (version)
274
+ paren = u"{}{}".format(node.version, cyclic)
275
+ lines.append(u"{}{} ({})".format(fill, node.pip_string, paren))
276
+ # fmt: on
234
277
  output += lines
235
278
  return "\n".join(output)
236
279
 
@@ -254,6 +297,94 @@ def render_json_tree_full(tree_root, max_depth, sort):
254
297
  return tree_dict_full
255
298
 
256
299
 
300
+ def reverse_tree(tree_root):
301
+ """Reverse the dependency tree to show dependents instead of dependencies.
302
+
303
+ Creates a new tree where:
304
+ - Each unique package becomes a root-level node (sorted alphabetically)
305
+ - Children are dependents with their requirement spec inline
306
+ - Format: name (version requires spec)
307
+ """
308
+ # Build reverse mapping: {extras_name: [(dependent_extras_name, dependent_node, req_spec), ...]}
309
+ # Use extras_name as key to distinguish packages with different extras (e.g., etils[enp] vs etils[epy])
310
+ reverse_map = {}
311
+ for node in PreOrderIter(tree_root):
312
+ if node.name == "__root__":
313
+ continue
314
+ for child in node.children:
315
+ # child is the dependency, node is the dependent
316
+ # child.pip_string = how node requires child (e.g., "numpy>=1.7")
317
+ child_key = child.extras_name
318
+ if child_key not in reverse_map:
319
+ reverse_map[child_key] = []
320
+ req_spec = child.pip_string
321
+ # Avoid duplicate entries for same dependent
322
+ if not any(d[0] == node.extras_name for d in reverse_map[child_key]):
323
+ reverse_map[child_key].append((node.extras_name, node, req_spec))
324
+
325
+ # Collect all unique packages with their resolved versions
326
+ all_packages = {} # extras_name -> (version, extras_name, extras)
327
+ for node in PreOrderIter(tree_root):
328
+ if node.name != "__root__":
329
+ all_packages[node.extras_name] = (
330
+ node.version,
331
+ node.extras_name,
332
+ node.extras,
333
+ )
334
+
335
+ def add_dependents(parent_node, pkg_extras_name, visited):
336
+ """Recursively add dependents with their requirement spec."""
337
+ dependents = reverse_map.get(pkg_extras_name, [])
338
+ for dependent_extras_name, dependent_node, req_spec in sorted(
339
+ dependents, key=lambda x: x[0]
340
+ ):
341
+ dep_child = Node(
342
+ dependent_node.name,
343
+ parent=parent_node,
344
+ version=dependent_node.version,
345
+ extras_name=dependent_node.extras_name,
346
+ extras=dependent_node.extras,
347
+ pip_string=dependent_node.extras_name, # Just the name
348
+ requires=req_spec, # How this dependent requires the parent
349
+ )
350
+ if dependent_extras_name in visited:
351
+ dep_child.cyclic = True
352
+ else:
353
+ # Recursively add this dependent's dependents
354
+ add_dependents(
355
+ dep_child, dependent_extras_name, visited | {dependent_extras_name}
356
+ )
357
+
358
+ # Create reversed tree
359
+ reversed_root = Node("__root__")
360
+
361
+ # Create root-level nodes for each package (sorted alphabetically by extras_name)
362
+ for pkg_extras_name in sorted(all_packages.keys()):
363
+ version, extras_name, extras = all_packages[pkg_extras_name]
364
+ pkg_node = Node(
365
+ pkg_extras_name.split("[")[0], # Base name for node.name
366
+ parent=reversed_root,
367
+ version=version,
368
+ extras_name=extras_name,
369
+ extras=extras,
370
+ pip_string=extras_name, # e.g., "numpy" or "etils[enp]" - version shown in parentheses
371
+ )
372
+ # Recursively add dependents
373
+ add_dependents(pkg_node, pkg_extras_name, {pkg_extras_name})
374
+
375
+ return reversed_root
376
+
377
+
378
+ def render_reversed_json_tree_full(reversed_tree_root, max_depth, sort):
379
+ """Render reversed tree to JSON with 'dependents' instead of 'dependencies'."""
380
+ maxlevel = max_depth + 1 if max_depth else None
381
+ exporter = ReversedDepTreeDictExporter(
382
+ maxlevel=maxlevel, attriter=sorted if sort else None
383
+ )
384
+ tree_dict_full = exporter.export(reversed_tree_root)["dependents"]
385
+ return tree_dict_full
386
+
387
+
257
388
  def render_lock(packages, include_dot=True, sort=False):
258
389
  fn = sorted if sort else list
259
390
  return fn(
@@ -304,7 +435,7 @@ def render_lock(packages, include_dot=True, sort=False):
304
435
  @click.option(
305
436
  "--json",
306
437
  is_flag=True,
307
- help="Output pins as JSON dict instead of newline-separated pins. Combine with --tree for a detailed nested JSON dependency tree.",
438
+ help="Output pins as JSON dict instead of newline-separated pins. Combine with --tree or --reversed-tree for a detailed nested JSON dependency tree.",
308
439
  )
309
440
  @click.option(
310
441
  "--sort",
@@ -338,6 +469,11 @@ def render_lock(packages, include_dot=True, sort=False):
338
469
  is_flag=True,
339
470
  help="Output human readable dependency tree (bottom-up).",
340
471
  )
472
+ @click.option(
473
+ "--reversed-tree-ascii",
474
+ is_flag=True,
475
+ help="Output human readable dependency tree (bottom-up) with ASCII tree markers.",
476
+ )
341
477
  @click.option(
342
478
  "--max-depth",
343
479
  type=click.INT,
@@ -407,6 +543,7 @@ def main(
407
543
  tree_json,
408
544
  tree_json_exact,
409
545
  reversed_tree,
546
+ reversed_tree_ascii,
410
547
  max_depth,
411
548
  cache_dir,
412
549
  no_cache_dir,
@@ -429,23 +566,22 @@ def main(
429
566
  logger.debug("pip version: %s", PIP_VERSION)
430
567
  logger.debug("pipgrip version: %s", __version__)
431
568
 
432
- if (
433
- sum(
434
- (
435
- pipe,
436
- (json or tree),
437
- tree_ascii,
438
- tree_json,
439
- tree_json_exact,
440
- reversed_tree,
441
- )
569
+ if sum(
570
+ (
571
+ pipe,
572
+ (json or tree or reversed_tree),
573
+ tree_ascii,
574
+ tree_json,
575
+ tree_json_exact,
576
+ reversed_tree_ascii,
442
577
  )
443
- > 1
444
- ):
578
+ ) > 1 or (tree and (reversed_tree or reversed_tree_ascii)):
445
579
  raise click.ClickException("Illegal combination of output formats selected")
446
580
 
447
- if tree_ascii or reversed_tree:
581
+ if tree_ascii:
448
582
  tree = True
583
+ if reversed_tree_ascii:
584
+ reversed_tree = True
449
585
  elif tree_json_exact:
450
586
  tree_json = True
451
587
 
@@ -561,9 +697,15 @@ def main(
561
697
  )
562
698
 
563
699
  if reversed_tree:
564
- raise NotImplementedError()
565
- # TODO tree_root = reverse_tree(tree_root)
566
- if tree:
700
+ reversed_tree_root = reverse_tree(tree_root)
701
+ if json:
702
+ output = dumps(
703
+ render_reversed_json_tree_full(reversed_tree_root, max_depth, sort),
704
+ default=sorted,
705
+ )
706
+ else:
707
+ output = render_tree(reversed_tree_root, max_depth, reversed_tree_ascii)
708
+ elif tree:
567
709
  if json:
568
710
  output = dumps(
569
711
  render_json_tree_full(tree_root, max_depth, sort), default=sorted
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipgrip
3
- Version: 0.11.1
3
+ Version: 0.12.0
4
4
  Summary: Lightweight pip dependency resolver with deptree preview functionality based on the PubGrub algorithm
5
5
  Home-page: https://github.com/ddelange/pipgrip
6
6
  Author: ddelange
@@ -140,8 +140,8 @@ Options:
140
140
  --pipe Output space-separated pins instead of newline-
141
141
  separated pins.
142
142
  --json Output pins as JSON dict instead of newline-
143
- separated pins. Combine with --tree for a detailed
144
- nested JSON dependency tree.
143
+ separated pins. Combine with --tree or --reversed-
144
+ tree for a detailed nested JSON dependency tree.
145
145
  --sort Sort pins alphabetically before writing out. Can
146
146
  be used bare, or in combination with --lock,
147
147
  --pipe, --json, --tree-json, or --tree-json-exact.
@@ -154,6 +154,8 @@ Options:
154
154
  --tree-ascii Output human readable dependency tree with ASCII
155
155
  tree markers.
156
156
  --reversed-tree Output human readable dependency tree (bottom-up).
157
+ --reversed-tree-ascii Output human readable dependency tree (bottom-up)
158
+ with ASCII tree markers.
157
159
  --max-depth INTEGER Maximum (JSON) tree rendering depth (default -1).
158
160
  --cache-dir DIRECTORY Use a custom cache dir.
159
161
  --no-cache-dir Disable pip cache for the wheels downloaded by
@@ -184,13 +186,14 @@ Exhaustive dependency trees without the need to install any packages ([at most b
184
186
  ```
185
187
  $ pipgrip --tree pipgrip
186
188
 
187
- pipgrip (0.11.0)
189
+ pipgrip (0.12.0)
188
190
  ├── anytree>=2.4.1 (2.13.0)
189
191
  ├── click>=7 (8.3.1)
190
- ├── packaging>=17 (25.0)
191
- ├── pip>=22.2 (25.3)
192
- ├── setuptools<81,>=38.3 (80.9.0)
193
- └── wheel (0.45.1)
192
+ ├── packaging>=17 (26.0)
193
+ ├── pip>=22.2 (26.0)
194
+ ├── setuptools<81,>=38.3 (80.10.2)
195
+ └── wheel (0.46.3)
196
+ └── packaging>=24.0 (26.0)
194
197
  ```
195
198
 
196
199
  For more details/further processing, combine `--tree` with `--json` for a detailed nested JSON dependency tree. See also `--tree-ascii` (no unicode tree markers), and `--tree-json` & `--tree-json-exact` (simplified JSON dependency trees).
@@ -1,6 +1,6 @@
1
1
  pipgrip/__init__.py,sha256=eF4gP3t6HU78VGDzL3PdQUP6dt_aBGEXLl9Xge9dAu4,1924
2
- pipgrip/_repo_version.py,sha256=SUcnFm7j3J3uMo-4MTj_VQdFouf2G1ZrvcI2NbsXESc,19
3
- pipgrip/cli.py,sha256=_FOT2bTgc2weECnNO-oLOjhg4mxCH4ZmMtVAS5PXlmc,21068
2
+ pipgrip/_repo_version.py,sha256=0d_eblF-LMXh1_mZw-q2DqmmX_vZj5tYDy8MONmqkNU,19
3
+ pipgrip/cli.py,sha256=7quSQL5L7qOr7RyR2_QhHUEyH9-j_nQMmX9EeXO7mPk,27062
4
4
  pipgrip/compat.py,sha256=54TukmlLmNW_va39lA8QiQlfGohzbs0kPHTfa2nTRfE,1889
5
5
  pipgrip/package_source.py,sha256=hGGzNyjHDgvDZrUjEGhjilfjxllQa-wkj8ucPXnKfwg,10035
6
6
  pipgrip/pipper.py,sha256=G_FvgYVEDeCq-qf-0-s5vAdzhR4fXQJnfJnooCpNyI8,19582
@@ -29,9 +29,9 @@ pipgrip/libs/semver/version.py,sha256=uNAkxpcQgWj047IJtZB9Fo_UlRFWCSAs8RxQ75ckTO
29
29
  pipgrip/libs/semver/version_constraint.py,sha256=nOFmEpOHCzpfqO1LFnXSqyZGGeN4mjLT228KYqh1qFY,2539
30
30
  pipgrip/libs/semver/version_range.py,sha256=NkmqYueJ_j8kxKd00BEykHFOWI39r31s38Csj8hu4-w,15350
31
31
  pipgrip/libs/semver/version_union.py,sha256=8ngwRmFKaOKmZ10M-ebyeRNB9uVl4HNL7FzoZYmOgDE,9801
32
- pipgrip-0.11.1.dist-info/licenses/LICENSE,sha256=ZoxfsQqxkYOcxTHFEye8YJRUUBAJISfdHej3Di7u_Bs,1587
33
- pipgrip-0.11.1.dist-info/METADATA,sha256=IzIQhkrZ1TpXXJc7GV_4dBpA9WxDtWRnFUe6sp5yu2M,18698
34
- pipgrip-0.11.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
35
- pipgrip-0.11.1.dist-info/entry_points.txt,sha256=VGqby8sWTjfkK20Vj_FqBZ_UxBlgmAOzq4rcJLYlRq0,45
36
- pipgrip-0.11.1.dist-info/top_level.txt,sha256=upoyu3ujOmKRvBUtTrwzk58e-r6zJahuT_8-RDsd2p0,8
37
- pipgrip-0.11.1.dist-info/RECORD,,
32
+ pipgrip-0.12.0.dist-info/licenses/LICENSE,sha256=ZoxfsQqxkYOcxTHFEye8YJRUUBAJISfdHej3Di7u_Bs,1587
33
+ pipgrip-0.12.0.dist-info/METADATA,sha256=ts6W4XWRCvtoHLCNO88q-woRUSiaAOgm6BkcTrPXmJM,18895
34
+ pipgrip-0.12.0.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110
35
+ pipgrip-0.12.0.dist-info/entry_points.txt,sha256=VGqby8sWTjfkK20Vj_FqBZ_UxBlgmAOzq4rcJLYlRq0,45
36
+ pipgrip-0.12.0.dist-info/top_level.txt,sha256=upoyu3ujOmKRvBUtTrwzk58e-r6zJahuT_8-RDsd2p0,8
37
+ pipgrip-0.12.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any