|
| 1 | +# Position Aware Editor |
| 2 | + |
| 3 | +The Position Aware Editor is the most advanced editor in Python Requirements Parser, designed for production environments where minimal changes are crucial. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The Position Aware Editor achieves **minimal diff editing** by: |
| 8 | +- Recording exact position information during parsing |
| 9 | +- Making surgical changes only to version constraints |
| 10 | +- Preserving all original formatting, comments, and structure |
| 11 | + |
| 12 | +## Key Features |
| 13 | + |
| 14 | +- **Minimal diff** - Only changes what's necessary |
| 15 | +- **Perfect format preservation** - Maintains comments, spacing, and structure |
| 16 | +- **High performance** - Nanosecond-level update operations |
| 17 | +- **Zero allocations** - Batch updates with no memory allocations |
| 18 | + |
| 19 | +## Performance Comparison |
| 20 | + |
| 21 | +| Editor | Single Update | Batch Update (10) | Diff Size | |
| 22 | +|--------|---------------|-------------------|-----------| |
| 23 | +| **PositionAwareEditor** | 67.67 ns | 374.1 ns | **5.9%** | |
| 24 | +| VersionEditorV2 | 2.1 µs | 15.2 µs | 11.8% | |
| 25 | +| VersionEditor | 5.3 µs | 42.1 µs | 15.2% | |
| 26 | + |
| 27 | +## Basic Usage |
| 28 | + |
| 29 | +```go |
| 30 | +package main |
| 31 | + |
| 32 | +import ( |
| 33 | + "fmt" |
| 34 | + "log" |
| 35 | + "os" |
| 36 | + |
| 37 | + "github.com/scagogogo/python-requirements-parser/pkg/editor" |
| 38 | +) |
| 39 | + |
| 40 | +func main() { |
| 41 | + // Create position-aware editor |
| 42 | + editor := editor.NewPositionAwareEditor() |
| 43 | + |
| 44 | + // Read requirements file |
| 45 | + content, err := os.ReadFile("requirements.txt") |
| 46 | + if err != nil { |
| 47 | + log.Fatal(err) |
| 48 | + } |
| 49 | + |
| 50 | + // Parse with position information |
| 51 | + doc, err := editor.ParseRequirementsFile(string(content)) |
| 52 | + if err != nil { |
| 53 | + log.Fatal(err) |
| 54 | + } |
| 55 | + |
| 56 | + // Update single package |
| 57 | + err = editor.UpdatePackageVersion(doc, "flask", "==2.0.1") |
| 58 | + if err != nil { |
| 59 | + log.Fatal(err) |
| 60 | + } |
| 61 | + |
| 62 | + // Serialize with minimal changes |
| 63 | + result := editor.SerializeToString(doc) |
| 64 | + |
| 65 | + // Write back to file |
| 66 | + err = os.WriteFile("requirements.txt", []byte(result), 0644) |
| 67 | + if err != nil { |
| 68 | + log.Fatal(err) |
| 69 | + } |
| 70 | + |
| 71 | + fmt.Println("✅ Updated requirements.txt with minimal changes") |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +## Batch Updates |
| 76 | + |
| 77 | +For maximum efficiency, use batch updates: |
| 78 | + |
| 79 | +```go |
| 80 | +func securityUpdates() error { |
| 81 | + editor := editor.NewPositionAwareEditor() |
| 82 | + |
| 83 | + content, err := os.ReadFile("requirements.txt") |
| 84 | + if err != nil { |
| 85 | + return err |
| 86 | + } |
| 87 | + |
| 88 | + doc, err := editor.ParseRequirementsFile(string(content)) |
| 89 | + if err != nil { |
| 90 | + return err |
| 91 | + } |
| 92 | + |
| 93 | + // Security updates from vulnerability scanner |
| 94 | + updates := map[string]string{ |
| 95 | + "django": ">=3.2.13,<4.0.0", // Security patch |
| 96 | + "requests": ">=2.28.0", // Security patch |
| 97 | + "cryptography": ">=39.0.2", // Security patch |
| 98 | + "pillow": ">=9.1.1", // Security patch |
| 99 | + } |
| 100 | + |
| 101 | + // Apply all updates in one operation |
| 102 | + err = editor.BatchUpdateVersions(doc, updates) |
| 103 | + if err != nil { |
| 104 | + return err |
| 105 | + } |
| 106 | + |
| 107 | + result := editor.SerializeToString(doc) |
| 108 | + return os.WriteFile("requirements.txt", []byte(result), 0644) |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +## Real-World Example |
| 113 | + |
| 114 | +Here's a complete example showing the power of minimal diff editing: |
| 115 | + |
| 116 | +```go |
| 117 | +package main |
| 118 | + |
| 119 | +import ( |
| 120 | + "fmt" |
| 121 | + "log" |
| 122 | + "strings" |
| 123 | + |
| 124 | + "github.com/scagogogo/python-requirements-parser/pkg/editor" |
| 125 | +) |
| 126 | + |
| 127 | +func main() { |
| 128 | + // Complex requirements.txt with various formats |
| 129 | + originalContent := `# Production dependencies |
| 130 | +flask==1.0.0 # Web framework |
| 131 | +django[rest,auth]>=3.2.0,<4.0.0 # Web framework with extras |
| 132 | +requests>=2.25.0,<3.0.0 # HTTP library |
| 133 | +
|
| 134 | +# VCS dependencies (should be preserved) |
| 135 | +git+https://github.com/company/[email protected]#egg=internal-package |
| 136 | +-e git+https://github.com/company/dev-tools.git@develop#egg=dev-tools |
| 137 | +
|
| 138 | +# URL dependencies (should be preserved) |
| 139 | +https://files.pythonhosted.org/packages/special-package-1.0.0.tar.gz |
| 140 | +
|
| 141 | +# Environment markers (should be preserved) |
| 142 | +pywin32>=1.0; platform_system == "Windows" |
| 143 | +dataclasses>=0.6; python_version < "3.7" |
| 144 | +
|
| 145 | +# File references (should be preserved) |
| 146 | +-r requirements-dev.txt |
| 147 | +-c constraints.txt |
| 148 | +
|
| 149 | +# Global options (should be preserved) |
| 150 | +--index-url https://pypi.company.com/simple/ |
| 151 | +--extra-index-url https://pypi.org/simple/ |
| 152 | +--trusted-host pypi.company.com` |
| 153 | + |
| 154 | + fmt.Println("Original requirements.txt:") |
| 155 | + fmt.Println(strings.Repeat("=", 50)) |
| 156 | + fmt.Println(originalContent) |
| 157 | + fmt.Println(strings.Repeat("=", 50)) |
| 158 | + fmt.Println() |
| 159 | + |
| 160 | + // Create editor and parse |
| 161 | + editor := editor.NewPositionAwareEditor() |
| 162 | + doc, err := editor.ParseRequirementsFile(originalContent) |
| 163 | + if err != nil { |
| 164 | + log.Fatal(err) |
| 165 | + } |
| 166 | + |
| 167 | + // Security updates |
| 168 | + updates := map[string]string{ |
| 169 | + "flask": "==2.0.1", |
| 170 | + "django": ">=3.2.13,<4.0.0", |
| 171 | + "requests": ">=2.28.0,<3.0.0", |
| 172 | + } |
| 173 | + |
| 174 | + fmt.Printf("Applying %d security updates...\n", len(updates)) |
| 175 | + for pkg, version := range updates { |
| 176 | + fmt.Printf(" 📦 %s: %s\n", pkg, version) |
| 177 | + } |
| 178 | + fmt.Println() |
| 179 | + |
| 180 | + err = editor.BatchUpdateVersions(doc, updates) |
| 181 | + if err != nil { |
| 182 | + log.Fatal(err) |
| 183 | + } |
| 184 | + |
| 185 | + result := editor.SerializeToString(doc) |
| 186 | + |
| 187 | + fmt.Println("Updated requirements.txt:") |
| 188 | + fmt.Println(strings.Repeat("=", 50)) |
| 189 | + fmt.Println(result) |
| 190 | + fmt.Println(strings.Repeat("=", 50)) |
| 191 | + fmt.Println() |
| 192 | + |
| 193 | + // Analyze the diff |
| 194 | + originalLines := strings.Split(originalContent, "\n") |
| 195 | + newLines := strings.Split(result, "\n") |
| 196 | + |
| 197 | + changedLines := 0 |
| 198 | + for i := 0; i < len(originalLines) && i < len(newLines); i++ { |
| 199 | + if originalLines[i] != newLines[i] { |
| 200 | + changedLines++ |
| 201 | + fmt.Printf("📝 Line %d changed:\n", i+1) |
| 202 | + fmt.Printf(" - %s\n", originalLines[i]) |
| 203 | + fmt.Printf(" + %s\n", newLines[i]) |
| 204 | + fmt.Println() |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + fmt.Printf("📊 Summary:\n") |
| 209 | + fmt.Printf(" Total lines: %d\n", len(originalLines)) |
| 210 | + fmt.Printf(" Changed lines: %d\n", changedLines) |
| 211 | + fmt.Printf(" Change rate: %.1f%%\n", float64(changedLines)/float64(len(originalLines))*100) |
| 212 | + fmt.Printf(" Preserved: VCS, URLs, file refs, global options, comments\n") |
| 213 | + |
| 214 | + fmt.Println("\n✅ Perfect minimal diff editing!") |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +## Advanced Features |
| 219 | + |
| 220 | +### Package Information |
| 221 | + |
| 222 | +```go |
| 223 | +// Get detailed package information |
| 224 | +info, err := editor.GetPackageInfo(doc, "django") |
| 225 | +if err != nil { |
| 226 | + log.Fatal(err) |
| 227 | +} |
| 228 | + |
| 229 | +fmt.Printf("Package: %s\n", info.Name) |
| 230 | +fmt.Printf("Version: %s\n", info.Version) |
| 231 | +fmt.Printf("Extras: %v\n", info.Extras) |
| 232 | +fmt.Printf("Markers: %s\n", info.Markers) |
| 233 | +fmt.Printf("Comment: %s\n", info.Comment) |
| 234 | + |
| 235 | +// Position information |
| 236 | +if info.PositionInfo != nil { |
| 237 | + fmt.Printf("Line: %d\n", info.PositionInfo.LineNumber) |
| 238 | + fmt.Printf("Version position: %d-%d\n", |
| 239 | + info.PositionInfo.VersionStartColumn, |
| 240 | + info.PositionInfo.VersionEndColumn) |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +### List All Packages |
| 245 | + |
| 246 | +```go |
| 247 | +packages := editor.ListPackages(doc) |
| 248 | +fmt.Printf("Found %d packages:\n", len(packages)) |
| 249 | + |
| 250 | +for _, pkg := range packages { |
| 251 | + fmt.Printf(" 📦 %s %s", pkg.Name, pkg.Version) |
| 252 | + if len(pkg.Extras) > 0 { |
| 253 | + fmt.Printf(" [%s]", strings.Join(pkg.Extras, ",")) |
| 254 | + } |
| 255 | + if pkg.Markers != "" { |
| 256 | + fmt.Printf(" ; %s", pkg.Markers) |
| 257 | + } |
| 258 | + if pkg.Comment != "" { |
| 259 | + fmt.Printf(" # %s", pkg.Comment) |
| 260 | + } |
| 261 | + fmt.Println() |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +## Error Handling |
| 266 | + |
| 267 | +```go |
| 268 | +// Handle package not found |
| 269 | +err := editor.UpdatePackageVersion(doc, "nonexistent", "==1.0.0") |
| 270 | +if err != nil { |
| 271 | + if strings.Contains(err.Error(), "not found") { |
| 272 | + fmt.Printf("Package not found, skipping update\n") |
| 273 | + } else { |
| 274 | + log.Fatalf("Update failed: %v", err) |
| 275 | + } |
| 276 | +} |
| 277 | + |
| 278 | +// Handle invalid version format |
| 279 | +err = editor.UpdatePackageVersion(doc, "flask", "invalid-version") |
| 280 | +if err != nil { |
| 281 | + fmt.Printf("Invalid version format: %v\n", err) |
| 282 | +} |
| 283 | + |
| 284 | +// Handle batch update failures |
| 285 | +err = editor.BatchUpdateVersions(doc, updates) |
| 286 | +if err != nil { |
| 287 | + fmt.Printf("Some updates failed: %v\n", err) |
| 288 | + // Continue with successful updates |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +## Production Use Cases |
| 293 | + |
| 294 | +### CI/CD Security Updates |
| 295 | + |
| 296 | +```go |
| 297 | +func ciSecurityUpdate() error { |
| 298 | + editor := editor.NewPositionAwareEditor() |
| 299 | + |
| 300 | + // Read current requirements |
| 301 | + content, err := os.ReadFile("requirements.txt") |
| 302 | + if err != nil { |
| 303 | + return err |
| 304 | + } |
| 305 | + |
| 306 | + doc, err := editor.ParseRequirementsFile(string(content)) |
| 307 | + if err != nil { |
| 308 | + return err |
| 309 | + } |
| 310 | + |
| 311 | + // Get security updates from vulnerability scanner |
| 312 | + securityUpdates := getSecurityUpdates() // Your implementation |
| 313 | + |
| 314 | + // Apply updates |
| 315 | + err = editor.BatchUpdateVersions(doc, securityUpdates) |
| 316 | + if err != nil { |
| 317 | + return err |
| 318 | + } |
| 319 | + |
| 320 | + // Write back with minimal changes |
| 321 | + result := editor.SerializeToString(doc) |
| 322 | + return os.WriteFile("requirements.txt", []byte(result), 0644) |
| 323 | +} |
| 324 | +``` |
| 325 | + |
| 326 | +### Development Workflow |
| 327 | + |
| 328 | +```go |
| 329 | +func upgradePackages(packages []string) error { |
| 330 | + editor := editor.NewPositionAwareEditor() |
| 331 | + |
| 332 | + content, err := os.ReadFile("requirements.txt") |
| 333 | + if err != nil { |
| 334 | + return err |
| 335 | + } |
| 336 | + |
| 337 | + doc, err := editor.ParseRequirementsFile(string(content)) |
| 338 | + if err != nil { |
| 339 | + return err |
| 340 | + } |
| 341 | + |
| 342 | + updates := make(map[string]string) |
| 343 | + |
| 344 | + // Get latest versions for specified packages |
| 345 | + for _, pkg := range packages { |
| 346 | + latestVersion, err := getLatestVersion(pkg) // Your implementation |
| 347 | + if err != nil { |
| 348 | + fmt.Printf("Warning: Could not get latest version for %s: %v\n", pkg, err) |
| 349 | + continue |
| 350 | + } |
| 351 | + updates[pkg] = latestVersion |
| 352 | + } |
| 353 | + |
| 354 | + if len(updates) == 0 { |
| 355 | + fmt.Println("No packages to update") |
| 356 | + return nil |
| 357 | + } |
| 358 | + |
| 359 | + fmt.Printf("Updating %d packages...\n", len(updates)) |
| 360 | + err = editor.BatchUpdateVersions(doc, updates) |
| 361 | + if err != nil { |
| 362 | + return err |
| 363 | + } |
| 364 | + |
| 365 | + result := editor.SerializeToString(doc) |
| 366 | + return os.WriteFile("requirements.txt", []byte(result), 0644) |
| 367 | +} |
| 368 | +``` |
| 369 | + |
| 370 | +## Best Practices |
| 371 | + |
| 372 | +1. **Always use batch updates** for multiple packages |
| 373 | +2. **Validate version formats** before updating |
| 374 | +3. **Handle errors gracefully** for production use |
| 375 | +4. **Reuse editor instances** for better performance |
| 376 | +5. **Test changes** before applying to production |
| 377 | + |
| 378 | +## Next Steps |
| 379 | + |
| 380 | +- **[API Reference](/api/editors)** - Complete editor API documentation |
| 381 | +- **[Performance Guide](/guide/performance)** - Optimization tips |
| 382 | +- **[Examples Overview](/examples/)** - More examples and tutorials |
0 commit comments