1+ from __future__ import annotations
2+
13import os
24from pathlib import Path
3- from typing import List , TextIO
5+ from typing import Dict , Iterable , List , TextIO , Tuple
46
57from packaging .markers import Marker
68from packaging .requirements import Requirement
@@ -15,6 +17,12 @@ def __init__(self, requirement_string: str) -> None:
1517 self .req = Requirement (requirement_string )
1618 self .comments = set ()
1719
20+ def __hash__ (self ) -> int :
21+ return hash (self .req )
22+
23+ def __eq__ (self , other : RequirementData ) -> bool :
24+ return self .req == other .req
25+
1826 @property
1927 def name (self ) -> str :
2028 return self .req .name
@@ -49,30 +57,47 @@ def get_requirements(fp: TextIO) -> List[RequirementData]:
4957 return requirements
5058
5159
60+ def iter_envs (envs : Iterable [str ]) -> Iterable [Tuple [str , str ]]:
61+ for env_name in envs :
62+ platform , python_version = env_name .split ("-" , maxsplit = 1 )
63+ yield (platform , python_version )
64+
65+
5266names = ["base" ]
5367names .extend (file .stem for file in REQUIREMENTS_FOLDER .glob ("extra-*.in" ))
54- base_requirements = []
68+ base_requirements : List [ RequirementData ] = []
5569
5670for name in names :
57- # {req_name: {sys_platform: RequirementData}
58- input_data = {}
71+ # {req_data: {sys_platform: RequirementData}
72+ input_data : Dict [RequirementData , Dict [str , RequirementData ]] = {}
73+ all_envs = set ()
5974 all_platforms = set ()
75+ all_python_versions = set ()
6076 for file in REQUIREMENTS_FOLDER .glob (f"*-{ name } .txt" ):
61- platform_name = file .stem .split ("-" , maxsplit = 1 )[0 ]
77+ platform_name , python_version , _ = file .stem .split ("-" , maxsplit = 2 )
78+ env_name = f"{ platform_name } -{ python_version } "
79+ all_envs .add (env_name )
6280 all_platforms .add (platform_name )
81+ all_python_versions .add (python_version )
6382 with file .open (encoding = "utf-8" ) as fp :
6483 requirements = get_requirements (fp )
6584
6685 for req in requirements :
67- platforms = input_data .setdefault (req . name , {})
68- platforms [ platform_name ] = req
86+ envs = input_data .setdefault (req , {})
87+ envs [ env_name ] = req
6988
7089 output = base_requirements if name == "base" else []
71- for req_name , platforms in input_data .items ():
72- req = next (iter (platforms .values ()))
73- for other_req in platforms .values ():
74- if req .req != other_req .req :
75- raise RuntimeError (f"Incompatible requirements for { req_name } ." )
90+ for req , envs in input_data .items ():
91+ # {platform: [python_versions...]}
92+ python_versions_per_platform : Dict [str , List [str ]] = {}
93+ # {python_version: [platforms...]}
94+ platforms_per_python_version : Dict [str , List [str ]] = {}
95+ platforms = python_versions_per_platform .keys ()
96+ python_versions = platforms_per_python_version .keys ()
97+ for env_name , other_req in envs .items ():
98+ platform_name , python_version = env_name .split ("-" , maxsplit = 1 )
99+ python_versions_per_platform .setdefault (platform_name , []).append (python_version )
100+ platforms_per_python_version .setdefault (python_version , []).append (platform_name )
76101
77102 req .comments .update (other_req .comments )
78103
@@ -84,30 +109,74 @@ def get_requirements(fp: TextIO) -> List[RequirementData]:
84109 old_req_marker = req .marker
85110 req .marker = base_req .marker = None
86111 if base_req .req != req .req :
87- raise RuntimeError (f"Incompatible requirements for { req_name } ." )
112+ raise RuntimeError (f"Incompatible requirements for { req . name } ." )
88113
89114 base_req .marker = old_base_marker
90115 req .marker = old_req_marker
91116 if base_req .marker is None or base_req .marker == req .marker :
92117 continue
93118
94- if len (platforms ) == len (all_platforms ):
119+ if len (envs ) == len (all_envs ):
95120 output .append (req )
96121 continue
97- elif len (platforms ) < len (all_platforms - platforms .keys ()):
98- platform_marker = " or " .join (
99- f"sys_platform == '{ platform } '" for platform in platforms
122+
123+ # At this point I'm wondering why I didn't just go for
124+ # a more generic boolean algebra simplification (sympy.simplify_logic())...
125+ if (
126+ len (set (map (frozenset , python_versions_per_platform .values ()))) == 1
127+ or len (set (map (frozenset , platforms_per_python_version .values ()))) == 1
128+ ):
129+ # Either all platforms have the same Python version set
130+ # or all Python versions have the same platform set.
131+ # We can generate markers for platform (platform_marker) and Python
132+ # (python_version_marker) version sets separately and then simply require
133+ # that both markers are fulfilled at the same time (env_marker).
134+
135+ python_version_marker = (
136+ # Requirement present on less Python versions than not.
137+ " or " .join (
138+ f"python_version == '{ python_version } '" for python_version in python_versions
139+ )
140+ if len (python_versions ) < len (all_python_versions - python_versions )
141+ # Requirement present on more Python versions than not
142+ # This may generate an empty string when Python version is irrelevant.
143+ else " and " .join (
144+ f"python_version != '{ python_version } '"
145+ for python_version in all_python_versions - python_versions
146+ )
100147 )
148+
149+ platform_marker = (
150+ # Requirement present on less platforms than not.
151+ " or " .join (f"sys_platform == '{ platform } '" for platform in platforms )
152+ if len (platforms ) < len (all_platforms - platforms )
153+ # Requirement present on more platforms than not
154+ # This may generate an empty string when platform is irrelevant.
155+ else " and " .join (
156+ f"sys_platform != '{ platform } '" for platform in all_platforms - platforms
157+ )
158+ )
159+
160+ if python_version_marker and platform_marker :
161+ env_marker = f"({ python_version_marker } ) and ({ platform_marker } )"
162+ else :
163+ env_marker = python_version_marker or platform_marker
101164 else :
102- platform_marker = " and " .join (
103- f"sys_platform != '{ platform } '" for platform in all_platforms - platforms .keys ()
165+ # Fallback to generic case.
166+ env_marker = (
167+ # Requirement present on less envs than not.
168+ " or " .join (
169+ f"(sys_platform == '{ platform } ' and python_version == '{ python_version } ')"
170+ for platform , python_version in iter_envs (envs )
171+ )
172+ if len (envs ) < len (all_envs - envs .keys ())
173+ else " and " .join (
174+ f"(sys_platform != '{ platform } ' and python_version != '{ python_version } ')"
175+ for platform , python_version in iter_envs (all_envs - envs .keys ())
176+ )
104177 )
105178
106- new_marker = (
107- f"({ req .marker } ) and ({ platform_marker } )"
108- if req .marker is not None
109- else platform_marker
110- )
179+ new_marker = f"({ req .marker } ) and ({ env_marker } )" if req .marker is not None else env_marker
111180 req .marker = Marker (new_marker )
112181 if base_req is not None and base_req .marker == req .marker :
113182 continue
0 commit comments