@@ -283,7 +283,6 @@ impl VirtualEnvironment {
283
283
Arc :: new ( pyvenv_cfg_path) ,
284
284
Some ( range) ,
285
285
) ) ;
286
-
287
286
Some ( PythonVersionWithSource { version, source } )
288
287
} ) ;
289
288
@@ -382,6 +381,7 @@ System site-packages will not be used for module resolution.",
382
381
/// See also: <https://snarky.ca/how-virtual-environments-work/>
383
382
#[ derive( Debug ) ]
384
383
struct PyvenvCfgParser < ' s > {
384
+ source : & ' s str ,
385
385
cursor : Cursor < ' s > ,
386
386
line_number : NonZeroUsize ,
387
387
data : RawPyvenvCfg < ' s > ,
@@ -390,6 +390,7 @@ struct PyvenvCfgParser<'s> {
390
390
impl < ' s > PyvenvCfgParser < ' s > {
391
391
fn new ( source : & ' s str ) -> Self {
392
392
Self {
393
+ source,
393
394
cursor : Cursor :: new ( source) ,
394
395
line_number : NonZeroUsize :: new ( 1 ) . unwrap ( ) ,
395
396
data : RawPyvenvCfg :: default ( ) ,
@@ -409,6 +410,7 @@ impl<'s> PyvenvCfgParser<'s> {
409
410
/// to the beginning of the next line.
410
411
fn parse_line ( & mut self ) -> Result < ( ) , PyvenvCfgParseErrorKind > {
411
412
let PyvenvCfgParser {
413
+ source,
412
414
cursor,
413
415
line_number,
414
416
data,
@@ -418,35 +420,24 @@ impl<'s> PyvenvCfgParser<'s> {
418
420
419
421
cursor. eat_while ( |c| c. is_whitespace ( ) && c != '\n' ) ;
420
422
421
- let remaining_file = cursor. chars ( ) . as_str ( ) ;
422
-
423
- let next_newline_position = remaining_file
424
- . find ( '\n' )
425
- . unwrap_or ( cursor. text_len ( ) . to_usize ( ) ) ;
423
+ let key_start = cursor. offset ( ) ;
424
+ cursor. eat_while ( |c| !matches ! ( c, '\n' | '=' ) ) ;
425
+ let key_end = cursor. offset ( ) ;
426
426
427
- // The Python standard-library's `site` module parses these files by splitting each line on
428
- // '=' characters, so that's what we should do as well.
429
- let Some ( eq_position) = remaining_file[ ..next_newline_position] . find ( '=' ) else {
427
+ if !cursor. eat_char ( '=' ) {
430
428
// Skip over any lines that do not contain '=' characters, same as the CPython stdlib
431
429
// <https://github.com/python/cpython/blob/e64395e8eb8d3a9e35e3e534e87d427ff27ab0a5/Lib/site.py#L625-L632>
432
- cursor. skip_bytes ( next_newline_position) ;
433
-
434
- debug_assert ! (
435
- matches!( cursor. first( ) , '\n' | ruff_python_trivia:: EOF_CHAR , ) ,
436
- "{}" ,
437
- cursor. first( )
438
- ) ;
439
-
440
430
cursor. eat_char ( '\n' ) ;
441
431
return Ok ( ( ) ) ;
442
- } ;
432
+ }
443
433
444
- let key = remaining_file [ ..eq_position ] . trim ( ) ;
434
+ let key = source [ TextRange :: new ( key_start , key_end ) ] . trim ( ) ;
445
435
446
- cursor. skip_bytes ( eq_position + 1 ) ;
447
436
cursor. eat_while ( |c| c. is_whitespace ( ) && c != '\n' ) ;
448
-
449
- let value = remaining_file[ eq_position + 1 ..next_newline_position] . trim ( ) ;
437
+ let value_start = cursor. offset ( ) ;
438
+ cursor. eat_while ( |c| c != '\n' ) ;
439
+ let value = source[ TextRange :: new ( value_start, cursor. offset ( ) ) ] . trim ( ) ;
440
+ cursor. eat_char ( '\n' ) ;
450
441
451
442
if value. is_empty ( ) {
452
443
return Err ( PyvenvCfgParseErrorKind :: MalformedKeyValuePair { line_number } ) ;
@@ -460,7 +451,7 @@ impl<'s> PyvenvCfgParser<'s> {
460
451
// `virtualenv` and `uv` call this key `version_info`,
461
452
// but the stdlib venv module calls it `version`
462
453
"version" | "version_info" => {
463
- let version_range = TextRange :: at ( cursor . offset ( ) , value. text_len ( ) ) ;
454
+ let version_range = TextRange :: at ( value_start , value. text_len ( ) ) ;
464
455
data. version = Some ( ( value, version_range) ) ;
465
456
}
466
457
"implementation" => {
@@ -479,8 +470,6 @@ impl<'s> PyvenvCfgParser<'s> {
479
470
_ => { }
480
471
}
481
472
482
- cursor. eat_while ( |c| c != '\n' ) ;
483
- cursor. eat_char ( '\n' ) ;
484
473
Ok ( ( ) )
485
474
}
486
475
}
@@ -1581,4 +1570,18 @@ mod tests {
1581
1570
assert_eq ! ( & pyvenv_cfg[ version. 1 ] , version. 0 ) ;
1582
1571
assert_eq ! ( parsed. implementation, PythonImplementation :: PyPy ) ;
1583
1572
}
1573
+
1574
+ #[ test]
1575
+ fn pyvenv_cfg_with_strange_whitespace_parses ( ) {
1576
+ let pyvenv_cfg = " home= /a path with whitespace/python\t \t \n version_info = 3.13 \n \n \n \n implementation =PyPy" ;
1577
+ let parsed = PyvenvCfgParser :: new ( pyvenv_cfg) . parse ( ) . unwrap ( ) ;
1578
+ assert_eq ! (
1579
+ parsed. base_executable_home_path,
1580
+ Some ( "/a path with whitespace/python" )
1581
+ ) ;
1582
+ let version = parsed. version . unwrap ( ) ;
1583
+ assert_eq ! ( version. 0 , "3.13" ) ;
1584
+ assert_eq ! ( & pyvenv_cfg[ version. 1 ] , version. 0 ) ;
1585
+ assert_eq ! ( parsed. implementation, PythonImplementation :: PyPy ) ;
1586
+ }
1584
1587
}
0 commit comments