0
# Utilities and Validation
1
2
Poetry Core provides various utility functions for JSON schema validation, SPDX license handling, file type detection, and other helper functions that support the core functionality.
3
4
## Core Imports
5
6
```python
7
# JSON validation
8
from poetry.core.json import validate_object, ValidationError
9
10
# SPDX license support
11
from poetry.core.spdx.license import License
12
from poetry.core.spdx.helpers import license_by_id
13
14
# Helper utilities
15
from poetry.core.utils.helpers import (
16
combine_unicode,
17
readme_content_type,
18
temporary_directory
19
)
20
21
# Version helpers
22
from poetry.core.version.helpers import format_python_constraint, PYTHON_VERSION
23
24
# VCS support
25
from poetry.core.vcs.git import Git, GitConfig, ParsedUrl, GitError
26
27
# Compatibility utilities
28
from poetry.core.utils._compat import WINDOWS, tomllib
29
``` { .api }
30
31
## JSON Schema Validation
32
33
### validate_object
34
35
```python
36
def validate_object(obj: dict[str, Any], schema_name: str) -> list[str]:
37
"""
38
Validate object against JSON schema.
39
40
Args:
41
obj: Dictionary object to validate
42
schema_name: Name of schema to validate against
43
44
Returns:
45
List of validation error messages (empty if valid)
46
47
Raises:
48
ValidationError: If validation fails with detailed error information
49
50
Available Schemas:
51
- "poetry-schema": Main Poetry configuration schema
52
- "poetry-plugins-schema": Poetry plugins configuration
53
54
Example:
55
>>> config = {"name": "my-package", "version": "1.0.0"}
56
>>> errors = validate_object(config, "poetry-schema")
57
>>> if errors:
58
... print("Validation errors:", errors)
59
... else:
60
... print("Configuration is valid")
61
"""
62
``` { .api }
63
64
### ValidationError
65
66
```python
67
class ValidationError(ValueError):
68
"""
69
JSON schema validation error.
70
71
Raised when object validation against schema fails,
72
providing detailed information about validation issues.
73
"""
74
75
def __init__(self, message: str, errors: list[str] | None = None) -> None:
76
"""
77
Create validation error.
78
79
Args:
80
message: Error message
81
errors: List of specific validation errors
82
"""
83
84
@property
85
def errors(self) -> list[str]:
86
"""List of specific validation errors."""
87
``` { .api }
88
89
## SPDX License Support
90
91
### License
92
93
```python
94
class License:
95
"""
96
SPDX license information.
97
98
Named tuple containing license metadata from SPDX license list.
99
"""
100
101
id: str # SPDX license identifier (e.g., "MIT", "Apache-2.0")
102
name: str # Full license name
103
is_osi_approved: bool # Whether OSI approved
104
is_deprecated: bool # Whether license is deprecated
105
106
def __init__(
107
self,
108
id: str,
109
name: str,
110
is_osi_approved: bool = False,
111
is_deprecated: bool = False
112
) -> None:
113
"""
114
Create license information.
115
116
Args:
117
id: SPDX license identifier
118
name: Full license name
119
is_osi_approved: Whether license is OSI approved
120
is_deprecated: Whether license is deprecated
121
122
Example:
123
>>> license = License(
124
... id="MIT",
125
... name="MIT License",
126
... is_osi_approved=True
127
... )
128
"""
129
``` { .api }
130
131
### license_by_id
132
133
```python
134
def license_by_id(license_id: str) -> License:
135
"""
136
Get license information by SPDX identifier.
137
138
Args:
139
license_id: SPDX license identifier (case-insensitive)
140
141
Returns:
142
License object with metadata
143
144
Raises:
145
ValueError: If license ID is not found
146
147
Example:
148
>>> mit = license_by_id("MIT")
149
>>> print(f"{mit.name} (OSI: {mit.is_osi_approved})")
150
MIT License (OSI: True)
151
152
>>> apache = license_by_id("apache-2.0") # Case insensitive
153
>>> print(apache.name)
154
Apache License 2.0
155
156
>>> # Check if license exists
157
>>> try:
158
... license = license_by_id("UNKNOWN")
159
... except ValueError:
160
... print("License not found")
161
"""
162
``` { .api }
163
164
## Helper Utilities
165
166
### combine_unicode
167
168
```python
169
def combine_unicode(string: str) -> str:
170
"""
171
Normalize Unicode string using NFC normalization.
172
173
Args:
174
string: String to normalize
175
176
Returns:
177
Normalized Unicode string
178
179
Note:
180
Uses Unicode NFC (Canonical Decomposition + Canonical Composition)
181
normalization to ensure consistent string representation.
182
183
Example:
184
>>> # Combining characters
185
>>> combined = combine_unicode("café") # e + ́ (combining acute)
186
>>> print(repr(combined)) # Single é character
187
188
>>> # Already normalized
189
>>> normal = combine_unicode("café") # Single é character
190
>>> combined == normal # True
191
"""
192
``` { .api }
193
194
### readme_content_type
195
196
```python
197
def readme_content_type(readme_path: Path) -> str:
198
"""
199
Detect README content type from file extension.
200
201
Args:
202
readme_path: Path to README file
203
204
Returns:
205
MIME content type string
206
207
Supported Extensions:
208
- .md, .markdown -> "text/markdown"
209
- .rst -> "text/x-rst"
210
- .txt -> "text/plain"
211
- Other -> "text/plain"
212
213
Example:
214
>>> content_type = readme_content_type(Path("README.md"))
215
>>> print(content_type)
216
text/markdown
217
218
>>> content_type = readme_content_type(Path("README.rst"))
219
>>> print(content_type)
220
text/x-rst
221
222
>>> content_type = readme_content_type(Path("README.txt"))
223
>>> print(content_type)
224
text/plain
225
"""
226
``` { .api }
227
228
### temporary_directory
229
230
```python
231
@contextmanager
232
def temporary_directory() -> Iterator[Path]:
233
"""
234
Context manager for temporary directory creation.
235
236
Yields:
237
Path to temporary directory
238
239
Note:
240
Directory is automatically cleaned up when context exits.
241
242
Example:
243
>>> from poetry.core.utils.helpers import temporary_directory
244
>>>
245
>>> with temporary_directory() as tmp_dir:
246
... print(f"Temp dir: {tmp_dir}")
247
... # Use temporary directory
248
... temp_file = tmp_dir / "test.txt"
249
... temp_file.write_text("Hello")
250
... # Directory automatically cleaned up here
251
"""
252
``` { .api }
253
254
## Version Helpers
255
256
### format_python_constraint
257
258
Formats Python version constraints into proper constraint strings, transforming disjunctive constraints into readable forms.
259
260
```python { .api }
261
def format_python_constraint(constraint: VersionConstraint) -> str:
262
"""
263
Format Python version constraint for display.
264
265
Transforms disjunctive version constraints into proper constraint strings
266
suitable for Python version specifications.
267
268
Args:
269
constraint: Version constraint to format
270
271
Returns:
272
Formatted constraint string (e.g., ">=3.8, !=3.9.*")
273
274
Examples:
275
- Version("3.8") -> "~3.8"
276
- Version("3") -> "^3.0"
277
- Complex unions -> ">=3.8, !=3.9.*, !=3.10.*"
278
"""
279
```
280
281
### PYTHON_VERSION
282
283
Comprehensive list of supported Python version patterns for constraint formatting.
284
285
```python { .api }
286
PYTHON_VERSION: list[str] = [
287
"2.7.*",
288
"3.0.*", "3.1.*", "3.2.*", "3.3.*", "3.4.*", "3.5.*",
289
"3.6.*", "3.7.*", "3.8.*", "3.9.*", "3.10.*", "3.11.*",
290
"3.12.*", "3.13.*"
291
]
292
```
293
294
## VCS Support
295
296
### Git
297
298
```python
299
class Git:
300
"""
301
Git operations wrapper.
302
303
Provides interface for common Git operations needed
304
for VCS dependency handling and project management.
305
"""
306
307
def __init__(self, work_dir: Path | None = None) -> None:
308
"""
309
Initialize Git wrapper.
310
311
Args:
312
work_dir: Working directory for Git operations
313
"""
314
315
@classmethod
316
def clone(
317
cls,
318
repository: str,
319
dest: Path,
320
branch: str | None = None,
321
tag: str | None = None,
322
) -> Git:
323
"""
324
Clone Git repository.
325
326
Args:
327
repository: Repository URL
328
dest: Destination directory
329
branch: Branch to checkout
330
tag: Tag to checkout
331
332
Returns:
333
Git instance for cloned repository
334
335
Raises:
336
GitError: If clone operation fails
337
"""
338
339
def checkout(self, rev: str) -> None:
340
"""
341
Checkout specific revision.
342
343
Args:
344
rev: Revision (branch, tag, commit hash)
345
346
Raises:
347
GitError: If checkout fails
348
"""
349
350
def rev_parse(self, rev: str) -> str:
351
"""
352
Get full commit hash for revision.
353
354
Args:
355
rev: Revision to resolve
356
357
Returns:
358
Full commit hash
359
360
Raises:
361
GitError: If revision cannot be resolved
362
"""
363
364
@property
365
def head(self) -> str:
366
"""Current HEAD commit hash."""
367
368
@property
369
def is_clean(self) -> bool:
370
"""Whether working directory is clean (no uncommitted changes)."""
371
``` { .api }
372
373
### GitConfig
374
375
```python
376
class GitConfig:
377
"""
378
Git configuration management.
379
380
Provides access to Git configuration values
381
with fallback to global and system configs.
382
"""
383
384
def get(self, key: str, default: str | None = None) -> str | None:
385
"""
386
Get Git configuration value.
387
388
Args:
389
key: Configuration key (e.g., "user.name", "user.email")
390
default: Default value if key not found
391
392
Returns:
393
Configuration value or default
394
395
Example:
396
>>> config = GitConfig()
397
>>> name = config.get("user.name")
398
>>> email = config.get("user.email")
399
>>> print(f"Git user: {name} <{email}>")
400
"""
401
``` { .api }
402
403
### ParsedUrl
404
405
```python
406
class ParsedUrl:
407
"""
408
Parsed Git URL representation.
409
410
Handles various Git URL formats and provides
411
normalized access to URL components.
412
"""
413
414
@classmethod
415
def parse(cls, url: str) -> ParsedUrl:
416
"""
417
Parse Git URL into components.
418
419
Args:
420
url: Git URL in various formats
421
422
Returns:
423
ParsedUrl instance with normalized components
424
425
Supported Formats:
426
- HTTPS: https://github.com/user/repo.git
427
- SSH: git@github.com:user/repo.git
428
- GitHub shorthand: github:user/repo
429
430
Example:
431
>>> parsed = ParsedUrl.parse("git@github.com:user/repo.git")
432
>>> print(parsed.url) # Normalized URL
433
>>> print(parsed.hostname) # github.com
434
"""
435
436
@property
437
def url(self) -> str:
438
"""Normalized Git URL."""
439
440
@property
441
def hostname(self) -> str:
442
"""Git server hostname."""
443
444
@property
445
def pathname(self) -> str:
446
"""Repository path."""
447
``` { .api }
448
449
### GitError
450
451
```python
452
class GitError(RuntimeError):
453
"""
454
Git operation error.
455
456
Raised when Git operations fail, providing
457
context about the specific operation and error.
458
"""
459
460
def __init__(self, message: str, return_code: int | None = None) -> None:
461
"""
462
Create Git error.
463
464
Args:
465
message: Error message
466
return_code: Git command return code
467
"""
468
``` { .api }
469
470
## Compatibility Utilities
471
472
### Platform Detection
473
474
```python
475
WINDOWS: bool
476
"""
477
Platform detection constant.
478
479
True if running on Windows, False otherwise.
480
Used for platform-specific code paths.
481
482
Example:
483
>>> from poetry.core.utils._compat import WINDOWS
484
>>> if WINDOWS:
485
... print("Running on Windows")
486
... else:
487
... print("Running on Unix-like system")
488
"""
489
``` { .api }
490
491
### TOML Support
492
493
```python
494
tomllib: ModuleType
495
"""
496
TOML parsing library compatibility layer.
497
498
Uses tomllib (Python 3.11+) or tomli (older Python versions).
499
Provides consistent TOML parsing interface across Python versions.
500
501
Example:
502
>>> from poetry.core.utils._compat import tomllib
503
>>>
504
>>> # Parse TOML file
505
>>> with open("pyproject.toml", "rb") as f:
506
... data = tomllib.load(f)
507
508
>>> # Parse TOML string
509
>>> toml_string = '''
510
... [tool.poetry]
511
... name = "my-package"
512
... '''
513
>>> data = tomllib.loads(toml_string)
514
"""
515
``` { .api }
516
517
## Usage Examples
518
519
### Configuration Validation
520
521
```python
522
from poetry.core.json import validate_object, ValidationError
523
from poetry.core.pyproject.toml import PyProjectTOML
524
525
def validate_poetry_config(pyproject_path: Path):
526
"""Validate Poetry configuration with detailed error reporting."""
527
528
try:
529
# Load configuration
530
pyproject = PyProjectTOML(pyproject_path)
531
poetry_config = pyproject.poetry_config
532
533
# Validate against schema
534
errors = validate_object(poetry_config, "poetry-schema")
535
536
if not errors:
537
print("✅ Configuration is valid")
538
return True
539
540
print("❌ Configuration validation failed:")
541
for error in errors:
542
print(f" • {error}")
543
544
return False
545
546
except ValidationError as e:
547
print(f"❌ Validation error: {e}")
548
if hasattr(e, 'errors') and e.errors:
549
for error in e.errors:
550
print(f" • {error}")
551
return False
552
553
except Exception as e:
554
print(f"❌ Unexpected error: {e}")
555
return False
556
557
# Usage
558
is_valid = validate_poetry_config(Path("pyproject.toml"))
559
``` { .api }
560
561
### License Information
562
563
```python
564
from poetry.core.spdx.helpers import license_by_id
565
566
def show_license_info(license_ids: list[str]):
567
"""Display information about SPDX licenses."""
568
569
print("📜 License Information:")
570
print("=" * 50)
571
572
for license_id in license_ids:
573
try:
574
license_info = license_by_id(license_id)
575
576
print(f"\n🔖 {license_info.id}")
577
print(f" Name: {license_info.name}")
578
print(f" OSI Approved: {'✅' if license_info.is_osi_approved else '❌'}")
579
print(f" Deprecated: {'⚠️' if license_info.is_deprecated else '✅'}")
580
581
except ValueError:
582
print(f"\n❌ {license_id}: License not found")
583
584
# Common licenses
585
common_licenses = [
586
"MIT", "Apache-2.0", "GPL-3.0-or-later",
587
"BSD-3-Clause", "ISC", "LGPL-2.1"
588
]
589
590
show_license_info(common_licenses)
591
``` { .api }
592
593
### File Type Detection
594
595
```python
596
from pathlib import Path
597
from poetry.core.utils.helpers import readme_content_type
598
599
def analyze_readme_files(project_dir: Path):
600
"""Analyze README files in project directory."""
601
602
readme_patterns = ["README*", "readme*", "Readme*"]
603
readme_files = []
604
605
for pattern in readme_patterns:
606
readme_files.extend(project_dir.glob(pattern))
607
608
if not readme_files:
609
print("❌ No README files found")
610
return
611
612
print("📄 README Files Analysis:")
613
print("=" * 40)
614
615
for readme_file in readme_files:
616
if readme_file.is_file():
617
content_type = readme_content_type(readme_file)
618
size = readme_file.stat().st_size
619
620
print(f"\n📝 {readme_file.name}")
621
print(f" Content-Type: {content_type}")
622
print(f" Size: {size:,} bytes")
623
624
# Preview content
625
try:
626
with readme_file.open('r', encoding='utf-8') as f:
627
preview = f.read(200).strip()
628
if len(preview) == 200:
629
preview += "..."
630
print(f" Preview: {preview[:50]}...")
631
except UnicodeDecodeError:
632
print(" Preview: <Binary content>")
633
634
# Usage
635
analyze_readme_files(Path("./my-project"))
636
``` { .api }
637
638
### VCS Operations
639
640
```python
641
from pathlib import Path
642
from poetry.core.vcs.git import Git, GitError, ParsedUrl
643
644
def clone_and_analyze_repo(repo_url: str, target_dir: Path):
645
"""Clone repository and analyze its structure."""
646
647
try:
648
# Parse repository URL
649
parsed_url = ParsedUrl.parse(repo_url)
650
print(f"🔗 Repository: {parsed_url.url}")
651
print(f" Host: {parsed_url.hostname}")
652
print(f" Path: {parsed_url.pathname}")
653
654
# Clone repository
655
print(f"\n📥 Cloning to {target_dir}...")
656
git = Git.clone(repo_url, target_dir)
657
658
# Get repository information
659
head_commit = git.head
660
is_clean = git.is_clean
661
662
print(f"✅ Clone successful")
663
print(f" HEAD: {head_commit[:8]}")
664
print(f" Clean: {'✅' if is_clean else '❌'}")
665
666
# Check for pyproject.toml
667
pyproject_file = target_dir / "pyproject.toml"
668
if pyproject_file.exists():
669
print(f"📋 Found pyproject.toml")
670
671
# Quick analysis
672
from poetry.core.pyproject.toml import PyProjectTOML
673
try:
674
pyproject = PyProjectTOML(pyproject_file)
675
if pyproject.is_poetry_project():
676
config = pyproject.poetry_config
677
print(f" Poetry project: {config.get('name', 'Unknown')}")
678
else:
679
print(f" Not a Poetry project")
680
except Exception as e:
681
print(f" Error reading config: {e}")
682
else:
683
print(f"❌ No pyproject.toml found")
684
685
return git
686
687
except GitError as e:
688
print(f"❌ Git error: {e}")
689
return None
690
691
except Exception as e:
692
print(f"❌ Unexpected error: {e}")
693
return None
694
695
# Usage
696
git = clone_and_analyze_repo(
697
"https://github.com/python-poetry/poetry-core.git",
698
Path("./temp-clone")
699
)
700
``` { .api }
701
702
### Temporary File Operations
703
704
```python
705
from poetry.core.utils.helpers import temporary_directory
706
from pathlib import Path
707
import shutil
708
709
def process_with_temp_workspace(source_files: list[Path]):
710
"""Process files using temporary workspace."""
711
712
with temporary_directory() as temp_dir:
713
print(f"🛠️ Working in: {temp_dir}")
714
715
# Copy source files to temp directory
716
temp_files = []
717
for source_file in source_files:
718
if source_file.exists():
719
temp_file = temp_dir / source_file.name
720
shutil.copy2(source_file, temp_file)
721
temp_files.append(temp_file)
722
print(f" Copied: {source_file.name}")
723
724
# Process files in temp directory
725
print(f"🔄 Processing {len(temp_files)} files...")
726
results = []
727
728
for temp_file in temp_files:
729
try:
730
# Example processing: count lines
731
with temp_file.open('r') as f:
732
lines = len(f.readlines())
733
734
results.append({
735
"file": temp_file.name,
736
"lines": lines,
737
"size": temp_file.stat().st_size
738
})
739
740
print(f" {temp_file.name}: {lines} lines")
741
742
except Exception as e:
743
print(f" ❌ Error processing {temp_file.name}: {e}")
744
745
# Temporary directory is automatically cleaned up
746
print(f"✅ Processing complete, temp directory cleaned")
747
return results
748
749
# Usage
750
source_files = [
751
Path("README.md"),
752
Path("pyproject.toml"),
753
Path("src/mypackage/__init__.py")
754
]
755
756
results = process_with_temp_workspace(source_files)
757
``` { .api }
758
759
### Unicode Normalization
760
761
```python
762
from poetry.core.utils.helpers import combine_unicode
763
import unicodedata
764
765
def normalize_project_strings(config: dict):
766
"""Normalize Unicode strings in project configuration."""
767
768
print("🔤 Unicode Normalization:")
769
770
string_fields = ["name", "description", "authors", "maintainers"]
771
772
for field in string_fields:
773
if field in config:
774
original = config[field]
775
776
if isinstance(original, str):
777
normalized = combine_unicode(original)
778
779
# Check if normalization changed anything
780
if original != normalized:
781
print(f" {field}: Normalized")
782
print(f" Before: {repr(original)}")
783
print(f" After: {repr(normalized)}")
784
config[field] = normalized
785
else:
786
print(f" {field}: Already normalized")
787
788
elif isinstance(original, list):
789
# Handle list of strings (authors, maintainers)
790
normalized_list = []
791
changed = False
792
793
for item in original:
794
if isinstance(item, str):
795
normalized_item = combine_unicode(item)
796
normalized_list.append(normalized_item)
797
if item != normalized_item:
798
changed = True
799
else:
800
normalized_list.append(item)
801
802
if changed:
803
print(f" {field}: Normalized list items")
804
config[field] = normalized_list
805
else:
806
print(f" {field}: List already normalized")
807
808
return config
809
810
# Example usage
811
project_config = {
812
"name": "café-package", # May contain combining characters
813
"description": "A café management system",
814
"authors": ["José García <jose@example.com>"]
815
}
816
817
normalized_config = normalize_project_strings(project_config)
818
``` { .api }
819
820
## Error Handling Utilities
821
822
### Safe Operations
823
824
```python
825
from poetry.core.json import ValidationError
826
from poetry.core.spdx.helpers import license_by_id
827
from poetry.core.vcs.git import GitError
828
829
def safe_validation_operations():
830
"""Demonstrate safe utility operations with error handling."""
831
832
# Safe license lookup
833
def safe_license_lookup(license_id: str):
834
try:
835
return license_by_id(license_id)
836
except ValueError:
837
print(f"❌ Unknown license: {license_id}")
838
return None
839
840
# Safe validation
841
def safe_validation(obj: dict, schema: str):
842
try:
843
from poetry.core.json import validate_object
844
errors = validate_object(obj, schema)
845
return errors
846
except ValidationError as e:
847
print(f"❌ Validation failed: {e}")
848
return [str(e)]
849
except Exception as e:
850
print(f"❌ Unexpected validation error: {e}")
851
return [f"Unexpected error: {e}"]
852
853
# Safe Git operations
854
def safe_git_operation(func, *args, **kwargs):
855
try:
856
return func(*args, **kwargs)
857
except GitError as e:
858
print(f"❌ Git operation failed: {e}")
859
return None
860
except Exception as e:
861
print(f"❌ Unexpected Git error: {e}")
862
return None
863
864
# Usage examples
865
license_info = safe_license_lookup("MIT")
866
if license_info:
867
print(f"✅ License found: {license_info.name}")
868
869
validation_errors = safe_validation({"name": "test"}, "poetry-schema")
870
if not validation_errors:
871
print("✅ Validation passed")
872
else:
873
print(f"❌ Validation errors: {validation_errors}")
874
875
safe_validation_operations()
876
``` { .api }
877
878
## Type Definitions
879
880
```python
881
from typing import Any, Dict, List, Iterator
882
from pathlib import Path
883
884
# Validation types
885
ValidationErrors = List[str]
886
SchemaName = str
887
888
# License types
889
LicenseId = str
890
891
# VCS types
892
GitUrl = str
893
GitRevision = str
894
GitBranch = str
895
GitTag = str
896
897
# Utility types
898
ContentType = str
899
UnicodeString = str
900
``` { .api }