Anbarryprojects / fastapi-babel

FastAPI babel support pybable tools like translation of text, formatting of dates, times and numbers, and time zones.
MIT License
46 stars 14 forks source link

LookupError in core.py after latest update (0.0.9) #36

Open StikyFingaz opened 4 months ago

StikyFingaz commented 4 months ago

Hi,

I just want to say thank you for your work. I use fastapi-babel and it has been working fine so far.

After the latest update I get this:

.../fastapi_babel/core.py", line 123, in _
    gettext = _context_var.get()
              ^^^^^^^^^^^^^^^^^^
LookupError: <ContextVar name='gettext' at 0x7f4b6dc20e00>

Here is my config file:

from pathlib import Path
from fastapi_babel import Babel, BabelConfigs

LANGUAGES = ['en', 'bg']

translations_dir = Path(__file__).parent.resolve() / 'translations'
configs = BabelConfigs(
    ROOT_DIR=__file__,
    BABEL_DEFAULT_LOCALE="en",
    BABEL_TRANSLATION_DIRECTORY=str(translations_dir),
)

babel = Babel(configs=configs)

if __name__ == "__main__":
    babel.run_cli()

Please note that I made a dependency to set the locale globally because using fastapi-babel as middleware was breaking my app in other ways.

async def get_locale(
        accept_language: Annotated[str | None, Header()] = None,
        x_user_locale: Annotated[str | None, Header()] = None):

    babel.locale = (
            x_user_locale or
            LanguageAccept(parse_accept_header(accept_language)).best_match(LANGUAGES) or
            'bg'
    )
Legopapurida commented 4 months ago

Hello I think you have missed adding the babel to the Fastapi middleware. please have a look at the examples.

from fastapi_babel import Babel, BabelConfigs, BabelMiddleware
from fastapi_babel.core import _

app = FastAPI()
babel_configs = BabelConfigs(
    ROOT_DIR=__file__,
    BABEL_DEFAULT_LOCALE="en",
    BABEL_TRANSLATION_DIRECTORY="lang",
)

app.add_middleware(
    BabelMiddleware, babel_configs=babel_configs
)

@app.get("/")
async def index():
    return {"text": _("Hello World")}

also, you can use the depends

from fastapi_babel import Babel, BabelConfigs, BabelMiddleware
from fastapi_babel.core import make_gettext

app = FastAPI()
babel_configs = BabelConfigs(
    ROOT_DIR=__file__,
    BABEL_DEFAULT_LOCALE="en",
    BABEL_TRANSLATION_DIRECTORY="lang",
)

app.add_middleware(
    BabelMiddleware, babel_configs=babel_configs
)

@app.get("/")
async def index(_: Annotated[Callable[[str], str], Depends(make_gettext)]):
    return {"text": _("Hello World")}
StikyFingaz commented 4 months ago

It seems that a minimal app works as expected as shown in your examples. However, in order to get strings from my API router, I had to use both gettext and make_gettext:

from typing import Annotated, Callable
from fastapi import APIRouter, Depends
from fastapi_babel.core import gettext as _
from fastapi_babel.core import make_gettext

router = APIRouter(
    prefix='/translations',
    tags=['translations'],
    dependencies=[],
    responses={}
)

@router.get('/')
async def fetch_common_strings(_: Annotated[Callable[[str], str], Depends(make_gettext)]):
    translated_strings = {k: _(v) for k, v in common_strings.items()}
    return translated_strings

common_strings = {
    'title': _('My Page Title'),
}

This was not the case in v0.0.8. Is this a reasonable approach?

Legopapurida commented 4 months ago

If you want to use _('WORD') outside of the request context (API scope) you have to use the lazy_gettext.

The best solution is: create the file messages.py and write the code below.

...
from fastapi_babel.core import lazy_gettext as _

common_strings = {
    'title': _('My Page Title'),
}

and use the from fastapi_babel import _ in other files.

Why you should use the lazygettext instead of simple `` outside of the request context? because _ depends on the request object and the income header to change the babel.locale. when you use that outside of the specific scope you must get the Context error.

so the ideal solution is to use the lazy_gettext instead of that.

Note You cannot import the lazy_gettext side by side of the simple gettext. I'd like to refer you to the Django documentation about this topic to get my statements. also, lazygettext never translates any text so we use it only for extracting messages to use throughout the files and APIs using real gettext `()`.

In this example, I've demonstrated the usage of the lazy_gettext for translating wtform input labels. https://github.com/Anbarryprojects/fastapi-babel/blob/main/examples/wtforms/forms.py

StikyFingaz commented 3 months ago

Thank you for your help.

levente-murgas commented 2 weeks ago

Hi!

First of all, thank you for this amazing library! I'd like to reopen this issue because I have a similar problem and I wasn't able to solve it. I'm using Version: 0.0.9.

