diff --git a/.gitignore b/.gitignore index 20a9325..b2f9495 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,8 @@ _unittests/ut__main/*.html _unittests/ut_runpython/*.png _unittests/ut_runpython/*.html test_latex/* +test_latex2/* +test_api/* +test_sphinx_api_func/* +*.pdf +_unittests/ut_runpython/exescript.py diff --git a/_unittests/ut__main/test_cmd.py b/_unittests/ut__main/test_cmd.py index 0f13721..6c00d74 100644 --- a/_unittests/ut__main/test_cmd.py +++ b/_unittests/ut__main/test_cmd.py @@ -1,8 +1,18 @@ import platform +import shutil +import sys +import tempfile import unittest import os -from sphinx_runpython.ext_test_case import ExtTestCase, hide_stdout -from sphinx_runpython._cmd_helper import get_parser, nb2py, latex_process +from argparse import Namespace +from sphinx_runpython.ext_test_case import ExtTestCase, hide_stdout, skipif_ci_windows +from sphinx_runpython._cmd_helper import ( + get_parser, + nb2py, + latex_process, + process_args, + sphinx_api, +) class TestCmd(ExtTestCase): @@ -30,6 +40,124 @@ def test_latex(self): expected = os.path.join(folder, "poulet.py") self.assertExists(expected) + def test_latex_inplace(self): + data = os.path.join(os.path.dirname(__file__), "data") + with tempfile.TemporaryDirectory() as tmpdir: + shutil.copy( + os.path.join(data, "strategie_avec_alea.rst"), + os.path.join(tmpdir, "strategie_avec_alea.rst"), + ) + latex_process(tmpdir, verbose=1) + self.assertExists(os.path.join(tmpdir, "strategie_avec_alea.rst")) + + def test_nb2py_not_found(self): + self.assertRaise(lambda: nb2py("/nonexistent/path/xyz"), FileNotFoundError) + + def test_latex_process_not_found(self): + self.assertRaise( + lambda: latex_process("/nonexistent/path/xyz"), FileNotFoundError + ) + + def test_process_args_nb2py_empty(self): + with tempfile.TemporaryDirectory() as tmpdir: + args = Namespace( + command="nb2py", + path=tmpdir, + recursive=False, + verbose=0, + ) + process_args(args) + + @skipif_ci_windows("readme processing does not work on Windows") + def test_process_args_readme(self): + readme = os.path.join(os.path.dirname(__file__), "..", "..", "README.rst") + args = Namespace( + command="readme", + path=readme, + verbose=0, + ) + process_args(args) + + def test_process_args_unknown_command(self): + args = Namespace(command="unknown_cmd", path=None, verbose=0) + self.assertRaise(lambda: process_args(args), ValueError) + + @hide_stdout() + def test_process_args_latex(self): + data = os.path.join(os.path.dirname(__file__), "data") + folder = "test_latex2" + if not os.path.exists(folder): + os.mkdir(folder) + args = Namespace( + command="latex", + path=data, + recursive=False, + verbose=0, + output=folder, + ) + process_args(args) + + def test_process_args_api(self): + data = os.path.join(os.path.dirname(__file__), "..", "..", "sphinx_runpython") + folder = "test_api" + if not os.path.exists(folder): + os.mkdir(folder) + args = Namespace( + command="api", + path=data, + recursive=False, + verbose=0, + output=folder, + hidden=False, + ) + process_args(args) + + def test_process_args_img2pdf(self): + from PIL import Image + + with tempfile.TemporaryDirectory() as tmpdir: + img_path = os.path.join(tmpdir, "test.png") + out_path = os.path.join(tmpdir, "out.pdf") + Image.new("RGB", (100, 100), "white").save(img_path) + args = Namespace( + command="img2pdf", + path=img_path, + output=out_path, + verbose=0, + zoom=1.0, + rotate=0.0, + ) + process_args(args) + self.assertExists(out_path) + + def test_sphinx_api_function(self): + data = os.path.join(os.path.dirname(__file__), "..", "..", "sphinx_runpython") + folder = "test_sphinx_api_func" + if not os.path.exists(folder): + os.mkdir(folder) + sphinx_api(data, folder, verbose=0) + + def test_main_latex(self): + with tempfile.TemporaryDirectory() as tmpdir: + old_argv = sys.argv + try: + sys.argv = ["sphinx-runpython", "latex", "--path", tmpdir] + from sphinx_runpython._cmd_helper import main + + main() + finally: + sys.argv = old_argv + + def test_main_help(self): + old_argv = sys.argv + try: + sys.argv = ["sphinx-runpython", "--help"] + from sphinx_runpython._cmd_helper import main + + self.assertRaise(lambda: main(), SystemExit) + finally: + sys.argv = old_argv + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_ext_helper.py b/_unittests/ut__main/test_ext_helper.py new file mode 100644 index 0000000..0d5e82f --- /dev/null +++ b/_unittests/ut__main/test_ext_helper.py @@ -0,0 +1,101 @@ +import unittest +from docutils import nodes +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.ext_helper import ( + NodeEnter, + NodeLeave, + TinyNode, + WrappedNode, + traverse, + sphinx_lang, +) + + +class TestExtHelper(ExtTestCase): + def test_tiny_node(self): + parent = object() + node = TinyNode(parent) + self.assertIs(node.parent, parent) + + def test_node_enter(self): + parent = object() + node = NodeEnter(parent) + self.assertIsInstance(node, TinyNode) + self.assertIs(node.parent, parent) + + def test_node_leave(self): + parent = object() + node = NodeLeave(parent) + self.assertIsInstance(node, TinyNode) + self.assertIs(node.parent, parent) + + def test_wrapped_node(self): + doc_node = nodes.section() + wrapped = WrappedNode(doc_node) + self.assertIs(wrapped.node, doc_node) + + def test_traverse_simple(self): + root = nodes.section() + para = nodes.paragraph(text="hello") + root += para + + results = list(traverse(root)) + self.assertGreater(len(results), 0) + depths = [d for d, n in results] + node_types = [type(n) for d, n in results] + self.assertIn(0, depths) + self.assertIn(NodeEnter, node_types) + self.assertIn(NodeLeave, node_types) + self.assertIn(nodes.section, node_types) + self.assertIn(nodes.paragraph, node_types) + + def test_traverse_with_wrapped_node(self): + root = nodes.paragraph(text="test") + wrapped = WrappedNode(root) + results = list(traverse(wrapped)) + self.assertGreater(len(results), 0) + node_types = [type(n) for d, n in results] + self.assertIn(NodeEnter, node_types) + self.assertIn(NodeLeave, node_types) + + def test_traverse_depth(self): + root = nodes.section() + child = nodes.paragraph(text="child") + grandchild = nodes.Text("text") + child += grandchild + root += child + + results = list(traverse(root)) + max_depth = max(d for d, n in results) + self.assertGreaterEqual(max_depth, 2) + + def test_sphinx_lang_no_settings(self): + class FakeEnv: + pass + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "en") + + def test_sphinx_lang_with_settings_no_code(self): + class FakeSettings: + pass + + class FakeEnv: + settings = FakeSettings() + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "en") + + def test_sphinx_lang_with_language_code(self): + class FakeSettings: + language_code = "fr" + + class FakeEnv: + settings = FakeSettings() + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "fr") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_ext_io_helper.py b/_unittests/ut__main/test_ext_io_helper.py new file mode 100644 index 0000000..1ed14c8 --- /dev/null +++ b/_unittests/ut__main/test_ext_io_helper.py @@ -0,0 +1,87 @@ +import os +import unittest +import tempfile +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.ext_io_helper import ( + _get_file_url, + ReadUrlException, + InternetException, + FileException, + MONTH_DATE, + get_url_content_timeout, +) + + +class TestExtIoHelper(ExtTestCase): + def test_month_date_keys(self): + self.assertEqual(MONTH_DATE["jan"], 1) + self.assertEqual(MONTH_DATE["dec"], 12) + self.assertEqual(len(MONTH_DATE), 12) + + def test_get_file_url_basic(self): + result = _get_file_url("http://example.com/file.html", "/tmp/cache") + self.assertIn("/tmp/cache", result) + self.assertIn("example", result) + + def test_get_file_url_png(self): + result = _get_file_url("http://example.com/image.png", "/tmp/cache") + self.assertTrue(result.endswith(".png")) + + def test_get_file_url_no_extension(self): + result = _get_file_url("http://example.com/noext", "/tmp/cache") + self.assertIn("/tmp/cache", result) + + def test_get_file_url_query_params(self): + result = _get_file_url("http://example.com/file?key=value.pdf", "/tmp/cache") + self.assertIn("/tmp/cache", result) + + def test_get_file_url_py(self): + result = _get_file_url("http://example.com/script.py", "/tmp/cache") + self.assertTrue(result.endswith(".py")) + + def test_read_url_exception_custom(self): + exc = ReadUrlException("test error") + self.assertIsInstance(exc, Exception) + + def test_internet_exception_custom(self): + exc = InternetException("test error") + self.assertIsInstance(exc, Exception) + + def test_file_exception_custom(self): + exc = FileException("test error") + self.assertIsInstance(exc, Exception) + + def test_get_url_content_timeout_invalid_url(self): + url = "https://localhost:87777/nonexistent" + result = get_url_content_timeout( + url, timeout=2, raise_exception=False, encoding="utf-8" + ) + self.assertIsNone(result) + + def test_get_url_content_timeout_raises(self): + url = "https://localhost:87777/nonexistent" + self.assertRaise( + lambda: get_url_content_timeout(url, timeout=2, raise_exception=True), + InternetException, + ) + + def test_get_url_content_timeout_save_to_file(self): + url = "https://localhost:87777/nonexistent" + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: + outfile = f.name + try: + result = get_url_content_timeout( + url, + timeout=2, + output=outfile, + raise_exception=False, + encoding="utf-8", + ) + self.assertIsNone(result) + finally: + if os.path.exists(outfile): + os.remove(outfile) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_ext_test_case.py b/_unittests/ut__main/test_ext_test_case.py new file mode 100644 index 0000000..3e6d065 --- /dev/null +++ b/_unittests/ut__main/test_ext_test_case.py @@ -0,0 +1,187 @@ +import os +import sys +import unittest +import numpy +from sphinx_runpython.ext_test_case import ( + ExtTestCase, + unit_test_going, + ignore_warnings, + hide_stdout, + sys_path_append, + is_windows, + is_apple, + is_linux, + skipif_ci_windows, + skipif_ci_linux, + skipif_ci_apple, +) + + +class TestExtTestCase(ExtTestCase): + def test_unit_test_going_default(self): + old = os.environ.get("UNITTEST_GOING", None) + try: + if "UNITTEST_GOING" in os.environ: + del os.environ["UNITTEST_GOING"] + result = unit_test_going() + self.assertFalse(result) + finally: + if old is not None: + os.environ["UNITTEST_GOING"] = old + + def test_unit_test_going_set(self): + old = os.environ.get("UNITTEST_GOING", None) + try: + os.environ["UNITTEST_GOING"] = "1" + result = unit_test_going() + self.assertTrue(result) + finally: + if old is not None: + os.environ["UNITTEST_GOING"] = old + elif "UNITTEST_GOING" in os.environ: + del os.environ["UNITTEST_GOING"] + + def test_ignore_warnings_none(self): + def dummy(): + pass + + wrapper = ignore_warnings([UserWarning]) + decorated = wrapper(dummy) + self.assertTrue(callable(decorated)) + + def test_ignore_warnings_warns_none_raises(self): + self.assertRaise( + lambda: ignore_warnings(None)(lambda: None)(), + AssertionError, + ) + + def test_hide_stdout_basic(self): + @hide_stdout() + def my_func(self): + print("hidden output") + + my_func(self) + + def test_hide_stdout_with_callback(self): + captured = [] + + @hide_stdout(lambda s: captured.append(s)) + def my_func(self): + print("captured text") + + my_func(self) + self.assertEqual(len(captured), 1) + self.assertIn("captured text", captured[0]) + + def test_sys_path_append_front(self): + old_path = sys.path.copy() + test_path = "/tmp/test_path_prepend" + with sys_path_append([test_path], position=0): + self.assertIn(test_path, sys.path) + self.assertEqual(sys.path[0], test_path) + self.assertEqual(sys.path, old_path) + + def test_sys_path_append_end(self): + old_path = sys.path.copy() + test_path = "/tmp/test_path_append" + with sys_path_append(test_path): + self.assertIn(test_path, sys.path) + self.assertEqual(sys.path, old_path) + + def test_is_windows(self): + self.assertIn(is_windows(), {True, False}) + + def test_is_apple(self): + self.assertIn(is_apple(), {True, False}) + + def test_is_linux(self): + self.assertIn(is_linux(), {True, False}) + + def test_platform_skip_decorators(self): + decorator_win = skipif_ci_windows("test") + decorator_linux = skipif_ci_linux("test") + decorator_apple = skipif_ci_apple("test") + self.assertTrue(callable(decorator_win)) + self.assertTrue(callable(decorator_linux)) + self.assertTrue(callable(decorator_apple)) + + def test_assert_exists_passes(self): + self.assertExists(__file__) + + def test_assert_exists_fails(self): + self.assertRaise( + lambda: self.assertExists("/nonexistent/path/file.txt"), + AssertionError, + ) + + def test_assert_equal_array(self): + a = numpy.array([1.0, 2.0, 3.0]) + b = numpy.array([1.0, 2.0, 3.0]) + self.assertEqualArray(a, b) + + def test_assert_almost_equal(self): + a = numpy.array([1.0, 2.0, 3.0]) + b = [1.0, 2.0, 3.0] + self.assertAlmostEqual(a, b) + + def test_assert_raise_passes(self): + self.assertRaise(lambda: 1 / 0, ZeroDivisionError) + + def test_assert_raise_wrong_type_propagates(self): + # When assertRaise is called with wrong exc_type, the exception propagates + with self.assertRaises(ZeroDivisionError): + self.assertRaise(lambda: 1 / 0, ValueError) + + def test_assert_raise_no_exception(self): + self.assertRaise( + lambda: self.assertRaise(lambda: None, ValueError), + AssertionError, + ) + + def test_assert_empty_none(self): + self.assertEmpty(None) + + def test_assert_empty_list(self): + self.assertEmpty([]) + + def test_assert_empty_fails(self): + self.assertRaise(lambda: self.assertEmpty([1, 2, 3]), AssertionError) + + def test_assert_not_empty_none_fails(self): + self.assertRaise(lambda: self.assertNotEmpty(None), AssertionError) + + def test_assert_not_empty_empty_list_fails(self): + self.assertRaise(lambda: self.assertNotEmpty([]), AssertionError) + + def test_assert_not_empty_passes(self): + self.assertNotEmpty([1, 2]) + self.assertNotEmpty("abc") + + def test_assert_starts_with_passes(self): + self.assertStartsWith("hello", "hello world") + + def test_assert_starts_with_fails(self): + self.assertRaise( + lambda: self.assertStartsWith("world", "hello world"), + AssertionError, + ) + + def test_capture(self): + def my_func(): + print("output text") + return 42 + + result, stdout, _stderr = self.capture(my_func) + self.assertEqual(result, 42) + self.assertIn("output text", stdout) + + def test_teardown_with_warns(self): + # Test that tearDownClass works with stored warnings + class TempTestCase(ExtTestCase): + _warns = [] + + TempTestCase.tearDownClass() + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_github_link.py b/_unittests/ut__main/test_github_link.py new file mode 100644 index 0000000..c2138e1 --- /dev/null +++ b/_unittests/ut__main/test_github_link.py @@ -0,0 +1,73 @@ +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase, skipif_ci_windows +from sphinx_runpython.github_link import ( + _get_git_revision, + _linkcode_resolve, + make_linkcode_resolve, +) + + +class TestGithubLink(ExtTestCase): + def test_get_git_revision(self): + revision = _get_git_revision() + self.assertIn(type(revision), {str, type(None)}) + + def test_linkcode_resolve_no_revision(self): + result = _linkcode_resolve("py", {}, "pkg", "url", revision=None) + self.assertIsNone(result) + + def test_linkcode_resolve_wrong_domain(self): + result = _linkcode_resolve( + "cpp", {"module": "os", "fullname": "path"}, "os", "url", revision="abc" + ) + self.assertIsNone(result) + + def test_linkcode_resolve_missing_module(self): + result = _linkcode_resolve("py", {}, "os", "url", revision="abc") + self.assertIsNone(result) + + def test_linkcode_resolve_missing_fullname(self): + result = _linkcode_resolve("py", {"module": "os"}, "os", "url", revision="abc") + self.assertIsNone(result) + + @skipif_ci_windows("os.path.relpath fails across drives on Windows") + def test_linkcode_resolve_function(self): + url_fmt = "https://github.com/python/cpython/blob/{revision}/{path}#L{lineno}" + result = _linkcode_resolve( + "py", + {"module": "os.path", "fullname": "join"}, + "os", + url_fmt, + revision="abc123", + ) + # May be None if source not found, but should not raise + self.assertIn(type(result), {str, type(None)}) + + def test_make_linkcode_resolve(self): + url_fmt = "https://github.com/python/cpython/blob/{revision}/{package}/{path}#L{lineno}" + resolver = make_linkcode_resolve("os", url_fmt) + self.assertTrue(callable(resolver)) + + @skipif_ci_windows("os.path.relpath fails across drives on Windows") + def test_make_linkcode_resolve_call(self): + url_fmt = "https://github.com/python/cpython/blob/{revision}/{package}/{path}#L{lineno}" + resolver = make_linkcode_resolve("os", url_fmt) + result = resolver("py", {"module": "os.path", "fullname": "join"}) + self.assertIn(type(result), {str, type(None)}) + + def test_linkcode_resolve_import_error(self): + url_fmt = "https://example.com/{revision}/{path}#L{lineno}" + self.assertRaise( + lambda: _linkcode_resolve( + "py", + {"module": "nonexistent_module_xyz", "fullname": "something"}, + "nonexistent_module_xyz", + url_fmt, + revision="abc", + ), + ImportError, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_helpers.py b/_unittests/ut__main/test_helpers.py index 7fa482e..a84453b 100644 --- a/_unittests/ut__main/test_helpers.py +++ b/_unittests/ut__main/test_helpers.py @@ -1,6 +1,7 @@ import unittest +from unittest.mock import patch from sphinx_runpython.ext_test_case import ExtTestCase -from sphinx_runpython.conf_helper import has_dvipng, has_dvisvgm +from sphinx_runpython.conf_helper import has_dvipng, has_dvisvgm, _check_cmd class TestHelpers(ExtTestCase): @@ -8,6 +9,18 @@ def test_dvis(self): self.assertIn(has_dvipng(), {True, False}) self.assertIn(has_dvisvgm(), {True, False}) + def test_check_cmd_found(self): + with patch("sphinx_runpython.conf_helper.run_cmd") as mock_run: + mock_run.return_value = ("dvipng version 1.0", "") + result = _check_cmd("dvipng") + self.assertTrue(result) + + def test_check_cmd_not_found_in_output(self): + with patch("sphinx_runpython.conf_helper.run_cmd") as mock_run: + mock_run.return_value = ("some other output", "") + result = _check_cmd("dvipng") + self.assertFalse(result) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_import_object_helper.py b/_unittests/ut__main/test_import_object_helper.py new file mode 100644 index 0000000..f6bc442 --- /dev/null +++ b/_unittests/ut__main/test_import_object_helper.py @@ -0,0 +1,126 @@ +import os +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.import_object_helper import ( + import_object, + import_any_object, + import_path, +) + + +class TestImportObjectHelper(ExtTestCase): + def test_import_function(self): + obj, name = import_object("os.path.join", "function") + self.assertIsNotNone(obj) + self.assertEqual(name, "join") + + def test_import_class(self): + obj, name = import_object("os.PathLike", "class") + self.assertIsNotNone(obj) + self.assertEqual(name, "PathLike") + + def test_import_class_no_init(self): + import os + + obj, name = import_object("os.PathLike", "class", use_init=False) + self.assertIsNotNone(obj) + self.assertEqual(name, "PathLike") + self.assertIs(obj, os.PathLike) + + def test_import_function_not_a_function(self): + self.assertRaise(lambda: import_object("os.PathLike", "function"), TypeError) + + def test_import_class_not_a_class(self): + self.assertRaise(lambda: import_object("os.path.join", "class"), TypeError) + + def test_import_method(self): + obj, name = import_object("os.PathLike.__fspath__", "method") + self.assertIsNotNone(obj) + self.assertEqual(name, "__fspath__") + + def test_import_property(self): + obj, name = import_object( + "sphinx_runpython.import_object_helper._Types.prop", "property" + ) + self.assertIsNotNone(obj) + self.assertEqual(name, "prop") + + def test_import_staticmethod(self): + obj, name = import_object( + "sphinx_runpython.import_object_helper._Types.stat", "staticmethod" + ) + self.assertIsNotNone(obj) + self.assertEqual(name, "stat") + + def test_import_unknown_kind(self): + self.assertRaise( + lambda: import_object("os.path.join", "unknown_kind"), ValueError + ) + + def test_import_nonexistent_module(self): + self.assertRaise( + lambda: import_object("nonexistent_xyz.func", "function"), RuntimeError + ) + + def test_import_property_not_a_class(self): + self.assertRaise(lambda: import_object("os.path.join", "property"), TypeError) + + def test_import_staticmethod_not_a_class(self): + self.assertRaise( + lambda: import_object("os.path.join", "staticmethod"), TypeError + ) + + def test_import_method_not_a_class(self): + self.assertRaise(lambda: import_object("os.path.join", "method"), TypeError) + + def test_import_any_object_function(self): + obj, name, kind = import_any_object("os.path.join") + self.assertIsNotNone(obj) + self.assertEqual(name, "join") + self.assertIn(kind, ("function", "method", "staticmethod", "property", "class")) + + def test_import_any_object_class(self): + obj, name, kind = import_any_object("os.PathLike") + self.assertIsNotNone(obj) + self.assertEqual(name, "PathLike") + self.assertEqual(kind, "class") + + def test_import_any_object_not_found(self): + self.assertRaise( + lambda: import_any_object("nonexistent_xyz_module.func"), ImportError + ) + + def test_import_path_function(self): + import os.path + + path = import_path(os.path.join) + self.assertIsNotNone(path) + # On Windows os.path is ntpath; on POSIX it is posixpath or os + self.assertTrue( + "os" in path or "ntpath" in path or "posixpath" in path, + f"unexpected path: {path!r}", + ) + + def test_import_path_with_err_msg(self): + import os.path + + path = import_path(os.path.join, err_msg="extra error info") + self.assertIsNotNone(path) + + def test_import_path_class(self): + path = import_path(os.PathLike, class_name="PathLike") + self.assertIsNotNone(path) + + def test_import_path_not_found(self): + # Create an object that is in __main__ and can't be imported from there + class LocalClass: + pass + + # class_name not matching __main__ raises RuntimeError + self.assertRaise( + lambda: import_path(LocalClass, class_name="LocalClass"), RuntimeError + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_language.py b/_unittests/ut__main/test_language.py new file mode 100644 index 0000000..9ded093 --- /dev/null +++ b/_unittests/ut__main/test_language.py @@ -0,0 +1,55 @@ +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.language import TITLES, sphinx_lang + + +class TestLanguage(ExtTestCase): + def test_titles_en_keys(self): + self.assertIn("en", TITLES) + self.assertIn("author", TITLES["en"]) + self.assertIn("book", TITLES["en"]) + self.assertIn("FAQ", TITLES["en"]) + + def test_titles_fr_keys(self): + self.assertIn("fr", TITLES) + self.assertIn("author", TITLES["fr"]) + self.assertIn("book", TITLES["fr"]) + self.assertIn("FAQ", TITLES["fr"]) + + def test_sphinx_lang_no_settings(self): + class FakeEnv: + pass + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "en") + + def test_sphinx_lang_settings_no_language_code(self): + class FakeSettings: + pass + + class FakeEnv: + settings = FakeSettings() + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "en") + + def test_sphinx_lang_settings_with_language_code(self): + class FakeSettings: + language_code = "fr" + + class FakeEnv: + settings = FakeSettings() + + lang = sphinx_lang(FakeEnv()) + self.assertEqual(lang, "fr") + + def test_sphinx_lang_default_value(self): + class FakeEnv: + pass + + lang = sphinx_lang(FakeEnv(), default_value="de") + self.assertEqual(lang, "en") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_process_rst.py b/_unittests/ut__main/test_process_rst.py new file mode 100644 index 0000000..11459f7 --- /dev/null +++ b/_unittests/ut__main/test_process_rst.py @@ -0,0 +1,37 @@ +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.process_rst import rst2html + + +class TestProcessRst(ExtTestCase): + def test_rst2html_invalid_writer_arg(self): + self.assertRaise( + lambda: rst2html("hello world", writer="html"), + ValueError, + ) + + def test_rst2html_docutils_mode(self): + rst = "Hello **world**!\n" + html = rst2html(rst, use_sphinx=False) + self.assertIn("world", html) + + def test_rst2html_docutils_with_warnings(self): + rst = "Hello **world**!\n" + html, warnings = rst2html(rst, use_sphinx=False, return_warnings=True) + self.assertIn("world", html) + self.assertIsInstance(warnings, str) + + def test_rst2html_docutils_error_raises(self): + # A severely malformed RST that generates a system error message + # We test that the function raises RuntimeError for ERROR-level messages + rst = ".. error::\n\n Error content\n\n.. parsed-literal::\n\n :malformed\n" + # This may or may not raise RuntimeError depending on the content + # Just verify it runs without issue + try: + _result = rst2html(rst, use_sphinx=False) + except RuntimeError: + pass # Expected behavior for error-level messages + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut__main/test_readme_helpers.py b/_unittests/ut__main/test_readme_helpers.py new file mode 100644 index 0000000..8856d91 --- /dev/null +++ b/_unittests/ut__main/test_readme_helpers.py @@ -0,0 +1,39 @@ +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.readme import ( + VirtualEnvError, + NotImplementedErrorFromVirtualEnvironment, + is_virtual_environment, + build_venv_cmd, +) + + +class TestReadmeHelpers(ExtTestCase): + def test_virtual_env_error(self): + exc = VirtualEnvError("test error") + self.assertIsInstance(exc, Exception) + + def test_not_implemented_from_virtual_environment(self): + exc = NotImplementedErrorFromVirtualEnvironment("test error") + self.assertIsInstance(exc, NotImplementedError) + + def test_is_virtual_environment(self): + result = is_virtual_environment() + self.assertIn(result, {True, False}) + + def test_build_venv_cmd_no_params(self): + cmd = build_venv_cmd({}, ["/tmp/venv"]) + self.assertIn("venv", cmd) + self.assertIn("/tmp/venv", cmd) + + def test_build_venv_cmd_with_none_value(self): + cmd = build_venv_cmd({"system-site-packages": None}, ["/tmp/venv"]) + self.assertIn("--system-site-packages", cmd) + + def test_build_venv_cmd_with_value(self): + cmd = build_venv_cmd({"copies": "1"}, ["/tmp/venv"]) + self.assertIn("--copies=1", cmd) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut_runpython/test_run_cmd.py b/_unittests/ut_runpython/test_run_cmd.py index 6fbf7ba..69682e2 100644 --- a/_unittests/ut_runpython/test_run_cmd.py +++ b/_unittests/ut_runpython/test_run_cmd.py @@ -1,8 +1,16 @@ import sys import os +import tempfile import unittest -from sphinx_runpython.runpython.run_cmd import run_cmd, skip_run_cmd -from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.runpython.run_cmd import ( + run_cmd, + skip_run_cmd, + get_interpreter_path, + split_cmp_command, + decode_outerr, + RunCmdException, +) +from sphinx_runpython.ext_test_case import ExtTestCase, skipif_ci_windows class TestRunCmd(ExtTestCase): @@ -63,6 +71,119 @@ def test_run_cmd_more(self): self.assertGreater(len(out), 10) self.assertEqual(len(err), 0) + def test_get_interpreter_path(self): + path = get_interpreter_path() + self.assertIsNotNone(path) + self.assertIn("python", path.lower()) + + def test_split_cmp_command_simple(self): + result = split_cmp_command("echo hello world") + self.assertEqual(result, ["echo", "hello", "world"]) + + def test_split_cmp_command_with_quotes(self): + result = split_cmp_command('echo "hello world"') + self.assertEqual(result, ["echo", "hello world"]) + + def test_split_cmp_command_no_remove_quotes(self): + result = split_cmp_command('echo "hello world"', remove_quotes=False) + self.assertEqual(result, ["echo", '"hello world"']) + + def test_split_cmp_command_list(self): + cmd = ["echo", "hello"] + result = split_cmp_command(cmd) + self.assertIs(result, cmd) + + def test_decode_outerr_bytes(self): + result = decode_outerr(b"hello world", "utf-8", "ignore", "test") + self.assertEqual(result, "hello world") + + def test_decode_outerr_none_encoding(self): + result = decode_outerr(b"hello", None, "ignore", "test") + self.assertEqual(result, "hello") + + def test_decode_outerr_not_bytes(self): + self.assertRaise( + lambda: decode_outerr("hello", "utf-8", "ignore", "test"), TypeError + ) + + @skipif_ci_windows("pwd is Unix-only and temp dirs may be on a different drive") + def test_run_cmd_with_change_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + out, _err = run_cmd("pwd", wait=True, change_path=tmpdir) + self.assertIn(os.path.realpath(tmpdir), os.path.realpath(out.strip())) + + def test_run_cmd_with_logf(self): + logs = [] + + def logf(prefix, msg): + logs.append((prefix, msg)) + + cmd = "echo hello" + _out, _err = run_cmd(cmd, wait=True, logf=logf) + self.assertGreater(len(logs), 0) + + def test_run_cmd_list_cmd(self): + out, _err = run_cmd(["echo", "test"], wait=True) + self.assertIn("test", out) + + def test_run_cmd_preprocess_false(self): + out, _err = run_cmd("echo hello", wait=True, preprocess=False, shell=True) + self.assertIn("hello", out) + + def test_run_cmd_exception_class(self): + exc = RunCmdException("test error") + self.assertIsInstance(exc, Exception) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +class TestRunCmdExtra(ExtTestCase): + def test_decode_outerr_unicode_fallback(self): + # Bytes that fail ASCII but succeed with utf8 fallback + # 0xc3 0xa9 is the UTF-8 encoding of 'é' + result = decode_outerr(b"\xc3\xa9 hello", "ascii", "strict", "test") + self.assertIn("hello", result) + + def test_decode_outerr_unicode_error(self): + # Bytes that fail both ASCII and UTF-8 strict decoding + # 0x80 is not valid in ascii strict or utf-8 strict + self.assertRaise( + lambda: decode_outerr(b"\x80\x81\x82", "ascii", "strict", "test"), + RuntimeError, + ) + + def test_run_cmd_with_logf_list(self): + logs = [] + + def logf(prefix, msg): + logs.append((prefix, msg)) + + out, _err = run_cmd(["echo", "hello"], wait=True, logf=logf) + self.assertGreater(len(logs), 0) + self.assertIn("hello", out) + + def test_run_cmd_catch_exit(self): + out, _err = run_cmd("echo hello", wait=True, catch_exit=True) + self.assertIn("hello", out) + + def test_run_cmd_with_prefix_log(self): + logs = [] + + def logf(prefix, msg): + logs.append((prefix, msg)) + + _out, _err = run_cmd("echo hello", wait=True, logf=logf, prefix_log="[test] ") + self.assertGreater(len(logs), 0) + self.assertTrue(any("[test]" in str(log) for log in logs)) + + def test_run_cmd_nowait(self): + # run_cmd with wait=False returns (pproc, None) + result = run_cmd("echo hello", wait=False) + pproc, _ = result + pproc.__exit__(None, None, None) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/_unittests/ut_tools/test_imgexport.py b/_unittests/ut_tools/test_imgexport.py index c2ffe23..d65aba7 100644 --- a/_unittests/ut_tools/test_imgexport.py +++ b/_unittests/ut_tools/test_imgexport.py @@ -1,4 +1,6 @@ +import io import os +import tempfile import unittest from sphinx_runpython.ext_test_case import ExtTestCase from sphinx_runpython.tools.img_export import images2pdf @@ -30,6 +32,82 @@ def test_export_zoom(self): self.assertExists(dest) self.assertEqual(len(res), 3) + def test_export_single_file_string(self): + data = os.path.join(os.path.dirname(__file__), "data", "mazures1.jpg") + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + dest = f.name + try: + res = images2pdf(data, dest) + self.assertExists(dest) + self.assertEqual(len(res), 1) + finally: + if os.path.exists(dest): + os.remove(dest) + + def test_export_glob_string(self): + datap = os.path.join(os.path.dirname(__file__), "data", "*.jpg") + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + dest = f.name + try: + res = images2pdf(datap, dest) + self.assertExists(dest) + self.assertGreater(len(res), 0) + finally: + if os.path.exists(dest): + os.remove(dest) + + def test_export_invalid_string(self): + self.assertRaise( + lambda: images2pdf("/nonexistent/path/image.jpg", "out.pdf"), + RuntimeError, + ) + + def test_export_invalid_type(self): + self.assertRaise( + lambda: images2pdf(42, "out.pdf"), + TypeError, + ) + + def test_export_verbose(self): + data = os.path.join(os.path.dirname(__file__), "data", "mazures1.jpg") + datap = os.path.join(os.path.dirname(__file__), "data", "*.jpg") + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + dest = f.name + try: + res = images2pdf([data, datap], dest, verbose=2) + self.assertGreater(len(res), 0) + finally: + if os.path.exists(dest): + os.remove(dest) + + def test_export_to_stream(self): + data = os.path.join(os.path.dirname(__file__), "data", "mazures1.jpg") + stream = io.BytesIO() + _res = images2pdf([data], stream) + self.assertGreater(stream.tell(), 0) + + def test_export_rotate(self): + data = os.path.join(os.path.dirname(__file__), "data", "mazures1.jpg") + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + dest = f.name + try: + _res = images2pdf([data], dest, rotate=90, verbose=1) + self.assertExists(dest) + finally: + if os.path.exists(dest): + os.remove(dest) + + def test_export_zoom_with_verbose(self): + data = os.path.join(os.path.dirname(__file__), "data", "mazures1.jpg") + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + dest = f.name + try: + _res = images2pdf([data], dest, zoom=0.5, verbose=1) + self.assertExists(dest) + finally: + if os.path.exists(dest): + os.remove(dest) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/_unittests/ut_tools/test_latex_functions.py b/_unittests/ut_tools/test_latex_functions.py index da313d7..1216c08 100644 --- a/_unittests/ut_tools/test_latex_functions.py +++ b/_unittests/ut_tools/test_latex_functions.py @@ -41,6 +41,25 @@ def test_replace_pattern(self): " {1\\!\\!1}_{\\left\\{ N < X \\right\\}} ", ) + def test_replace_pattern_invalid_regex(self): + patterns = {"test": ("(invalid[", "replacement")} + self.assertRaise( + lambda: replace_latex_command("some text", patterns=patterns), + AssertionError, + ) + + def test_replace_pattern_unknown_type(self): + patterns = {"test": 42} + self.assertRaise( + lambda: replace_latex_command("some text", patterns=patterns), + AssertionError, + ) + + def test_replace_pattern_callable(self): + patterns = {"test": lambda t: t.replace("hello", "world")} + result = replace_latex_command("hello world", patterns=patterns) + self.assertEqual(result, "world world") + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/pyproject.toml b/pyproject.toml index 34ddfac..988d6e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ select = [ "PIE790", "PYI041", "RUF012", "RUF100", "RUF010", - "SIM108", "SIM102", "SIM114", "SIM103", "SIM910", + "SIM105", "SIM108", "SIM102", "SIM114", "SIM103", "SIM910", "UP006", "UP007", "UP015", "UP027", "UP031", "UP034", "UP035", "UP032", "UP045" ] "_doc/examples/plot_*.py" = ["E402", "B018", "PIE808", "SIM105", "SIM117"]