1- # This is a utility for generating test specific data in conda-pypi
2- # only. It is not appropriate to use this to generate production level
3- # repodata.
1+ """
2+ Utility for generating test-specific local channel repodata.
3+
4+ This is test data generation logic for conda-pypi only; it is not intended for
5+ production repodata generation.
6+
7+ Marker conversion policy for this test channel:
8+ - Convert Python markers to `python...` matchspec fragments, including
9+ `python_version not in "x, y"` -> `(python!=x and python!=y)`.
10+ - Convert platform/os markers to virtual packages when feasible
11+ (`__win`, `__linux`, `__osx`, `__unix`).
12+ - Keep extras in `extra_depends`, with remaining non-extra marker logic
13+ encoded via `[when="..."]`.
14+ - Drop unsupported marker dimensions (for example interpreter/machine-specific
15+ variants) for these noarch channel tests.
16+ """
417
518import json
6- import re
719import requests
820from concurrent .futures import ThreadPoolExecutor , as_completed
21+ from enum import StrEnum
22+ from packaging .markers import Marker
923from packaging .requirements import Requirement
1024from typing import Any
1125
12- EXTRA_MARKER_RE = re .compile (r'extra\s*==\s*["\']([^"\']+)["\']' )
26+
27+ class MarkerVar (StrEnum ):
28+ PYTHON_VERSION = "python_version"
29+ PYTHON_FULL_VERSION = "python_full_version"
30+ EXTRA = "extra"
31+ SYS_PLATFORM = "sys_platform"
32+ PLATFORM_SYSTEM = "platform_system"
33+ OS_NAME = "os_name"
34+ IMPLEMENTATION_NAME = "implementation_name"
35+ PLATFORM_PYTHON_IMPLEMENTATION = "platform_python_implementation"
36+ PLATFORM_MACHINE = "platform_machine"
37+
38+
39+ class MarkerOp (StrEnum ):
40+ EQ = "=="
41+ NE = "!="
42+ NOT_IN = "not in"
43+
44+
45+ SYSTEM_TO_VIRTUAL_PACKAGE = {
46+ "windows" : "__win" ,
47+ "win32" : "__win" ,
48+ "linux" : "__linux" ,
49+ "darwin" : "__osx" ,
50+ "cygwin" : "__unix" ,
51+ }
52+
53+ OS_NAME_TO_VIRTUAL_PACKAGE = {
54+ "nt" : "__win" ,
55+ "windows" : "__win" ,
56+ "posix" : "__unix" ,
57+ }
1358
1459
1560def normalize_name (name : str ) -> str :
1661 """Normalize a package name to conda conventions (lowercase, _ -> -)."""
1762 return name .lower ().replace ("_" , "-" )
1863
1964
65+ def _marker_value (token : Any ) -> str :
66+ """Extract the textual value from packaging marker tokens."""
67+ return getattr (token , "value" , str (token ))
68+
69+
70+ def _normalize_marker_clause (marker_name : str , op : str , marker_value : str ) -> str | None :
71+ """Map a single PEP 508 marker atom to a MatchSpec-like fragment.
72+
73+ Examples:
74+ - ("sys_platform", "==", "win32") -> "__win"
75+ - ("python_version", "<", "3.11") -> "python<3.11"
76+ - ("python_version", "not in", "3.0, 3.1") -> "(python!=3.0 and python!=3.1)"
77+ - ("implementation_name", "==", "cpython") -> None
78+ """
79+ marker_name = marker_name .lower ()
80+ marker_value = marker_value .lower ()
81+
82+ if marker_name in {MarkerVar .PYTHON_VERSION , MarkerVar .PYTHON_FULL_VERSION }:
83+ if op == MarkerOp .NOT_IN :
84+ excluded_versions = [
85+ version .strip () for version in marker_value .split ("," ) if version .strip ()
86+ ]
87+ if not excluded_versions :
88+ return None
89+ clauses = [f"python!={ version } " for version in excluded_versions ]
90+ if len (clauses ) == 1 :
91+ return clauses [0 ]
92+ return f"({ ' and ' .join (clauses )} )"
93+ return f"python{ op } { marker_value } "
94+
95+ if marker_name == MarkerVar .EXTRA and op == MarkerOp .EQ :
96+ return None
97+
98+ if marker_name in {MarkerVar .SYS_PLATFORM , MarkerVar .PLATFORM_SYSTEM }:
99+ mapped = SYSTEM_TO_VIRTUAL_PACKAGE .get (marker_value )
100+ if op == MarkerOp .EQ and mapped :
101+ return mapped
102+ if op == MarkerOp .NE and marker_value in {"win32" , "windows" , "cygwin" }:
103+ return "__unix"
104+ if op == MarkerOp .NE and marker_value == "emscripten" :
105+ return None
106+ return None
107+
108+ if marker_name == MarkerVar .OS_NAME :
109+ mapped = OS_NAME_TO_VIRTUAL_PACKAGE .get (marker_value )
110+ if not mapped :
111+ return None
112+ if op == MarkerOp .EQ :
113+ return mapped
114+ if op == MarkerOp .NE :
115+ return "__unix" if mapped == "__win" else "__win"
116+ return None
117+
118+ if marker_name in {MarkerVar .IMPLEMENTATION_NAME , MarkerVar .PLATFORM_PYTHON_IMPLEMENTATION }:
119+ if marker_value in {"cpython" , "pypy" , "jython" }:
120+ return None
121+ return None
122+
123+ if marker_name == MarkerVar .PLATFORM_MACHINE :
124+ return None
125+
126+ return None
127+
128+
129+ def _combine_conditions (left : str | None , op : str , right : str | None ) -> str | None :
130+ """Combine optional left/right expressions with a boolean operator."""
131+ if left is None :
132+ return right
133+ if right is None :
134+ return left
135+ if left == right :
136+ return left
137+ return f"({ left } { op } { right } )"
138+
139+
140+ def extract_marker_condition_and_extras (marker : Marker ) -> tuple [str | None , list [str ]]:
141+ """Split a Marker into optional non-extra condition and extra group names.
142+
143+ Examples:
144+ - `extra == "docs"` -> `(None, ["docs"])`
145+ - `python_version < "3.11" and extra == "test"` -> `("python<3.11", ["test"])`
146+ - `sys_platform == "win32"` -> `("__win", [])`
147+ """
148+ extras : list [str ] = []
149+ seen_extras : set [str ] = set ()
150+
151+ def parse_marker_node (node : Any ) -> str | None :
152+ if isinstance (node , tuple ) and len (node ) == 3 :
153+ marker_name = _marker_value (node [0 ])
154+ op = _marker_value (node [1 ])
155+ marker_value = _marker_value (node [2 ])
156+
157+ if marker_name .lower () == MarkerVar .EXTRA and op == MarkerOp .EQ :
158+ extra_name = marker_value .lower ()
159+ if extra_name not in seen_extras :
160+ seen_extras .add (extra_name )
161+ extras .append (extra_name )
162+ return None
163+
164+ return _normalize_marker_clause (marker_name , op , marker_value )
165+
166+ if isinstance (node , list ):
167+ if not node :
168+ return None
169+
170+ condition_expr = parse_marker_node (node [0 ])
171+ for op , rhs in zip (node [1 ::2 ], node [2 ::2 ]):
172+ right_condition = parse_marker_node (rhs )
173+ condition_expr = _combine_conditions (
174+ condition_expr , str (op ).lower (), right_condition
175+ )
176+ return condition_expr
177+
178+ return None
179+
180+ # Marker._markers is a private packaging attribute; keep access isolated here.
181+ condition = parse_marker_node (getattr (marker , "_markers" , []))
182+ return condition , extras
183+
184+
20185def pypi_to_repodata_noarch_whl_entry (
21186 pypi_data : dict [str , Any ],
22187) -> dict [str , Any ] | None :
23188 """
24- Convert PyPI JSON endpoint data to a repodata.json packages .whl entry for a
189+ Convert PyPI JSON endpoint data to a repodata.json v3 .whl entry for a
25190 pure Python (noarch) wheel.
26191
27192 Args:
28193 pypi_data: Dictionary containing the complete info from PyPI JSON endpoint
29194
30195 Returns:
31- Dictionary representing the entry for packages .whl, or None if no pure
196+ Dictionary representing the entry for v3 .whl, or None if no pure
32197 Python wheel (platform tag "none-any") is found
33198 """
34199 # Find a pure Python wheel (platform tag "none-any")
@@ -48,17 +213,26 @@ def pypi_to_repodata_noarch_whl_entry(
48213 pypi_info = pypi_data .get ("info" )
49214
50215 depends_list : list [str ] = []
51- extras_dict : dict [str , list [str ]] = {}
216+ extra_depends_dict : dict [str , list [str ]] = {}
52217 for dep in pypi_info .get ("requires_dist" ) or []:
53218 req = Requirement (dep )
54219 conda_dep = normalize_name (req .name ) + str (req .specifier )
55220
56221 if req .marker :
57- extra_match = EXTRA_MARKER_RE .search (str (req .marker ))
58- if extra_match :
59- extras_dict .setdefault (extra_match .group (1 ), []).append (conda_dep )
222+ non_extra_condition , extra_names = extract_marker_condition_and_extras (req .marker )
223+ if extra_names :
224+ for extra_name in extra_names :
225+ extra_dep = conda_dep
226+ if non_extra_condition :
227+ marker_condition = json .dumps (non_extra_condition )
228+ extra_dep = f"{ extra_dep } [when={ marker_condition } ]"
229+ extra_depends_dict .setdefault (extra_name , []).append (extra_dep )
60230 else :
61- depends_list .append (conda_dep )
231+ if non_extra_condition :
232+ marker_condition = json .dumps (non_extra_condition )
233+ depends_list .append (f"{ conda_dep } [when={ marker_condition } ]" )
234+ else :
235+ depends_list .append (conda_dep )
62236 else :
63237 depends_list .append (conda_dep )
64238
@@ -78,7 +252,7 @@ def pypi_to_repodata_noarch_whl_entry(
78252 "build" : "py3_none_any_0" ,
79253 "build_number" : 0 ,
80254 "depends" : depends_list ,
81- "extras " : extras_dict ,
255+ "extra_depends " : extra_depends_dict ,
82256 "fn" : f"{ pypi_info .get ('name' )} -{ pypi_info .get ('version' )} -py3-none-any.whl" ,
83257 "sha256" : wheel_url .get ("digests" , {}).get ("sha256" , "" ),
84258 "size" : wheel_url .get ("size" , 0 ),
@@ -137,9 +311,9 @@ def get_repodata_entry(name: str, version: str) -> dict[str, Any] | None:
137311 "packages" : {},
138312 "packages.conda" : {},
139313 "removed" : [],
140- "repodata_version" : 1 ,
314+ "repodata_version" : 3 ,
141315 "signatures" : {},
142- "packages. whl" : {key : value for key , value in sorted (pkg_whls .items ())},
316+ "v3" : { " whl" : {key : value for key , value in sorted (pkg_whls .items ())} },
143317 }
144318
145319 with open (wheel_repodata , "w" ) as f :
0 commit comments