Poor man's f-strings in Python 2.6
The thing most likely, in my mind, to cause problems here is that using frames is not guaranteed to work in any other Python implementation besides CPython.

inspect.currentframe()
Return the frame object for the caller’s stack frame.
CPython implementation detail: This function relies on Python stack frame support in the interpreter, which isn’t guaranteed to exist in all implementations of Python. If running in an implementation without Python stack frame support this function returns None.

This will end up throwing an error if you try to unpack None because the implementation doesn't support your function. If you were dead-set on using stack inspection, then you should give a more useful error message than AttributeError: 'NoneType' object has no attribute 'f_back'. You should test for if you get a frame or not, and then use platform to print a more helpful error message.
def f(s):
    frame = inspect.currentframe()
    if frame is not None:
        return s.format(**frame.f_back.f_locals)
    else:
        raise NotImplementedError(
            &quot;Python implementation {} does not support stack inspection.&quot;.format(
                platform.python_implementation()
            )
        )

At this point, then, you should make sure this is a well-documented limitation of your library, and I would probably write some unit tests where it asserts that is raised for certain implementations.
The real problem, however, isn't that you're using something that isn't guaranteed (although that's certainly a problem). The real problem is that stack inspection is almost never the right answer. We don't need it here either.  What we're going to want to do is something like this:
def _poor_fstring(namespace):
    def _():
        return namespace
    return _

get_namespace = _poor_fstring(locals())

def f(s):
    return s.format(**get_namespace())

This still isn't ideal - you'll need to redeclare get_namespace in any scope you want access to it - but you avoid stack inspection, which is worth it in my mind.
Really though, I don't see a huge benefit between using your function and just calling s.format(**namespace), where namespace is whatever lookup table you want.  The one benefit would be if, as you said, it supported arbitrary expressions. And the only way to get arbitrary expressions is to, well, evaluate them.  You'll basically have to parse the string and extract everything between curly braces. Then try a lookup in the namespace and replace that, otherwise compile it, and then eval what you find. Hopefully I don't need to point out that evaling random strings is... dangerous.
Going into actually parsing a format string is way beyond the scope of this question.
In short, my recommendation would be to just suffer through... or upgrade your Python version.