1010from functools import lru_cache
1111from pathlib import Path
1212from tempfile import NamedTemporaryFile , TemporaryDirectory
13- from typing import Any , AnyStr , Dict , List , Mapping , Optional , Sequence , Union
13+ from typing import Any , AnyStr , Dict , List , Mapping , Optional , Sequence , Tuple , Union
1414from urllib .parse import urlparse , urlsplit , urlunparse , urlunsplit
1515
16+ from pipenv .exceptions import PipenvUsageError
1617from pipenv .patched .pip ._internal .models .link import Link
1718from pipenv .patched .pip ._internal .network .download import Downloader
1819from pipenv .patched .pip ._internal .req .constructors import (
@@ -1083,13 +1084,72 @@ def normalize_vcs_url(vcs_url):
10831084 return vcs_url , vcs_ref
10841085
10851086
1086- def install_req_from_pipfile (name , pipfile ):
1087- """Creates an InstallRequirement from a name and a pipfile entry.
1088- Handles VCS, local & remote paths, and regular named requirements.
1089- "file" and "path" entries are treated the same.
1087+ class VCSURLProcessor :
1088+ """Handles processing and environment variable expansion in VCS URLs."""
1089+
1090+ ENV_VAR_PATTERN = re .compile (r"\${([^}]+)}|\$([a-zA-Z_][a-zA-Z0-9_]*)" )
1091+
1092+ @classmethod
1093+ def expand_env_vars (cls , value : str ) -> str :
1094+ """
1095+ Expands environment variables in a string, with detailed error handling.
1096+ Supports both ${VAR} and $VAR syntax.
1097+ """
1098+
1099+ def _replace_var (match ):
1100+ var_name = match .group (1 ) or match .group (2 )
1101+ if var_name not in os .environ :
1102+ raise PipenvUsageError (
1103+ f"Environment variable '${ var_name } ' not found. "
1104+ "Please ensure all required environment variables are set."
1105+ )
1106+ return os .environ [var_name ]
1107+
1108+ try :
1109+ return cls .ENV_VAR_PATTERN .sub (_replace_var , value )
1110+ except Exception as e :
1111+ raise PipenvUsageError (f"Error expanding environment variables: { str (e )} " )
1112+
1113+ @classmethod
1114+ def process_vcs_url (cls , url : str ) -> str :
1115+ """
1116+ Processes a VCS URL, expanding environment variables in individual components.
1117+ Handles URLs of the form: vcs+protocol://username:password@hostname/path
1118+ """
1119+ parsed = urlparse (url )
1120+
1121+ # Process each component separately
1122+ netloc_parts = parsed .netloc .split ("@" )
1123+ if len (netloc_parts ) > 1 :
1124+ # Handle auth information
1125+ auth , host = netloc_parts
1126+ if ":" in auth :
1127+ username , password = auth .split (":" )
1128+ username = cls .expand_env_vars (username )
1129+ password = cls .expand_env_vars (password )
1130+ auth = f"{ username } :{ password } "
1131+ else :
1132+ auth = cls .expand_env_vars (auth )
1133+ netloc = f"{ auth } @{ host } "
1134+ else :
1135+ netloc = cls .expand_env_vars (parsed .netloc )
1136+
1137+ # Reconstruct URL with processed components
1138+ processed_parts = list (parsed )
1139+ processed_parts [1 ] = netloc # Update netloc
1140+ processed_parts [2 ] = cls .expand_env_vars (parsed .path ) # Update path
1141+
1142+ return urlunparse (tuple (processed_parts ))
1143+
1144+
1145+ def install_req_from_pipfile (name : str , pipfile : Dict [str , Any ]) -> Tuple [Any , Any , str ]:
1146+ """
1147+ Creates an InstallRequirement from a name and a pipfile entry.
1148+ Enhanced to handle environment variables within VCS URLs.
10901149 """
10911150 _pipfile = {}
10921151 vcs = None
1152+
10931153 if hasattr (pipfile , "keys" ):
10941154 _pipfile = dict (pipfile ).copy ()
10951155 else :
@@ -1098,43 +1158,41 @@ def install_req_from_pipfile(name, pipfile):
10981158 _pipfile [vcs ] = pipfile
10991159
11001160 extras = _pipfile .get ("extras" , [])
1101- extras_str = ""
1102- if extras :
1103- extras_str = f"[{ ',' .join (extras )} ]"
1161+ extras_str = f"[{ ',' .join (extras )} ]" if extras else ""
1162+
11041163 if not vcs :
11051164 vcs = next (iter ([vcs for vcs in VCS_LIST if vcs in _pipfile ]), None )
11061165
11071166 if vcs :
1108- vcs_url = _pipfile [vcs ]
1109- subdirectory = _pipfile .get ("subdirectory" , "" )
1110- if subdirectory :
1111- subdirectory = f"#subdirectory={ subdirectory } "
1112- vcs_url , fallback_ref = normalize_vcs_url (vcs_url )
1113- req_str = f"{ vcs_url } @{ _pipfile .get ('ref' , fallback_ref )} { extras_str } "
1114- if not req_str .startswith (f"{ vcs } +" ):
1115- req_str = f"{ vcs } +{ req_str } "
1116- if _pipfile .get ("editable" , False ):
1117- req_str = f"-e { name } { extras_str } @ { req_str } { subdirectory } "
1118- else :
1119- req_str = f"{ name } { extras_str } @ { req_str } { subdirectory } "
1120- elif "path" in _pipfile :
1121- req_str = file_path_from_pipfile (_pipfile ["path" ], _pipfile )
1122- elif "file" in _pipfile :
1123- req_str = file_path_from_pipfile (_pipfile ["file" ], _pipfile )
1124- else :
1125- # We ensure version contains an operator. Default to equals (==)
1126- _pipfile ["version" ] = version = get_version (pipfile )
1127- if version and not is_star (version ) and COMPARE_OP .match (version ) is None :
1128- _pipfile ["version" ] = f"=={ version } "
1129- if is_star (version ) or version == "==*" :
1130- version = ""
1131- req_str = f"{ name } { extras_str } { version } "
1167+ try :
1168+ vcs_url = _pipfile [vcs ]
1169+ subdirectory = _pipfile .get ("subdirectory" , "" )
1170+ if subdirectory :
1171+ subdirectory = f"#subdirectory={ subdirectory } "
1172+
1173+ # Process VCS URL with environment variable handling
1174+ vcs_url , fallback_ref = normalize_vcs_url (vcs_url )
1175+ ref = _pipfile .get ("ref" , fallback_ref )
1176+
1177+ # Construct requirement string
1178+ req_str = f"{ vcs_url } @{ ref } { extras_str } "
1179+ if not req_str .startswith (f"{ vcs } +" ):
1180+ req_str = f"{ vcs } +{ req_str } "
1181+
1182+ if _pipfile .get ("editable" , False ):
1183+ req_str = f"-e { name } { extras_str } @ { req_str } { subdirectory } "
1184+ else :
1185+ req_str = f"{ name } { extras_str } @ { req_str } { subdirectory } "
11321186
1133- # Handle markers before constructing InstallRequirement
1134- markers = PipenvMarkers .from_pipfile (name , _pipfile )
1135- if markers :
1136- req_str = f"{ req_str } ;{ markers } "
1187+ except PipenvUsageError as e :
1188+ raise PipenvUsageError (
1189+ f"Error processing VCS URL for requirement '{ name } ': { str (e )} "
1190+ )
1191+ else :
1192+ # Handle non-VCS requirements (unchanged)
1193+ req_str = handle_non_vcs_requirement (name , _pipfile , extras_str )
11371194
1195+ # Create InstallRequirement
11381196 install_req , _ = expansive_install_req_from_line (
11391197 req_str ,
11401198 comes_from = None ,
@@ -1144,10 +1202,33 @@ def install_req_from_pipfile(name, pipfile):
11441202 constraint = False ,
11451203 expand_env = True ,
11461204 )
1205+
11471206 markers = PipenvMarkers .from_pipfile (name , _pipfile )
11481207 return install_req , markers , req_str
11491208
11501209
1210+ def handle_non_vcs_requirement (
1211+ name : str , _pipfile : Dict [str , Any ], extras_str : str
1212+ ) -> str :
1213+ """Helper function to handle non-VCS requirements."""
1214+ if "path" in _pipfile :
1215+ return file_path_from_pipfile (_pipfile ["path" ], _pipfile )
1216+ elif "file" in _pipfile :
1217+ return file_path_from_pipfile (_pipfile ["file" ], _pipfile )
1218+ else :
1219+ version = get_version (_pipfile )
1220+ if version and not is_star (version ) and COMPARE_OP .match (version ) is None :
1221+ version = f"=={ version } "
1222+ if is_star (version ) or version == "==*" :
1223+ version = ""
1224+
1225+ req_str = f"{ name } { extras_str } { version } "
1226+ markers = PipenvMarkers .from_pipfile (name , _pipfile )
1227+ if markers :
1228+ req_str = f"{ req_str } ;{ markers } "
1229+ return req_str
1230+
1231+
11511232def from_pipfile (name , pipfile ):
11521233 install_req , markers , req_str = install_req_from_pipfile (name , pipfile )
11531234 if markers :
0 commit comments