0
# PEP 440 Version and Environment Markers
1
2
Poetry Core provides comprehensive support for PEP 440 version specifications and PEP 508 environment markers. This enables precise version handling and conditional dependency specification based on the installation environment.
3
4
## Core Imports
5
6
```python
7
# PEP 440 version components
8
from poetry.core.version.pep440 import (
9
PEP440Version,
10
Release,
11
ReleaseTag,
12
LocalSegmentType
13
)
14
15
# Environment markers
16
from poetry.core.version.markers import (
17
BaseMarker,
18
AnyMarker,
19
EmptyMarker,
20
SingleMarker,
21
MultiMarker,
22
MarkerUnion,
23
parse_marker,
24
intersection,
25
union
26
)
27
28
# Marker exceptions
29
from poetry.core.version.markers import (
30
InvalidMarkerError,
31
UndefinedComparisonError,
32
UndefinedEnvironmentNameError
33
)
34
``` { .api }
35
36
## PEP 440 Version Implementation
37
38
### PEP440Version
39
40
```python
41
class PEP440Version:
42
"""
43
Core PEP 440 version implementation.
44
45
Supports all PEP 440 version formats including:
46
- Release versions: 1.2.3, 2.0.0
47
- Pre-releases: 1.0.0a1, 2.0.0b2, 1.5.0rc1
48
- Post-releases: 1.0.0.post1
49
- Development releases: 1.0.0.dev0
50
- Local versions: 1.0.0+local.1
51
"""
52
53
@classmethod
54
def parse(cls, version: str) -> PEP440Version:
55
"""
56
Parse version string into PEP440Version object.
57
58
Args:
59
version: PEP 440 compliant version string
60
61
Returns:
62
PEP440Version instance
63
64
Raises:
65
InvalidVersionError: If version string is not PEP 440 compliant
66
67
Examples:
68
>>> v1 = PEP440Version.parse("1.2.3")
69
>>> v2 = PEP440Version.parse("2.0.0a1") # Alpha pre-release
70
>>> v3 = PEP440Version.parse("1.0.0.post1") # Post-release
71
>>> v4 = PEP440Version.parse("1.0.0.dev0") # Development
72
>>> v5 = PEP440Version.parse("1.0.0+local") # Local version
73
"""
74
75
@property
76
def epoch(self) -> int:
77
"""Version epoch (defaults to 0)."""
78
79
@property
80
def release(self) -> Release:
81
"""Release version tuple (e.g., (1, 2, 3))."""
82
83
@property
84
def pre(self) -> ReleaseTag | None:
85
"""Pre-release information (alpha, beta, rc)."""
86
87
@property
88
def post(self) -> int | None:
89
"""Post-release number."""
90
91
@property
92
def dev(self) -> int | None:
93
"""Development release number."""
94
95
@property
96
def local(self) -> str | None:
97
"""Local version identifier."""
98
99
@property
100
def is_prerelease(self) -> bool:
101
"""Whether version is a pre-release."""
102
103
@property
104
def is_postrelease(self) -> bool:
105
"""Whether version is a post-release."""
106
107
@property
108
def is_devrelease(self) -> bool:
109
"""Whether version is a development release."""
110
111
def __str__(self) -> str:
112
"""String representation of version."""
113
114
def __eq__(self, other: object) -> bool:
115
"""Version equality comparison."""
116
117
def __lt__(self, other: PEP440Version) -> bool:
118
"""Version less-than comparison."""
119
120
def __le__(self, other: PEP440Version) -> bool:
121
"""Version less-than-or-equal comparison."""
122
123
def __gt__(self, other: PEP440Version) -> bool:
124
"""Version greater-than comparison."""
125
126
def __ge__(self, other: PEP440Version) -> bool:
127
"""Version greater-than-or-equal comparison."""
128
``` { .api }
129
130
### Release
131
132
```python
133
class Release:
134
"""
135
Version release component representing the main version tuple.
136
137
Handles version tuples like (1, 2, 3) with proper comparison
138
and normalization according to PEP 440.
139
"""
140
141
def __init__(self, *parts: int) -> None:
142
"""
143
Create release version.
144
145
Args:
146
parts: Version parts (e.g., 1, 2, 3 for "1.2.3")
147
148
Example:
149
>>> release = Release(1, 2, 3)
150
>>> print(release) # 1.2.3
151
"""
152
153
@property
154
def parts(self) -> tuple[int, ...]:
155
"""Version parts tuple."""
156
157
def __str__(self) -> str:
158
"""String representation (e.g., "1.2.3")."""
159
160
def __eq__(self, other: object) -> bool:
161
"""Equality comparison with normalization."""
162
163
def __lt__(self, other: Release) -> bool:
164
"""Less-than comparison."""
165
``` { .api }
166
167
### ReleaseTag
168
169
```python
170
class ReleaseTag:
171
"""
172
Pre-release tag information (alpha, beta, release candidate).
173
174
Represents pre-release versions like 1.0.0a1, 2.0.0b2, 1.5.0rc1
175
with proper ordering and normalization.
176
"""
177
178
def __init__(self, tag: str, number: int) -> None:
179
"""
180
Create release tag.
181
182
Args:
183
tag: Tag type ("a", "b", "rc" or full forms)
184
number: Tag number
185
186
Example:
187
>>> alpha = ReleaseTag("a", 1) # alpha 1
188
>>> beta = ReleaseTag("b", 2) # beta 2
189
>>> rc = ReleaseTag("rc", 1) # release candidate 1
190
"""
191
192
@property
193
def tag(self) -> str:
194
"""Normalized tag type ("a", "b", "rc")."""
195
196
@property
197
def number(self) -> int:
198
"""Tag number."""
199
200
def __str__(self) -> str:
201
"""String representation (e.g., "a1", "b2", "rc1")."""
202
203
def __eq__(self, other: object) -> bool:
204
"""Equality comparison."""
205
206
def __lt__(self, other: ReleaseTag) -> bool:
207
"""Less-than comparison (a < b < rc)."""
208
``` { .api }
209
210
### LocalSegmentType
211
212
```python
213
LocalSegmentType = Union[int, str]
214
"""
215
Type for local version segments.
216
217
Local versions can contain integers and strings separated by dots or hyphens,
218
like "1.0.0+local.1" or "1.0.0+abc.123".
219
"""
220
``` { .api }
221
222
## Environment Markers
223
224
Environment markers provide conditional dependency installation based on the target environment.
225
226
### parse_marker
227
228
```python
229
def parse_marker(marker: str) -> BaseMarker:
230
"""
231
Parse PEP 508 environment marker string.
232
233
Args:
234
marker: PEP 508 marker expression
235
236
Returns:
237
BaseMarker implementation representing the parsed marker
238
239
Raises:
240
InvalidMarkerError: If marker syntax is invalid
241
242
Examples:
243
>>> marker = parse_marker("python_version >= '3.8'")
244
>>> marker = parse_marker("sys_platform == 'win32'")
245
>>> marker = parse_marker("python_version >= '3.8' and sys_platform != 'win32'")
246
>>> marker = parse_marker("extra == 'dev'")
247
"""
248
``` { .api }
249
250
### intersection
251
252
```python
253
def intersection(*markers: BaseMarker) -> BaseMarker:
254
"""
255
Create intersection (AND) of multiple markers.
256
257
Args:
258
markers: Markers to intersect
259
260
Returns:
261
BaseMarker representing the intersection
262
263
Example:
264
>>> marker1 = parse_marker("python_version >= '3.8'")
265
>>> marker2 = parse_marker("sys_platform != 'win32'")
266
>>> combined = intersection(marker1, marker2)
267
>>> # Equivalent to: python_version >= '3.8' and sys_platform != 'win32'
268
"""
269
``` { .api }
270
271
### union
272
273
```python
274
def union(*markers: BaseMarker) -> BaseMarker:
275
"""
276
Create union (OR) of multiple markers.
277
278
Args:
279
markers: Markers to union
280
281
Returns:
282
BaseMarker representing the union
283
284
Example:
285
>>> marker1 = parse_marker("sys_platform == 'win32'")
286
>>> marker2 = parse_marker("sys_platform == 'darwin'")
287
>>> combined = union(marker1, marker2)
288
>>> # Equivalent to: sys_platform == 'win32' or sys_platform == 'darwin'
289
"""
290
``` { .api }
291
292
## Marker Classes
293
294
### BaseMarker
295
296
```python
297
class BaseMarker:
298
"""
299
Abstract base class for all environment markers.
300
301
Provides the interface for marker evaluation, intersection,
302
and union operations.
303
"""
304
305
@abstractmethod
306
def intersect(self, other: BaseMarker) -> BaseMarker:
307
"""Create intersection with another marker."""
308
309
@abstractmethod
310
def union(self, other: BaseMarker) -> BaseMarker:
311
"""Create union with another marker."""
312
313
def is_any(self) -> bool:
314
"""Check if marker matches any environment."""
315
316
def is_empty(self) -> bool:
317
"""Check if marker matches no environment."""
318
319
@property
320
def complexity(self) -> tuple[int, int]:
321
"""
322
Marker complexity metrics for optimization.
323
324
Returns:
325
Tuple of (detailed_count, simplified_count)
326
"""
327
328
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
329
"""
330
Evaluate marker against environment.
331
332
Args:
333
environment: Environment variables dict (uses current if None)
334
335
Returns:
336
True if marker matches the environment
337
"""
338
``` { .api }
339
340
### AnyMarker
341
342
```python
343
class AnyMarker(BaseMarker):
344
"""
345
Marker that matches any environment.
346
347
Represents the absence of restrictions - all environments are valid.
348
"""
349
350
def is_any(self) -> bool:
351
"""Always returns True."""
352
353
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
354
"""Always returns True."""
355
356
def intersect(self, other: BaseMarker) -> BaseMarker:
357
"""Returns the other marker (intersection with 'any')."""
358
359
def union(self, other: BaseMarker) -> BaseMarker:
360
"""Returns self (union with 'any' is 'any')."""
361
``` { .api }
362
363
### EmptyMarker
364
365
```python
366
class EmptyMarker(BaseMarker):
367
"""
368
Marker that matches no environment.
369
370
Represents impossible conditions or explicit exclusion.
371
"""
372
373
def is_empty(self) -> bool:
374
"""Always returns True."""
375
376
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
377
"""Always returns False."""
378
379
def intersect(self, other: BaseMarker) -> BaseMarker:
380
"""Returns self (intersection with 'empty' is 'empty')."""
381
382
def union(self, other: BaseMarker) -> BaseMarker:
383
"""Returns the other marker (union with 'empty')."""
384
``` { .api }
385
386
### SingleMarker
387
388
```python
389
class SingleMarker(BaseMarker):
390
"""
391
Single condition marker (e.g., "python_version >= '3.8'").
392
393
Represents a single comparison operation between an environment
394
variable and a value.
395
"""
396
397
def __init__(self, name: str, constraint: BaseConstraint) -> None:
398
"""
399
Create single marker.
400
401
Args:
402
name: Environment variable name
403
constraint: Constraint to apply to the variable
404
405
Example:
406
>>> from poetry.core.constraints.generic import parse_constraint
407
>>> marker = SingleMarker("python_version", parse_constraint(">= 3.8"))
408
"""
409
410
@property
411
def name(self) -> str:
412
"""Environment variable name."""
413
414
@property
415
def constraint(self) -> BaseConstraint:
416
"""Constraint applied to the variable."""
417
418
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
419
"""Evaluate constraint against environment variable."""
420
``` { .api }
421
422
### MultiMarker
423
424
```python
425
class MultiMarker(BaseMarker):
426
"""
427
Conjunction of multiple markers (AND operation).
428
429
All constituent markers must be satisfied.
430
"""
431
432
def __init__(self, *markers: BaseMarker) -> None:
433
"""
434
Create conjunction of markers.
435
436
Args:
437
markers: Markers that must all be satisfied
438
439
Example:
440
>>> marker1 = parse_marker("python_version >= '3.8'")
441
>>> marker2 = parse_marker("sys_platform != 'win32'")
442
>>> multi = MultiMarker(marker1, marker2)
443
"""
444
445
@property
446
def markers(self) -> tuple[BaseMarker, ...]:
447
"""Constituent markers."""
448
449
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
450
"""Check if all markers are satisfied."""
451
``` { .api }
452
453
### MarkerUnion
454
455
```python
456
class MarkerUnion(BaseMarker):
457
"""
458
Disjunction of multiple markers (OR operation).
459
460
Any constituent marker can be satisfied.
461
"""
462
463
def __init__(self, *markers: BaseMarker) -> None:
464
"""
465
Create disjunction of markers.
466
467
Args:
468
markers: Markers where any can be satisfied
469
470
Example:
471
>>> marker1 = parse_marker("sys_platform == 'win32'")
472
>>> marker2 = parse_marker("sys_platform == 'darwin'")
473
>>> union = MarkerUnion(marker1, marker2)
474
"""
475
476
@property
477
def markers(self) -> tuple[BaseMarker, ...]:
478
"""Constituent markers."""
479
480
def evaluate(self, environment: dict[str, Any] | None = None) -> bool:
481
"""Check if any marker is satisfied."""
482
``` { .api }
483
484
## Environment Variables
485
486
### Supported Environment Variables
487
488
```python
489
# Standard PEP 508 environment variables
490
ENVIRONMENT_VARIABLES = {
491
"implementation_name", # Python implementation (cpython, pypy, etc.)
492
"implementation_version", # Implementation version
493
"os_name", # Operating system name (posix, nt, java)
494
"platform_machine", # Machine type (x86_64, i386, etc.)
495
"platform_python_implementation", # Python implementation
496
"platform_release", # Platform release
497
"platform_system", # Platform system (Linux, Windows, Darwin)
498
"platform_version", # Platform version
499
"python_full_version", # Full Python version (3.9.1)
500
"python_version", # Python version (3.9)
501
"sys_platform", # Platform string (linux, win32, darwin)
502
"extra", # Extra/feature being installed
503
}
504
505
# Variable aliases for compatibility
506
ALIASES = {
507
"os.name": "os_name",
508
"sys.platform": "sys_platform",
509
"platform.version": "platform_version",
510
"platform.machine": "platform_machine",
511
"platform.python_implementation": "platform_python_implementation",
512
"python_implementation": "platform_python_implementation",
513
}
514
``` { .api }
515
516
## Usage Examples
517
518
### Version Parsing and Comparison
519
520
```python
521
from poetry.core.version.pep440 import PEP440Version
522
523
def version_examples():
524
"""Demonstrate PEP 440 version parsing and comparison."""
525
526
# Parse various version formats
527
versions = [
528
"1.0.0", # Release
529
"2.0.0a1", # Alpha pre-release
530
"1.5.0b2", # Beta pre-release
531
"2.1.0rc1", # Release candidate
532
"1.0.0.post1", # Post-release
533
"2.0.0.dev0", # Development release
534
"1.0.0+local.1", # Local version
535
"1!2.0.0", # Epoch version
536
]
537
538
parsed_versions = []
539
for version_str in versions:
540
try:
541
version = PEP440Version.parse(version_str)
542
parsed_versions.append(version)
543
544
print(f"Version: {version}")
545
print(f" Release: {version.release}")
546
print(f" Pre-release: {version.pre}")
547
print(f" Post-release: {version.post}")
548
print(f" Dev release: {version.dev}")
549
print(f" Local: {version.local}")
550
print(f" Is pre-release: {version.is_prerelease}")
551
print()
552
553
except Exception as e:
554
print(f"Failed to parse '{version_str}': {e}")
555
556
# Version comparison
557
print("Version Comparison:")
558
sorted_versions = sorted(parsed_versions)
559
for version in sorted_versions:
560
print(f" {version}")
561
562
version_examples()
563
``` { .api }
564
565
### Environment Marker Parsing
566
567
```python
568
from poetry.core.version.markers import parse_marker
569
570
def marker_examples():
571
"""Demonstrate environment marker parsing and evaluation."""
572
573
markers = [
574
"python_version >= '3.8'",
575
"sys_platform == 'win32'",
576
"python_version >= '3.8' and sys_platform != 'win32'",
577
"python_version < '3.9' or python_version >= '3.10'",
578
"implementation_name == 'cpython'",
579
"platform_machine == 'x86_64'",
580
"extra == 'dev'",
581
"os_name == 'posix' and platform_system == 'Linux'",
582
]
583
584
print("Parsing Environment Markers:")
585
for marker_str in markers:
586
try:
587
marker = parse_marker(marker_str)
588
print(f"✓ {marker_str}")
589
print(f" Type: {type(marker).__name__}")
590
print(f" Is any: {marker.is_any()}")
591
print(f" Is empty: {marker.is_empty()}")
592
593
except Exception as e:
594
print(f"✗ {marker_str}: {e}")
595
print()
596
597
marker_examples()
598
``` { .api }
599
600
### Marker Evaluation
601
602
```python
603
import platform
604
import sys
605
from poetry.core.version.markers import parse_marker
606
607
def evaluate_markers():
608
"""Evaluate markers against current environment."""
609
610
# Get current environment
611
current_env = {
612
"implementation_name": sys.implementation.name,
613
"implementation_version": ".".join(map(str, sys.implementation.version[:2])),
614
"os_name": os.name,
615
"platform_machine": platform.machine(),
616
"platform_python_implementation": platform.python_implementation(),
617
"platform_release": platform.release(),
618
"platform_system": platform.system(),
619
"platform_version": platform.version(),
620
"python_full_version": ".".join(map(str, sys.version_info)),
621
"python_version": ".".join(map(str, sys.version_info[:2])),
622
"sys_platform": sys.platform,
623
}
624
625
print(f"Current Environment:")
626
for key, value in current_env.items():
627
print(f" {key}: {value}")
628
print()
629
630
# Test markers
631
test_markers = [
632
"python_version >= '3.8'",
633
"python_version >= '3.12'",
634
"sys_platform == 'win32'",
635
"sys_platform == 'linux'",
636
"implementation_name == 'cpython'",
637
"implementation_name == 'pypy'",
638
]
639
640
print("Marker Evaluation:")
641
for marker_str in test_markers:
642
try:
643
marker = parse_marker(marker_str)
644
result = marker.evaluate(current_env)
645
status = "✓ MATCH" if result else "✗ NO MATCH"
646
print(f"{status}: {marker_str}")
647
648
except Exception as e:
649
print(f"ERROR: {marker_str} -> {e}")
650
651
evaluate_markers()
652
``` { .api }
653
654
### Complex Marker Operations
655
656
```python
657
from poetry.core.version.markers import parse_marker, intersection, union
658
659
def complex_marker_operations():
660
"""Demonstrate complex marker operations."""
661
662
# Create individual markers
663
py38_plus = parse_marker("python_version >= '3.8'")
664
not_windows = parse_marker("sys_platform != 'win32'")
665
linux_only = parse_marker("sys_platform == 'linux'")
666
macos_only = parse_marker("sys_platform == 'darwin'")
667
668
# Intersection (AND)
669
py38_not_win = intersection(py38_plus, not_windows)
670
print(f"Python 3.8+ AND not Windows: {py38_not_win}")
671
672
# Union (OR)
673
linux_or_macos = union(linux_only, macos_only)
674
print(f"Linux OR macOS: {linux_or_macos}")
675
676
# Complex combination
677
complex_marker = intersection(py38_plus, linux_or_macos)
678
print(f"Python 3.8+ AND (Linux OR macOS): {complex_marker}")
679
680
# Test evaluation
681
test_environments = [
682
{"python_version": "3.9", "sys_platform": "linux"},
683
{"python_version": "3.7", "sys_platform": "linux"},
684
{"python_version": "3.9", "sys_platform": "win32"},
685
{"python_version": "3.9", "sys_platform": "darwin"},
686
]
687
688
print("\nEvaluation Results:")
689
for env in test_environments:
690
result = complex_marker.evaluate(env)
691
py_ver = env["python_version"]
692
platform = env["sys_platform"]
693
status = "✓" if result else "✗"
694
print(f" {status} Python {py_ver} on {platform}")
695
696
complex_marker_operations()
697
``` { .api }
698
699
### Practical Dependency Scenarios
700
701
```python
702
from poetry.core.version.markers import parse_marker
703
704
def dependency_scenarios():
705
"""Show practical dependency marker scenarios."""
706
707
scenarios = {
708
"Windows-specific dependency": "sys_platform == 'win32'",
709
"Unix-only dependency": "os_name == 'posix'",
710
"Modern Python requirement": "python_version >= '3.8'",
711
"Legacy Python support": "python_version < '3.8'",
712
"CPython optimization": "implementation_name == 'cpython'",
713
"PyPy compatibility": "implementation_name == 'pypy'",
714
"Development extra": "extra == 'dev'",
715
"Testing extra": "extra == 'test'",
716
"macOS ARM64": "sys_platform == 'darwin' and platform_machine == 'arm64'",
717
"Linux x86_64": "sys_platform == 'linux' and platform_machine == 'x86_64'",
718
"Not Windows": "sys_platform != 'win32'",
719
"Python 3.8-3.11": "python_version >= '3.8' and python_version < '3.12'",
720
}
721
722
print("Common Dependency Marker Scenarios:")
723
print("=" * 50)
724
725
for scenario, marker_str in scenarios.items():
726
try:
727
marker = parse_marker(marker_str)
728
print(f"{scenario}:")
729
print(f" Marker: {marker_str}")
730
print(f" Parsed: {marker}")
731
print(f" Type: {type(marker).__name__}")
732
print()
733
734
except Exception as e:
735
print(f"❌ {scenario}: {e}")
736
print()
737
738
dependency_scenarios()
739
``` { .api }
740
741
## Error Handling
742
743
### Marker Exceptions
744
745
```python
746
from poetry.core.version.markers import (
747
InvalidMarkerError,
748
UndefinedComparisonError,
749
UndefinedEnvironmentNameError,
750
parse_marker
751
)
752
753
def safe_marker_operations():
754
"""Demonstrate safe marker parsing and evaluation."""
755
756
def safe_parse_marker(marker_str: str):
757
"""Safely parse marker with error handling."""
758
try:
759
return parse_marker(marker_str)
760
except InvalidMarkerError as e:
761
print(f"Invalid marker syntax '{marker_str}': {e}")
762
return None
763
except Exception as e:
764
print(f"Unexpected error parsing '{marker_str}': {e}")
765
return None
766
767
def safe_evaluate_marker(marker, environment):
768
"""Safely evaluate marker with error handling."""
769
try:
770
return marker.evaluate(environment)
771
except UndefinedEnvironmentNameError as e:
772
print(f"Undefined environment variable: {e}")
773
return False
774
except UndefinedComparisonError as e:
775
print(f"Invalid comparison: {e}")
776
return False
777
except Exception as e:
778
print(f"Evaluation error: {e}")
779
return False
780
781
# Test various marker strings
782
test_markers = [
783
"python_version >= '3.8'", # Valid
784
"python_version >= 3.8", # Invalid (no quotes)
785
"invalid_var == 'test'", # Valid syntax, undefined var
786
"python_version >> '3.8'", # Invalid operator
787
"python_version", # Incomplete
788
"", # Empty
789
]
790
791
print("Safe Marker Operations:")
792
for marker_str in test_markers:
793
print(f"\nTesting: '{marker_str}'")
794
marker = safe_parse_marker(marker_str)
795
796
if marker:
797
env = {"python_version": "3.9"}
798
result = safe_evaluate_marker(marker, env)
799
print(f" ✓ Parsed successfully")
800
print(f" ✓ Evaluation result: {result}")
801
else:
802
print(f" ✗ Failed to parse")
803
804
safe_marker_operations()
805
``` { .api }