Repository: xonsh/xonsh

Issue:
Add pf-strings
Add support for pf-strings. That is, an f-string that is cast to a `Path`.


Discussion:
Could you post a link to a description of those 'pf-strings'?
Hey @t184256 -- they don't really exist.  We have p-strings in xonsh, that work like:

```
 ~ 🐚  p'/home/gil/xonsh'
PosixPath('/home/gil/xonsh')
```

So a pf-string might be something like
```
 ~ 🐚  pf'{HOME}/xonsh'
PosixPath('/home/gil/xonsh')
```
I want to mention that the evaluation of env variables inside of p-strings is already supported. Just the literal ones, the `${...}` isn't supported.
```
$ p'$HOME'
PosixPath('/home/arch')
```
Seems like this need some additions [here](https://github.com/xonsh/xonsh/blob/fd448e515a36cf23a4f03f4e6b16c9dd227196b7/xonsh/parsers/base.py#L2322-L2335).
It might also need some additions to the completer as well.
@MrQubo - yeah, it probably does :)

Resolving patch:
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
--- a/xonsh/parsers/base.py
+++ b/xonsh/parsers/base.py
@@ -2422,8 +2422,18 @@ def p_nocloser_many(self, p):
     def p_string_literal(self, p):
         """string_literal : string_tok"""
         p1 = p[1]
-        prefix = RE_STRINGPREFIX.match(p1.value).group()
-        if "p" in prefix:
+        prefix = RE_STRINGPREFIX.match(p1.value).group().lower()
+        if "p" in prefix and "f" in prefix:
+            new_pref = prefix.replace("p", "")
+            value_without_p = new_pref + p1.value[len(prefix):]
+            s = eval_fstr_fields(value_without_p, new_pref, filename=self.lexer.fname)
+            s = pyparse(s).body[0].value
+            s = ast.increment_lineno(s, p1.lineno - 1)
+            p[0] = xonsh_call(
+                "__xonsh__.path_literal", [s], lineno=p1.lineno, col=p1.lexpos
+            )
+
+        elif "p" in prefix:
             value_without_p = prefix.replace("p", "") + p1.value[len(prefix) :]
             s = ast.Str(
                 s=ast.literal_eval(value_without_p),
@@ -2433,14 +2443,14 @@ def p_string_literal(self, p):
             p[0] = xonsh_call(
                 "__xonsh__.path_literal", [s], lineno=p1.lineno, col=p1.lexpos
             )
-        elif "f" in prefix or "F" in prefix:
+        elif "f" in prefix:
             s = eval_fstr_fields(p1.value, prefix, filename=self.lexer.fname)
             s = pyparse(s).body[0].value
             s = ast.increment_lineno(s, p1.lineno - 1)
             p[0] = s
         else:
             s = ast.literal_eval(p1.value)
-            is_bytes = "b" in prefix or "B" in prefix
+            is_bytes = "b" in prefix
             cls = ast.Bytes if is_bytes else ast.Str
             p[0] = cls(s=s, lineno=p1.lineno, col_offset=p1.lexpos)
 
diff --git a/xonsh/tokenize.py b/xonsh/tokenize.py
--- a/xonsh/tokenize.py
+++ b/xonsh/tokenize.py
@@ -273,7 +273,7 @@ def maybe(*choices):
 Imagnumber = group(r"[0-9](?:_?[0-9])*[jJ]", Floatnumber + r"[jJ]")
 Number = group(Imagnumber, Floatnumber, Intnumber)
 
-StringPrefix = r"(?:[bBp][rR]?|[rR][bBpfF]?|[uU]|[fF][rR]?)?"
+StringPrefix = r"(?:[bB][rR]?|[p][fFrR]?|[rR][bBpfF]?|[uU]|[fF][rR]?[p]?)?"
 
 # Tail end of ' string.
 Single = r"[^'\\]*(?:\\.[^'\\]*)*'"
@@ -394,6 +394,8 @@ def _compile(expr):
     'br"""': Double3,
     "fr'''": Single3,
     'fr"""': Double3,
+    "fp'''": Single3,
+    'fp"""': Double3,
     "bR'''": Single3,
     'bR"""': Double3,
     "Br'''": Single3,
@@ -408,6 +410,8 @@ def _compile(expr):
     'Rb"""': Double3,
     "Fr'''": Single3,
     'Fr"""': Double3,
+    "Fp'''": Single3,
+    'Fp"""': Double3,
     "rB'''": Single3,
     'rB"""': Double3,
     "rF'''": Single3,
@@ -424,6 +428,10 @@ def _compile(expr):
     'p"""': Double3,
     "pr'''": Single3,
     'pr"""': Double3,
+    "pf'''": Single3,
+    'pf"""': Double3,
+    "pF'''": Single3,
+    'pF"""': Double3,
     "pR'''": Single3,
     'pR"""': Double3,
     "rp'''": Single3,
@@ -503,6 +511,14 @@ def _compile(expr):
     'rp""""',
     "Rp'''",
     'Rp""""',
+    "pf'''",
+    'pf""""',
+    "pF'''",
+    'pF""""',
+    "fp'''",
+    'fp""""',
+    "Fp'''",
+    'Fp""""',
 ):
     triple_quoted[t] = t
 single_quoted = {}
