Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

bug: `{{ var }}` with inner whitespace is neither substituted nor reported as unresolved

Open Beginner friendly
#48 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
80/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
cli

Research direction

Start in git_api/cli.py by comparing _interpolate() with _unresolved_placeholders(); the issue identifies their differing placeholder grammars. Check the existing tests for interpolation and unresolved-variable warnings, then add coverage for the spaced placeholder with a defined variable. Done when it is substituted or reported as unresolved, never silently passed through; also consider the replay path noted in the issue.

Written by the indexing model from the issue text.

Description

bug

Summary

The placeholder finder and the placeholder substituter disagree on the syntax. _unresolved_placeholders() accepts whitespace inside the braces ({{ token }}), but _interpolate() only matches the bare form ({{token}}). So a variable that is defined, written as {{ token }}, is neither substituted nor reported as missing — the request goes out with the literal placeholder still in it, silently.

The warning added in 0.1.0 (CHANGELOG.md: "a request that still contains an unresolved {{...}} after substitution now says so instead of sending the placeholder silently") does not fire in exactly the case where the variable exists, so the guarantee it advertises does not hold.

Steps to reproduce

mkdir /tmp/gapi-demo && cd /tmp/gapi-demo
git init -q
git-api

Inside the REPL:

var token=s3cretvalue
var base_url=http://127.0.0.1:8000
GET "/x?t={{ token }}"

The URL must be quoted, otherwise shlex.split splits it on the space before any of this code runs.

Actual output

Two code paths, two different definitions of a placeholder:

  text            : '/x?t={{token}}'
  after interpolate: '/x?t=s3cretvalue'
  warned as missing: []
  -> substituted?  True

  text            : '/x?t={{ token }}'
  after interpolate: '/x?t={{ token }}'
  warned as missing: []
  -> substituted?  False
  -> warned?        False

  text            : '/x?t={{  token  }}'
  after interpolate: '/x?t={{  token  }}'
  warned as missing: []
  -> substituted?  False
  -> warned?        False

So '{{ token }}' is defined-but-never-substituted, and nothing warns:
  (no warning printed above)

For contrast, an unknown variable is reported correctly in both spellings:

  '/x?t={{nope}}' -> missing=['nope'] (warns fine)
  '/x?t={{ nope }}' -> missing=['nope'] (warns fine)

End-to-end through the real REPL, showing the placeholder shipped to the server while the warning system stays quiet:

🌿  Git-API REPL 0.1.0
    Type 'help' for commands, 'quit' to exit.

default> Set base_url = http://127.0.0.1:44349
default> Set token = ghp_ab...art2
default>
→  GET http://127.0.0.1:44349/whoami?t={{
←  200
{
  "ok": true
}

  (use 'save get--whoami?t={{' to save)
default> Bye.
========== SERVER RECEIVED ==========
[
  {
    "method": "GET",
    "path": "/whoami?t={{",
    "body": "token"
  }
]

The server received /whoami?t={{ with token as the body — the literal placeholder went over the wire, and no ⚠️ Unresolved variable(s) line was printed.

(That run also shows the unquoted form of the same input splitting on whitespace: the URL becomes /whoami?t={{ and token }} becomes a request body. That is the separate shlex.split truncation defect, not this one.)

Expected output

One of two consistent behaviours, both acceptable:

  1. Whitespace-insensitive substitution — {{ token }} resolves to s3cretvalue, matching what the finder already accepts; or
  2. Whitespace-sensitive, and then it must warn — {{ token }} is not a valid placeholder, so it should be reported as unresolved rather than passing through.

What must not happen is the current state: not substituted and not reported. The GET above either resolves the variable or says it could not.

Root cause

git_api/cli.py:92-101 vs git_api/cli.py:115. The two functions use different grammars for the same construct.

Substitution does a literal string replace on the exact key spelling, with no whitespace tolerance:

def _interpolate(text: str, env: dict) -> str:
    for key, value in env.items():
        if isinstance(value, str):
            text = text.replace(f"{{{{{key}}}}}", value)   # only "{{token}}"
    return text

Detection uses a regex that explicitly allows whitespace, then strips it:

found = re.findall(r"\{\{\s*([^{}]+?)\s*\}\}", text)   # also matches "{{ token }}"
...
name = name.strip()                                     # -> "token"
if name and name not in seen and not isinstance(env.get(name), str):
    seen.append(name)

The final guard is the trap: _unresolved_placeholders only reports names that are absent from env. For {{ token }} with token defined, the name resolves fine, so the function reports nothing — the caller concludes the text is clean. But _interpolate never touched it, because it only replaces {{token}}. The check that was added specifically to catch leftover placeholders is blind to every placeholder whose variable happens to exist.

Net effect: {{ token }} with token defined is the only combination that is both unsubstituted and unreported. Every other combination either works or warns.

Suggested fix

Make the substituter regex-based so it accepts the same grammar as the finder, and use one compiled pattern for both:

--- a/git_api/cli.py
+++ b/git_api/cli.py
@@
+import re
+
+PLACEHOLDER = re.compile(r"\{\{\s*([^{}]+?)\s*\}\}")
+
+
 def _interpolate(text: str, env: dict) -> str:
     """Replace every ``{{var}}`` in *text* with the matching string in *env*.
@@
-    for key, value in env.items():
-        if isinstance(value, str):
-            text = text.replace(f"{{{{{key}}}}}", value)
-    return text
+    def _sub(m: re.Match[str]) -> str:
+        value = env.get(m.group(1).strip())
+        return value if isinstance(value, str) else m.group(0)
+
+    return PLACEHOLDER.sub(_sub, text)
@@
-    found = re.findall(r"\{\{\s*([^{}]+?)\s*\}\}", text)
+    found = PLACEHOLDER.findall(text)

Returning m.group(0) for a non-string or missing value leaves the placeholder in place, which is what makes the existing _unresolved_placeholders warning fire correctly.

Worth a test for each of the three spellings in the table above, asserting that a defined variable is either substituted or reported — never silently passed through.

Related

  • Interacts with the shlex.split truncation defect: an unquoted {{ token }} is split before these functions see it, which is why the reproduction has to quote the URL.
  • Also reachable via replay, which routes saved URLs through the same _interpolate at cli.py:427 — a request saved with a spaced placeholder replays the same broken URL.
  • CHANGELOG.md 0.1.0 "Fixed" claims unresolved placeholders now say so; this is the case where that claim does not hold.
Dominant language
Python
Stars
0
Forks
1
Avg merge
37m
Merged PRs (30d)
9

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from yunaremaia/git-api

All issues in yunaremaia/git-api

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.