pipgrip 0.11.0__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.0"
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(
@@ -265,7 +396,7 @@ def render_lock(packages, include_dot=True, sort=False):
265
396
 
266
397
  @click.command(
267
398
  context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 84},
268
- help="pipgrip is a lightweight pip dependency resolver with deptree preview functionality based on the PubGrub algorithm, which is also used by poetry. For one or more PEP 508 dependency specifications, pipgrip recursively fetches/builds the Python wheels necessary for version solving, and optionally renders the full resulting dependency tree.",
399
+ help="pipgrip is a lightweight pip dependency resolver with deptree preview functionality based on the PubGrub algorithm, which is also used by poetry. For one or more PEP 508 dependency specifications, pipgrip recursively fetches Python wheel metadata necessary for version solving (with fallback to building the wheel if no metadata is available), and optionally renders the full resulting dependency tree.",
269
400
  )
270
401
  @click.argument("dependencies", nargs=-1)
271
402
  @click.option(
@@ -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.0
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
@@ -64,7 +64,7 @@ Dynamic: summary
64
64
  [![python](https://img.shields.io/pypi/pyversions/pipgrip.svg?logo=python&logoColor=white)](https://pypi.org/project/pipgrip/)
65
65
  [![downloads](https://static.pepy.tech/badge/pipgrip)](https://pypistats.org/packages/pipgrip)
66
66
 
67
- [pipgrip](https://github.com/ddelange/pipgrip) is a lightweight pip dependency resolver with deptree preview functionality based on the [PubGrub algorithm](https://medium.com/@nex3/pubgrub-2fb6470504f), which is also used by [poetry](https://github.com/python-poetry/poetry). For one or more [PEP 508](https://www.python.org/dev/peps/pep-0508/) dependency specifications, pipgrip recursively fetches/builds the Python wheels necessary for version solving, and optionally renders the full resulting dependency tree.
67
+ [pipgrip](https://github.com/ddelange/pipgrip) is a lightweight pip dependency resolver with deptree preview functionality based on the [PubGrub algorithm](https://medium.com/@nex3/pubgrub-2fb6470504f), which is also used by [poetry](https://github.com/python-poetry/poetry). For one or more [PEP 508](https://www.python.org/dev/peps/pep-0508/) dependency specifications, pipgrip recursively fetches Python wheel metadata necessary for version solving (with fallback to building the wheel if no metadata is available), and optionally renders the full resulting dependency tree.
68
68
 
69
69
  ```
70
70
  $ pipgrip --tree fastapi~=0.94
@@ -123,9 +123,10 @@ Usage: pipgrip [OPTIONS] [DEPENDENCIES]...
123
123
 
124
124
  pipgrip is a lightweight pip dependency resolver with deptree preview
125
125
  functionality based on the PubGrub algorithm, which is also used by poetry. For
126
- one or more PEP 508 dependency specifications, pipgrip recursively
127
- fetches/builds the Python wheels necessary for version solving, and optionally
128
- renders the full resulting dependency tree.
126
+ one or more PEP 508 dependency specifications, pipgrip recursively fetches
127
+ Python wheel metadata necessary for version solving (with fallback to building
128
+ the wheel if no metadata is available), and optionally renders the full
129
+ resulting dependency tree.
129
130
 
130
131
  Options:
131
132
  --install Install full dependency tree after resolving.
@@ -139,8 +140,8 @@ Options:
139
140
  --pipe Output space-separated pins instead of newline-
140
141
  separated pins.
141
142
  --json Output pins as JSON dict instead of newline-
142
- separated pins. Combine with --tree for a detailed
143
- nested JSON dependency tree.
143
+ separated pins. Combine with --tree or --reversed-
144
+ tree for a detailed nested JSON dependency tree.
144
145
  --sort Sort pins alphabetically before writing out. Can
145
146
  be used bare, or in combination with --lock,
146
147
  --pipe, --json, --tree-json, or --tree-json-exact.
@@ -153,6 +154,8 @@ Options:
153
154
  --tree-ascii Output human readable dependency tree with ASCII
154
155
  tree markers.
155
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.
156
159
  --max-depth INTEGER Maximum (JSON) tree rendering depth (default -1).
157
160
  --cache-dir DIRECTORY Use a custom cache dir.
158
161
  --no-cache-dir Disable pip cache for the wheels downloaded by
@@ -170,8 +173,8 @@ Options:
170
173
  dependencies (WARNING), -vv will show solving
171
174
  decisions (INFO), -vvv for development (DEBUG).
172
175
  --skip-invalid-input Skip invalid requirements (e.g. internal
173
- repositories, typos) and continue processing
174
- other dependencies.
176
+ repositories, typos) and continue processing other
177
+ dependencies.
175
178
  --version Show the version and exit.
176
179
  -h, --help Show this message and exit.
177
180
  ```
@@ -183,14 +186,14 @@ Exhaustive dependency trees without the need to install any packages ([at most b
183
186
  ```
184
187
  $ pipgrip --tree pipgrip
185
188
 
186
- pipgrip (0.10.6)
187
- ├── anytree>=2.4.1 (2.9.0)
188
- │ └── six (1.16.0)
189
- ├── click>=7 (8.1.6)
190
- ├── packaging>=17 (23.1)
191
- ├── pip>=22.2 (23.2.1)
192
- ├── setuptools>=38.3 (68.0.0)
193
- └── wheel (0.41.1)
189
+ pipgrip (0.12.0)
190
+ ├── anytree>=2.4.1 (2.13.0)
191
+ ├── click>=7 (8.3.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=dRrvxj3J72feH1jJTMOOTBdWRMp_uivh7mhwD2v-fMg,19
3
- pipgrip/cli.py,sha256=fbDkH9-ja-8GoJIlWAh7EqZq3X5f11_gFSdyzO_Oyus,21005
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.0.dist-info/licenses/LICENSE,sha256=ZoxfsQqxkYOcxTHFEye8YJRUUBAJISfdHej3Di7u_Bs,1587
33
- pipgrip-0.11.0.dist-info/METADATA,sha256=aza8VPqH9MtyNMUmC2tUyUo0NRKXa24o_t-rsflw-ek,18596
34
- pipgrip-0.11.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
35
- pipgrip-0.11.0.dist-info/entry_points.txt,sha256=VGqby8sWTjfkK20Vj_FqBZ_UxBlgmAOzq4rcJLYlRq0,45
36
- pipgrip-0.11.0.dist-info/top_level.txt,sha256=upoyu3ujOmKRvBUtTrwzk58e-r6zJahuT_8-RDsd2p0,8
37
- pipgrip-0.11.0.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