fix: default the weight unit to the trainee's wger profile - #16
Open
wromansky wants to merge 2 commits into
Open
fix: default the weight unit to the trainee's wger profile#16wromansky wants to merge 2 commits into
wromansky wants to merge 2 commits into
Conversation
log_set and add_exercise_with_sets both carried a hardcoded weight_unit of "kg". A trainee whose wger profile is set to pounds, reporting "225", had it stored as 225 kg — wrong by a factor of 2.2, and invisible afterwards, because the number is plausible in either unit and nothing in the record says which one was meant. The profile already knows the answer, and the server already reads it for whoami and the nutrition targets. Omitting weight_unit now takes the unit from /userprofile/ through a per-registration cached resolver, in the same shape as language_id_resolver: the setting does not change mid-session, so one lookup covers it. An explicit weight_unit still wins, so existing callers are unaffected. An unreadable profile falls back to "kg" and is not cached, so a transient error neither fails the write nor pins the wrong unit for the session.
The resolver added in a384fae cached the trainee's unit in a closure built by register_all, which build_server calls once per process. mcp_auth defaults to oidc, and under it one shared api client serves every user — _ProviderAuth resolves the Authorization header per request. So the first trainee to omit weight_unit pinned their unit onto every other trainee's writes until restart: the same silent wrong-unit write this change exists to prevent, only spread across users. language_id_resolver caches safely because wger's language table is global and static; a per-user profile field is not. The cache is gone, and with it the closure that only existed to hold it: profile_weight_unit is a plain coroutine taking the client. One extra GET alongside the six add_exercise_with_sets already makes is noise. An unparseable profile is caught too. Userprofile.from_dict routes weight_unit through check_weight_unit_enum, which raises TypeError, not ValueError — neither the old except tuple nor api_tool caught it, so a profile carrying a unit the pinned client does not know (a null one included) would crash log_set, a tool that never read the profile before. Also drops a dead getattr(unit, "value", unit): WeightUnitEnum is Literal["kg", "lb"] in the pinned client, a plain str, so there is no enum to unwrap. Corrects a384fae, whose message and docstring claimed "the setting does not change mid-session, so one lookup covers it". The scope was never a session. test: stop the routines tests reaching for the real /userprofile/ _mock_creation never patched userprofile_retrieve and tests/conftest.py has no autouse respx guard, so test_weight_may_be_omitted and test_rir_is_optional each sent a real GET to https://wger.test, took a ConnectError and landed on the kg fallback. test_rir_is_optional's weight_unit == 1 was asserting the error path while reading as the profile path. _mock_creation now stands in for the profile, and a new case covers add_exercise_with_sets taking its default from a pounds profile. Verified with a temporary autouse guard over httpx.AsyncClient.send: 2 of the file's tests failed before, none after. docs: README said kg was the default for log_set's weight_unit; the profile is.
wromansky
marked this pull request as ready for review
September 1, 2026 19:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
log_setandadd_exercise_with_setsboth defaultweight_unitto a hardcoded"kg":The wger profile already records which unit the trainee works in, and that default ignores it. A trainee whose profile is set to pounds reports "225", the caller omits the unit, and 225 is stored as kilograms — wrong by a factor of 2.2.
The failure is silent and unrecoverable after the fact. 225 is a plausible number in either unit, and the stored row does not say which one the trainee meant, so no later read can tell a mis-unit log from a correct one.
I hit this with an LLM client driving the server: the model omitted
weight_uniton a routine build, and every planned entry landed as kg against a pounds profile. A prompt rule telling the client to always passlbis a workaround for a default that is simply wrong for that user.The change
Omitting
weight_unitnow takes the unit from/userprofile/.profile_weight_unitcoroutine intools/common.py. It reads the profile on each call that needs it, and is deliberately not cached and not a per-registration closure — see below.whoamiand the nutrition targets, so no new dependency or scope.Why there is no cache
The first draft of this PR cached the unit in a closure, copying the shape of
language_id_resolver. That was wrong, and the fix is in this branch.register_allruns once per process frombuild_server,mcp_authdefaults tooidc, and_ProviderAuthresolves theAuthorizationheader per request off a ContextVar — one shared client serves every user. A cache in that closure is therefore process-wide, not per session and not per request, so the first trainee to omitweight_unitwould pin their unit onto every other trainee's writes until restart. That is the same silent wrong-unit write this PR exists to prevent, only spread across users.language_id_resolvercaches safely because wger's language table is global and static. A per-user profile field is not, and the earlier claim that "the setting does not change mid-session, so one call covers it" was answering the wrong question — the scope was never a session.One extra GET, against the roughly six requests
add_exercise_with_setsalready makes, is noise. A per-user keyed cache would be more code for no measured benefit.Behaviour
An explicit
weight_unitstill wins, so callers that pass one are unaffected. Callers that omit one change behaviour by design: a pounds profile now records pounds where it previously recorded kilograms. That is the point of the change.A profile that cannot be read falls back to
"kg"— today's behaviour — so a transient error never fails the write.Userprofile.from_dictroutesweight_unitthroughcheck_weight_unit_enum, which raisesTypeError(notValueError) for anything outside{kg, lb}, a null value included;TypeErroris caught alongsideUnexpectedStatusandhttpx.HTTPError, so an unparseable profile cannot crashlog_set— a tool that never read the profile before this PR.Tests
tests/test_weight_units_and_rir.py:kgprofile (log_set)lbprofile — 225 stays 225 lb (log_set)kgbeats anlbprofilekgwithout failing the write, parametrised over an unreachable profile and an unparseable onelbprofile onadd_exercise_with_setstoo — the other tool this PR changesThe existing
test_kilograms_remain_the_defaultasserted the hardcoded default; it becomes the profile-says-kg case, which is the same assertion for the right reason._mock_creationnow stands in for/userprofile/as well. Without that, the two pre-existing routines tests that omitweight_unit(test_weight_may_be_omitted,test_rir_is_optional) fell through the new lookup withuserprofile_retrieveunmocked and sent a real request tohttps://wger.test/api/v2/userprofile/—CONTRIBUTINGrequiresrespxfor outbound HTTP, andtests/conftest.pyhas no autouse guard. They passed only because theConnectErrorlanded on thekgfallback, sotest_rir_is_optional'sweight_unit == 1was asserting the error path while reading as the profile path. Verified with a temporary autouse guard overhttpx.AsyncClient.send: 2 of the file's tests attempted outbound HTTP before the fix, none after. Both still assert what they were written to assert.Full suite: 252 passed.
ruff check .clean;ruff format --checkclean on the touched files.Collisions with the other open PRs
Checked with
git merge-treeagainst the current head of each branch.feat/repetition-unit-by-name) — one conflict, the## Unreleasedblock inCHANGELOG.md, for whichever lands second.common.py,routines.pyandworkout_logs.pyall merge cleanly. In particular this PR does not touchas_weight_unit; only feat(routines): take unit names on slot entries, not just wger's ids #18 changes that function's signature, so there is no collision there.feat/max-reps-on-add-exercise) — two conflicts: theCHANGELOG.md## Unreleasedblock, andsrc/wger_mcp/tools/routines.py. Both PRs edit the same statement inadd_exercise_with_sets: this branch replacesunit = as_weight_unit(weight_unit)with the profile lookup, and feat(routines): let add_exercise_with_sets record a rep range #17 inserts itsmax_reps < repsguard immediately after it. Resolution is mechanical — keep both — but it is a real code conflict, not just a changelog one.README.mdandtests/test_weight_units_and_rir.pymerge cleanly.Worth noting for whichever lands second: three of feat(routines): let add_exercise_with_sets record a rep range #17's new tests call
_mock_creationand omitweight_unit, so they depend on this PR's_mock_creationmocking/userprofile/. Drop that during conflict resolution and those tests start reaching for the network.docs/log-set-exercise-id) — merges cleanly, no conflict.