Repository: roskakori/pygount

Issue:
JSON output fails with `TypeError: Object of type LookupError is not JSON serializable`
https://github.com/roskakori/pygount/blob/a55717e4f1c2dd2bf4c27b2e51abd8fd2e980523/pygount/write.py#L265
Should be rewritten as `json.dump(json_map, self._target_stream, default=repr)`.

Discussion:

Patch:
diff --git a/docs/changes.rst b/docs/changes.rst
index d995064..f680aeb 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -5,6 +5,9 @@ This chapter describes the changes coming with each new version of pygount.
 
 Version 2.0.0, 2025-03-xx
 
+* Fix ``LookupError`` when processing files with a magic encoding comment
+  specifying an unknown encoding and using ``--format=json`` (contributed by
+  PyHedgehog, issue `#176 <https://github.com/roskakori/pygount/issues/176>`_)
 * Add support for Python 3.13 and later, issue
   `#174 <https://github.com/roskakori/pygount/issues/174>`_)
 * Remove temporary directory in the output of a git analysis (contributed by
diff --git a/pygount/analysis.py b/pygount/analysis.py
index 3c2d732..3233064 100644
--- a/pygount/analysis.py
+++ b/pygount/analysis.py
@@ -264,6 +264,10 @@ class SourceAnalysis:
 
     @staticmethod
     def _check_state_info(state: SourceState, state_info: Optional[str]):
+        assert state_info is None or isinstance(state_info, str), (
+            f"state_info must be be None or str but is: {state_info!r}"
+        )
+
         states_that_require_state_info = [SourceState.duplicate, SourceState.error, SourceState.generated]
         assert (state in states_that_require_state_info) == (state_info is not None), (
             f"state={state} and state_info={state_info} "
@@ -341,7 +345,7 @@ class SourceAnalysis:
                     source_code = file_handle.read()
             except (LookupError, OSError, UnicodeError) as error:
                 _log.warning("cannot read %s using encoding %s: %s", source_path, encoding, error)
-                result = SourceAnalysis.from_state(source_path, group, SourceState.error, error)
+                result = SourceAnalysis.from_state(source_path, group, SourceState.error, str(error))
             if result is None:
                 lexer = guess_lexer(source_path, source_code)
                 assert lexer is not None


Test patch:
diff --git a/tests/test_analysis.py b/tests/test_analysis.py
index 3196d12..9b5ae90 100644
--- a/tests/test_analysis.py
+++ b/tests/test_analysis.py
@@ -261,6 +261,12 @@ class FileAnalysisTest(TempFolderTest):
         assert source_analysis.language.lower() == "html"
         assert source_analysis.code_count == 3
 
+    def test_can_analyze_unknown_magic_comment_encoding(self):
+        test_python_path = self.create_temp_file("some.py", ["# -*- coding: no_such_encoding -*-", "print('hello')"])
+        source_analysis = analysis.SourceAnalysis.from_file(test_python_path, "test")
+        assert source_analysis.language.lower() == "__error__"
+        assert source_analysis.state_info == "unknown encoding: no_such_encoding"
+
     def test_fails_on_non_seekable_file_handle_with_encoding_automatic(self):
         file_handle = _NonSeekableEmptyBytesIO()