Skip to content

fix: default the weight unit to the trainee's wger profile - #16

Open
wromansky wants to merge 2 commits into
wger-project:masterfrom
wromansky:fix/weight-unit-follows-profile
Open

fix: default the weight unit to the trainee's wger profile#16
wromansky wants to merge 2 commits into
wger-project:masterfrom
wromansky:fix/weight-unit-follows-profile

Conversation

@wromansky

@wromansky wromansky commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The problem

log_set and add_exercise_with_sets both default weight_unit to a hardcoded "kg":

routines.py       weight_unit: str = "kg",
workout_logs.py   weight_unit: str = "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_unit on a routine build, and every planned entry landed as kg against a pounds profile. A prompt rule telling the client to always pass lb is a workaround for a default that is simply wrong for that user.

The change

Omitting weight_unit now takes the unit from /userprofile/.

  • New profile_weight_unit coroutine in tools/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.
  • Both write tools resolve the unit only when the caller omitted it.
  • The server already reads this endpoint for whoami and 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_all runs once per process from build_server, mcp_auth defaults to oidc, and _ProviderAuth resolves the Authorization header 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 omit weight_unit would 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_resolver caches 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_sets already makes, is noise. A per-user keyed cache would be more code for no measured benefit.

Behaviour

An explicit weight_unit still 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_dict routes weight_unit through check_weight_unit_enum, which raises TypeError (not ValueError) for anything outside {kg, lb}, a null value included; TypeError is caught alongside UnexpectedStatus and httpx.HTTPError, so an unparseable profile cannot crash log_set — a tool that never read the profile before this PR.

Tests

tests/test_weight_units_and_rir.py:

  • omitted unit follows a kg profile (log_set)
  • omitted unit follows an lb profile — 225 stays 225 lb (log_set)
  • explicit kg beats an lb profile
  • a profile that cannot be read falls back to kg without failing the write, parametrised over an unreachable profile and an unparseable one
  • omitted unit follows an lb profile on add_exercise_with_sets too — the other tool this PR changes

The existing test_kilograms_remain_the_default asserted the hardcoded default; it becomes the profile-says-kg case, which is the same assertion for the right reason.

_mock_creation now stands in for /userprofile/ as well. Without that, the two pre-existing routines tests that omit weight_unit (test_weight_may_be_omitted, test_rir_is_optional) fell through the new lookup with userprofile_retrieve unmocked and sent a real request to https://wger.test/api/v2/userprofile/CONTRIBUTING requires respx for outbound HTTP, and tests/conftest.py has no autouse guard. They passed only because the ConnectError landed on the kg fallback, so test_rir_is_optional's weight_unit == 1 was asserting the error path while reading as the profile path. Verified with a temporary autouse guard over httpx.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 --check clean on the touched files.

Collisions with the other open PRs

Checked with git merge-tree against the current head of each branch.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant