Skip to content

Commit 5f7f605

Browse files
authored
Merge pull request #65 from cadenmyers13/copy-func-test
test: `copy_examples` test
2 parents 54b75fb + 87c1f43 commit 5f7f605

File tree

7 files changed

+432
-93
lines changed

7 files changed

+432
-93
lines changed

.github/workflows/tests-on-pr.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
project: diffpy.cmi
1212
c_extension: false
1313
headless: false
14+
python_version: 3.13
1415
run: |
1516
set -Eeuo pipefail
1617
echo "Test cmds"

news/copy-func-test.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Add test for ``copy_examples``.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/cmi/cli.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import argparse
1717
from pathlib import Path
1818
from shutil import copytree
19-
from typing import Dict, List, Optional, Tuple
19+
from typing import List, Optional, Tuple
2020

2121
from diffpy.cmi import __version__
2222
from diffpy.cmi.conda import env_info
@@ -25,22 +25,6 @@
2525
from diffpy.cmi.profilesmanager import ProfilesManager
2626

2727

28-
def copy_examples(
29-
examples_dict: Dict[str, List[Tuple[str, Path]]], target_dir: Path = None
30-
) -> None:
31-
"""Copy an example into the the target or current working directory.
32-
33-
Parameters
34-
----------
35-
examples_dict : dict
36-
Dictionary mapping pack name -> list of (example, path) tuples.
37-
target_dir : pathlib.Path, optional
38-
Target directory to copy examples into. Defaults to current
39-
working directory.
40-
"""
41-
return
42-
43-
4428
# Examples
4529
def _get_examples_dir() -> Path:
4630
"""Return the absolute path to the installed examples directory.

src/diffpy/cmi/packsmanager.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See LICENSE.rst for license information.
1313
#
1414
##############################################################################
15-
15+
import shutil
1616
from importlib.resources import as_file
1717
from pathlib import Path
1818
from typing import List, Union
@@ -133,6 +133,129 @@ def available_examples(self) -> dict[str, List[tuple[str, Path]]]:
133133
)
134134
return examples_dict
135135

136+
def copy_examples(
137+
self,
138+
examples_to_copy: List[str],
139+
target_dir: Path = None,
140+
force: bool = False,
141+
) -> None:
142+
"""Copy examples or packs into the target or current working
143+
directory.
144+
145+
Parameters
146+
----------
147+
examples_to_copy : list of str
148+
User-specified pack(s), example(s), or "all" to copy all.
149+
target_dir : pathlib.Path, optional
150+
Target directory to copy examples into. Defaults to current
151+
working directory.
152+
force : bool, optional
153+
Defaults to ``False``. If ``True``, existing files are
154+
overwritten and directories are merged
155+
(extra files in the target are preserved).
156+
"""
157+
self._target_dir = target_dir.resolve() if target_dir else Path.cwd()
158+
self._force = force
159+
160+
if "all" in examples_to_copy:
161+
self._copy_all()
162+
return
163+
164+
for item in examples_to_copy:
165+
if item in self.available_examples():
166+
self._copy_pack(item)
167+
elif self._is_example_name(item):
168+
self._copy_example(item)
169+
else:
170+
raise FileNotFoundError(
171+
f"No examples or packs found for input: '{item}'"
172+
)
173+
del self._target_dir
174+
del self._force
175+
return
176+
177+
def _copy_all(self):
178+
"""Copy all packs and examples."""
179+
for pack_name in self.available_examples():
180+
self._copy_pack(pack_name)
181+
182+
def _copy_pack(self, pack_name):
183+
"""Copy all examples in a single pack."""
184+
examples = self.available_examples().get(pack_name, [])
185+
for ex_name, ex_path in examples:
186+
self._copy_tree_to_target(pack_name, ex_name, ex_path)
187+
188+
def _copy_example(self, example_name):
189+
"""Copy a single example by its name."""
190+
example_found = False
191+
for pack_name, examples in self.available_examples().items():
192+
for ex_name, ex_path in examples:
193+
if ex_name == example_name:
194+
self._copy_tree_to_target(pack_name, ex_name, ex_path)
195+
example_found = True
196+
if not example_found:
197+
raise FileNotFoundError(
198+
f"No examples or packs found for input: '{example_name}'"
199+
)
200+
201+
def _is_example_name(self, name):
202+
"""Return True if the given name matches any known example."""
203+
for pack_name, examples in self.available_examples().items():
204+
for example_name, _ in examples:
205+
if example_name == name:
206+
return True
207+
return False
208+
209+
def _copy_tree_to_target(self, pack_name, example_name, example_origin):
210+
"""Copy an example folder from source to the user's target
211+
directory."""
212+
target_dir = self._target_dir / pack_name / example_name
213+
target_dir.parent.mkdir(parents=True, exist_ok=True)
214+
if target_dir.exists() and self._force:
215+
self._overwrite_example(
216+
example_origin, target_dir, pack_name, example_name
217+
)
218+
return
219+
if target_dir.exists():
220+
self._copy_missing_files(example_origin, target_dir)
221+
print(
222+
f"WARNING: Example '{pack_name}/{example_name}'"
223+
" already exists at the specified target directory. "
224+
"Existing files were left unchanged; "
225+
"new or missing files were copied. To overwrite everything, "
226+
"rerun with --force."
227+
)
228+
return
229+
self._copy_new_example(
230+
example_origin, target_dir, pack_name, example_name
231+
)
232+
233+
def _overwrite_example(
234+
self, example_origin, target, pack_name, example_name
235+
):
236+
"""Delete target and copy example."""
237+
shutil.rmtree(target)
238+
shutil.copytree(example_origin, target)
239+
print(f"Overwriting example '{pack_name}/{example_name}'.")
240+
241+
def _copy_missing_files(self, example_origin, target):
242+
"""Copy only files and directories that are missing in the
243+
target."""
244+
for example_item in example_origin.rglob("*"):
245+
rel_path = example_item.relative_to(example_origin)
246+
target_item = target / rel_path
247+
if example_item.is_dir():
248+
target_item.mkdir(parents=True, exist_ok=True)
249+
elif example_item.is_file() and not target_item.exists():
250+
target_item.parent.mkdir(parents=True, exist_ok=True)
251+
shutil.copy2(example_item, target_item)
252+
253+
def _copy_new_example(
254+
self, example_origin, target, pack_name, example_name
255+
):
256+
shutil.copytree(example_origin, target)
257+
print(f"Copied example '{pack_name}/{example_name}'.")
258+
136259
def _resolve_pack_file(self, identifier: Union[str, Path]) -> Path:
137260
"""Resolve a pack identifier to an absolute .txt path.
138261

