diff --git a/src/diffpy/cmi/__init__.py b/src/diffpy/cmi/__init__.py index 62108fd..6253d84 100644 --- a/src/diffpy/cmi/__init__.py +++ b/src/diffpy/cmi/__init__.py @@ -18,8 +18,23 @@ from importlib.resources import as_file, files -def get_package_dir(): - resource = files(__name__) +def get_package_dir(root_path=None): + """Get the package directory as a context manager. + + Parameters + ---------- + root_path : str, optional + Used for testing, overrides the files(__name__) call. + + Returns + ------- + context manager + A context manager that yields a pathlib.Path to the package directory. + """ + if root_path is None: + resource = files(__name__) + else: + resource = root_path return as_file(resource) diff --git a/src/diffpy/cmi/packsmanager.py b/src/diffpy/cmi/packsmanager.py index bd8b10b..166ded5 100644 --- a/src/diffpy/cmi/packsmanager.py +++ b/src/diffpy/cmi/packsmanager.py @@ -28,9 +28,9 @@ __all__ = ["PacksManager"] -def _installed_packs_dir() -> Path: +def _installed_packs_dir(root_path=None) -> Path: """Locate requirements/packs/ for the installed package.""" - with get_package_dir() as pkgdir: + with get_package_dir(root_path) as pkgdir: pkg = Path(pkgdir).resolve() for c in ( pkg / "requirements" / "packs", @@ -51,10 +51,29 @@ class PacksManager: packs_dir : pathlib.Path Absolute path to the installed packs directory. Defaults to `requirements/packs` under the installed package. + examples_dir : pathlib.Path + Absolute path to the installed examples directory. + Defaults to `docs/examples` under the installed package. """ - def __init__(self) -> None: - self.packs_dir = _installed_packs_dir() + def __init__(self, root_path=None) -> None: + self.packs_dir = _installed_packs_dir(root_path) + self.examples_dir = self._get_examples_dir() + + def _get_examples_dir(self) -> Path: + """Return the absolute path to the installed examples directory. + + Returns + ------- + pathlib.Path + Directory containing shipped examples. + + Raises + ------ + FileNotFoundError + If the examples directory cannot be located in the installation. + """ + return (self.packs_dir / ".." / ".." / "docs" / "examples").resolve() def available_packs(self) -> List[str]: """List all available packs. @@ -68,6 +87,37 @@ def available_packs(self) -> List[str]: p.stem for p in self.packs_dir.glob("*.txt") if p.is_file() ) + def available_examples(self) -> dict[str, List[tuple[str, Path]]]: + """Finds all examples for each pack and builds a dict. + + Parameters + ---------- + root_path : Path + Root path to the examples directory. + Returns + ------- + dict + A dictionary mapping pack names to lists of example names. + + Raises + ------ + FileNotFoundError + If the provided root_path does not exist or is not a directory. + """ + example_dir = self.examples_dir + examples_dict = {} + for pack_path in sorted(example_dir.iterdir()): + if pack_path.is_dir(): + pack_name = pack_path.stem + examples_dict[pack_name] = [] + for example_path in sorted(pack_path.iterdir()): + if example_path.is_dir(): + example_name = example_path.stem + examples_dict[pack_name].append( + (example_name, example_path) + ) + return examples_dict + def _resolve_pack_file(self, identifier: Union[str, Path]) -> Path: """Resolve a pack identifier to an absolute .txt path. diff --git a/tests/conftest.py b/tests/conftest.py index 3eaae7d..ebcfad9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,84 @@ def tmp_examples(tmp_path_factory): yield tmp_examples +@pytest.fixture(scope="session") +def example_cases(tmp_path_factory): + """Copy the entire examples tree into a temp directory once per test + session. + + Returns the path to that copy. + """ + root_temp_dir = tmp_path_factory.mktemp("temp") + + # case 1: pack with no examples + case1ex_dir = root_temp_dir / "case1" / "docs" / "examples" + case1 = case1ex_dir / "empty_pack" # empty_pack + case1.mkdir(parents=True, exist_ok=True) + case1req_dir = root_temp_dir / "case1" / "requirements" / "packs" + case1req_dir.mkdir(parents=True, exist_ok=True) + + # Case 2: pack with multiple examples + case2ex_dir = root_temp_dir / "case2" / "docs" / "examples" + case2a = ( + case2ex_dir / "full_pack" / "ex1" / "solution" / "diffpy-cmi" + ) # full_pack, ex1 + case2a.mkdir(parents=True, exist_ok=True) + (case2a / "script1.py").touch() + case2b = ( + case2ex_dir / "full_pack" / "ex2" / "random" / "path" + ) # full_pack, ex2 + case2b.mkdir(parents=True, exist_ok=True) + (case2b / "script1.py").touch() + (case2b / "script2.py").touch() + case2req_dir = root_temp_dir / "case2" / "requirements" / "packs" + case2req_dir.mkdir(parents=True, exist_ok=True) + + # Case 3: multiple packs with multiple examples + case3ex_dir = root_temp_dir / "case3" / "docs" / "examples" + case3a = case3ex_dir / "packA" / "ex1" # packA, ex1 + case3a.mkdir(parents=True, exist_ok=True) + (case3a / "script1.py").touch() + case3b = case3ex_dir / "packA" / "ex2" / "solutions" # packA, ex2 + case3b.mkdir(parents=True, exist_ok=True) + (case3b / "script2.py").touch() + case3c = ( + case3ex_dir / "packB" / "ex3" / "more" / "random" / "path" + ) # packB, ex3 + case3c.mkdir(parents=True, exist_ok=True) + (case3c / "script3.py").touch() + (case3c / "script4.py").touch() + case3req_dir = root_temp_dir / "case3" / "requirements" / "packs" + case3req_dir.mkdir(parents=True, exist_ok=True) + + # Case 4: no pack found (empty directory) + case4ex_dir = root_temp_dir / "case4" / "docs" / "examples" + case4 = case4ex_dir + case4.mkdir(parents=True, exist_ok=True) + case4req_dir = root_temp_dir / "case4" / "requirements" / "packs" + case4req_dir.mkdir(parents=True, exist_ok=True) + + # Case 5: multiple packs with the same example names + case5ex_dir = root_temp_dir / "case5" / "docs" / "examples" + case5a = case5ex_dir / "packA" / "ex1" / "path1" # packA, ex1 + case5a.mkdir(parents=True, exist_ok=True) + (case5a / "script1.py").touch() + case5b = case5ex_dir / "packB" / "ex1" / "path2" # packB, ex1 + case5b.mkdir(parents=True, exist_ok=True) + (case5b / "script2.py").touch() + case5req_dir = root_temp_dir / "case5" / "requirements" / "packs" + case5req_dir.mkdir(parents=True, exist_ok=True) + + yield root_temp_dir + + +@pytest.fixture(scope="session") +def target_dir(tmp_path_factory): + """Create a temporary directory to serve as the target for copying + examples.""" + target_directory = tmp_path_factory.mktemp("copy_target") + yield target_directory + + @pytest.fixture(scope="session", autouse=True) def use_headless_matplotlib(): """Force matplotlib to use a headless backend during tests.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 9eb9712..884c9f5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,60 @@ import os from pathlib import Path +from shutil import copytree import pytest from diffpy.cmi import cli +from diffpy.cmi.packsmanager import PacksManager +@pytest.mark.parametrize( + "input,to_cwd,expected", + [ + # PARAMS: + # input: list - list of example(s) and/or pack(s) to copy + # to_cwd: bool - whether to copy to cwd (default) or a target dir + # expected: list - path of copied example(s) and/or pack(s) + # 1a) user wants to copy one example to cwd + # 1b) user wants to copy one example to a target dir + (), + # 2a) user wants to copy multiple examples to cwd + # 2b) user wants to copy multiple examples to a target dir + (), + # 3a) user wants to copy all examples from a pack to cwd + # 3b) user wants to copy all examples from a pack to a target dir + (), + # 4a) user wants to copy all examples from multiple packs to cwd + # 4b) user wants to copy all examples from multiple packs to target dir + (), + # 5a) user wants to copy a combination of packs and examples to cwd + # 5b) user wants to copy a combination of packs and examples to target + (), + # 6a) user wants to copy all examples from all packs to cwd + # 6b) user wants to copy all examples from all packs to a target dir + (), + ], +) +def test_copy_examples(input, to_cwd, expected, example_cases, target_dir): + tmp_ex_dir = example_cases / input + copytree(tmp_ex_dir, target_dir) + # pkmg = PacksManager() + # actual = cli.copy_examples(str(target_dir)) + assert False + + +def test_print_info(temp_path, capsys): + pkmg = PacksManager() + actual = pkmg.available_examples(temp_path) + # pretty print the actual dict + pkmg.print_info(actual) + captured = capsys.readouterr() + output = captured.out.strip() + # check that output contains expected headers + assert "Available packs" in output or "Installed packs" in output + + +# NOTE: double check and remove these test after new above tests are made def test_map_pack_to_examples_structure(): """Test that map_pack_to_examples returns the right shape of data.""" diff --git a/tests/test_packsmanager.py b/tests/test_packsmanager.py new file mode 100644 index 0000000..d3af5d8 --- /dev/null +++ b/tests/test_packsmanager.py @@ -0,0 +1,93 @@ +import pytest + +from diffpy.cmi.packsmanager import PacksManager + +example_params = [ + # 1) pack with no examples. Expect {'empty_pack': []} + # 2) pack with multiple examples. + # Expect {'full_pack': [('example1`, path_to_1'), 'example2', path_to_2)] + # 3) multiple packs. Expect dict with multiple pack:tuple pairs + # 4) no pack found. Expect {} + # case 1: pack with no examples. Expect {'empty_pack': []} + # 5) multiple packs with the same example names + # Expect dict with multiple pack:tuple pairs + ( + "case1", + {"empty_pack": []}, + ), + # case 2: pack with multiple examples. + # Expect {'full_pack': [('example1', path_to_1), + # ('example2', path_to_2)]} + ( + "case2", + { + "full_pack": [ + ("ex1", "case2/docs/examples/full_pack/ex1"), + ("ex2", "case2/docs/examples/full_pack/ex2"), + ] + }, + ), + # case 3: multiple packs. Expect dict with multiple pack:tuple pairs + ( + "case3", + { + "packA": [ + ("ex1", "case3/docs/examples/packA/ex1"), + ("ex2", "case3/docs/examples/packA/ex2"), + ], + "packB": [("ex3", "case3/docs/examples/packB/ex3")], + }, + ), + ( # case 4: no pack found. Expect {} + "case4", + {}, + ), + ( # case 5: multiple packs with duplicate example names + # Expect dict with multiple pack:tuple pairs + "case5", + { + "packA": [ + ("ex1", "case5/docs/examples/packA/ex1"), + ], + "packB": [ + ("ex1", "case5/docs/examples/packB/ex1"), + ], + }, + ), +] + + +def paths_and_names_match(expected, actual, root): + """Compare two (example_name, path) lists ignoring temp dir + differences.""" + if len(expected) != len(actual): + return False + for (exp_name, exp_path), (act_name, act_path) in zip(expected, actual): + if exp_name != act_name: + return False + actual_rel_path = str(act_path.relative_to(root)) + if actual_rel_path != exp_path: + return False + return True + + +@pytest.mark.parametrize("input,expected", example_params) +def test_available_examples(input, expected, example_cases): + case_dir = example_cases / input + pkmg = PacksManager(case_dir) + actual = pkmg.available_examples() + assert actual.keys() == expected.keys() + for pack in expected: + assert paths_and_names_match( + expected[pack], actual[pack], example_cases + ) + + +@pytest.mark.parametrize("input,expected", example_params) +def test_tmp_file_structure(input, expected, example_cases): + example_path = example_cases / input + for path in example_path.rglob("*"): + if path.suffix: + assert path.is_file() + else: + assert path.is_dir()