""" Tests for langbot.pkg.utils.importutil module. Tests import utility functions: - import_dir: imports modules from a directory - import_modules_in_pkg: imports all modules in a package - import_modules_in_pkgs: imports all modules in multiple packages - import_dot_style_dir: imports modules using dot notation path - read_resource_file: reads a text resource file - read_resource_file_bytes: reads a binary resource file - list_resource_files: lists files in a resource directory Uses mocking for import operations to avoid actual module imports. """ import pytest import importlib import os from unittest.mock import patch, MagicMock, call class TestImportDir: """Test import_dir function.""" def test_calls_importlib_for_each_python_file(self, tmp_path, monkeypatch): """Should call importlib.import_module for each .py file.""" module_dir = tmp_path / 'test_modules' module_dir.mkdir() (module_dir / '__init__.py').write_text('') (module_dir / 'module_a.py').write_text("VALUE_A = 'a'\n") (module_dir / 'module_b.py').write_text("VALUE_B = 'b'\n") (module_dir / 'readme.txt').write_text('not a module') from langbot.pkg.utils import importutil monkeypatch.setattr(importutil, '__file__', str(tmp_path / 'pkg' / 'utils' / 'importutil.py')) with patch.object(importlib, 'import_module') as mock_import: importutil.import_dir(str(module_dir), path_prefix='test_prefix.') # Should call import_module for each .py file (excluding __init__.py) assert mock_import.call_count == 2 mock_import.assert_has_calls( [call('test_prefix.test_modules.module_a'), call('test_prefix.test_modules.module_b')], any_order=True, ) def test_skips_init_py(self, tmp_path, monkeypatch): """Should skip __init__.py when importing.""" module_dir = tmp_path / 'test_modules' module_dir.mkdir() (module_dir / '__init__.py').write_text('') (module_dir / 'regular.py').write_text('VALUE = 1\n') from langbot.pkg.utils import importutil monkeypatch.setattr(importutil, '__file__', str(tmp_path / 'pkg' / 'utils' / 'importutil.py')) with patch.object(importlib, 'import_module') as mock_import: importutil.import_dir(str(module_dir), path_prefix='test_prefix.') mock_import.assert_called_once_with('test_prefix.test_modules.regular') def test_ignores_non_py_files(self, tmp_path, monkeypatch): """Should ignore non-.py files.""" module_dir = tmp_path / 'test_modules' module_dir.mkdir() (module_dir / 'module.py').write_text('VALUE = 1\n') (module_dir / 'readme.txt').write_text('text') (module_dir / 'data.json').write_text('{}') from langbot.pkg.utils import importutil monkeypatch.setattr(importutil, '__file__', str(tmp_path / 'pkg' / 'utils' / 'importutil.py')) with patch.object(importlib, 'import_module') as mock_import: importutil.import_dir(str(module_dir), path_prefix='test_prefix.') mock_import.assert_called_once_with('test_prefix.test_modules.module') class TestImportModulesInPkg: """Test import_modules_in_pkg function.""" def test_imports_modules_from_package(self, tmp_path): """Should import all modules from a package object.""" mock_pkg = MagicMock() mock_pkg.__file__ = str(tmp_path / '__init__.py') (tmp_path / '__init__.py').write_text('') (tmp_path / 'mod1.py').write_text('MOD1 = 1\n') from langbot.pkg.utils import importutil with patch.object(importutil, 'import_dir') as mock_import_dir: importutil.import_modules_in_pkg(mock_pkg) mock_import_dir.assert_called_once() call_path = mock_import_dir.call_args[0][0] assert call_path == str(tmp_path) class TestImportModulesInPkgs: """Test import_modules_in_pkgs function.""" def test_imports_from_multiple_packages(self): """Should call import_modules_in_pkg for each package.""" from langbot.pkg.utils import importutil mock_pkg1 = MagicMock() mock_pkg1.__file__ = '/path/to/pkg1/__init__.py' mock_pkg2 = MagicMock() mock_pkg2.__file__ = '/path/to/pkg2/__init__.py' with patch.object(importutil, 'import_modules_in_pkg') as mock_import: importutil.import_modules_in_pkgs([mock_pkg1, mock_pkg2]) assert mock_import.call_args_list == [call(mock_pkg1), call(mock_pkg2)] class TestImportDotStyleDir: """Test import_dot_style_dir function.""" def test_converts_dot_notation_to_path(self, tmp_path): """Should convert dot notation to path and import.""" # Create structure matching the dot notation (tmp_path / 'my').mkdir() (tmp_path / 'my' / 'pkg').mkdir() (tmp_path / 'my' / 'pkg' / 'test').mkdir() from langbot.pkg.utils import importutil with patch.object(importutil, 'import_dir') as mock_import_dir: importutil.import_dot_style_dir('my.pkg.test') mock_import_dir.assert_called_once_with(os.path.join('my', 'pkg', 'test')) class TestReadResourceFile: """Test read_resource_file function.""" def test_reads_resource_file_content(self): """Should read content from a resource file.""" from langbot.pkg.utils import importutil content = importutil.read_resource_file('templates/config.yaml') assert 'api:' in content assert 'edition: community' in content def test_raises_for_nonexistent_file(self): """Should raise exception for non-existent resource file.""" from langbot.pkg.utils import importutil with pytest.raises((FileNotFoundError, Exception)): importutil.read_resource_file('nonexistent/path/file.txt') class TestReadResourceFileBytes: """Test read_resource_file_bytes function.""" def test_reads_resource_file_as_bytes(self): """Should read content as bytes from a resource file.""" from langbot.pkg.utils import importutil content = importutil.read_resource_file_bytes('templates/config.yaml') assert b'api:' in content assert b'edition: community' in content def test_raises_for_nonexistent_file_bytes(self): """Should raise exception for non-existent resource file.""" from langbot.pkg.utils import importutil with pytest.raises((FileNotFoundError, Exception)): importutil.read_resource_file_bytes('nonexistent/path/file.txt') class TestListResourceFiles: """Test list_resource_files function.""" def test_lists_files_in_resource_directory(self): """Should list files in a resource directory.""" from langbot.pkg.utils import importutil files = importutil.list_resource_files('templates') assert 'config.yaml' in files assert 'default-pipeline-config.json' in files assert all(isinstance(file, str) for file in files) def test_raises_for_nonexistent_directory(self): """Should raise exception for non-existent directory.""" from langbot.pkg.utils import importutil with pytest.raises((FileNotFoundError, Exception)): importutil.list_resource_files('nonexistent_directory_xyz') if __name__ == '__main__': pytest.main([__file__, '-v'])