diff --git a/.hermes/plans/2026-06-06_103000-fsize-issue-3-fix.md b/.hermes/plans/2026-06-06_103000-fsize-issue-3-fix.md new file mode 100644 index 0000000..a04aee4 --- /dev/null +++ b/.hermes/plans/2026-06-06_103000-fsize-issue-3-fix.md @@ -0,0 +1,136 @@ +# FSize __format__ empty spec fix Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Fix issue #3: format(fsize, "") should equal str(fsize) by returning str(self) when format_spec is empty. + +**Architecture:** Add an early guard clause in FSize.__format__ to handle empty format_spec, preserving existing behavior for non-empty specs. + +**Tech Stack:** Python, pytest. + +--- + +### Task 1: Write failing test for empty format spec + +**Objective:** Add a test that verifies format(x, "") == str(x) for various FSize values. + +**Files:** +- Create: (none) +- Modify: tests/test_fsize_init.py: (add test function) +- Test: tests/test_fsize_init.py + +**Step 1: Write failing test** + +```python +def test_format_empty_spec(): + """Test that format with empty string equals str.""" + x = FSize(1024) + assert format(x, "") == str(x) + assert f"{x}" == str(x) # f-string with no spec + # Test with different units + y = FSize(1, "MiB") + assert format(y, "") == str(y) + z = FSize(1, "KB") + assert format(z, "") == str(z) +``` + +**Step 2: Run test to verify failure** + +Run: .venv/bin/pytest tests/test_fsize_init.py::test_format_empty_spec -v + +Expected: FAIL — because format(x, "") currently returns K-unit value. + +**Step 3: Write minimal implementation** + +Modify src/fsize/__init__.py in __format__ method: + +Add at the very beginning of the method: + +```python + def __format__(self, format_spec: str) -> str: + """Format the FSize value.""" + if not format_spec: + return str(self) + # ... rest of existing code unchanged +``` + +**Step 4: Run test to verify pass** + +Run: .venv/bin/pytest tests/test_fsize_init.py::test_format_empty_spec -v + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/fsize/__init__.py tests/test_fsize_init.py +git commit -m "fix: handle empty format spec in __format__" +``` + +### Task 2: Run full test suite to ensure no regressions + +**Objective:** Ensure all existing tests still pass after the change. + +**Files:** +- Test: run full pytest + +**Step 1: Run all tests** + +Run: .venv/bin/pytest -v + +Expected: all tests pass + +**Step 2: Commit if any changes (should be none)** + +If any test fails, investigate and fix. + +### Task 3: Verify with mypy and pylint + +**Objective:** Ensure code passes type checking and linting. + +**Files:** +- Check: run mypy and pylint + +**Step 1: Run mypy** + +Run: .venv/bin/mypy . + +Expected: no errors + +**Step 2: Run pylint** + +Run: .venv/bin/pylint . + +Expected: no errors (or only existing ones? but we assume none) + +**Step 3: Commit if any fixes needed** + +If errors, fix them and commit. + +### Task 4: Update documentation if needed (optional) + +**Objective:** Ensure any docstrings or comments are accurate. + +**Files:** +- Possibly modify src/fsize/__init__.py docstring for __format__ to mention empty spec behavior. + +But the existing docstring says default is "K". We might want to note that empty spec returns str(self). However, the issue description says Python's data model requires format(x, "") == str(x). We can update docstring. + +We'll add a note. + +**Step 1: Update docstring** + +In __format__ docstring, add a line: "An empty format_spec returns str(self) as per Python's data model." + +**Step 2: Commit** + +```bash +git add src/fsize/__init__.py +git commit -m "doc: clarify __format__ behavior for empty spec" +``` + +### Task 5: Final verification + +Run full test suite, mypy, pylint one more time. + +Then consider the task complete. \ No newline at end of file diff --git a/src/fsize/__init__.py b/src/fsize/__init__.py index be419c8..21df579 100644 --- a/src/fsize/__init__.py +++ b/src/fsize/__init__.py @@ -200,7 +200,7 @@ def __format__(self, format_spec: str) -> str: raise AssertionError(f"unhandled unit: {unit!r}") n = self.real / self._convert ** _UNIT_POWERS[unit] - log_digits = math.ceil(math.log10(n)) if n > 0 else 0 + log_digits = math.floor(math.log10(n)) + 1 if n > 0 else 0 out_format_spec = ( f"{fill}{align}{width}{grouping}" + "." diff --git a/tests/test_fsize_init.py b/tests/test_fsize_init.py index c2646d1..6379031 100755 --- a/tests/test_fsize_init.py +++ b/tests/test_fsize_init.py @@ -442,6 +442,13 @@ def test_format_empty_spec(): assert format(z, "") == str(z) +def test_format_no_scientific_notation(): + """Test that exact powers of 10 do not produce scientific notation.""" + assert format(FSize(10 * 1024), "K") == "10" + assert format(FSize(100 * 1024), "K") == "100" + assert format(FSize(1000 * 1024), "K") == "1000" + + def test_format_all_units(): """Test that every unit in _UNIT_POWERS works via format.""" # Binary FSize (1 EiB)