tests/conftest.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ def tmp_examples(tmp_path_factory):
2222
yield tmp_examples
2323

2424

25-
@pytest.fixture(scope="session")
25+
@pytest.fixture(scope="function")
2626
def example_cases(tmp_path_factory):
2727
"""Copy the entire examples tree into a temp directory once per test
2828
session.
2929
3030
Returns the path to that copy.
3131
"""
3232
root_temp_dir = tmp_path_factory.mktemp("temp")
33+
cwd = root_temp_dir / "cwd"
34+
cwd.mkdir(parents=True, exist_ok=True)
35+
existing_dir = cwd / "existing_target"
36+
existing_dir.mkdir(parents=True, exist_ok=True)
3337

3438
# case 1: pack with no examples
3539
case1ex_dir = root_temp_dir / "case1" / "docs" / "examples"
@@ -44,30 +48,35 @@ def example_cases(tmp_path_factory):
4448
case2ex_dir / "full_pack" / "ex1" / "solution" / "diffpy-cmi"
4549
) # full_pack, ex1
4650
case2a.mkdir(parents=True, exist_ok=True)
47-
(case2a / "script1.py").touch()
51+
(case2a / "script1.py").write_text(f"# {case2a.name} script1\n")
52+
4853
case2b = (
4954
case2ex_dir / "full_pack" / "ex2" / "random" / "path"
5055
) # full_pack, ex2
5156
case2b.mkdir(parents=True, exist_ok=True)
52-
(case2b / "script1.py").touch()
53-
(case2b / "script2.py").touch()
57+
(case2b / "script1.py").write_text(f"# {case2b.name} script1\n")
58+
(case2b / "script2.py").write_text(f"# {case2b.name} script2\n")
59+
5460
case2req_dir = root_temp_dir / "case2" / "requirements" / "packs"
5561
case2req_dir.mkdir(parents=True, exist_ok=True)
5662

5763
# Case 3: multiple packs with multiple examples
5864
case3ex_dir = root_temp_dir / "case3" / "docs" / "examples"
5965
case3a = case3ex_dir / "packA" / "ex1" # packA, ex1
6066
case3a.mkdir(parents=True, exist_ok=True)
61-
(case3a / "script1.py").touch()
67+
(case3a / "script1.py").write_text(f"# {case3a.name} script1\n")
68+
6269
case3b = case3ex_dir / "packA" / "ex2" / "solutions" # packA, ex2
6370
case3b.mkdir(parents=True, exist_ok=True)
64-
(case3b / "script2.py").touch()
71+
(case3b / "script2.py").write_text(f"# {case3b.name} script2\n")
72+
6573
case3c = (
6674
case3ex_dir / "packB" / "ex3" / "more" / "random" / "path"
6775
) # packB, ex3
6876
case3c.mkdir(parents=True, exist_ok=True)
69-
(case3c / "script3.py").touch()
70-
(case3c / "script4.py").touch()
77+
(case3c / "script3.py").write_text(f"# {case3c.name} script3\n")
78+
(case3c / "script4.py").write_text(f"# {case3c.name} script4\n")
79+
7180
case3req_dir = root_temp_dir / "case3" / "requirements" / "packs"
7281
case3req_dir.mkdir(parents=True, exist_ok=True)
7382

