diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e2e70395..9e4edd39 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +latest +------ + +* Raise ``ValueError`` instead of panicking when ``Graph.add_import`` is called with only one of + ``line_number`` and ``line_contents``. + 3.16 (2026-08-28) ----------------- diff --git a/docs/usage.rst b/docs/usage.rst index 169ef18c..1d83a4e0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -571,6 +571,8 @@ Methods for manipulating the graph :param str line_contents: The line that contains the import statement. :return: None + :raises: ``ValueError`` if only one of ``line_number`` or ``line_contents`` is supplied. + .. py:function:: ImportGraph.remove_import(importer, imported) Remove a direct import between two modules. Does not remove the modules themselves. diff --git a/rust/src/graph/mod.rs b/rust/src/graph/mod.rs index 822184b5..8bf287f0 100644 --- a/rust/src/graph/mod.rs +++ b/rust/src/graph/mod.rs @@ -271,22 +271,26 @@ impl GraphWrapper { imported: &str, line_number: Option, line_contents: Option<&str>, - ) { - let importer = self._graph.get_or_add_module(importer).token(); - let imported = self._graph.get_or_add_module(imported).token(); + ) -> PyResult<()> { match (line_number, line_contents) { (Some(line_number), Some(line_contents)) => { + let importer = self._graph.get_or_add_module(importer).token(); + let imported = self._graph.get_or_add_module(imported).token(); self._graph - .add_detailed_import(importer, imported, line_number, line_contents) + .add_detailed_import(importer, imported, line_number, line_contents); } (None, None) => { + let importer = self._graph.get_or_add_module(importer).token(); + let imported = self._graph.get_or_add_module(imported).token(); self._graph.add_import(importer, imported); } _ => { - // TODO handle better. - panic!("Expected line_number and line_contents, or neither."); + return Err(PyValueError::new_err( + "Expected line_number and line_contents, or neither.", + )); } } + Ok(()) } #[pyo3(signature = (*, importer, imported))] diff --git a/src/grimp/application/graph.py b/src/grimp/application/graph.py index e50c9682..037385b3 100644 --- a/src/grimp/application/graph.py +++ b/src/grimp/application/graph.py @@ -142,6 +142,9 @@ def add_import( """ Add a direct import between two modules to the graph. If the modules are not already present, they will be added to the graph. + + Raises: + ValueError if only one of line_number and line_contents is supplied. """ self._cached_modules = None self._rustgraph.add_import( diff --git a/tests/unit/application/graph/test_manipulation.py b/tests/unit/application/graph/test_manipulation.py index d4854855..c457aabd 100644 --- a/tests/unit/application/graph/test_manipulation.py +++ b/tests/unit/application/graph/test_manipulation.py @@ -171,6 +171,25 @@ def test_add_import(add_module): assert set() == graph.find_modules_directly_imported_by(b) +@pytest.mark.parametrize( + "kwargs", + ( + {"line_number": 1}, + {"line_contents": "import bar"}, + ), +) +def test_add_import_with_only_one_of_line_number_and_line_contents_raises(kwargs): + graph = ImportGraph() + + with pytest.raises( + ValueError, match=re.escape("Expected line_number and line_contents, or neither.") + ): + graph.add_import(importer="foo", imported="bar", **kwargs) + + # The graph should be left untouched. + assert graph.modules == set() + + class TestRemoveImport: def test_removes_from_modules(self): graph = ImportGraph() diff --git a/uv.lock b/uv.lock index 1696e27d..7a18037d 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -415,7 +415,7 @@ wheels = [ [[package]] name = "grimp" -version = "3.15" +version = "3.16" source = { editable = "." } [package.dev-dependencies] @@ -1161,23 +1161,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -1192,23 +1192,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version == '3.11.*'" }, - { name = "babel", marker = "python_full_version == '3.11.*'" }, - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, - { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -1224,23 +1224,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.15'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [