Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions news/dict-build-test.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Add test for building examples dict.

**Changed:**

* Change example dict build process.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
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
53 changes: 49 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 (
Copy link
Contributor

Choose a reason for hiding this comment

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

do we know what this is doing? it seems to worry that the pkgdir coming from the init is sometimes something and sometimes something else. Why would that be? Its possible this is needed depending on whether the package is installed using -e or not. @Tieqiong do you comments on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sbillinge Not super familiar so I gave chatgpt your comments and it said this. It looks like it does have to do with installing with -e.

Screenshot 2025-10-10 at 11 04 21 AM

It also recommended something more readable like this (I left docstring but we can tune it as desired):

def _installed_packs_dir(root_path: Path | None = None) -> Path:
    """Return the path to the `requirements/packs` directory for this package.

    This function handles both normal and editable (`pip install -e .`)
    installation modes, which place package files in slightly different
    locations on disk.

    Parameters
    ----------
    root_path : Path or None, optional
        Used mainly for testing. Overrides the package directory
        returned by :func:`get_package_dir`.

    Returns
    -------
    Path
        The resolved path to `requirements/packs`.

    Raises
    ------
    FileNotFoundError
        If the `requirements/packs` directory cannot be found.
    """
    with get_package_dir(root_path) as pkgdir:
        pkgdir = Path(pkgdir).resolve()
        installed_layout = pkgdir / "requirements" / "packs"
        editable_layout = pkgdir.parents[1] / "requirements" / "packs"
        for candidate in (installed_layout, editable_layout):
            if candidate.is_dir():
                return candidate

    raise FileNotFoundError(
        f"Could not locate requirements/packs under {pkgdir}. "
        "Check your installation or installation mode (-e)."
    )

pkg / "requirements" / "packs",
Expand All @@ -51,10 +51,24 @@ 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.
"""
return (self.packs_dir / ".." / ".." / "docs" / "examples").resolve()

def available_packs(self) -> List[str]:
"""List all available packs.
Expand All @@ -68,6 +82,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
94 changes: 94 additions & 0 deletions tests/test_packsmanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest

from diffpy.cmi.packsmanager import PacksManager


def paths_and_names_match(expected, actual, root):
"""Compare two tuples (example_name, path), 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


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"),
],
},
),
]


@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