In my application I use the _() inside a worker function using ThreadPoolExecutor. The error I'm receiving is the same as @StikyFingaz 's:

\fastapi_babel\core.py", line 123, in _
    gettext = _context_var.get()
LookupError: <ContextVar name='gettext' at 0x0000016824CC2B30>

Here's a simplified version of my issue, which still illustrates the problem

from fastapi_babel import Babel, BabelConfigs, BabelMiddleware
from fastapi_babel.core import _
from fastapi import FastAPI, Header
from typing_extensions import Annotated
from typing import Union, Callable

from concurrent.futures import ThreadPoolExecutor, as_completed

app = FastAPI()
babel_configs = BabelConfigs(
    ROOT_DIR=__file__,
    BABEL_DEFAULT_LOCALE="en",
    BABEL_TRANSLATION_DIRECTORY="lang",
)

app.add_middleware(
    BabelMiddleware, babel_configs=babel_configs
)

# Passes, no problemo
@app.get("/test0")
async def func_0(accept_language: Annotated[Union[str, None], Header()] = None):
    return {"text": _("Hello, World!")}

def getHW():
    return _("Hello, World!")

# Still passes, no problemo
@app.get("/test1")
async def func_1(accept_language: Annotated[Union[str, None], Header()] = None):
    return {"text": getHW()}

# LookupError: <ContextVar name='gettext' at 0x0000016824CC2B30> :(
@app.get("/test2")
async def func_2(accept_language: Annotated[Union[str, None], Header()] = None
    ):
    future_to_index = {}
    index_to_result = {}
    with ThreadPoolExecutor() as executor:
        future_to_index = {executor.submit(getHW): i for i in range(10)}
        for future in as_completed(future_to_index):
            index = future_to_index[future]
            res = future.result()
            index_to_result[index] = res
    return {"text": index_to_result}

Do you have any suggestions on how to resolve this issue @Legopapurida ? I read your comment mentioning lazy_gettext, but I was not able to apply it to my case...

StikyFingaz commented 2 weeks ago

Updating to 0.0.9 breaks my app. This happened again with 0.0.8. I think @Legopapurida might be going in circles on this one. I don't have the time to try to find a workaround so I downgraded to 0.0.8. If you don't have problems with that version you might stick with it for now. It works perfectly fine for me.

Legopapurida commented 2 weeks ago

Hi!

First of all, thank you for this amazing library! I'd like to reopen this issue because I have a similar problem and I wasn't able to solve it. I'm using Version: 0.0.9.

In my application I use the _() inside a worker function using ThreadPoolExecutor. The error I'm receiving is the same as @StikyFingaz 's:

\fastapi_babel\core.py", line 123, in _
    gettext = _context_var.get()
LookupError: <ContextVar name='gettext' at 0x0000016824CC2B30>

Here's a simplified version of my issue, which still illustrates the problem

from fastapi_babel import Babel, BabelConfigs, BabelMiddleware
from fastapi_babel.core import _
from fastapi import FastAPI, Header
from typing_extensions import Annotated
from typing import Union, Callable

from concurrent.futures import ThreadPoolExecutor, as_completed

app = FastAPI()
babel_configs = BabelConfigs(
    ROOT_DIR=__file__,
    BABEL_DEFAULT_LOCALE="en",
    BABEL_TRANSLATION_DIRECTORY="lang",
)

app.add_middleware(
    BabelMiddleware, babel_configs=babel_configs
)

# Passes, no problemo
@app.get("/test0")
async def func_0(accept_language: Annotated[Union[str, None], Header()] = None):
    return {"text": _("Hello, World!")}

def getHW():
    return _("Hello, World!")

# Still passes, no problemo
@app.get("/test1")
async def func_1(accept_language: Annotated[Union[str, None], Header()] = None):
    return {"text": getHW()}

# LookupError: <ContextVar name='gettext' at 0x0000016824CC2B30> :(
@app.get("/test2")
async def func_2(accept_language: Annotated[Union[str, None], Header()] = None
    ):
    future_to_index = {}
    index_to_result = {}
    with ThreadPoolExecutor() as executor:
        future_to_index = {executor.submit(getHW): i for i in range(10)}
        for future in as_completed(future_to_index):
            index = future_to_index[future]
            res = future.result()
            index_to_result[index] = res
    return {"text": index_to_result}

Do you have any suggestions on how to resolve this issue @Legopapurida ? I read your comment mentioning lazy_gettext, but I was not able to apply it to my case...

Hello dear friend I've figured out your problem domain.

I should create a context invoker for resolving this problem.

Legopapurida commented 1 week ago

You must use the FastAPI background task and make_gettext as dependency injection. you need to merge these two approaches to achieve the best result.

If you want to handle this issue without my idea, you may need to use the queue and thread shared memory concept.

please try those solutions, If you can't get good output please let me know to find a better way to handle this issue. Otherwise, I have to write a PushContext API to resolve the problem.