@@ -80,12 +89,27 @@ def example_cases(tmp_path_factory):
8089

8190
# Case 5: multiple packs with the same example names
8291
case5ex_dir = root_temp_dir / "case5" / "docs" / "examples"
92+
8393
case5a = case5ex_dir / "packA" / "ex1" / "path1" # packA, ex1
8494
case5a.mkdir(parents=True, exist_ok=True)
85-
(case5a / "script1.py").touch()
95+
(case5a / "script1.py").write_text(f"# {case5a.name} script1\n")
96+
8697
case5b = case5ex_dir / "packB" / "ex1" / "path2" # packB, ex1
8798
case5b.mkdir(parents=True, exist_ok=True)
88-
(case5b / "script2.py").touch()
99+
(case5b / "script2.py").write_text(f"# {case5b.name} script2\n")
100+
101+
case5c = case5ex_dir / "packA" / "ex2" # packA, ex2
102+
case5c.mkdir(parents=True, exist_ok=True)
103+
(case5c / "script3.py").write_text(f"# {case5c.name} script3\n")
104+
105+
case5d = case5ex_dir / "packB" / "ex3"
106+
case5d.mkdir(parents=True, exist_ok=True)
107+
(case5d / "script4.py").write_text(f"# {case5d.name} script4\n")
108+
109+
case5e = case5ex_dir / "packB" / "ex4"
110+
case5e.mkdir(parents=True, exist_ok=True)
111+
(case5e / "script5.py").write_text(f"# {case5e.name} script5\n")
112+
89113
case5req_dir = root_temp_dir / "case5" / "requirements" / "packs"
90114
case5req_dir.mkdir(parents=True, exist_ok=True)
91115

tests/test_cli.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +0,0 @@
1-
import os
2-
from pathlib import Path
3-
4-
import pytest
5-
6-
from diffpy.cmi import cli
7-
8-
9-
def test_map_pack_to_examples_structure():
10-
"""Test that map_pack_to_examples returns the right shape of
11-
data."""
12-
actual = cli.map_pack_to_examples()
13-
assert isinstance(actual, dict)
14-
for pack, exdirs in actual.items():
15-
assert isinstance(pack, str)
16-
assert isinstance(exdirs, list)
17-
for ex in exdirs:
18-
assert isinstance(ex, str)
19-
# Check for known packs
20-
assert "core" in actual.keys()
21-
assert "pdf" in actual.keys()
22-
# Check for known examples
23-
assert ["linefit"] in actual.values()
24-
25-
26-
@pytest.mark.parametrize(
27-
"input_valid_str",
28-
[
29-
"core/linefit",
30-
"pdf/ch03NiModelling",
31-
],
32-
)
33-
def test_copy_example_success(tmp_path, input_valid_str):
34-
"""Given a valid example format (<pack>/<ex>), test that its copied
35-
to the temp dir."""
36-
os.chdir(tmp_path)
37-
actual = cli.copy_example(input_valid_str)
38-
expected = tmp_path / Path(input_valid_str).name
39-
assert expected.exists() and expected.is_dir()
40-
assert actual == expected
41-
42-
43-
def test_copy_example_fnferror():
44-
"""Test that FileNotFoundError is raised when the example does not
45-
exist."""
46-
with pytest.raises(FileNotFoundError):
47-
cli.copy_example("pack/example1")
48-
49-
50-
@pytest.mark.parametrize(
51-
"input_bad_str",
52-
[
53-
"", # empty string
54-
"/", # missing pack and example
55-
"corelinefit", # missing slash
56-
"linefit", # missing pack and slash
57-
"core/", # missing example
58-
"/linefit", # missing pack
59-
"core/linefit/extra", # too many slashes
60-
],
61-
)
62-
def test_copy_example_valueerror(input_bad_str):
63-
"""Test that ValueError is raised when the format is invalid."""
64-
with pytest.raises(ValueError):
65-
cli.copy_example(input_bad_str)

0 commit comments

Comments
 (0)