Summary
src/llmflux/core/client.py:14 calls load_dotenv(override=True) at import time. Because llmflux/__init__.py imports client transitively, merely importing llmflux stamps a .env file over the process environment — and override=True means it beats real environment variables.
Worse, python-dotenv resolves that file relative to client.py's own directory, not the user's working directory. For an editable install, that means a .env anywhere at or above the checkout wins over the .env in the directory the user is running from.
The result is a configuration that cannot be changed by editing the .env you are looking at, with no diagnostic explaining why.
Two loaders, opposite precedence
| Loader |
Search origin |
Precedence vs os.environ |
Config._load_env_file (config.py:240-287) |
cwd + 2 parents |
Respects it — sets a key only if key not in os.environ |
client.py:14 load_dotenv(override=True) |
walks up from client.py |
Overrides it |
The second runs first (at import), so it wins. .env.example documents the opposite: "Path precedence: code paths > environment variables > defaults".
Reproduction
Two .env files, one in the checkout root and one in the run directory:
checkout/.env : LLMFLUX_WORKSPACE=/scratch/STALE-repo-root/llmflux
rundir/.env : LLMFLUX_WORKSPACE=/scratch/CORRECT-from-cwd/llmflux
Running a script from rundir:
before import : None
after import : /scratch/STALE-repo-root/llmflux ← set by importing llmflux
Config sees : /scratch/STALE-repo-root/llmflux
An exported variable loses the same way:
$ SLURM_ACCOUNT=from-export python -c "
import os; import llmflux.core.config
print(os.environ['SLURM_ACCOUNT'])"
test # ← the .env value, not from-export
Note the python -c detail: python-dotenv treats -c, REPLs and notebooks as interactive and searches cwd there, but walks up from client.py for a real script or console entry point. So the same .env can win or lose depending on how Python was started, which makes this very hard to reason about from the outside.
Real-world cost
Encountered on Delta with an editable install at /projects/<...>/code/llmflux. A stale LLMFLUX_WORKSPACE in /projects/<...>/code/.env — one level above the checkout — silently overrode both the shell environment and the .env in the working directory. Editing the working-directory .env had no effect; env | grep LLMFLUX showed nothing, because the value is injected inside the process. It took three rounds of debugging and a walk of every .env above the package directory to locate the responsible file.
Suggested fix
Config already has a .env loader with correct, documented precedence. Options, roughly in order of preference:
- Delete the
load_dotenv call. client.py reads its own settings via os.getenv and any entry point that builds a Config gets .env loading anyway. This removes an import-time side effect on global state, which is the underlying problem.
- Change to
load_dotenv(override=False, dotenv_path=find_dotenv(usecwd=True)) so both loaders agree on origin and precedence.
- Keep it, but move it out of import time into an explicit entry-point call.
Whichever route, .env resolution should happen in exactly one place with one documented rule.
Related
Summary
src/llmflux/core/client.py:14callsload_dotenv(override=True)at import time. Becausellmflux/__init__.pyimportsclienttransitively, merely importingllmfluxstamps a.envfile over the process environment — andoverride=Truemeans it beats real environment variables.Worse, python-dotenv resolves that file relative to
client.py's own directory, not the user's working directory. For an editable install, that means a.envanywhere at or above the checkout wins over the.envin the directory the user is running from.The result is a configuration that cannot be changed by editing the
.envyou are looking at, with no diagnostic explaining why.Two loaders, opposite precedence
os.environConfig._load_env_file(config.py:240-287)if key not in os.environclient.py:14load_dotenv(override=True)client.pyThe second runs first (at import), so it wins.
.env.exampledocuments the opposite: "Path precedence: code paths > environment variables > defaults".Reproduction
Two
.envfiles, one in the checkout root and one in the run directory:Running a script from
rundir:An exported variable loses the same way:
Note the
python -cdetail: python-dotenv treats-c, REPLs and notebooks as interactive and searches cwd there, but walks up fromclient.pyfor a real script or console entry point. So the same.envcan win or lose depending on how Python was started, which makes this very hard to reason about from the outside.Real-world cost
Encountered on Delta with an editable install at
/projects/<...>/code/llmflux. A staleLLMFLUX_WORKSPACEin/projects/<...>/code/.env— one level above the checkout — silently overrode both the shell environment and the.envin the working directory. Editing the working-directory.envhad no effect;env | grep LLMFLUXshowed nothing, because the value is injected inside the process. It took three rounds of debugging and a walk of every.envabove the package directory to locate the responsible file.Suggested fix
Configalready has a.envloader with correct, documented precedence. Options, roughly in order of preference:load_dotenvcall.client.pyreads its own settings viaos.getenvand any entry point that builds aConfiggets.envloading anyway. This removes an import-time side effect on global state, which is the underlying problem.load_dotenv(override=False, dotenv_path=find_dotenv(usecwd=True))so both loaders agree on origin and precedence.Whichever route,
.envresolution should happen in exactly one place with one documented rule.Related
.envwas loaded, and no warning for variables that were set but not read. That diagnostic would have made this findable in seconds; this issue is the underlying defect.