20
20
import toml
21
21
from packaging import requirements
22
22
from packaging .utils import canonicalize_name
23
+ from py ._path .common import PathBase
23
24
24
25
import tox
25
26
from tox .constants import INFO
@@ -163,6 +164,8 @@ def postprocess(self, testenv_config, value):
163
164
deps = []
164
165
config = testenv_config .config
165
166
for depline in value :
167
+ if isinstance (depline , PathBase ):
168
+ depline = str (depline )
166
169
m = re .match (r":(\w+):\s*(\S+)" , depline )
167
170
if m :
168
171
iname , name = m .groups ()
@@ -416,7 +419,7 @@ def export(self):
416
419
# such as {} being escaped using \{\}, suitable for use with
417
420
# os.environ .
418
421
return {
419
- name : Replacer ._unescape (value )
422
+ name : str ( value ) if isinstance ( value , PathBase ) else Replacer ._unescape (value )
420
423
for name , value in self .items ()
421
424
if value is not self ._DUMMY
422
425
}
@@ -1151,6 +1154,8 @@ def line_of_default_to_zero(section, name=None):
1151
1154
hash_seed = config .option .hashseed
1152
1155
config .hashseed = hash_seed
1153
1156
1157
+ config .literal_paths = reader .getbool ("literal_paths" , True )
1158
+
1154
1159
reader .addsubstitutions (toxinidir = config .toxinidir , homedir = config .homedir )
1155
1160
1156
1161
if config .option .workdir is None :
@@ -1364,7 +1369,13 @@ def _list_section_factors(self, section):
1364
1369
1365
1370
def make_envconfig (self , name , section , subs , config , replace = True ):
1366
1371
factors = set (name .split ("-" ))
1367
- reader = SectionReader (section , self ._cfg , fallbacksections = ["testenv" ], factors = factors )
1372
+ reader = SectionReader (
1373
+ section ,
1374
+ self ._cfg ,
1375
+ fallbacksections = ["testenv" ],
1376
+ factors = factors ,
1377
+ literal_paths = config .literal_paths ,
1378
+ )
1368
1379
tc = TestenvConfig (name , config , factors , reader )
1369
1380
reader .addsubstitutions (
1370
1381
envname = name ,
@@ -1578,6 +1589,7 @@ def __init__(
1578
1589
factors = (),
1579
1590
prefix = None ,
1580
1591
posargs = "" ,
1592
+ literal_paths = True ,
1581
1593
):
1582
1594
if prefix is None :
1583
1595
self .section_name = section_name
@@ -1590,6 +1602,7 @@ def __init__(
1590
1602
self ._subststack = []
1591
1603
self ._setenv = None
1592
1604
self .posargs = posargs
1605
+ self .literal_paths = literal_paths
1593
1606
1594
1607
def get_environ_value (self , name ):
1595
1608
if self ._setenv is None :
@@ -1602,7 +1615,7 @@ def addsubstitutions(self, _posargs=None, **kw):
1602
1615
self .posargs = _posargs
1603
1616
1604
1617
def getpath (self , name , defaultpath , replace = True ):
1605
- path = self .getstring (name , defaultpath , replace = replace )
1618
+ path = self .getstring (name , defaultpath , replace = replace , is_path = True )
1606
1619
if path is not None :
1607
1620
toxinidir = self ._subs ["toxinidir" ]
1608
1621
return toxinidir .join (path , abs = True )
@@ -1611,6 +1624,8 @@ def getlist(self, name, sep="\n"):
1611
1624
s = self .getstring (name , None )
1612
1625
if s is None :
1613
1626
return []
1627
+ if isinstance (s , PathBase ):
1628
+ return [s ]
1614
1629
return [x .strip () for x in s .split (sep ) if x .strip ()]
1615
1630
1616
1631
def getdict (self , name , default = None , sep = "\n " , replace = True ):
@@ -1698,7 +1713,15 @@ def getargv_install_command(self, name, default="", replace=True):
1698
1713
1699
1714
return _ArgvlistReader .getargvlist (self , s , replace = replace )[0 ]
1700
1715
1701
- def getstring (self , name , default = None , replace = True , crossonly = False , no_fallback = False ):
1716
+ def getstring (
1717
+ self ,
1718
+ name ,
1719
+ default = None ,
1720
+ replace = True ,
1721
+ crossonly = False ,
1722
+ no_fallback = False ,
1723
+ is_path = False ,
1724
+ ):
1702
1725
x = None
1703
1726
sections = [self .section_name ] + ([] if no_fallback else self .fallbacksections )
1704
1727
for s in sections :
@@ -1716,10 +1739,16 @@ def getstring(self, name, default=None, replace=True, crossonly=False, no_fallba
1716
1739
# process. Once they are unwrapped, we call apply factors
1717
1740
# again for those new dependencies.
1718
1741
x = self ._apply_factors (x )
1719
- x = self ._replace_if_needed (x , name , replace , crossonly )
1742
+ x = self ._replace_if_needed (x , name , replace , crossonly , is_path = is_path )
1743
+ if isinstance (x , PathBase ):
1744
+ return x
1720
1745
x = self ._apply_factors (x )
1721
1746
1722
- x = self ._replace_if_needed (x , name , replace , crossonly )
1747
+ if isinstance (x , PathBase ):
1748
+ raise RuntimeError (name )
1749
+ return x
1750
+
1751
+ x = self ._replace_if_needed (x , name , replace , crossonly , is_path = is_path )
1723
1752
return x
1724
1753
1725
1754
def getposargs (self , default = None ):
@@ -1733,9 +1762,9 @@ def getposargs(self, default=None):
1733
1762
else :
1734
1763
return default or ""
1735
1764
1736
- def _replace_if_needed (self , x , name , replace , crossonly ):
1765
+ def _replace_if_needed (self , x , name , replace , crossonly , is_path = False ):
1737
1766
if replace and x and hasattr (x , "replace" ):
1738
- x = self ._replace (x , name = name , crossonly = crossonly )
1767
+ x = self ._replace (x , name = name , crossonly = crossonly , is_path = is_path )
1739
1768
return x
1740
1769
1741
1770
def _apply_factors (self , s ):
@@ -1754,14 +1783,16 @@ def factor_line(line):
1754
1783
lines = s .strip ().splitlines ()
1755
1784
return "\n " .join (filter (None , map (factor_line , lines )))
1756
1785
1757
- def _replace (self , value , name = None , section_name = None , crossonly = False ):
1786
+ def _replace (self , value , name = None , section_name = None , crossonly = False , is_path = False ):
1758
1787
if "{" not in value :
1759
1788
return value
1760
1789
1761
1790
section_name = section_name if section_name else self .section_name
1762
1791
self ._subststack .append ((section_name , name ))
1763
1792
try :
1764
- replaced = Replacer (self , crossonly = crossonly ).do_replace (value )
1793
+ replaced = Replacer (self , crossonly = crossonly ).do_replace (
1794
+ value , is_path = is_path , literal_paths = self .literal_paths ,
1795
+ )
1765
1796
assert self ._subststack .pop () == (section_name , name )
1766
1797
except tox .exception .MissingSubstitution :
1767
1798
if not section_name .startswith (testenvprefix ):
@@ -1789,19 +1820,41 @@ def __init__(self, reader, crossonly=False):
1789
1820
self .reader = reader
1790
1821
self .crossonly = crossonly
1791
1822
1792
- def do_replace (self , value ):
1823
+ def do_replace (self , value , is_path = False , literal_paths = True ):
1793
1824
"""
1794
1825
Recursively expand substitutions starting from the innermost expression
1795
1826
"""
1796
1827
1797
- def substitute_once (x ):
1798
- return self .RE_ITEM_REF .sub (self ._replace_match , x )
1799
-
1800
- expanded = substitute_once (value )
1828
+ def substitute_each (s ):
1829
+ parts = []
1830
+ pos = 0
1831
+ for match in self .RE_ITEM_REF .finditer (s ):
1832
+ start = match .start ()
1833
+ if start :
1834
+ parts .append (s [pos :start ])
1835
+ parts .append (self ._replace_match (match ))
1836
+ pos = match .end ()
1837
+
1838
+ tail = s [pos :]
1839
+ if tail :
1840
+ parts .append (tail )
1841
+
1842
+ if not parts :
1843
+ return ""
1844
+ if (literal_paths or is_path ) and isinstance (parts [0 ], PathBase ):
1845
+ if len (parts ) == 1 :
1846
+ return parts [0 ]
1847
+ return parts [0 ].join (* parts [1 :])
1848
+
1849
+ return "" .join (str (part ) for part in parts )
1850
+
1851
+ expanded = substitute_each (value )
1852
+ if isinstance (expanded , PathBase ):
1853
+ return expanded
1801
1854
1802
1855
while expanded != value : # substitution found
1803
1856
value = expanded
1804
- expanded = substitute_once (value )
1857
+ expanded = substitute_each (value )
1805
1858
1806
1859
return expanded
1807
1860
@@ -1888,6 +1941,8 @@ def _replace_substitution(self, match):
1888
1941
val = self ._substitute_from_other_section (sub_key )
1889
1942
if callable (val ):
1890
1943
val = val ()
1944
+ if isinstance (val , PathBase ):
1945
+ return val
1891
1946
return str (val )
1892
1947
1893
1948
@@ -1949,8 +2004,12 @@ def processcommand(cls, reader, command, replace=True):
1949
2004
1950
2005
new_arg = ""
1951
2006
new_word = reader ._replace (word )
1952
- new_word = reader ._replace (new_word )
1953
- new_word = Replacer ._unescape (new_word )
2007
+ if not isinstance (new_word , PathBase ):
2008
+ new_word = reader ._replace (new_word )
2009
+ if not isinstance (new_word , PathBase ):
2010
+ new_word = Replacer ._unescape (new_word )
2011
+ if isinstance (new_word , PathBase ):
2012
+ new_word = str (new_word )
1954
2013
new_arg += new_word
1955
2014
newcommand += new_arg
1956
2015
else :
0 commit comments