From 97b63b1dd21eb5fb2b4d92bd9e1534b44b995b57 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 22:09:48 +0000 Subject: [PATCH 1/2] Code quality improvements: dead code cleanup and test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added comprehensive dead code analysis documentation in misc/docs/code-quality-improvements.md - Removed dead code: unreachable statements after raise, unused imports (getsource) - Added TODO comments to incomplete implementations (skip_lines, extension_based, delegate_as) - Added clarifying comments to signature template functions - Improved test coverage with new test files: - dol/tests/test_errors.py (15 tests) - coverage: 46% → 92% - dol/tests/test_dig.py (16 tests) - coverage: 46% → 93% - Overall test coverage improved from 58% to 59% - All tests passing (105 passed, 2 skipped) --- dol/base.py | 1 + dol/kv_codecs.py | 3 + dol/tests/test_dig.py | 233 +++++++++++++++++++++++++ dol/tests/test_errors.py | 207 ++++++++++++++++++++++ dol/util.py | 24 +-- dol/zipfiledol.py | 1 - misc/docs/code-quality-improvements.md | 229 ++++++++++++++++++++++++ 7 files changed, 679 insertions(+), 19 deletions(-) create mode 100644 dol/tests/test_dig.py create mode 100644 dol/tests/test_errors.py create mode 100644 misc/docs/code-quality-improvements.md diff --git a/dol/base.py b/dol/base.py index e4b3d541..022f5161 100644 --- a/dol/base.py +++ b/dol/base.py @@ -1014,6 +1014,7 @@ def rewind(self, instance): instance.seek(0) def skip_lines(self, instance, n_lines_to_skip=0): + # TODO: Implement line skipping - n_lines_to_skip parameter is currently unused instance.seek(0) diff --git a/dol/kv_codecs.py b/dol/kv_codecs.py index 82b8a8cf..70503830 100644 --- a/dol/kv_codecs.py +++ b/dol/kv_codecs.py @@ -30,6 +30,8 @@ def _string(string: str): ... +# Signature template functions - not meant to be called, only used for signature composition +# via @Sig decorator. Parameters appear "unused" but are actually captured for later use. @Sig def _csv_rw_sig( dialect: str = "excel", @@ -591,3 +593,4 @@ def extension_based( ): """A factory that creates a key-value codec that uses the file extension to determine the value codec to use.""" + # TODO: Implement this function - ext_mapping parameter is currently unused diff --git a/dol/tests/test_dig.py b/dol/tests/test_dig.py new file mode 100644 index 00000000..fafba09a --- /dev/null +++ b/dol/tests/test_dig.py @@ -0,0 +1,233 @@ +"""Tests for dol.dig module (layer introspection utilities)""" + +import pytest +from dol.dig import ( + get_first_attr_found, + recursive_get_attr, + re_get_attr, + dig_up, + store_trans_path, + print_trans_path, + last_element, + inner_most, + unravel_key, + inner_most_key, + next_layer, + recursive_calls, + layers, + trace_getitem, + not_found, + no_default, +) +from io import StringIO +import sys + + +class SimpleStore: + """A simple test store with nested structure""" + + def __init__(self, data, inner_store=None): + self.data = data + self.attr1 = "value1" + if inner_store: + self.store = inner_store + + def _id_of_key(self, key): + return f"id_{key}" + + def _data_of_obj(self, obj): + return f"data_{obj}" + + +def test_get_first_attr_found(): + """Test getting first found attribute""" + store = SimpleStore({}) + store.attr2 = "value2" + + # Should find first existing attribute + result = get_first_attr_found(store, ["nonexistent", "attr1", "attr2"]) + assert result == "value1" + + # Should find second if first doesn't exist + result = get_first_attr_found(store, ["nonexistent", "attr2"]) + assert result == "value2" + + +def test_get_first_attr_found_with_default(): + """Test get_first_attr_found with default value""" + store = SimpleStore({}) + + # Should return default when no attributes found + result = get_first_attr_found(store, ["x", "y", "z"], default="default_value") + assert result == "default_value" + + +def test_get_first_attr_found_no_default(): + """Test get_first_attr_found raises when no default and no attr found""" + store = SimpleStore({}) + + with pytest.raises(AttributeError, match="None of the attributes were found"): + get_first_attr_found(store, ["x", "y", "z"]) + + +def test_recursive_get_attr(): + """Test recursive attribute lookup""" + inner_store = SimpleStore({}) + inner_store.deep_attr = "deep_value" + + outer_store = SimpleStore({}, inner_store=inner_store) + + # Should find attribute in current store + result = recursive_get_attr(outer_store, "attr1") + assert result == "value1" + + # Should recursively find in inner store + result = recursive_get_attr(outer_store, "deep_attr") + assert result == "deep_value" + + # Should return default if not found + result = recursive_get_attr(outer_store, "nonexistent", default="my_default") + assert result == "my_default" + + +def test_re_get_attr_and_dig_up_aliases(): + """Test that re_get_attr and dig_up are aliases for recursive_get_attr""" + assert re_get_attr is recursive_get_attr + assert dig_up is recursive_get_attr + + +def test_store_trans_path(): + """Test store transformation path""" + inner_store = SimpleStore({}) + outer_store = SimpleStore({}, inner_store=inner_store) + + result = list(store_trans_path(outer_store, "key", "_id_of_key")) + # Should yield transformed keys at each level + assert "id_key" in result + + +def test_print_trans_path(capsys): + """Test printing transformation path""" + store = SimpleStore({}) + + # Capture stdout + print_trans_path(store, "test", "_id_of_key") + captured = capsys.readouterr() + assert "test" in captured.out + assert "id_test" in captured.out + + +def test_print_trans_path_with_type(capsys): + """Test printing transformation path with type info""" + store = SimpleStore({}) + + print_trans_path(store, "test", "_id_of_key", with_type=True) + captured = capsys.readouterr() + assert "" in captured.out + + +def test_last_element(): + """Test getting last element from generator""" + gen = (x for x in [1, 2, 3, 4, 5]) + assert last_element(gen) == 5 + + # Empty generator should return None + gen = (x for x in []) + assert last_element(gen) is None + + +def test_inner_most(): + """Test getting innermost transformation""" + store = SimpleStore({}) + result = inner_most(store, "test", "_id_of_key") + # Should return the final transformed value + assert result is not None + + +def test_next_layer(): + """Test getting next layer of store""" + inner_store = SimpleStore({}) + outer_store = SimpleStore({}, inner_store=inner_store) + + # Should return inner store + result = next_layer(outer_store) + assert result is inner_store + + # Should return not_found if no next layer + result = next_layer(inner_store) + assert result is not_found + + +def test_recursive_calls(): + """Test recursive function calls generator""" + # Test with simple increment until sentinel + def increment(x): + if x >= 5: + return not_found + return x + 1 + + result = list(recursive_calls(increment, 0)) + assert result == [0, 1, 2, 3, 4, 5] + + +def test_layers(): + """Test getting all layers of a store""" + inner_store = SimpleStore({}) + middle_store = SimpleStore({}, inner_store=inner_store) + outer_store = SimpleStore({}, inner_store=middle_store) + + result = layers(outer_store) + assert len(result) == 3 + assert result[0] is outer_store + assert result[1] is middle_store + assert result[2] is inner_store + + +def test_trace_getitem(): + """Test tracing getitem operations through layers""" + from dol.trans import wrap_kvs + + # Create a simple layered store as shown in docstring + d = {"a.num": "1000", "b.num": "2000"} + + s = wrap_kvs( + d, + key_of_id=lambda x: x[: -len(".num")], + id_of_key=lambda x: x + ".num", + obj_of_data=lambda x: int(x), + data_of_obj=lambda x: str(x), + ) + + ss = wrap_kvs( + s, + key_of_id=lambda x: x.upper(), + id_of_key=lambda x: x.lower(), + ) + + # Trace should show transformation through layers + trace = list(trace_getitem(ss, "A")) + assert len(trace) > 0 + + # Check that trace includes _id_of_key and __getitem__ steps + methods = [method for _, method, _ in trace] + assert "_id_of_key" in methods + assert "__getitem__" in methods + + +def test_unravel_key(): + """Test key unraveling (specialized store_trans_path)""" + inner_store = SimpleStore({}) + outer_store = SimpleStore({}, inner_store=inner_store) + + result = list(unravel_key(outer_store, "mykey")) + # Should show key transformations + assert len(result) > 0 + + +def test_inner_most_key(): + """Test getting innermost key transformation""" + store = SimpleStore({}) + + result = inner_most_key(store, "test") + # Should return final key transformation or None + assert result is None or isinstance(result, str) diff --git a/dol/tests/test_errors.py b/dol/tests/test_errors.py new file mode 100644 index 00000000..0be4afa6 --- /dev/null +++ b/dol/tests/test_errors.py @@ -0,0 +1,207 @@ +"""Tests for dol.errors module""" + +import pytest +from dol.errors import ( + items_with_caught_exceptions, + _assert_condition, + NotValid, + KeyValidationError, + NoSuchKeyError, + NotAllowed, + OperationNotAllowed, + ReadsNotAllowed, + WritesNotAllowed, + DeletionsNotAllowed, + IterationNotAllowed, + OverWritesNotAllowedError, + AlreadyExists, + MethodNameAlreadyExists, + MethodFuncNotValid, + SetattrNotAllowed, +) +from collections.abc import Mapping + + +class OddKeyErrorMapping(Mapping): + """A test mapping that raises KeyError for odd keys""" + + def __init__(self, n=10): + self.n = n + + def __iter__(self): + yield from range(2, self.n) + + def __len__(self): + return self.n - 2 + + def __getitem__(self, k): + if k % 2 == 0: + return k * 10 + else: + raise KeyError(f"Key {k} is odd") + + +def test_items_with_caught_exceptions_basic(): + """Test basic functionality of items_with_caught_exceptions""" + test_map = OddKeyErrorMapping(10) + result = list(items_with_caught_exceptions(test_map)) + # Should only get even keys (2, 4, 6, 8) + assert result == [(2, 20), (4, 40), (6, 60), (8, 80)] + + +def test_items_with_caught_exceptions_with_callback(): + """Test items_with_caught_exceptions with a callback""" + test_map = OddKeyErrorMapping(8) + caught_keys = [] + + def callback(k, e): + caught_keys.append(k) + + result = list(items_with_caught_exceptions(test_map, callback=callback)) + assert result == [(2, 20), (4, 40), (6, 60)] + # Odd keys should have been caught: 3, 5, 7 + assert caught_keys == [3, 5, 7] + + +def test_items_with_caught_exceptions_with_index_callback(): + """Test callback with index parameter""" + test_map = OddKeyErrorMapping(6) + caught_indices = [] + + def callback(i): + caught_indices.append(i) + + result = list(items_with_caught_exceptions(test_map, callback=callback)) + assert result == [(2, 20), (4, 40)] + # Indices where exceptions occurred: 1 (key 3), 3 (key 5) + assert caught_indices == [1, 3] + + +def test_items_with_caught_exceptions_yield_callback_output(): + """Test yielding callback output""" + test_map = OddKeyErrorMapping(6) + + def callback(k): + return f"error_{k}" + + result = list( + items_with_caught_exceptions( + test_map, callback=callback, yield_callback_output=True + ) + ) + # Should yield both successful items and callback outputs + assert (2, 20) in result + assert (4, 40) in result + assert "error_3" in result + assert "error_5" in result + + +def test_items_with_caught_exceptions_specific_exceptions(): + """Test catching specific exception types""" + + class SpecialMapping(Mapping): + def __iter__(self): + yield from ["a", "b", "c"] + + def __len__(self): + return 3 + + def __getitem__(self, k): + if k == "a": + return "value_a" + elif k == "b": + raise KeyError("Key error") + else: + raise ValueError("Value error") + + # Only catch KeyError - should catch 'b' but raise exception on 'c' + with pytest.raises(ValueError, match="Value error"): + list( + items_with_caught_exceptions( + SpecialMapping(), catch_exceptions=(KeyError,) + ) + ) + + +def test_assert_condition(): + """Test _assert_condition helper function""" + # Should not raise when condition is True + _assert_condition(True, "Should not raise") + + # Should raise AssertionError when condition is False + with pytest.raises(AssertionError, match="Test error"): + _assert_condition(False, "Test error") + + # Should raise custom error class + with pytest.raises(ValueError, match="Custom error"): + _assert_condition(False, "Custom error", ValueError) + + +# Test exception classes hierarchy +def test_not_valid_exception(): + """Test NotValid exception is both ValueError and TypeError""" + exc = NotValid("test") + assert isinstance(exc, ValueError) + assert isinstance(exc, TypeError) + + +def test_key_validation_error(): + """Test KeyValidationError inherits from NotValid""" + exc = KeyValidationError("invalid key") + assert isinstance(exc, NotValid) + assert isinstance(exc, ValueError) + + +def test_no_such_key_error(): + """Test NoSuchKeyError inherits from KeyError""" + exc = NoSuchKeyError("missing") + assert isinstance(exc, KeyError) + + +def test_operation_not_allowed(): + """Test OperationNotAllowed hierarchy""" + exc = OperationNotAllowed("not allowed") + assert isinstance(exc, NotAllowed) + assert isinstance(exc, NotImplementedError) + + +def test_specific_operation_not_allowed_exceptions(): + """Test specific operation exception types""" + reads = ReadsNotAllowed("no reads") + assert isinstance(reads, OperationNotAllowed) + + writes = WritesNotAllowed("no writes") + assert isinstance(writes, OperationNotAllowed) + + deletes = DeletionsNotAllowed("no deletes") + assert isinstance(deletes, OperationNotAllowed) + + iteration = IterationNotAllowed("no iteration") + assert isinstance(iteration, OperationNotAllowed) + + overwrites = OverWritesNotAllowedError("no overwrites") + assert isinstance(overwrites, OperationNotAllowed) + + +def test_already_exists(): + """Test AlreadyExists exception""" + exc = AlreadyExists("exists") + assert isinstance(exc, ValueError) + + +def test_method_name_already_exists(): + """Test MethodNameAlreadyExists exception""" + exc = MethodNameAlreadyExists("method exists") + assert isinstance(exc, AlreadyExists) + + +def test_method_func_not_valid(): + """Test MethodFuncNotValid exception""" + exc = MethodFuncNotValid("invalid method") + assert isinstance(exc, NotValid) + + +def test_setattr_not_allowed(): + """Test SetattrNotAllowed exception""" + exc = SetattrNotAllowed("cannot set") + assert isinstance(exc, NotAllowed) diff --git a/dol/util.py b/dol/util.py index 6439d168..5fe0cd72 100644 --- a/dol/util.py +++ b/dol/util.py @@ -22,7 +22,7 @@ from functools import wraps as _wraps from functools import partialmethod, partial, WRAPPER_ASSIGNMENTS from types import MethodType, FunctionType -from inspect import Signature, signature, Parameter, getsource, ismethod +from inspect import Signature, signature, Parameter, ismethod Key = TypeVar("Key") @@ -1591,24 +1591,12 @@ def __str__(self): def delegate_as(delegate_cls, to="delegate", include=frozenset(), exclude=frozenset()): + # TODO: Complete this implementation - currently raises NotImplementedError + # The intended implementation would: + # 1. Turn include and exclude into sets + # 2. Get delegate_attrs from delegate_cls.__dict__.keys() + # 3. Create a decorator that adds delegated attributes to the target class raise NotImplementedError("Didn't manage to make this work fully") - # turn include and ignore into sets, if they aren't already - include = set(include) - exclude = set(exclude) - delegate_attrs = set(delegate_cls.__dict__.keys()) - attributes = include | delegate_attrs - exclude - - def inner(cls): - # create property for storing the delegate - setattr(cls, to, property()) - # don't bother adding attributes that the class already has - attrs = attributes - set(cls.__dict__.keys()) - # set all the attributes - for attr in attrs: - setattr(cls, attr, DelegatedAttribute(to, attr)) - return cls - - return inner class HashableMixin: diff --git a/dol/zipfiledol.py b/dol/zipfiledol.py index fb18ae7e..4bbe8b50 100644 --- a/dol/zipfiledol.py +++ b/dol/zipfiledol.py @@ -264,7 +264,6 @@ def if_i_zipped_stats(b: bytes): stats[name]["uncomp_time"] = elapsed except Exception: raise - pass return stats diff --git a/misc/docs/code-quality-improvements.md b/misc/docs/code-quality-improvements.md new file mode 100644 index 00000000..d393d175 --- /dev/null +++ b/misc/docs/code-quality-improvements.md @@ -0,0 +1,229 @@ +# Code Quality and Improvement Opportunities + +This document tracks potential improvements identified through code analysis, including dead code detection, test coverage gaps, and code smells. + +**Last Updated**: 2025-11-21 +**Analysis Tools**: vulture (dead code detection), coverage.py (test coverage) + +--- + +## Dead Code Analysis + +### High Priority Items (True Dead Code) + +#### 1. Unreachable Code After Raise Statement +**File**: `dol/util.py`, lines 1596-1609 +**Issue**: The function `delegate_as` raises `NotImplementedError` but has 14+ lines of code after the raise that will never execute. +**Status**: ⚠️ Needs attention +**Recommendation**: Remove unreachable code or convert to comments if implementation is planned + +```python +def delegate_as(delegate_cls, to="delegate", include=frozenset(), exclude=frozenset()): + raise NotImplementedError("Didn't manage to make this work fully") + # All code after this is unreachable... +``` + +#### 2. Pass Statement After Raise +**File**: `dol/zipfiledol.py`, line 267 +**Issue**: `pass` statement after `raise` is unreachable +**Status**: ⚠️ Needs attention +**Recommendation**: Remove the pass statement + +```python +except Exception: + raise + pass # <- Dead code +``` + +#### 3. Unused Imports +**File**: `dol/util.py`, line 25 +**Issue**: `from inspect import getsource` - imported but never used +**Status**: ⚠️ Needs attention +**Recommendation**: Remove import + +--- + +### Medium Priority Items (Incomplete Implementations) + +#### 1. Unused Function Parameter: `n_lines_to_skip` +**File**: `dol/base.py`, line 1016 +**Issue**: The `skip_lines` method accepts parameter `n_lines_to_skip` but never uses it +**Status**: 🔍 Under review +**Recommendation**: Either implement the skipping logic or remove the parameter + +```python +def skip_lines(self, instance, n_lines_to_skip=0): + instance.seek(0) # <- Should use n_lines_to_skip? +``` + +#### 2. Unused Parameter: `ext_mapping` +**File**: `dol/kv_codecs.py`, line 588 +**Issue**: Function `extension_based` accepts `ext_mapping` parameter but doesn't use it in the function body +**Status**: 🔍 Under review +**Recommendation**: Complete implementation or document why parameter exists + +#### 3. Unused Variable: `key_condition` +**File**: `dol/filesys.py`, line 726 +**Status**: 🔍 Under review +**Recommendation**: Investigate whether this was meant to be used in a condition + +#### 4. Unused Variable: `key_to_value` +**File**: `dol/paths.py`, line 682 +**Status**: 🔍 Under review +**Recommendation**: Investigate intended usage + +#### 5. Unused Variable: `disable_deletes` +**File**: `dol/trans.py`, line 551 +**Status**: 🔍 Under review +**Recommendation**: Investigate whether this controls deletion behavior + +#### 6. Unused Exception Variable +**File**: `dol/zipfiledol.py`, line 874 +**Issue**: Exception caught but the variable binding is unused +**Status**: 🔍 Under review + +--- + +### Low Priority Items (False Positives / By Design) + +#### 1. Signature Capture Functions +**Files**: `dol/kv_codecs.py` (lines 34-50), `dol/signatures.py` (lines 4442-4473) +**Issue**: Vulture reports function parameters as unused +**Status**: ✅ False positive - by design +**Explanation**: These functions are decorated with `@Sig` and exist solely to capture parameter signatures for later composition. They are never meant to be called. + +**Example**: +```python +@Sig +def _csv_rw_sig( + dialect: str = "excel", # Reported as "unused" + delimiter: str = ",", # But actually used via signature + # ... +): ... + +# Later composed into actual function signatures: +@__csv_rw_sig +def csv_encode(...): ... +``` + +**Recommendation**: Add documentation comment to clarify the pattern: +```python +# Signature template (not called, used for signature composition via @Sig decorator) +@Sig +def _csv_rw_sig(...): ... +``` + +#### 2. Type Annotation Imports +**Multiple Files**: Various imports of `Optional`, `Tuple`, `Dict`, `List`, `TypedDict`, etc. +**Issue**: Vulture reports as unused (90% confidence) +**Status**: ✅ False positive +**Explanation**: These are used in type annotations, which static analysis tools sometimes don't detect +**Recommendation**: Keep these imports + +--- + +## Test Coverage Analysis + +**Overall Coverage**: 58% +**Test Command**: `python -m coverage run -m pytest && python -m coverage report` + +### Files with Low Coverage (<60%) + +| File | Coverage | Missing Lines | Priority | +|------|----------|---------------|----------| +| `dol/naming.py` | 31.4% | 293 | High | +| `dol/appendable.py` | 37.8% | 97 | High | +| `dol/util.py` | 41.7% | 359 | High | +| `dol/zipfiledol.py` | 45.5% | 176 | Medium | +| `dol/dig.py` | 45.9% | 46 | Medium | +| `dol/errors.py` | 46.2% | 21 | Low | +| `dol/signatures.py` | 46.2% | 597 | Medium | +| `dol/trans.py` | 50.1% | 394 | Medium | +| `dol/sources.py` | 50.4% | 134 | Medium | +| `dol/explicit.py` | 50.7% | 34 | Low | +| `dol/caching.py` | 56.4% | 281 | Medium | +| `dol/paths.py` | 58.2% | 213 | Medium | + +### High Priority Coverage Gaps + +#### 1. `dol/naming.py` (31% coverage) +- **Size**: 427 statements, 293 missing +- **Description**: Naming and naming templates functionality +- **Recommendation**: This is a substantial module with very low coverage. Should add comprehensive tests for: + - Template construction and validation + - Name generation and parsing + - Edge cases with special characters + - Template composition + +#### 2. `dol/appendable.py` (38% coverage) +- **Size**: 156 statements, 97 missing +- **Description**: Appendable store functionality +- **Recommendation**: Add tests for: + - Appending operations + - Edge cases (empty stores, concurrent appends if supported) + - Integration with different store backends + +#### 3. `dol/util.py` (42% coverage) +- **Size**: 616 statements, 359 missing +- **Description**: General utility functions +- **Recommendation**: This is the largest file with low coverage. Focus on: + - Most commonly used utility functions + - Critical path utilities + - Public API functions + +--- + +## Code Smells and Improvement Opportunities + +### 1. Unused Typing Imports Pattern +**Pattern**: Many files import typing constructs that may not be actively used +**Recommendation**: Use `ruff` or `mypy` to automatically detect and remove truly unused typing imports + +### 2. Large Files with Low Coverage +**Files**: `dol/util.py` (616 lines), `dol/signatures.py` (1109 lines), `dol/trans.py` (789 lines) +**Recommendation**: Consider refactoring these large modules into smaller, more focused modules that are easier to test and maintain + +### 3. Exception Handling Patterns +Several instances of bare `except Exception:` or catching exceptions without using the exception object +**Recommendation**: Review exception handling to ensure proper error context is preserved + +--- + +## Action Items + +### Immediate (High Priority) +- [ ] Remove unreachable code in `dol/util.py:1596-1609` +- [ ] Remove pass statement in `dol/zipfiledol.py:267` +- [ ] Remove unused import `getsource` in `dol/util.py:25` +- [ ] Add TODO comments to incomplete implementations (base.py, kv_codecs.py) +- [ ] Add tests for `dol/naming.py` to improve coverage from 31% to >60% +- [ ] Add tests for `dol/appendable.py` to improve coverage from 38% to >60% + +### Medium Term +- [ ] Investigate and resolve unused variables in filesys.py, paths.py, trans.py +- [ ] Add documentation comments to signature capture functions +- [ ] Increase coverage for `dol/util.py`, `dol/signatures.py`, `dol/trans.py` +- [ ] Consider refactoring large files into smaller modules + +### Long Term +- [ ] Achieve 70%+ overall test coverage +- [ ] Set up automated dead code detection in CI +- [ ] Implement pre-commit hooks for code quality checks +- [ ] Document common patterns (like signature capture) in developer guide + +--- + +## Notes + +- This analysis was performed using vulture with 80% confidence threshold and excluded test and scrap directories +- Coverage analysis was run with pytest and coverage.py +- Some "unused" code may be part of the public API and should be retained for backward compatibility +- Signature capture pattern in this codebase is intentional and should not be "fixed" + +--- + +## References + +- [Vulture Documentation](https://github.com/jendrikseipp/vulture) +- [Coverage.py Documentation](https://coverage.readthedocs.io/) +- [Python Code Quality Tools](https://realpython.com/python-code-quality/) From 344ca2085a498dd16c282dce266b734a5e251140 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 22:11:21 +0000 Subject: [PATCH 2/2] Add coverage.json to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 74ac0925..28741e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +coverage.json *.cover .hypothesis/ .pytest_cache/