Skip to content

fix: treat empty integer env vars as unset, not ValueError - #921

Open
yzxcj797 wants to merge 1 commit into
tirth8205:stagingfrom
yzxcj797:fix/912-empty-int-env
Open

yzxcj797 wants to merge 1 commit into
tirth8205:stagingfrom
yzxcj797:fix/912-empty-int-env

Conversation

@yzxcj797

Copy link
Copy Markdown
Contributor

Summary

An empty string environment variable (VAR= from a shell wrapper, CI template, or .env file) hit int() at module import and killed every subcommand with an uncaught ValueError (#912). os.environ.get(NAME, default) only returns the default when the variable is absent, not when it is present but empty.

A single _int_env() helper in constants.py now:

  • treats empty/whitespace as unset (returns default)
  • parses valid integers normally
  • logs a named warning with the variable name and falls back to the default for invalid values

All module-level int(os.environ.get(...)) sites now use it:

  • constants.py: CRG_MAX_IMPACT_NODES, CRG_MAX_IMPACT_DEPTH, CRG_MAX_BFS_DEPTH, CRG_MAX_SEARCH_RESULTS
  • changes.py: CRG_GIT_TIMEOUT, CRG_MAX_CHANGED_FUNCS
  • incremental.py: CRG_PARSE_WORKERS, CRG_MODULE_SCAN_DEPTH, CRG_MODULE_SCAN_MAX_DIRS, CRG_GIT_TIMEOUT

Testing

New tests/test_int_env.py (7 tests):

  • test_unset_returns_default / test_empty_string_returns_default / test_whitespace_returns_default — the helper's three "fall back" branches
  • test_valid_integer — normal parsing
  • test_invalid_integer_warns_and_returns_default — warning includes the variable name
  • test_empty_git_timeout_does_not_crash_import / test_empty_max_impact_nodes_does_not_crash_import — subprocess verification that the original reproduction commands now succeed
pytest tests/test_int_env.py -q
→ 7 passed

pytest tests/test_int_env.py tests/test_incremental.py -q (2 symlink-privilege deselected)
→ 112 passed, 2 deselected

ruff check code_review_graph/constants.py code_review_graph/changes.py code_review_graph/incremental.py tests/test_int_env.py
→ All checks passed!

Fixes #912

An empty string environment variable (VAR= from a shell wrapper, CI template, or .env file) hit int() at module import and killed every subcommand with an uncaught ValueError (tirth8205#912). os.environ.get(NAME, default) only returns the default when the variable is absent, not when it is present but empty.

A single _int_env() helper in constants.py now treats empty/whitespace as unset, parses valid integers, and warns with the variable name before falling back to the default for invalid values. All module-level int(os.environ.get(...)) sites across constants, changes, and incremental now use it. Seven regression tests cover the helper's five branches plus subprocess import with empty CRG_GIT_TIMEOUT and CRG_MAX_IMPACT_NODES.
@github-actions

Copy link
Copy Markdown

code-review-graph review

Overall risk: 0.30 (LOW) — 11 changed function(s)/class(es), 2 affected flow(s), 2 test gap(s)

Risk-scored changes

Risk Level Symbol Location Tested
0.30 low code_review_graph/changes.py::analyze_changes code_review_graph/changes.py:394 yes
0.30 low code_review_graph/constants.py::_int_env code_review_graph/constants.py:44 yes
0.30 low tests/test_int_env.py::TestImportWithEmptyEnv tests/test_int_env.py:36 no
0.30 low tests/test_int_env.py::TestImportWithEmptyEnv.test_empty_git_timeout_does_not_crash_import tests/test_int_env.py:39 (test)
0.30 low tests/test_int_env.py::TestImportWithEmptyEnv.test_empty_max_impact_nodes_does_not_crash_import tests/test_int_env.py:53 (test)
0.05 low tests/test_int_env.py::TestIntEnv tests/test_int_env.py:10 no
0.05 low tests/test_int_env.py::TestIntEnv.test_unset_returns_default tests/test_int_env.py:11 (test)
0.05 low tests/test_int_env.py::TestIntEnv.test_empty_string_returns_default tests/test_int_env.py:16 (test)
0.05 low tests/test_int_env.py::TestIntEnv.test_whitespace_returns_default tests/test_int_env.py:20 (test)
0.05 low tests/test_int_env.py::TestIntEnv.test_valid_integer tests/test_int_env.py:24 (test)

Affected execution flows

  • _run — criticality 0.36, 68 node(s) across 1 file(s)
  • process — criticality 0.35, 41 node(s) across 1 file(s)

Test gaps

  • tests/test_int_env.py::TestIntEnv (tests/test_int_env.py:10)
  • tests/test_int_env.py::TestImportWithEmptyEnv (tests/test_int_env.py:36)

Token savings: this graph-backed report used ~20,797 fewer tokens (~65%) than reading every changed file in full (estimated, chars/4 approximation).


Powered by code-review-graph — local-first analysis; no code leaves the CI runner.

@tirth8205

Copy link
Copy Markdown
Owner

The helper is the right idea and it does fix the reported command — CRG_GIT_TIMEOUT="" python -c "import code_review_graph.incremental" crashes on the parent and imports cleanly here. Two things to finish before it goes in.

The sweep stops less than half way, and #912's own scope list is not covered. The PR body says "All module-level int(os.environ.get(...)) sites now use it." Ten module-level sites still crash at import with an empty value on this branch:

incremental.py:1136  CRG_DEPENDENT_HOPS          ← named in #912's Scope section
incremental.py:1601  CRG_WATCH_PLAN_DEPTH
incremental.py:1602  CRG_MAX_WATCH_SCHEDULES
incremental.py:1607  CRG_WATCH_SPLIT_MIN_DIRS
incremental.py:243   CRG_NESTED_IGNORE_TTL       (float)
incremental.py:1608  CRG_WATCH_HEALTH_INTERVAL   (float)
daemon.py:106-108,544  CRG_RESTART_BACKOFF, _MAX, CRG_RESTART_HEALTHY_AFTER, CRG_WATCH_HEALTH_STALE (float)

CRG_DEPENDENT_HOPS="" reproduces the reporter's exact traceback, and it lives in the same file you edited in three other places. Three call-time sites also still raise, and one of them is user-facing: CRG_TOOL_TIMEOUT="" makes detect_changes_tool raise ValueError instead of returning a structured error (and leaks an un-awaited asyncio.to_thread coroutine on the way out). CRG_MAX_TRANSITIVE_FRONTIER and CRG_LEIDEN_SEED are the other two. Both CRG_TOOL_TIMEOUT and CRG_MAX_TRANSITIVE_FRONTIER are documented in the README env table.

No clamping, so the crash moves rather than disappears. Empty → default is right, and invalid → warning + default is a reasonable call. But out-of-range values pass through untouched, and two of them are actively harmful:

  • CRG_PARSE_WORKERS=0 (or -4) now imports fine and then kills code-review-graph build on any repo with 8+ files with an unhandled ValueError: max_workers must be greater than 0.
  • CRG_GIT_TIMEOUT=0 or -1 makes subprocess.run(timeout=…) raise TimeoutExpired on every git call, which changes.py swallows into {} — a typo'd config silently produces exactly the "analysed nothing" failure that A git timeout in changes.py is returned as 'no changes': same empty result, exit code 0 #913 is about.
  • CRG_MAX_IMPACT_NODES=999999999999999999999999 is accepted, which defeats the response bounds.

constants.py:10 already has _bounded_float_env(name, default, *, lower, upper) with a range check, tested in tests/test_graph.py:778. Your _int_env lands 34 lines below it and reimplements the parse half without the bounds half, and changes.py:244 has yet a third pattern for the same job. A bounded _int_env(name, default, *, lower, upper) would cover the finding above and let all three converge — and the float vars need the same treatment.

Minor: the tests fail on the parent as a collection error (cannot import name '_int_env') rather than a behavioral failure, so the two subprocess tests that actually reproduce #912 never run; decoupling them from the helper import makes them genuinely protective. import logging is deferred inside _int_env with no circular-import reason. And the README env table should state the new "invalid → warning + default" contract.

Gates are green: 2992 passed, ruff and mypy clean, 13/13 checks.

@tirth8205

Copy link
Copy Markdown
Owner

Changes required: an empty documented numeric setting still prevents the module from importing. Reproduce with CRG_DEPENDENT_HOPS='' python -c 'import code_review_graph.incremental', which raises ValueError on this PR. Apply consistent bounded parsing across the remaining integer and float settings, including zero and out-of-range values.

@tirth8205
tirth8205 changed the base branch from main to staging September 15, 2026 13:11
@tirth8205

tirth8205 commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Checks pass and the code looks right. Missing: tests for the edge cases it introduces.

  • Convert the four remaining module-level int() sites in code_review_graph/incremental.py to _int_env: line 1313 _MAX_DEPENDENT_HOPS (CRG_DEPENDENT_HOPS, explicitly named in issue An empty integer env var (CRG_GIT_TIMEOUT=) kills the tool at import with an uncaught ValueError #912), line 1865 _WATCH_PLAN_DEPTH (CRG_WATCH_PLAN_DEPTH), line 1866 _MAX_WATCH_SCHEDULES (CRG_MAX_WATCH_SCHEDULES), line...
  • Handle the module-level float() sites with the same empty/invalid semantics (a _float_env helper or by extending _bounded_float_env to treat '' as unset): incremental.py:279 CRG_NESTED_IGNORE_TTL, incremental.py:1871 CRG_WATCH_HEALTH_INTERVAL, daemon.py:106-108 CRG_RESTART_BACKOFF / CRG_RESTART_BACK...
  • Optionally convert the call-time int() sites (graph.py:665 CRG_MAX_TRANSITIVE_FRONTIER, main.py:725 CRG_TOOL_TIMEOUT, communities.py:413 and :650 CRG_LEIDEN_SEED) so the helper is the single parsing path the PR description claims it is.
  • Correct the PR description: 'All module-level int(os.environ.get(...)) sites now use it' is false on the current head, and 'Fixes An empty integer env var (CRG_GIT_TIMEOUT=) kills the tool at import with an uncaught ValueError #912' is not accurate while CRG_DEPENDENT_HOPS (listed in the issue) still crashes.

Every new branch in the code should have a test, including the failure branch. Disagree with any of these? Say so here first.

PRs now target staging, not main. Yours was retargeted already, so nothing to do there.

@tirth8205 tirth8205 added the needs-tests Code is fine, edge cases are untested label Sep 15, 2026
@tirth8205

Copy link
Copy Markdown
Owner

This no longer merges into staging. 3 files conflict and the branch is 226 commits behind:

  • code_review_graph/changes.py
  • code_review_graph/constants.py
  • code_review_graph/incremental.py

Worth knowing before you resolve:

  • code_review_graph/changes.py: staging moved _GIT_TIMEOUT into constants.py as GIT_TIMEOUT and added a separate discovery budget.
  • code_review_graph/incremental.py: staging added a build-state checkpoint at the tail of full_build and moved the Git timeout into constants.py.
git fetch origin
git merge origin/staging
# resolve, then
uv run pytest tests/ -q
uv run ruff check code_review_graph/
uv run mypy code_review_graph/ --ignore-missing-imports --no-strict-optional
git push

I have not reviewed the change itself yet. That comes once it merges and the checks run against the merged state, since staging has moved a long way and the result is what matters.

@tirth8205

Copy link
Copy Markdown
Owner

Thank you for this PR.

I tested empty values for each setting, on your branch and on staging.

This fix is already on staging. It came in with #1029. That PR added env_int and env_float in constants.py. All number settings now use them. This includes every setting your PR changes. It also covers CRG_DEPENDENT_HOPS and the watch settings.

On your branch, CRG_DEPENDENT_HOPS= still stops the import with a ValueError at incremental.py:1137. On staging, it prints a warning and keeps going.

So I will not merge this PR.

Thank you again for your help.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-tests Code is fine, edge cases are untested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An empty integer env var (CRG_GIT_TIMEOUT=) kills the tool at import with an uncaught ValueError

2 participants