dependence 1.0.5__tar.gz → 1.1.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dependence
3
- Version: 1.0.5
3
+ Version: 1.1.0
4
4
  Summary: A dependency management tool for python projects
5
5
  Project-URL: Documentation, https://dependence.enorganic.org
6
6
  Project-URL: Repository, https://github.com/enorganic/dependence
@@ -21,11 +21,11 @@ Description-Content-Type: text/markdown
21
21
  [![test](https://github.com/enorganic/dependence/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/enorganic/dependence/actions/workflows/test.yml)
22
22
  [![PyPI version](https://badge.fury.io/py/dependence.svg?icon=si%3Apython)](https://badge.fury.io/py/dependence)
23
23
 
24
- Dependence provides a Command Line Interface and library for aligning
25
- a python projects' declared dependencies with the package versions
26
- installed in the environment in which `dependence` is executed, and for
27
- "freezing" recursively resolved package dependencies (like `pip freeze`, but
28
- for a package, instead of the entire environment).
24
+ Dependence provides a Command Line Interface and library for performing
25
+ dependency upgrades on a python project, aligning declared dependencies with
26
+ the package versions installed in the environment in which `dependence` is
27
+ executed, and for "freezing" recursively resolved package dependencies
28
+ (like `pip freeze`, but for a package, instead of the entire environment).
29
29
 
30
30
  - [Documentation](https://enorganic.github.io/dependence/)
31
31
  - [Contributing](https://enorganic.github.io/dependence/contributing)
@@ -38,7 +38,72 @@ You can install `dependence` with pip:
38
38
  pip3 install dependence
39
39
  ```
40
40
 
41
- ## Example Usage
41
+ ## Usage
42
+
43
+ ### Upgrading Dependencies
44
+
45
+ The `dependence upgrade` command, and the `dependence.upgrade.upgrade`
46
+ function, discover and upgrade project and environment dependencies in the
47
+ environment in which dependence is installed to their latest version
48
+ aligned with project and dependency requirements, then selectively update
49
+ requirement specifiers in any specified TOML files (such as pyproject.toml),
50
+ setup.cfg file, requirements.txt files, or tox.ini files. Because
51
+ pyproject.toml files may contain dependencies for more than one environment,
52
+ such as when using [hatch](https://hatch.pypa.io/) environments,
53
+ [JSON-style pointers](https://datatracker.ietf.org/doc/html/rfc6901) are used
54
+ to include or exclude specific parts of TOML files.
55
+
56
+ For example, in [this project's Makefile
57
+ ](https://github.com/enorganic/dependence/blob/main/Makefile#L27), we define a
58
+ `make upgrade` target as follows:
59
+
60
+ ```Makefile
61
+ SHELL := bash
62
+ PYTHON_VERSION := 3.9
63
+
64
+ upgrade:
65
+ hatch run dependence upgrade\
66
+ --include-pointer /tool/hatch/envs/default\
67
+ --include-pointer /project\
68
+ pyproject.toml && \
69
+ hatch run docs:dependence upgrade\
70
+ --include-pointer /tool/hatch/envs/docs\
71
+ --include-pointer /project\
72
+ pyproject.toml && \
73
+ hatch run hatch-static-analysis:dependence upgrade\
74
+ --include-pointer /tool/hatch/envs/docs\
75
+ --include-pointer /project\
76
+ pyproject.toml && \
77
+ hatch run hatch-test.py$(PYTHON_VERSION):dependence upgrade\
78
+ --include-pointer /tool/hatch/envs/hatch-test\
79
+ --include-pointer /project\
80
+ pyproject.toml && \
81
+ make requirements
82
+ ```
83
+
84
+ You can reference the [associated pyproject.toml file for this project
85
+ ](https://github.com/enorganic/dependence/blob/main/pyproject.toml#L21)
86
+ for reference concerning the implications of `--include-pointer`, which
87
+ uses identical syntax to [JSON pointers
88
+ ](https://datatracker.ietf.org/doc/html/rfc6901). The `--exclude-pointer`
89
+ parameter works identically, but in reverse. If both `--include-pointer`
90
+ and `--exclude-pointer` are used, only sections which match both conditions
91
+ will be updated.
92
+
93
+ You may refer to the [`dependence upgrade` CLI reference](./cli.md#dependence-upgrade)
94
+ and/or [`dependence.upgrade` API reference](./api/upgrade.md) for details
95
+ concerning this command/module, related options, and more complex use case
96
+ examples.
97
+
98
+ The `dependence upgrade` command, and the `dependence.upgrade.upgrade`
99
+ function, are simply a composite of the dependency listing and update
100
+ functionalities covered below, but which a `pip install --upgrade`
101
+ command executed in between—so please read further for additional details.
102
+ All parameters are directly passed, with the exception of
103
+ `--ignore-update`/`ignore_update`, which is translated to the
104
+ `--ignore`/`ignore` parameter for
105
+ `dependence update`/`dependence.update.update` (renamed in this operation
106
+ for clarity of purpose).
42
107
 
43
108
  ### Listing Dependencies
44
109
 
@@ -48,8 +113,8 @@ requirements.txt, pyproject.toml, setup.cfg, or tox.ini files. The output
48
113
  format matches that of `pip freeze`, but only lists dependencies of indicated
49
114
  packages and/or editable project locations.
50
115
 
51
- You may refer to the [`dependence freeze` CLI reference](https://dependence.enorganic.org/cli/#dependence-freeze)
52
- and/or [`dependence.freeze` API reference](https://dependence.enorganic.org/api/freeze/) for details
116
+ You may refer to the [`dependence freeze` CLI reference](./cli.md#dependence-freeze)
117
+ and/or [`dependence.freeze` API reference](./api/freeze.md) for details
53
118
  concerning this command/module, related options, and more complex use case
54
119
  examples.
55
120
 
@@ -157,7 +222,7 @@ $ diff pyproject_before.toml pyproject_after.toml
157
222
  ```
158
223
 
159
224
  As you can see, only the version specifier for tomli changed. We know that
160
- every dependency was upgraded, wo why was only the `tomli` version specifier
225
+ every dependency was upgraded, so why was only the `tomli` version specifier
161
226
  updated? By design. Here are the rules `dependence update` adheres to:
162
227
 
163
228
  - We only update requirements versions when they have *inclusive* specifiers.
@@ -174,7 +239,7 @@ updated? By design. Here are the rules `dependence update` adheres to:
174
239
  - If your requirement is unversioned, we don't touch it, of course. This is
175
240
  why you didn't see any change for "pip".
176
241
 
177
- You may refer to the [`dependence update` CLI reference](https://dependence.enorganic.org/cli/#dependence-update)
178
- and/or [`dependence.update` API reference](https://dependence.enorganic.org/api/update/) for details
242
+ You may refer to the [`dependence update` CLI reference](./cli.md#dependence-update)
243
+ and/or [`dependence.update` API reference](./api/update.md) for details
179
244
  concerning this command/module, related options, and more complex use
180
245
  cases/examples.
@@ -3,11 +3,11 @@
3
3
  [![test](https://github.com/enorganic/dependence/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/enorganic/dependence/actions/workflows/test.yml)
4
4
  [![PyPI version](https://badge.fury.io/py/dependence.svg?icon=si%3Apython)](https://badge.fury.io/py/dependence)
5
5
 
6
- Dependence provides a Command Line Interface and library for aligning
7
- a python projects' declared dependencies with the package versions
8
- installed in the environment in which `dependence` is executed, and for
9
- "freezing" recursively resolved package dependencies (like `pip freeze`, but
10
- for a package, instead of the entire environment).
6
+ Dependence provides a Command Line Interface and library for performing
7
+ dependency upgrades on a python project, aligning declared dependencies with
8
+ the package versions installed in the environment in which `dependence` is
9
+ executed, and for "freezing" recursively resolved package dependencies
10
+ (like `pip freeze`, but for a package, instead of the entire environment).
11
11
 
12
12
  - [Documentation](https://enorganic.github.io/dependence/)
13
13
  - [Contributing](https://enorganic.github.io/dependence/contributing)
@@ -20,7 +20,72 @@ You can install `dependence` with pip:
20
20
  pip3 install dependence
21
21
  ```
22
22
 
23
- ## Example Usage
23
+ ## Usage
24
+
25
+ ### Upgrading Dependencies
26
+
27
+ The `dependence upgrade` command, and the `dependence.upgrade.upgrade`
28
+ function, discover and upgrade project and environment dependencies in the
29
+ environment in which dependence is installed to their latest version
30
+ aligned with project and dependency requirements, then selectively update
31
+ requirement specifiers in any specified TOML files (such as pyproject.toml),
32
+ setup.cfg file, requirements.txt files, or tox.ini files. Because
33
+ pyproject.toml files may contain dependencies for more than one environment,
34
+ such as when using [hatch](https://hatch.pypa.io/) environments,
35
+ [JSON-style pointers](https://datatracker.ietf.org/doc/html/rfc6901) are used
36
+ to include or exclude specific parts of TOML files.
37
+
38
+ For example, in [this project's Makefile
39
+ ](https://github.com/enorganic/dependence/blob/main/Makefile#L27), we define a
40
+ `make upgrade` target as follows:
41
+
42
+ ```Makefile
43
+ SHELL := bash
44
+ PYTHON_VERSION := 3.9
45
+
46
+ upgrade:
47
+ hatch run dependence upgrade\
48
+ --include-pointer /tool/hatch/envs/default\
49
+ --include-pointer /project\
50
+ pyproject.toml && \
51
+ hatch run docs:dependence upgrade\
52
+ --include-pointer /tool/hatch/envs/docs\
53
+ --include-pointer /project\
54
+ pyproject.toml && \
55
+ hatch run hatch-static-analysis:dependence upgrade\
56
+ --include-pointer /tool/hatch/envs/docs\
57
+ --include-pointer /project\
58
+ pyproject.toml && \
59
+ hatch run hatch-test.py$(PYTHON_VERSION):dependence upgrade\
60
+ --include-pointer /tool/hatch/envs/hatch-test\
61
+ --include-pointer /project\
62
+ pyproject.toml && \
63
+ make requirements
64
+ ```
65
+
66
+ You can reference the [associated pyproject.toml file for this project
67
+ ](https://github.com/enorganic/dependence/blob/main/pyproject.toml#L21)
68
+ for reference concerning the implications of `--include-pointer`, which
69
+ uses identical syntax to [JSON pointers
70
+ ](https://datatracker.ietf.org/doc/html/rfc6901). The `--exclude-pointer`
71
+ parameter works identically, but in reverse. If both `--include-pointer`
72
+ and `--exclude-pointer` are used, only sections which match both conditions
73
+ will be updated.
74
+
75
+ You may refer to the [`dependence upgrade` CLI reference](./cli.md#dependence-upgrade)
76
+ and/or [`dependence.upgrade` API reference](./api/upgrade.md) for details
77
+ concerning this command/module, related options, and more complex use case
78
+ examples.
79
+
80
+ The `dependence upgrade` command, and the `dependence.upgrade.upgrade`
81
+ function, are simply a composite of the dependency listing and update
82
+ functionalities covered below, but which a `pip install --upgrade`
83
+ command executed in between—so please read further for additional details.
84
+ All parameters are directly passed, with the exception of
85
+ `--ignore-update`/`ignore_update`, which is translated to the
86
+ `--ignore`/`ignore` parameter for
87
+ `dependence update`/`dependence.update.update` (renamed in this operation
88
+ for clarity of purpose).
24
89
 
25
90
  ### Listing Dependencies
26
91
 
@@ -30,8 +95,8 @@ requirements.txt, pyproject.toml, setup.cfg, or tox.ini files. The output
30
95
  format matches that of `pip freeze`, but only lists dependencies of indicated
31
96
  packages and/or editable project locations.
32
97
 
33
- You may refer to the [`dependence freeze` CLI reference](https://dependence.enorganic.org/cli/#dependence-freeze)
34
- and/or [`dependence.freeze` API reference](https://dependence.enorganic.org/api/freeze/) for details
98
+ You may refer to the [`dependence freeze` CLI reference](./cli.md#dependence-freeze)
99
+ and/or [`dependence.freeze` API reference](./api/freeze.md) for details
35
100
  concerning this command/module, related options, and more complex use case
36
101
  examples.
37
102
 
@@ -139,7 +204,7 @@ $ diff pyproject_before.toml pyproject_after.toml
139
204
  ```
140
205
 
141
206
  As you can see, only the version specifier for tomli changed. We know that
142
- every dependency was upgraded, wo why was only the `tomli` version specifier
207
+ every dependency was upgraded, so why was only the `tomli` version specifier
143
208
  updated? By design. Here are the rules `dependence update` adheres to:
144
209
 
145
210
  - We only update requirements versions when they have *inclusive* specifiers.
@@ -156,7 +221,7 @@ updated? By design. Here are the rules `dependence update` adheres to:
156
221
  - If your requirement is unversioned, we don't touch it, of course. This is
157
222
  why you didn't see any change for "pip".
158
223
 
159
- You may refer to the [`dependence update` CLI reference](https://dependence.enorganic.org/cli/#dependence-update)
160
- and/or [`dependence.update` API reference](https://dependence.enorganic.org/api/update/) for details
224
+ You may refer to the [`dependence update` CLI reference](./cli.md#dependence-update)
225
+ and/or [`dependence.update` API reference](./api/update.md) for details
161
226
  concerning this command/module, related options, and more complex use
162
227
  cases/examples.
@@ -23,7 +23,10 @@ def _print_help() -> None:
23
23
  "installed\n"
24
24
  " distribution or project, in a similar "
25
25
  "format\n"
26
- " to the output of `pip freeze`."
26
+ " to the output of `pip freeze`.\n"
27
+ " upgrade Upgrade all dependencies and align "
28
+ "project\n"
29
+ " requirement specifiers to match."
27
30
  )
28
31
 
29
32
 
@@ -25,6 +25,7 @@ from typing import (
25
25
  Callable,
26
26
  TypedDict,
27
27
  cast,
28
+ overload,
28
29
  )
29
30
  from warnings import warn
30
31
 
@@ -37,6 +38,63 @@ _BUILTIN_DISTRIBUTION_NAMES: tuple[str] = ("distribute",)
37
38
  _UNSAFE_CHARACTERS_PATTERN: re.Pattern = re.compile("[^A-Za-z0-9.]+")
38
39
 
39
40
 
41
+ class DefinitionExistsError(Exception):
42
+ """
43
+ This error is raised when an attempt is made to redefine
44
+ a singleton class instance.
45
+ """
46
+
47
+
48
+ _module_locals: dict[str, Any] = locals()
49
+
50
+
51
+ class Undefined:
52
+ """
53
+ This class is intended to indicate that a parameter has not been passed
54
+ to a keyword argument in situations where `None` is to be used as a
55
+ meaningful value.
56
+
57
+ The `Undefined` class is a singleton, so only one instance of this class
58
+ is permitted: `sob.UNDEFINED`.
59
+ """
60
+
61
+ __module__ = "sob"
62
+
63
+ def __init__(self) -> None:
64
+ # Only one instance of `Undefined` is permitted, so initialization
65
+ # checks to make sure this is the first use.
66
+ if "UNDEFINED" in _module_locals:
67
+ message: str = f"{self!r} may only be instantiated once."
68
+ raise DefinitionExistsError(message)
69
+
70
+ def __repr__(self) -> str:
71
+ # Represent instances of this class using the qualified name for the
72
+ # constant `UNDEFINED`.
73
+ return "sob.UNDEFINED"
74
+
75
+ def __bool__(self) -> bool:
76
+ # `UNDEFINED` cast as a boolean is `False` (as with `None`)
77
+ return False
78
+
79
+ def __hash__(self) -> int:
80
+ return 0
81
+
82
+ def __eq__(self, other: object) -> bool:
83
+ # Another object is only equal to this if it shares the same id, since
84
+ # there should only be one instance of this class defined
85
+ return other is self
86
+
87
+ def __reduce__(self) -> tuple[Callable[[], Undefined], tuple]:
88
+ return _undefined, ()
89
+
90
+
91
+ UNDEFINED: Undefined = Undefined()
92
+
93
+
94
+ def _undefined() -> Undefined:
95
+ return UNDEFINED
96
+
97
+
40
98
  def iter_distinct(items: Iterable[Hashable]) -> Iterable:
41
99
  """
42
100
  Yield distinct elements, preserving order
@@ -292,10 +350,14 @@ class ConfigurationFileType(Enum):
292
350
 
293
351
 
294
352
  @functools.lru_cache
295
- def get_configuration_file_type(path: str) -> ConfigurationFileType:
296
- if not os.path.isfile(path):
353
+ def get_configuration_file_type(
354
+ path: str | Path, default: Any = UNDEFINED
355
+ ) -> ConfigurationFileType:
356
+ if isinstance(path, str):
357
+ path = Path(path)
358
+ if not path.is_file():
297
359
  raise FileNotFoundError(path)
298
- basename: str = os.path.basename(path).lower()
360
+ basename: str = path.name.lower()
299
361
  if basename == "setup.cfg":
300
362
  return ConfigurationFileType.SETUP_CFG
301
363
  if basename == "tox.ini":
@@ -306,16 +368,54 @@ def get_configuration_file_type(path: str) -> ConfigurationFileType:
306
368
  return ConfigurationFileType.REQUIREMENTS_TXT
307
369
  if basename.endswith(".toml"):
308
370
  return ConfigurationFileType.TOML
309
- message: str = f"{path} is not a recognized type of configuration file."
310
- raise ValueError(message)
371
+ if default is UNDEFINED:
372
+ message: str = (
373
+ f"{path!s} is not a recognized type of configuration file."
374
+ )
375
+ raise ValueError(message)
376
+ return default
311
377
 
312
378
 
313
379
  def is_configuration_file(path: str) -> bool:
314
- try:
315
- get_configuration_file_type(path)
316
- except (FileNotFoundError, ValueError):
317
- return False
318
- return True
380
+ return get_configuration_file_type(path, default=None) is not None
381
+
382
+
383
+ @overload
384
+ def iter_configuration_files(path: str) -> Iterable[str]: ...
385
+
386
+
387
+ @overload
388
+ def iter_configuration_files(path: Path) -> Iterable[Path]: ...
389
+
390
+
391
+ def iter_configuration_files(path: str | Path) -> Iterable[Path | str]:
392
+ """
393
+ Iterate over the project configuration files for the given path.
394
+ If the path is a file path—yields only that path. If the path is a
395
+ directory, yields all configuration files in that directory.
396
+ """
397
+ if os.path.exists(path):
398
+ if os.path.isdir(path):
399
+ child: Path
400
+ for child in filter(
401
+ Path.is_file,
402
+ (
403
+ path.iterdir()
404
+ if isinstance(path, Path)
405
+ else Path(path).iterdir()
406
+ ),
407
+ ):
408
+ if (
409
+ get_configuration_file_type(child, default=None)
410
+ is not None
411
+ ):
412
+ yield (
413
+ child
414
+ if isinstance(path, Path)
415
+ else str(child.absolute())
416
+ )
417
+ elif get_configuration_file_type(path, default=None) is not None:
418
+ yield path
319
419
 
320
420
 
321
421
  class _EditablePackageMetadata(TypedDict):
@@ -7,15 +7,16 @@ from functools import partial
7
7
  from importlib.metadata import Distribution
8
8
  from importlib.metadata import distribution as _get_distribution
9
9
  from itertools import chain
10
- from typing import cast
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, cast
11
12
 
12
13
  from dependence._utilities import (
13
14
  get_distribution,
14
15
  get_required_distribution_names,
15
16
  get_requirement_string_distribution_name,
16
17
  install_requirement,
17
- is_configuration_file,
18
18
  iter_configuration_file_requirement_strings,
19
+ iter_configuration_files,
19
20
  iter_distinct,
20
21
  iter_parse_delimited_values,
21
22
  normalize_name,
@@ -74,8 +75,8 @@ def _iter_sort_dependents_last(requirements: Iterable[str]) -> Iterable[str]:
74
75
  del dependent_dependencies[dependent]
75
76
 
76
77
 
77
- def get_frozen_requirements(
78
- requirements: Iterable[str] = (),
78
+ def get_frozen_requirements( # noqa: C901
79
+ requirements: Iterable[str | Path] = (),
79
80
  *,
80
81
  exclude: Iterable[str] = (),
81
82
  exclude_recursive: Iterable[str] = (),
@@ -109,21 +110,30 @@ def get_frozen_requirements(
109
110
  exclude_pointers: A tuple of JSON pointers indicating elements to
110
111
  exclude (defaults to no exclusions). Only applies to TOML files.
111
112
  """
112
- # Separate requirement strings from requirement files
113
- if isinstance(requirements, str):
114
- requirements = {requirements}
113
+ if isinstance(requirements, (str, Path)):
114
+ requirements = {str(requirements)}
115
115
  else:
116
- requirements = set(requirements)
116
+ requirements = set(map(str, requirements))
117
117
  if isinstance(no_version, str):
118
118
  no_version = (no_version,)
119
119
  elif not isinstance(no_version, tuple):
120
120
  no_version = tuple(no_version)
121
- requirement_files: MutableSet[str] = set(
122
- filter(is_configuration_file, requirements)
123
- )
124
- requirement_strings: MutableSet[str] = cast(
125
- MutableSet[str], requirements - requirement_files
126
- )
121
+ # Separate requirement strings from requirement files
122
+ configuration_files: MutableSet[str] = set()
123
+ requirement_strings: MutableSet[str] = set()
124
+ requirement: str | Path
125
+ for requirement in requirements:
126
+ if TYPE_CHECKING:
127
+ assert isinstance(requirement, str)
128
+ requirement_configuration_files: set[str] = set(
129
+ iter_configuration_files(requirement)
130
+ )
131
+ if requirement_configuration_files:
132
+ configuration_files |= requirement_configuration_files
133
+ else:
134
+ if requirement.startswith("setup.py"):
135
+ raise ValueError(requirement)
136
+ requirement_strings.add(requirement)
127
137
  frozen_requirements: Iterable[str] = iter_distinct(
128
138
  chain(
129
139
  requirement_strings,
@@ -133,10 +143,11 @@ def get_frozen_requirements(
133
143
  include_pointers=include_pointers,
134
144
  exclude_pointers=exclude_pointers,
135
145
  ),
136
- requirement_files,
146
+ configuration_files,
137
147
  ),
138
148
  )
139
149
  )
150
+ frozen_requirements = tuple(frozen_requirements)
140
151
  if depth is not None:
141
152
  depth -= 1
142
153
  if (depth is None) or depth >= 0:
@@ -242,7 +253,7 @@ def _iter_frozen_requirements(
242
253
 
243
254
 
244
255
  def freeze(
245
- requirements: Iterable[str] = (),
256
+ requirements: Iterable[str | Path] = (),
246
257
  *,
247
258
  exclude: Iterable[str] = (),
248
259
  exclude_recursive: Iterable[str] = (),
@@ -263,9 +274,10 @@ def freeze(
263
274
  paths to a setup.py, setup.cfg, pyproject.toml, tox.ini or
264
275
  requirements.txt file
265
276
  exclude: One or more distributions to exclude/ignore
266
- exclude_recursive: One or more distributions to exclude/ignore.
267
- Note: Excluding a distribution here halts recursive
268
- discovery of requirements.
277
+ exclude_recursive: One or more distributions to exclude.
278
+ exclude_recursive: One or more distributions to exclude. Recursive
279
+ dependency discovery is also halted for these distributions,
280
+ unlike those passed to `exclude`.
269
281
  no_version: Exclude version numbers from the output
270
282
  (only print distribution names) for package names matching any of
271
283
  these patterns
@@ -8,6 +8,7 @@ from copy import deepcopy
8
8
  from dataclasses import dataclass
9
9
  from io import StringIO
10
10
  from itertools import chain
11
+ from pathlib import Path
11
12
  from typing import (
12
13
  IO,
13
14
  TYPE_CHECKING,
@@ -423,12 +424,13 @@ def _get_updated_toml(
423
424
 
424
425
 
425
426
  def _update(
426
- path: str,
427
+ path: str | Path,
427
428
  ignore: Iterable[str] = (),
428
429
  all_extra_name: str = "",
429
430
  include_pointers: tuple[str, ...] = (),
430
431
  exclude_pointers: tuple[str, ...] = (),
431
432
  ) -> None:
433
+ message: str
432
434
  data: str
433
435
  update_function: Callable[[str], str]
434
436
  kwargs: dict[str, str | Iterable[str]] = {}
@@ -457,8 +459,8 @@ def _update(
457
459
  elif configuration_file_type == ConfigurationFileType.REQUIREMENTS_TXT:
458
460
  update_function = _get_updated_requirements_txt
459
461
  else:
460
- msg = f"Updating requirements for {path} is not supported"
461
- raise NotImplementedError(msg)
462
+ message = f"Updating requirements for {path!s} is not supported"
463
+ raise NotImplementedError(message)
462
464
  kwargs["ignore"] = ignore
463
465
  file_io: IO[str]
464
466
  with open(path) as file_io:
@@ -466,18 +468,19 @@ def _update(
466
468
  updated_data: str = update_function(data, **kwargs)
467
469
  if updated_data == data:
468
470
  print( # noqa: T201
469
- f"All requirements were already up-to-date in {path}"
471
+ f"All requirements were already up-to-date in {path!s}"
470
472
  )
471
473
  else:
472
474
  print( # noqa: T201
473
- f"Updating requirements in {path}"
475
+ f"Updating requirements in {path!s}"
474
476
  )
475
477
  with open(path, "w") as file_io:
476
478
  file_io.write(updated_data)
477
479
 
478
480
 
479
481
  def update(
480
- paths: Iterable[str],
482
+ paths: Iterable[str | Path],
483
+ *,
481
484
  ignore: Iterable[str] = (),
482
485
  all_extra_name: str = "",
483
486
  include_pointers: tuple[str, ...] = (),
@@ -487,22 +490,27 @@ def update(
487
490
  Update requirement versions in the specified files.
488
491
 
489
492
  Parameters:
490
- path: One or more local paths to a setup.cfg,
493
+ paths: One or more local paths to a pyproject.toml,
491
494
  setup.cfg, and/or requirements.txt files
492
- ignore: One or more project names to ignore (leave as-is)
495
+ ignore: One or more project/package names to ignore (leave
496
+ as-is) when updating dependency requirement specifiers.
493
497
  all_extra_name: If provided, an extra which consolidates
494
498
  the requirements for all other extras will be added/updated to
495
- setup.cfg or setup.cfg (this argument is ignored for
499
+ pyproject.toml or setup.cfg (this argument is ignored for
496
500
  requirements.txt files)
497
501
  include_pointers: A tuple of JSON pointers indicating elements to
498
- include (defaults to all elements).
502
+ include (defaults to all elements). This applies only to TOML
503
+ files (including pyproject.toml), and is ignored for all other
504
+ file types.
499
505
  exclude_pointers: A tuple of JSON pointers indicating elements to
500
- exclude (defaults to no exclusions).
506
+ exclude (defaults to no exclusions). This applies only to TOML
507
+ files (including pyproject.toml), and is ignored for all other
508
+ file types.
501
509
  """
502
- if isinstance(paths, str):
510
+ if isinstance(paths, (str, Path)):
503
511
  paths = (paths,)
504
512
 
505
- def update_(path: str) -> None:
513
+ def update_(path: str | Path) -> None:
506
514
  _update(
507
515
  path,
508
516
  ignore=ignore,
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from itertools import chain
6
+ from typing import TYPE_CHECKING
7
+
8
+ from dependence._utilities import (
9
+ check_output,
10
+ iter_configuration_files,
11
+ iter_parse_delimited_values,
12
+ )
13
+ from dependence.freeze import get_frozen_requirements
14
+ from dependence.update import update
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterable
18
+
19
+
20
+ def upgrade(
21
+ requirements: Iterable[str],
22
+ *,
23
+ ignore_update: Iterable[str] = (),
24
+ all_extra_name: str = "",
25
+ include_pointers: tuple[str, ...] = (),
26
+ exclude_pointers: tuple[str, ...] = (),
27
+ exclude: Iterable[str] = (),
28
+ exclude_recursive: Iterable[str] = (),
29
+ depth: int | None = None,
30
+ ) -> None:
31
+ """
32
+ This function obtains a list of dependencies for the specified
33
+ `requirements` using `dependence.update.get_frozen_requirements()`,
34
+ upgrades all of these dependencies in the current environment,
35
+ then updates version specifiers for all requirements/dependencies
36
+ in any or the `requirements` which are project config files
37
+ to align with the newly installed package versions (using
38
+ `dependence.update.update()`).
39
+
40
+ Parameters:
41
+ requirements: One or more requirement specifiers (for example:
42
+ "requirement-name[extra-a,extra-b]" or ".[extra-a, extra-b]) and/or
43
+ paths to a setup.py, setup.cfg, pyproject.toml, tox.ini or
44
+ requirements.txt file.
45
+ ignore_update: One or more project names to ignore (leave as-is)
46
+ when updating a dependency's requirement specifier in the
47
+ provided pyproject.toml/setup.cfg/requirements.txt/tox.ini
48
+ file(s). Note that this does not prevent the package from being
49
+ upgraded—for that you need to pass the package name in
50
+ `exclude` or `exclude_recursive`.
51
+ all_extra_name: If provided, an extra which consolidates
52
+ the requirements for all other extras will be added/updated to
53
+ pyproject.toml or setup.cfg (this argument is ignored for
54
+ requirements.txt and tox.ini files).
55
+ include_pointers: A tuple of JSON pointers indicating elements to
56
+ include (defaults to all elements). This applies only to TOML
57
+ files (including pyproject.toml), and is ignored for all other
58
+ file types.
59
+ exclude_pointers: A tuple of JSON pointers indicating elements to
60
+ exclude (defaults to no exclusions). This applies only to TOML
61
+ files (including pyproject.toml), and is ignored for all other
62
+ file types.
63
+ exclude: One or more distributions to exclude when upgrading packages.
64
+ exclude_recursive: One or more distributions to exclude when
65
+ upgrading packages. Recursive dependency discovery is also
66
+ halted for these distributions, unlike those passed to `exclude`.
67
+ depth: The maximum recursion depth to traverse when discovering
68
+ dependencies. If `None` (the default), all dependencies are
69
+ discovered.
70
+ """
71
+ frozen_requirements: tuple[str, ...] = get_frozen_requirements(
72
+ requirements,
73
+ exclude=exclude,
74
+ exclude_recursive=exclude_recursive,
75
+ no_version="*",
76
+ depth=depth,
77
+ include_pointers=include_pointers,
78
+ exclude_pointers=exclude_pointers,
79
+ )
80
+ command: tuple[str, ...] = (
81
+ sys.executable,
82
+ "-m",
83
+ "pip",
84
+ "install",
85
+ "--upgrade",
86
+ *frozen_requirements,
87
+ )
88
+ check_output(command)
89
+ configuration_files: tuple[str, ...] = tuple(
90
+ chain(
91
+ *map(iter_configuration_files, requirements) # type: ignore
92
+ )
93
+ )
94
+ if configuration_files:
95
+ update(
96
+ configuration_files,
97
+ ignore=ignore_update,
98
+ all_extra_name=all_extra_name,
99
+ include_pointers=include_pointers,
100
+ exclude_pointers=exclude_pointers,
101
+ )
102
+
103
+
104
+ def main() -> None:
105
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(
106
+ prog="dependence upgrade",
107
+ description=(
108
+ "Upgrade all dependencies for specified packages/projects, "
109
+ "then upgrade version specifiers in the project files "
110
+ "to align with newly installed versions of each distribution."
111
+ ),
112
+ )
113
+ parser.add_argument(
114
+ "-iu",
115
+ "--ignore-update",
116
+ default=[],
117
+ type=str,
118
+ action="append",
119
+ help=(
120
+ "A comma-separated list of distributions to ignore (leave "
121
+ "any requirements pertaining to the package as-is) when "
122
+ "updating project files"
123
+ ),
124
+ )
125
+ parser.add_argument(
126
+ "-aen",
127
+ "--all-extra-name",
128
+ default="",
129
+ type=str,
130
+ help=(
131
+ "If provided, an extra which consolidates the requirements "
132
+ "for all other extras will be added/updated to pyproject.toml "
133
+ "or setup.cfg (this argument is ignored for "
134
+ "requirements.txt files and other TOML files)"
135
+ ),
136
+ )
137
+ parser.add_argument(
138
+ "--include-pointer",
139
+ default=[],
140
+ type=str,
141
+ action="append",
142
+ help=(
143
+ "One or more JSON pointers of elements to *include* "
144
+ "(applies to TOML files only)"
145
+ ),
146
+ )
147
+ parser.add_argument(
148
+ "--exclude-pointer",
149
+ default=[],
150
+ type=str,
151
+ action="append",
152
+ help=(
153
+ "One or more JSON pointers of elements to *exclude* "
154
+ "(applies to TOML files only)"
155
+ ),
156
+ )
157
+ parser.add_argument(
158
+ "-e",
159
+ "--exclude",
160
+ default=[],
161
+ type=str,
162
+ action="append",
163
+ help=(
164
+ "A distribution (or comma-separated list of distributions) to "
165
+ "exclude when performing upgrades"
166
+ ),
167
+ )
168
+ parser.add_argument(
169
+ "-er",
170
+ "--exclude-recursive",
171
+ default=[],
172
+ type=str,
173
+ action="append",
174
+ help=(
175
+ "A distribution (or comma-separated list of distributions) to "
176
+ "exclude when performing upgrades. Unlike -e / --exclude, "
177
+ "this argument also precludes recursive requirement discovery "
178
+ "for the specified packages, thereby excluding all of the "
179
+ "excluded package's requirements which are not required by "
180
+ "another (non-excluded) distribution from the upgrade."
181
+ ),
182
+ )
183
+ parser.add_argument(
184
+ "-d",
185
+ "--depth",
186
+ default=None,
187
+ type=int,
188
+ help="Depth of recursive requirement discovery",
189
+ )
190
+ parser.add_argument(
191
+ "requirement",
192
+ nargs="+",
193
+ type=str,
194
+ help=(
195
+ "One or more requirement specifiers (for example: "
196
+ '"requirement-name", "requirement-name[extra-a,extra-b]", '
197
+ '".[extra-a, extra-b]" or '
198
+ '"../other-editable-package-directory[extra-a, extra-b]) '
199
+ "and/or paths to a setup.py, setup.cfg, pyproject.toml, "
200
+ "tox.ini or requirements.txt file"
201
+ ),
202
+ )
203
+ namespace: argparse.Namespace = parser.parse_args()
204
+ upgrade(
205
+ requirements=namespace.requirement,
206
+ exclude=tuple(iter_parse_delimited_values(namespace.exclude)),
207
+ exclude_recursive=tuple(
208
+ iter_parse_delimited_values(namespace.exclude_recursive)
209
+ ),
210
+ ignore_update=tuple(
211
+ iter_parse_delimited_values(namespace.ignore_update)
212
+ ),
213
+ all_extra_name=namespace.all_extra_name,
214
+ include_pointers=tuple(namespace.include_pointer),
215
+ exclude_pointers=tuple(namespace.exclude_pointer),
216
+ )
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "dependence"
9
- version = "1.0.5"
9
+ version = "1.1.0"
10
10
  description = "A dependency management tool for python projects"
11
11
  readme = "README.md"
12
12
  license = "MIT"
@@ -67,7 +67,6 @@ post-install-commands = [
67
67
  [tool.hatch.envs.default.scripts]
68
68
  lint = "ruff check . && ruff format --check . && mypy"
69
69
 
70
-
71
70
  [tool.hatch.envs.docs]
72
71
  template = "docs"
73
72
  python = "3.13"
@@ -77,9 +76,15 @@ dependencies = [
77
76
  "black",
78
77
  ]
79
78
 
79
+ [tool.hatch.envs.hatch-static-analysis]
80
+ skip-install = false
81
+
80
82
  [tool.hatch.envs.hatch-test]
81
83
  template = "hatch-test"
82
84
  extra-dependencies = []
85
+ extra-arguments = [
86
+ "--cache-clear",
87
+ ]
83
88
 
84
89
  [[tool.hatch.envs.hatch-test.matrix]]
85
90
  python = [
@@ -90,11 +95,9 @@ python = [
90
95
  "3.13",
91
96
  ]
92
97
 
93
-
94
98
  [tool.ruff]
95
99
  line-length = 79
96
100
 
97
-
98
101
  [tool.ruff.lint]
99
102
  ignore = [
100
103
  "F842",
@@ -141,14 +144,17 @@ disallow_untyped_defs = true
141
144
  disallow_incomplete_defs = true
142
145
 
143
146
  [tool.coverage.run]
147
+ include = [
148
+ "src/**/*.py",
149
+ ]
144
150
  omit = [
145
- "tests/test_projects",
151
+ "src/**/_*.py",
146
152
  ]
147
153
 
148
154
  [tool.coverage.paths]
149
155
  source = [
150
- "src/",
156
+ "src/**",
151
157
  ]
152
158
 
153
159
  [tool.coverage.report]
154
- fail_under = 80
160
+ fail_under = 70
File without changes