@@ -567,6 +583,14 @@ def _compile(expr):
     'rp"',
     "Rp'",
     'Rp"',
+    "pf'",
+    'pf"',
+    "pF'",
+    'pF"',
+    "fp'",
+    'fp"',
+    "Fp'",
+    'Fp"',
 ):
     single_quoted[t] = t
 


Test patch:
diff --git a/tests/test_lexer.py b/tests/test_lexer.py
--- a/tests/test_lexer.py
+++ b/tests/test_lexer.py
@@ -384,6 +384,17 @@ def test_path_string_literal():
     assert check_token('rp"/foo"', ["STRING", 'rp"/foo"', 0])
 
 
+def test_path_fstring_literal():
+    assert check_token("pf'/foo'", ["STRING", "pf'/foo'", 0])
+    assert check_token('pf"/foo"', ["STRING", 'pf"/foo"', 0])
+    assert check_token("fp'/foo'", ["STRING", "fp'/foo'", 0])
+    assert check_token('fp"/foo"', ["STRING", 'fp"/foo"', 0])
+    assert check_token("pF'/foo'", ["STRING", "pF'/foo'", 0])
+    assert check_token('pF"/foo"', ["STRING", 'pF"/foo"', 0])
+    assert check_token("Fp'/foo'", ["STRING", "Fp'/foo'", 0])
+    assert check_token('Fp"/foo"', ["STRING", 'Fp"/foo"', 0])
+
+
 def test_regex_globs():
     for i in (".*", r"\d*", ".*#{1,2}"):
         for p in ("", "r", "g", "@somethingelse", "p", "pg"):
diff --git a/tests/test_parser.py b/tests/test_parser.py
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -2042,6 +2042,17 @@ def test_path_literal():
     check_xonsh_ast({}, 'Rp"/foo"', False)
 
 
+def test_path_fstring_literal():
+    check_xonsh_ast({}, 'pf"/foo"', False)
+    check_xonsh_ast({}, 'fp"/foo"', False)
+    check_xonsh_ast({}, 'pF"/foo"', False)
+    check_xonsh_ast({}, 'Fp"/foo"', False)
+    check_xonsh_ast({}, 'pf"/foo{1+1}"', False)
+    check_xonsh_ast({}, 'fp"/foo{1+1}"', False)
+    check_xonsh_ast({}, 'pF"/foo{1+1}"', False)
+    check_xonsh_ast({}, 'Fp"/foo{1+1}"', False)
+
+
 def test_dollar_name():
     check_xonsh_ast({"WAKKA": 42}, "$WAKKA")