0
# Package Upgrading
1
2
Package installation via pip and atomic updating of version numbers in requirements files, with dry-run support and error handling.
3
4
## Capabilities
5
6
### Package Installation and Requirements Updating
7
8
Executes the final upgrade process by installing packages via pip and updating version numbers in requirements files.
9
10
```python { .api }
11
class PackagesUpgrader:
12
def __init__(self, selected_packages, requirements_files, options):
13
"""
14
Initialize package upgrader.
15
16
Args:
17
selected_packages (list): List of package dictionaries from interactive selector
18
requirements_files (list): List of requirements file paths to update
19
options (dict): Command-line options including:
20
--dry-run (bool): Simulate without actual changes
21
--skip-package-installation (bool): Only update files, don't install
22
"""
23
24
def do_upgrade(self):
25
"""
26
Execute upgrade process for all selected packages.
27
28
Returns:
29
list: List of successfully upgraded package dictionaries
30
31
Process for each package:
32
1. Install package via pip (unless skipped)
33
2. Update version in all requirements files
34
3. Handle installation failures gracefully
35
"""
36
```
37
38
### Individual Package Processing
39
40
Handles the upgrade process for a single package with proper error handling.
41
42
```python { .api }
43
def _update_package(self, package):
44
"""
45
Update (install) a single package and update requirements files.
46
47
Args:
48
package (dict): Package dictionary with name, current_version, latest_version
49
50
Process:
51
1. Install exact version via pip (unless dry-run or skip-installation)
52
2. Update requirements files only if installation succeeds
53
3. Handle CalledProcessError from pip installation
54
"""
55
```
56
57
### Requirements File Updating
58
59
Atomically updates version numbers in requirements files with rollback on failure.
60
61
```python { .api }
62
def _update_requirements_package(self, package):
63
"""
64
Update package version in all requirements files.
65
66
Args:
67
package (dict): Package dictionary with version information
68
69
Atomic operation:
70
1. Read all lines from each requirements file
71
2. Update matching package lines
72
3. Write updated content to file
73
4. Rollback original content on any exception
74
"""
75
76
def _maybe_update_line_package(self, line, package):
77
"""
78
Update package version in a single requirements line if it matches.
79
80
Args:
81
line (str): Single line from requirements file
82
package (dict): Package dictionary with version information
83
84
Returns:
85
str: Updated line or original line if no match
86
87
Matching criteria:
88
- Package name matches (case-insensitive regex)
89
- Uses exact version pinning (==)
90
- Handles package extras [extra1,extra2]
91
- Preserves line formatting and comments
92
"""
93
```
94
95
## Installation Process
96
97
### Pip Installation
98
99
Packages are installed using exact version pinning:
100
101
```bash
102
pip install django==4.1.0
103
pip install requests[security]==2.28.1
104
```
105
106
**Installation modes:**
107
108
1. **Normal mode**: Installs packages via subprocess.check_call
109
2. **Dry-run mode**: Skips installation, prints simulation message
110
3. **Skip-installation mode**: Only updates requirements files
111
112
### Subprocess Execution
113
114
```python
115
import subprocess
116
117
# Install exact version
118
pinned = f"{package['name']}=={package['latest_version']}"
119
subprocess.check_call(['pip', 'install', pinned])
120
```
121
122
**Error handling:**
123
- `CalledProcessError`: Installation failure, skips requirements update
124
- Continues processing other packages on individual failures
125
126
## Requirements File Updates
127
128
### Line Matching
129
130
Uses regex pattern matching to identify package lines:
131
132
```python
133
import re
134
135
pattern = r'\b{package}(?:\[\w*\])?=={old_version}\b'.format(
136
package=re.escape(package['name']),
137
old_version=re.escape(str(package['current_version']))
138
)
139
```
140
141
**Pattern features:**
142
- Word boundaries prevent partial matches
143
- Optional extras support: `package[extra]==version`
144
- Exact version matching only
145
- Case-insensitive matching
146
147
### Atomic File Updates
148
149
Requirements files are updated atomically:
150
151
1. **Read**: Load all lines into memory
152
2. **Process**: Update matching lines
153
3. **Write**: Write all lines to file
154
4. **Rollback**: Restore original content on any exception
155
156
```python
157
# Atomic update process
158
lines = []
159
with open(filename, 'r') as frh:
160
for line in frh:
161
lines.append(line)
162
163
try:
164
with open(filename, 'w') as fwh:
165
for line in lines:
166
updated_line = self._maybe_update_line_package(line, package)
167
fwh.write(updated_line)
168
except Exception as e:
169
# Rollback on any error
170
with open(filename, 'w') as fwh:
171
for line in lines:
172
fwh.write(line)
173
raise e
174
```
175
176
## Usage Examples
177
178
### Basic Package Upgrading
179
180
```python
181
from pip_upgrader.packages_upgrader import PackagesUpgrader
182
183
# Selected packages from interactive selector
184
selected_packages = [
185
{
186
'name': 'django',
187
'current_version': Version('3.2.0'),
188
'latest_version': Version('4.1.0'),
189
'upgrade_available': True,
190
'upload_time': '2022-08-03 08:15:30'
191
}
192
]
193
194
# Requirements files to update
195
requirements_files = ['requirements.txt', 'requirements/dev.txt']
196
197
# Options
198
options = {
199
'--dry-run': False,
200
'--skip-package-installation': False
201
}
202
203
# Execute upgrades
204
upgrader = PackagesUpgrader(selected_packages, requirements_files, options)
205
upgraded = upgrader.do_upgrade()
206
207
print(f"Successfully upgraded {len(upgraded)} packages")
208
```
209
210
### Dry Run Mode
211
212
```python
213
# Simulate upgrades without making changes
214
options = {
215
'--dry-run': True,
216
'--skip-package-installation': False
217
}
218
219
upgrader = PackagesUpgrader(selected_packages, requirements_files, options)
220
upgraded = upgrader.do_upgrade()
221
222
# Prints simulation messages, no actual changes made
223
```
224
225
### Skip Package Installation
226
227
```python
228
# Only update requirements files, don't install packages
229
options = {
230
'--dry-run': False,
231
'--skip-package-installation': True
232
}
233
234
upgrader = PackagesUpgrader(selected_packages, requirements_files, options)
235
upgraded = upgrader.do_upgrade()
236
237
# Files updated, but pip install not executed
238
```
239
240
## Requirements File Format Support
241
242
### Standard Format
243
244
```
245
django==3.2.0
246
requests==2.25.1
247
flask==2.0.0
248
```
249
250
**Updated to:**
251
```
252
django==4.1.0
253
requests==2.28.1
254
flask==2.2.2
255
```
256
257
### Package Extras
258
259
```
260
django[rest]==3.2.0
261
requests[security,socks]==2.25.1
262
```
263
264
**Updated to:**
265
```
266
django[rest]==4.1.0
267
requests[security,socks]==2.28.1
268
```
269
270
### Mixed Requirements
271
272
```
273
# Web framework
274
django==3.2.0 # Core framework
275
django-rest-framework==3.12.0
276
277
# HTTP client
278
requests==2.25.1
279
urllib3>=1.26.0 # Not updated (not exact pin)
280
281
# Development
282
pytest==6.2.4
283
```
284
285
Only exact pins (`==`) are updated. Other version specifiers (`>=`, `~=`, etc.) are left unchanged.
286
287
## Error Handling
288
289
### Installation Failures
290
291
When `pip install` fails:
292
293
```python
294
try:
295
subprocess.check_call(['pip', 'install', pinned])
296
self._update_requirements_package(package)
297
except CalledProcessError:
298
print(Color('{{autored}}Failed to install package "{}"{{/autored}}'.format(package['name'])))
299
# Continue with next package
300
```
301
302
**Behavior:**
303
- Prints colored error message
304
- Skips requirements file update for failed package
305
- Continues processing remaining packages
306
- Does not add failed package to upgraded list
307
308
### File Update Failures
309
310
Requirements file update failures trigger rollback:
311
312
```python
313
try:
314
# Write updated lines
315
pass
316
except Exception as e:
317
# Restore original file content
318
with open(filename, 'w') as fwh:
319
for line in lines:
320
fwh.write(line)
321
raise e
322
```
323
324
**Common failure scenarios:**
325
- File permission errors
326
- Disk space issues
327
- File locking by other processes
328
329
### Environment Variable Override
330
331
The `PIP_UPGRADER_SKIP_PACKAGE_INSTALLATION` environment variable can override the skip installation option:
332
333
```bash
334
export PIP_UPGRADER_SKIP_PACKAGE_INSTALLATION=1
335
pip-upgrade requirements.txt
336
```
337
338
This forces skip-installation mode regardless of command-line options.
339
340
## Dry Run Output
341
342
In dry-run mode, the upgrader provides detailed simulation output:
343
344
### Package Installation Simulation
345
346
```
347
[Dry Run]: skipping package installation: django
348
[Dry Run]: skipping package installation: requests
349
```
350
351
### Requirements Update Simulation
352
353
```
354
[Dry Run]: skipping requirements replacement: django==3.2.0 / django==4.1.0
355
[Dry Run]: skipping requirements replacement: requests==2.25.1 / requests==2.28.1
356
```
357
358
**Features:**
359
- Shows original and updated versions
360
- Indicates which operations are being skipped
361
- Maintains upgrade tracking for final summary
362
363
## Return Value
364
365
The `do_upgrade()` method returns a list of successfully upgraded packages:
366
367
```python
368
[
369
{
370
'name': 'django',
371
'current_version': Version('3.2.0'),
372
'latest_version': Version('4.1.0'),
373
'upgrade_available': True,
374
'upload_time': '2022-08-03 08:15:30'
375
}
376
]
377
```
378
379
**Inclusion criteria:**
380
- Package was found and updated in at least one requirements file
381
- Installation succeeded (if not skipped)
382
- No file update errors occurred
383
384
**Exclusion criteria:**
385
- Package installation failed (CalledProcessError)
386
- Package not found in any requirements file
387
- File update exceptions occurred