Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
54974ba
initial listing functions for cli
cadenmyers13 Oct 6, 2025
c9cf238
initial tests
cadenmyers13 Oct 6, 2025
f9a14c3
test for listing available examples
cadenmyers13 Oct 6, 2025
71423d5
test pretty print
cadenmyers13 Oct 6, 2025
a1ee907
api for copying packs and examples
cadenmyers13 Oct 6, 2025
b16c90b
have one function for copying examples
cadenmyers13 Oct 7, 2025
15c8111
move print_info to cli, add params to docstring in available_examples
cadenmyers13 Oct 7, 2025
db2ac9a
add fnferror to api
cadenmyers13 Oct 7, 2025
9fcef71
update available_examples test
cadenmyers13 Oct 7, 2025
c70ca27
have pkmg methods take temp_path instead of pkmg
cadenmyers13 Oct 7, 2025
d32201a
test print info and test copy examples
cadenmyers13 Oct 7, 2025
fbbf88c
checkpoint1 commit of working available examples tests
cadenmyers13 Oct 7, 2025
a458c5e
checkpoint conftest, build example_cases fixture
cadenmyers13 Oct 7, 2025
736b519
checkpoint packsmanager.py file
cadenmyers13 Oct 8, 2025
0bd26fc
checkpoint: conftest is building correctly
cadenmyers13 Oct 8, 2025
4be38f6
checkpoint, working version that gets examples dir outside of PacksMa…
cadenmyers13 Oct 8, 2025
280390c
checkpoint: _get_examples_dir moved back in PacksManager and paths ar…
cadenmyers13 Oct 8, 2025
99c4951
forgot to add conftest to previous checkpoint commit, so its added in…
cadenmyers13 Oct 8, 2025
aa65d02
checkpoint: test that the path and examples match
cadenmyers13 Oct 8, 2025
4aa15b0
checkpoint: add all 4 cases to test_packsmanager
cadenmyers13 Oct 8, 2025
cfb2273
Add case 5, split into separate tests, and refactor code for style/re…
cadenmyers13 Oct 9, 2025
fdf09b2
sort packs and examples when building dict with available_examples()
cadenmyers13 Oct 9, 2025
53ac9a2
add case 5 to conftest
cadenmyers13 Oct 9, 2025
ce1efa7
remove printed fail statement
cadenmyers13 Oct 9, 2025
c39dfae
rm unused helper function I made in conftest
cadenmyers13 Oct 9, 2025
c2e8076
add comment for case5 ex
cadenmyers13 Oct 9, 2025
d1038b7
UCs for copy_examples tests
cadenmyers13 Oct 9, 2025
85bb747
treat each 'caseX' as the root dir, build out from there
cadenmyers13 Oct 9, 2025
0d08dbe
change wording for comment
cadenmyers13 Oct 9, 2025
fcbb1a2
update innit to accept root_path
cadenmyers13 Oct 9, 2025
52aa4e7
better function name
cadenmyers13 Oct 9, 2025
e8c8ee5
revert some files to upstream/main, doing only dict building tests on…
cadenmyers13 Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/diffpy/cmi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
58 changes: 54 additions & 4 deletions src/diffpy/cmi/packsmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand All @@ -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.

Expand Down
78 changes: 78 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a requirements/packs dir that we we will use as root_dir in the test

Copy link
Contributor Author

@cadenmyers13 cadenmyers13 Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the tree structure here: #53 (comment). This is what the temp dir looks like. let me know if this looks okay. I believe everything is being correctly and there is no magic

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."""
Expand Down
49 changes: 49 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -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
(),
],
Copy link
Contributor Author

@cadenmyers13 cadenmyers13 Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbillinge ready for review here^^^^. Some use cases I jotted down. Let me know what you think.

)
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heads up, I am, not reviewing these till we have the dict building done.

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."""
Expand Down
93 changes: 93 additions & 0 deletions tests/test_packsmanager.py
Original file line number Diff line number Diff line change
@@ -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 {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we leave these here, we need to add case 5 here also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# 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()
Loading