dependence 0.3.6__py3-none-any.whl → 1.0.0__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.
dependence/freeze.py CHANGED
@@ -1,18 +1,20 @@
1
1
  import argparse
2
2
  from fnmatch import fnmatch
3
+ from functools import partial
3
4
  from importlib.metadata import Distribution
4
5
  from importlib.metadata import distribution as _get_distribution
5
6
  from itertools import chain
6
7
  from typing import Dict, Iterable, MutableSet, Optional, Tuple, cast
7
8
 
8
- from ._utilities import iter_distinct, iter_parse_delimited_values
9
- from .utilities import (
9
+ from ._utilities import (
10
10
  get_distribution,
11
11
  get_required_distribution_names,
12
12
  get_requirement_string_distribution_name,
13
13
  install_requirement,
14
14
  is_configuration_file,
15
15
  iter_configuration_file_requirement_strings,
16
+ iter_distinct,
17
+ iter_parse_delimited_values,
16
18
  normalize_name,
17
19
  )
18
20
 
@@ -40,12 +42,15 @@ def _iter_sort_dependents_last(requirements: Iterable[str]) -> Iterable[str]:
40
42
  dependent: str
41
43
  dependencies: MutableSet[str]
42
44
  item: Tuple[str, MutableSet[str]]
43
- for dependent, dependencies in sorted(
45
+ for dependent, dependencies in sorted( # noqa: C414
44
46
  tuple(dependent_dependencies.items()),
45
47
  key=lambda item: item[0].lower(),
46
48
  ):
47
49
 
48
- def is_non_circular_requirement(dependency: str) -> bool:
50
+ def is_non_circular_requirement(
51
+ dependency: str,
52
+ dependent: str,
53
+ ) -> bool:
49
54
  """
50
55
  Return `True` if the dependency is still among the unaccounted
51
56
  for requirements, and is not a circular reference
@@ -53,13 +58,12 @@ def _iter_sort_dependents_last(requirements: Iterable[str]) -> Iterable[str]:
53
58
  return (dependency in dependent_dependencies) and (
54
59
  # Exclude interdependent distributions
55
60
  # (circular references)
56
- dependent
57
- not in dependent_dependencies[dependency]
61
+ dependent not in dependent_dependencies[dependency]
58
62
  )
59
63
 
60
64
  if (not dependencies) or not any(
61
65
  map(
62
- is_non_circular_requirement,
66
+ partial(is_non_circular_requirement, dependent=dependent),
63
67
  dependencies,
64
68
  )
65
69
  ):
@@ -75,30 +79,35 @@ def get_frozen_requirements(
75
79
  dependency_order: bool = False,
76
80
  reverse: bool = False,
77
81
  depth: Optional[int] = None,
82
+ include_pointers: Tuple[str, ...] = (),
83
+ exclude_pointers: Tuple[str, ...] = (),
78
84
  ) -> Tuple[str, ...]:
79
85
  """
80
86
  Get the (frozen) requirements for one or more specified distributions or
81
87
  configuration files.
82
88
 
83
89
  Parameters:
84
-
85
- - requirements ([str]): One or more requirement specifiers (for example:
86
- "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or paths
87
- to a setup.cfg, pyproject.toml, tox.ini or requirements.txt file
88
- - exclude ([str]): One or more distributions to exclude/ignore
89
- - exclude_recursive ([str]): One or more distributions to exclude/ignore.
90
- Note: Excluding a distribution here excludes all requirements which would
91
- be identified through recursively.
92
- those requirements occur elsewhere.
93
- - no_version ([str]) = (): Exclude version numbers from the output
94
- (only return distribution names)
95
- - dependency_order (bool) = False: Sort requirements so that dependents
96
- precede dependencies
97
- - depth (int|None) = None: Depth of recursive requirement discovery
90
+ requirements: One or more requirement specifiers (for example:
91
+ "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or
92
+ paths to a setup.cfg, pyproject.toml, tox.ini or requirements.txt
93
+ file
94
+ exclude: One or more distributions to exclude/ignore
95
+ exclude_recursive: One or more distributions to exclude/ignore.
96
+ Note: Excluding a distribution here excludes all requirements which
97
+ would be identified through recursion.
98
+ no_version: Exclude version numbers from the output
99
+ (only return distribution names)
100
+ dependency_order: Sort requirements so that dependents
101
+ precede dependencies
102
+ depth: Depth of recursive requirement discovery
103
+ include_pointers: A tuple of JSON pointers indicating elements to
104
+ include (defaults to all elements). Only applies to TOML files.
105
+ exclude_pointers: A tuple of JSON pointers indicating elements to
106
+ exclude (defaults to no exclusions). Only applies to TOML files.
98
107
  """
99
108
  # Separate requirement strings from requirement files
100
109
  if isinstance(requirements, str):
101
- requirements = set((requirements,))
110
+ requirements = {requirements}
102
111
  else:
103
112
  requirements = set(requirements)
104
113
  if isinstance(no_version, str):
@@ -115,7 +124,11 @@ def get_frozen_requirements(
115
124
  chain(
116
125
  requirement_strings,
117
126
  *map(
118
- iter_configuration_file_requirement_strings,
127
+ partial(
128
+ iter_configuration_file_requirement_strings,
129
+ include_pointers=include_pointers,
130
+ exclude_pointers=exclude_pointers,
131
+ ),
119
132
  requirement_files,
120
133
  ),
121
134
  )
@@ -210,14 +223,14 @@ def _iter_frozen_requirements(
210
223
  distribution_names - exclude,
211
224
  )
212
225
 
213
- distribution_names: MutableSet[str]
226
+ requirement_string: str
214
227
  requirements: Iterable[str] = iter_distinct(
215
228
  chain(
216
- *map(
217
- lambda distribution_names: get_required_distribution_names_(
218
- distribution_names, None if (depth is None) else depth - 1
219
- ),
220
- requirement_strings,
229
+ *(
230
+ get_required_distribution_names_(
231
+ requirement_string, None if (depth is None) else depth - 1
232
+ )
233
+ for requirement_string in requirement_strings
221
234
  )
222
235
  ),
223
236
  )
@@ -233,28 +246,32 @@ def freeze(
233
246
  dependency_order: bool = False,
234
247
  reverse: bool = False,
235
248
  depth: Optional[int] = None,
249
+ include_pointers: Tuple[str, ...] = (),
250
+ exclude_pointers: Tuple[str, ...] = (),
236
251
  ) -> None:
237
252
  """
238
253
  Print the (frozen) requirements for one or more specified requirements or
239
254
  configuration files.
240
255
 
241
256
  Parameters:
242
-
243
- - requirements ([str]): One or more requirement specifiers (for example:
244
- "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or paths
245
- to a setup.py, setup.cfg, pyproject.toml, tox.ini or requirements.txt
246
- file
247
- - exclude ([str]): One or more distributions to exclude/ignore
248
- - exclude_recursive ([str]): One or more distributions to exclude/ignore.
249
- Note: Excluding a distribution here excludes all requirements which would
250
- be identified through recursively.
251
- those requirements occur elsewhere.
252
- - no_version ([str]) = (): Exclude version numbers from the output
253
- (only print distribution names) for package names matching any of these
254
- patterns
255
- - dependency_order (bool) = False: Sort requirements so that dependents
256
- precede dependencies
257
- - depth (int|None) = None: Depth of recursive requirement discovery
257
+ requirements: One or more requirement specifiers (for example:
258
+ "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or
259
+ paths to a setup.py, setup.cfg, pyproject.toml, tox.ini or
260
+ requirements.txt file
261
+ exclude: One or more distributions to exclude/ignore
262
+ exclude_recursive: One or more distributions to exclude/ignore.
263
+ Note: Excluding a distribution here halts recursive
264
+ discovery of requirements.
265
+ no_version: Exclude version numbers from the output
266
+ (only print distribution names) for package names matching any of
267
+ these patterns
268
+ dependency_order: Sort requirements so that dependents
269
+ precede dependencies
270
+ depth: Depth of recursive requirement discovery
271
+ include_pointers: If this not empty, *only* these TOML tables will
272
+ inspected (for pyproject.toml files)
273
+ exclude_pointers: If not empty, these TOML tables will *not* be
274
+ inspected (for pyproject.toml files)
258
275
  """
259
276
  print(
260
277
  "\n".join(
@@ -266,6 +283,8 @@ def freeze(
266
283
  dependency_order=dependency_order,
267
284
  reverse=reverse,
268
285
  depth=depth,
286
+ include_pointers=include_pointers,
287
+ exclude_pointers=exclude_pointers,
269
288
  )
270
289
  )
271
290
  )
@@ -356,6 +375,26 @@ def main() -> None:
356
375
  type=int,
357
376
  help="Depth of recursive requirement discovery",
358
377
  )
378
+ parser.add_argument(
379
+ "--include-pointer",
380
+ default=[],
381
+ type=str,
382
+ action="append",
383
+ help=(
384
+ "One or more JSON pointers of elements to *include* "
385
+ "(applies to TOML files only)"
386
+ ),
387
+ )
388
+ parser.add_argument(
389
+ "--exclude-pointer",
390
+ default=[],
391
+ type=str,
392
+ action="append",
393
+ help=(
394
+ "One or more JSON pointers of elements to *exclude* "
395
+ "(applies to TOML files only)"
396
+ ),
397
+ )
359
398
  namespace: argparse.Namespace = parser.parse_args()
360
399
  freeze(
361
400
  requirements=namespace.requirement,
@@ -366,6 +405,8 @@ def main() -> None:
366
405
  no_version=namespace.no_version,
367
406
  dependency_order=namespace.dependency_order,
368
407
  depth=namespace.depth,
408
+ include_pointers=tuple(namespace.include_pointer),
409
+ exclude_pointers=tuple(namespace.exclude_pointer),
369
410
  )
370
411
 
371
412
 
dependence/update.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import argparse
2
- import os
3
2
  import re
4
3
  from collections import deque
5
4
  from configparser import ConfigParser, SectionProxy
5
+ from copy import deepcopy
6
6
  from dataclasses import dataclass
7
7
  from importlib.metadata import Distribution
8
8
  from io import StringIO
9
+ from itertools import chain
9
10
  from typing import (
10
11
  IO,
11
12
  Any,
@@ -26,10 +27,14 @@ from packaging.specifiers import Specifier, SpecifierSet
26
27
  from packaging.version import Version
27
28
  from packaging.version import parse as parse_version
28
29
 
29
- from ._utilities import iter_distinct, iter_parse_delimited_values
30
- from .utilities import (
30
+ from ._utilities import (
31
+ ConfigurationFileType,
32
+ get_configuration_file_type,
31
33
  get_installed_distributions,
32
34
  is_requirement_string,
35
+ iter_distinct,
36
+ iter_find_requirements_lists,
37
+ iter_parse_delimited_values,
33
38
  normalize_name,
34
39
  )
35
40
 
@@ -299,103 +304,173 @@ def get_updated_tox_ini(data: str, ignore: Iterable[str] = ()) -> str:
299
304
  return f"{tox_ini}\n"
300
305
 
301
306
 
302
- def get_updated_pyproject_toml(
303
- data: str, ignore: Iterable[str] = (), all_extra_name: str = ""
307
+ def _update_document_requirements(
308
+ document: Dict[str, Any],
309
+ ignore: Iterable[str] = (),
310
+ include_pointers: Tuple[str, ...] = (),
311
+ exclude_pointers: Tuple[str, ...] = (),
312
+ ) -> None:
313
+ ignore_set: Set[str] = _normalize_ignore_argument(ignore)
314
+
315
+ def get_updated_requirement_string(requirement: str) -> str:
316
+ return _get_updated_requirement_string(requirement, ignore=ignore_set)
317
+
318
+ # Find and update requirements
319
+ requirements_list: List[str]
320
+ for requirements_list in iter_find_requirements_lists(
321
+ document,
322
+ include_pointers=include_pointers,
323
+ exclude_pointers=exclude_pointers,
324
+ ):
325
+ requirements_list[:] = list(
326
+ map(
327
+ get_updated_requirement_string,
328
+ requirements_list,
329
+ )
330
+ )
331
+
332
+
333
+ def _get_updated_pyproject_toml(
334
+ data: str,
335
+ ignore: Iterable[str] = (),
336
+ all_extra_name: str = "",
337
+ include_pointers: Tuple[str, ...] = (),
338
+ exclude_pointers: Tuple[str, ...] = (),
304
339
  ) -> str:
305
340
  """
306
- Return the contents of a *setup.cfg* file, updated to reflect the
341
+ Return the contents of a *pyproject.toml* file, updated to reflect the
307
342
  currently installed project versions, excluding those specified in
308
343
  `ignore`.
309
344
 
310
345
  Parameters:
311
-
312
- - data (str): The contents of a *setup.cfg* file
313
- - ignore ([str]): One or more project names to leave as-is
314
- - all_extra_name (str): An (optional) extra name which will
315
- consolidate requirements from all other extras
346
+ data: The contents of a *pyproject.toml* file
347
+ ignore: One or more project names to leave as-is
348
+ all_extra_name: An (optional) extra name which will
349
+ consolidate requirements from all other extras
350
+ include_pointers: A tuple of JSON pointers indicating elements to
351
+ include (defaults to all elements).
352
+ exclude_pointers: A tuple of JSON pointers indicating elements to
353
+ exclude (defaults to no exclusions).
316
354
 
317
355
  Returns:
318
-
319
- The contents of the update pyproject.toml file.
356
+ The contents of the updated pyproject.toml file.
320
357
  """
321
- ignore_set: Set[str] = _normalize_ignore_argument(ignore)
322
-
323
- def get_updated_requirement_string(requirement: str) -> str:
324
- return _get_updated_requirement_string(requirement, ignore=ignore_set)
325
-
326
358
  # Parse pyproject.toml
327
- pyproject: Dict[str, Any] = tomli.loads(data)
328
- build_system_requires: List[str] = pyproject.get("build-system", {}).get(
329
- "requires", []
359
+ original_pyproject: Dict[str, Any] = tomli.loads(data)
360
+ updated_pyproject: Dict[str, Any] = deepcopy(original_pyproject)
361
+ # Find and update requirements
362
+ _update_document_requirements(
363
+ updated_pyproject,
364
+ ignore=ignore,
365
+ include_pointers=include_pointers,
366
+ exclude_pointers=exclude_pointers,
330
367
  )
331
- if build_system_requires:
332
- # Update build dependency versions
333
- pyproject["build-system"]["requires"] = list(
334
- map(
335
- get_updated_requirement_string,
336
- build_system_requires,
337
- )
338
- )
339
- project: Dict[str, Any] = pyproject.get("project", {})
340
- project_dependencies: List[str] = project.get("dependencies", [])
341
- if project_dependencies:
342
- # Update project dependency versions
343
- pyproject["project"]["dependencies"] = list(
344
- map(
345
- get_updated_requirement_string,
346
- project_dependencies,
368
+ # Update consolidated optional requirements
369
+ project_optional_dependencies: Dict[str, List[str]] = (
370
+ updated_pyproject.get("project", {}).get("optional-dependencies", {})
371
+ )
372
+ # Update an extra indicated to encompass all other extras
373
+ if project_optional_dependencies and all_extra_name:
374
+ key: str
375
+ dependencies: List[str]
376
+ project_optional_dependencies[all_extra_name] = list(
377
+ iter_distinct(
378
+ chain(
379
+ *(
380
+ dependencies
381
+ for key, dependencies in (
382
+ project_optional_dependencies.items()
383
+ )
384
+ if key != all_extra_name
385
+ )
386
+ )
347
387
  )
348
388
  )
349
- project_optional_dependencies: Dict[str, List[str]] = project.get(
350
- "optional-dependencies", {}
389
+ # Only dump the data if something was updated
390
+ if original_pyproject != updated_pyproject:
391
+ return tomli_w.dumps(updated_pyproject)
392
+ return data
393
+
394
+
395
+ def _get_updated_toml(
396
+ data: str,
397
+ ignore: Iterable[str] = (),
398
+ include_pointers: Tuple[str, ...] = (),
399
+ exclude_pointers: Tuple[str, ...] = (),
400
+ ) -> str:
401
+ """
402
+ Return the contents of a TOML file, updated to reflect the
403
+ currently installed project versions, excluding those specified in
404
+ `ignore`.
405
+
406
+ Note: This functions identically to `get_updated_pyproject_toml`, but
407
+ does not consolidate optional dependencies.
408
+
409
+ Parameters:
410
+ data: The contents of a TOML file
411
+ ignore: One or more package names to leave as-is
412
+ include_pointers: A tuple of JSON pointers indicating elements to
413
+ include (defaults to all elements).
414
+ exclude_pointers: A tuple of JSON pointers indicating elements to
415
+ exclude (defaults to no exclusions).
416
+
417
+ Returns:
418
+ The contents of the updated TOML file.
419
+ """
420
+ # Parse pyproject.toml
421
+ original_pyproject: Dict[str, Any] = tomli.loads(data)
422
+ updated_pyproject: Dict[str, Any] = deepcopy(original_pyproject)
423
+ # Find and update requirements
424
+ _update_document_requirements(
425
+ updated_pyproject,
426
+ ignore=ignore,
427
+ include_pointers=include_pointers,
428
+ exclude_pointers=exclude_pointers,
351
429
  )
352
- if project_optional_dependencies:
353
- # Update optional dependency versions
354
- all_extra_requirements: List[str] = []
355
- extra_name: str
356
- extra_requirements: List[str]
357
- for (
358
- extra_name,
359
- extra_requirements,
360
- ) in project_optional_dependencies.items():
361
- if extra_name == all_extra_name:
362
- continue
363
- extra_requirements = list(
364
- map(get_updated_requirement_string, extra_requirements)
365
- )
366
- if all_extra_name:
367
- all_extra_requirements += extra_requirements
368
- project_optional_dependencies[extra_name] = extra_requirements
369
- if all_extra_name:
370
- project_optional_dependencies[all_extra_name] = list(
371
- iter_distinct(all_extra_requirements)
372
- )
373
- if (
374
- build_system_requires
375
- or project_dependencies
376
- or project_optional_dependencies
377
- ):
378
- return tomli_w.dumps(pyproject)
430
+ # Only dump the data if something was updated
431
+ if original_pyproject != updated_pyproject:
432
+ return tomli_w.dumps(updated_pyproject)
379
433
  return data
380
434
 
381
435
 
382
436
  def _update(
383
- path: str, ignore: Iterable[str] = (), all_extra_name: str = ""
437
+ path: str,
438
+ ignore: Iterable[str] = (),
439
+ all_extra_name: str = "",
440
+ include_pointers: Tuple[str, ...] = (),
441
+ exclude_pointers: Tuple[str, ...] = (),
384
442
  ) -> None:
385
443
  data: str
386
444
  update_function: Callable[[str], str]
387
445
  kwargs: Dict[str, Union[str, Iterable[str]]] = {}
388
- base_file_name: str = os.path.basename(path).lower()
389
- if base_file_name == "setup.cfg":
446
+ configuration_file_type: ConfigurationFileType = (
447
+ get_configuration_file_type(path)
448
+ )
449
+ if configuration_file_type == ConfigurationFileType.SETUP_CFG:
390
450
  update_function = get_updated_setup_cfg
391
451
  if all_extra_name:
392
452
  kwargs["all_extra_name"] = all_extra_name
393
- elif base_file_name == "pyproject.toml":
394
- update_function = get_updated_pyproject_toml
395
- elif base_file_name == "tox.ini":
453
+ elif configuration_file_type == ConfigurationFileType.PYPROJECT_TOML:
454
+ update_function = _get_updated_pyproject_toml
455
+ kwargs.update(
456
+ all_extra_name=all_extra_name,
457
+ include_pointers=include_pointers,
458
+ exclude_pointers=exclude_pointers,
459
+ )
460
+ elif configuration_file_type == ConfigurationFileType.TOML:
461
+ update_function = _get_updated_toml
462
+ kwargs.update(
463
+ include_pointers=include_pointers,
464
+ exclude_pointers=exclude_pointers,
465
+ )
466
+ elif configuration_file_type == ConfigurationFileType.TOX_INI:
396
467
  update_function = get_updated_tox_ini
397
- else:
468
+ elif configuration_file_type == ConfigurationFileType.REQUIREMENTS_TXT:
398
469
  update_function = get_updated_requirements_txt
470
+ else:
471
+ raise NotImplementedError(
472
+ f"Updating requirements for {path} is not supported"
473
+ )
399
474
  kwargs["ignore"] = ignore
400
475
  file_io: IO[str]
401
476
  with open(path) as file_io:
@@ -413,25 +488,36 @@ def update(
413
488
  paths: Iterable[str],
414
489
  ignore: Iterable[str] = (),
415
490
  all_extra_name: str = "",
491
+ include_pointers: Tuple[str, ...] = (),
492
+ exclude_pointers: Tuple[str, ...] = (),
416
493
  ) -> None:
417
494
  """
418
495
  Update requirement versions in the specified files.
419
496
 
420
497
  Parameters:
421
-
422
- - path (str|[str}): One or more local paths to a setup.cfg,
423
- setup.cfg, and/or requirements.txt files
424
- - ignore ([str]): One or more project names to ignore (leave as-is)
425
- - all_extra_name (str): If provided, an extra which consolidates
426
- the requirements for all other extras will be added/updated to
427
- setup.cfg or setup.cfg (this argument is ignored for
428
- requirements.txt files)
498
+ path: One or more local paths to a setup.cfg,
499
+ setup.cfg, and/or requirements.txt files
500
+ ignore: One or more project names to ignore (leave as-is)
501
+ all_extra_name: If provided, an extra which consolidates
502
+ the requirements for all other extras will be added/updated to
503
+ setup.cfg or setup.cfg (this argument is ignored for
504
+ requirements.txt files)
505
+ include_pointers: A tuple of JSON pointers indicating elements to
506
+ include (defaults to all elements).
507
+ exclude_pointers: A tuple of JSON pointers indicating elements to
508
+ exclude (defaults to no exclusions).
429
509
  """
430
510
  if isinstance(paths, str):
431
511
  paths = (paths,)
432
512
 
433
513
  def update_(path: str) -> None:
434
- _update(path, ignore=ignore, all_extra_name=all_extra_name)
514
+ _update(
515
+ path,
516
+ ignore=ignore,
517
+ all_extra_name=all_extra_name,
518
+ include_pointers=include_pointers,
519
+ exclude_pointers=exclude_pointers,
520
+ )
435
521
 
436
522
  deque(map(update_, paths), maxlen=0)
437
523
 
@@ -467,6 +553,26 @@ def main() -> None:
467
553
  "requirements.txt files)"
468
554
  ),
469
555
  )
556
+ parser.add_argument(
557
+ "--include-pointer",
558
+ default=[],
559
+ type=str,
560
+ action="append",
561
+ help=(
562
+ "One or more JSON pointers of elements to *include* "
563
+ "(applies to TOML files only)"
564
+ ),
565
+ )
566
+ parser.add_argument(
567
+ "--exclude-pointer",
568
+ default=[],
569
+ type=str,
570
+ action="append",
571
+ help=(
572
+ "One or more JSON pointers of elements to *exclude* "
573
+ "(applies to TOML files only)"
574
+ ),
575
+ )
470
576
  parser.add_argument(
471
577
  "path",
472
578
  nargs="+",
@@ -476,11 +582,13 @@ def main() -> None:
476
582
  "and/or requirements.txt file"
477
583
  ),
478
584
  )
479
- arguments: argparse.Namespace = parser.parse_args()
585
+ namespace: argparse.Namespace = parser.parse_args()
480
586
  update(
481
- paths=arguments.path,
482
- ignore=tuple(iter_parse_delimited_values(arguments.ignore)),
483
- all_extra_name=arguments.all_extra_name,
587
+ paths=namespace.path,
588
+ ignore=tuple(iter_parse_delimited_values(namespace.ignore)),
589
+ all_extra_name=namespace.all_extra_name,
590
+ include_pointers=tuple(namespace.include_pointer),
591
+ exclude_pointers=tuple(namespace.exclude_pointer),
484
592
  )
485
593
 
486
594