Skip to content

Conversation

cadenmyers13
Copy link
Contributor

cli commands

Copy link

github-actions bot commented Oct 6, 2025

Warning! No news item is found for this PR. If this is a user-facing
change/feature/fix,
please add a news item by copying the format from news/TEMPLATE.rst.
For best practices, please visit
https://scikit-package.github.io/scikit-package/frequently-asked-questions.html#billinge-group-standards.

@cadenmyers13
Copy link
Contributor Author

@sbillinge test test_available_examples ready for review. Also if you have any comments on my API that would be great. those are still a work in progress.

@cadenmyers13
Copy link
Contributor Author

pretty print test function is also ready for review

packs: str | List[str],
target_dir: Optional[Path] = None,
) -> List[Path]:
"""Copy all examples from one or more packs to a target directory.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think about this and the above function? This could be broken out into 4 functions for each UC (copy one ex, one pack, multiple exs, or multiple packs). Or, it could be combined into one function that handles all 4 of these use cases. Right now, its at the middle ground with two functions which felt like a safe in-between place.

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need this function because we have (or will have) a copy_examples function which copies examples, right? I think we can handle all the copy UCs in one function, no problem.

Also, we may want to decide if we want this to be a method in the PacksManager class or standalone here. It can probably be argued both ways, and we can look in main to see how these are handled there by Tieqiong,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As it stands, copy_examples is not a method in PacksManager. I think its okay to leave it as a standalone function.

If the examples directory cannot be located in the installation.
"""
with get_package_dir() as pkgdir:
with get_package_dir(root_path) as pkgdir:
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need this (yet). Here the function is being called, not defined, and we are still working on the API and tests.

Parameters
----------
packs : str or list of str
Copy link
Contributor

Choose a reason for hiding this comment

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

do you want this structure? Maybe better to just take a list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, true. just take a list

return


def copy_packs(
Copy link
Contributor

Choose a reason for hiding this comment

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

why are we copying packs? I am not sure what this means. Don't we want to copy examples?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant this as it copies examples given paths, but since were using a dictionary to hold everything this is unnecessary. We can just have one function copy_examples

for pack in expected_pack:
assert pack in returned_pack, f"{pack} not found in returned packs."
expected_examples = expected_dict[pack]
returned_examples = returned_dict.get(pack, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

we normally would call this actual

returned_pack = list(returned_dict.keys())
for pack in expected_pack:
assert pack in returned_pack, f"{pack} not found in returned packs."
expected_examples = expected_dict[pack]
Copy link
Contributor

Choose a reason for hiding this comment

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

call this expected

returned_dict = pkmg.available_examples()
expected_pack = list(expected_dict.keys())
returned_pack = list(returned_dict.keys())
for pack in expected_pack:
Copy link
Contributor

Choose a reason for hiding this comment

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

this is too specific I think because some cases won't have packs, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right



def test_print_info(capsys):
"""Test that print_info prints expected information to stdout."""
Copy link
Contributor

Choose a reason for hiding this comment

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

see above

],
)
def test_available_examples(expected_dict):
"""Test that available_examples returns a dict."""
Copy link
Contributor

Choose a reason for hiding this comment

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

no docstring, but please write all the cases you want to test, then we can write the code to do that.

@cadenmyers13
Copy link
Contributor Author

@sbillinge responded to comments and made changes. still need to address comments on test__init__.py though

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

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

good progress. Please see my comments.

Returns
-------
list of Path
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure we need to return this. How will it be used?

Raises
------
ValueError
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to raise this. We just copy all versions that we find. This will be a case we test.

We often don't do this Raises section, the interesting raises are caught in the tests. Maybe we should but our current style is not to. It can be tmi

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay I'll leave it empty for now

examples: List[str],
target_dir: Optional[Path] = None,
) -> List[Path]:
"""Copy one or more examples to a target directory.
Copy link
Contributor

Choose a reason for hiding this comment

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

how about:
"""Copy one or more examples from the installed package to a target directory.

"expected",
[
{
# test with pack that has examples
Copy link
Contributor

Choose a reason for hiding this comment

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

How about this?

# pack with no examples.  Expect {'empty_pack': []}
# pack with multiple examples.  Expect {'full_pack': [('example1`, path_to_1'), 'example2', path_to_2)]
# multiple packs.  Expect dict with multiple pack:tuple pairs
# no pack found. Expect {}

],
)
def test_available_examples(temp_path, expected):
for pack, examples in expected.items():
Copy link
Contributor

Choose a reason for hiding this comment

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

This is all wrong, but we can revisit this when we have the cases sorted out. Basically, we will need to build a different set of folders for each case and the paramertrize will have to hand the path to each case as an input. Paramatrize usually has a kind of [('input','expected')] feel to it which it doesn't atm here.

ex_dir = pack_dir / ex
ex_dir.mkdir(parents=True, exist_ok=True)
pkmg = PacksManager(temp_path)
actual = pkmg.available_examples(temp_path)
Copy link
Contributor

Choose a reason for hiding this comment

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

you seem to be handing temp_path to this twice. Think about how you want it to work (the behavior you want) and then do it that way.

Copy link
Contributor Author

@cadenmyers13 cadenmyers13 Oct 7, 2025

Choose a reason for hiding this comment

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

I've made the change. the methods are taking temp_path as input

@sbillinge
Copy link
Contributor

we need a news

@cadenmyers13
Copy link
Contributor Author

@sbillinge I was able to move _get_examples_dir back inside PacksManager. Let me know what you think of the current packsmanager.py and its test.

@cadenmyers13
Copy link
Contributor Author

cadenmyers13 commented Oct 8, 2025

Also added test for the stored examples and packs. There are some sporadic comments that can be ignored for now until we have a complete version

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

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

great progress. Please see comments. Mostly on style.

],
)
def test_available_examples(input, expected, example_cases):
root_path = example_cases / input
Copy link
Contributor

Choose a reason for hiding this comment

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

give this a more readable name than root_path Give it a name that makes logical sense.

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.

called it tmp_ex_dir since it terminates at examples/caseX. This also pairs well with the assert case below

def test_available_examples(input, expected, example_cases):
root_path = example_cases / input
pkmg = PacksManager(root_path)
print()
Copy link
Contributor

Choose a reason for hiding this comment

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

remove all debugging print statements and comments.

In general, replace with a comment like the cases above about what behavior is being tested. But see below.

# print("packsmananger_dir:", pkmg.examples_dir)
# print("root_path:", root_path)
for path in root_path.rglob("*"):
print(" -", path.relative_to(example_cases))
Copy link
Contributor

Choose a reason for hiding this comment

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

remove debugging prints

print()
# print("packsmananger_dir:", pkmg.examples_dir)
# print("root_path:", root_path)
for path in root_path.rglob("*"):
Copy link
Contributor

Choose a reason for hiding this comment

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

move this out of this test. I like that we have these tests, but they should be run in a separate function, something like test_tmp_filesystem() or sthg, so they are just run once.

# print(" +", path.relative_to(pkmg.examples_dir.parent))

actual = pkmg.available_examples()
print("expected:")
Copy link
Contributor

Choose a reason for hiding this comment

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

remove remove remove etc.


assert (
actual.keys() == expected.keys()
), f"Keys differ: expected {expected.keys()}, got {actual.keys()}"
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need printed outputs for test fails.

assert (
actual.keys() == expected.keys()
), f"Keys differ: expected {expected.keys()}, got {actual.keys()}"
# if values of excepted are in actual assert true
Copy link
Contributor

Choose a reason for hiding this comment

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

ummm why is this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no reason, I didn't clean comments before pushing. its been removed

# if values of excepted are in actual assert true
# assert actual == expected
for expected_pack, expected_tuple in expected.items():
assert expected_pack in actual, f"Missing pack: {expected_pack}"
Copy link
Contributor

Choose a reason for hiding this comment

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

this assert could be stricter... ==

remove comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second thought, we can probably just remove this assert because about with have

assert actual.keys() == expected.keys()

which does the same thing.

for expected_pack, expected_tuple in expected.items():
assert expected_pack in actual, f"Missing pack: {expected_pack}"
for expected_name, expected_rel_path in expected_tuple:
matches = [
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems to be a rather unintuitive and unreadable way of doing this, is there a reason you chose this approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree its unreadable. I've made the change to make it more readable.

@cadenmyers13
Copy link
Contributor Author

cadenmyers13 commented Oct 9, 2025

@sbillinge I made a push to this branch addressing your comments but for some reason its not showing up here. Its showing up on my origin though... https://github.com/cadenmyers13/diffpy.cmi/blob/making-cli/tests/test_packsmanager.py. Maybe github is getting overworked?

edit: it was just lagging, commits are coming through 👍

@cadenmyers13
Copy link
Contributor Author

@sbillinge Also, I wrote the function for this tests as well so that is ready for review under packsmanager.py

# 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.

@cadenmyers13
Copy link
Contributor Author

@sbillinge jotted down some UCs for the copy_examples on this PR. on second thought however, maybe we want to merge the other test first and do the copy_exmaples test on a separate PR (if so, I will clean up this branch a bit).

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

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

next round

# root_path option provided for testing
else:
self.packs_dir = Path(root_path).resolve()
self.examples_dir = Path(root_path).resolve()
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 this would be easier to read if we just move self.examples_dir = self._get_examples_dir() outside of the if/else clause.

"""
examples_dir = tmp_path_factory.mktemp("examples")

# 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

from diffpy.cmi.packsmanager import PacksManager


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.

# 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

"case3",
{
"packA": [
("my_ex1", "case3/packA/my_ex1"),
Copy link
Contributor

Choose a reason for hiding this comment

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

not a big deal, but to make the intent cleearer let's call the examples exampleA1 and so on and use the same pattern for all the cases.

# Ensure the example directory is correctly set
assert pkmg.examples_dir == tmp_ex_dir
actual = pkmg.available_examples()
# Verify that the keys (pack names) are correct
Copy link
Contributor

Choose a reason for hiding this comment

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

please delete gratuitous comments

actual = pkmg.available_examples()
# Verify that the keys (pack names) are correct
assert set(actual.keys()) == set(expected.keys())
# Verify that each expected example exists in actual
Copy link
Contributor

Choose a reason for hiding this comment

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

this comment is also gratuitous, but a more informational comment would be helpful here. Something like # unpack the actual dict to test the values individually. global identity will fail because of the temp path

# Verify that the keys (pack names) are correct
assert set(actual.keys()) == set(expected.keys())
# Verify that each expected example exists in actual
for expected_pack, expected_list in expected.items():
Copy link
Contributor

Choose a reason for hiding this comment

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

I asked chatGPT for a more pythonic way of doing this and got the following, which I think I prefer:

def values_match(list_a, list_b, tol=1e-9):
    if len(list_a) != len(list_b):
        return False
    for (num_a, str_a), (num_b, str_b) in zip(list_a, list_b):
        if abs(num_a - num_b) > tol:
            return False
        if not (str_a in str_b or str_b in str_a):
            return False
    return True

all(values_match(a[k], b[k]) for k in a)

function definition would be outside the test function, which makes the test function very readable. Of course variable names etc. should be modified.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

implemented something similar



@pytest.mark.parametrize("input,expected", example_params)
def test_tmp_(input, expected, example_cases):
Copy link
Contributor

Choose a reason for hiding this comment

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

this is good

example_path = example_cases / input
for path in example_path.rglob("*"):
if path.suffix:
# Checks temp files are files and not dirs
Copy link
Contributor

Choose a reason for hiding this comment

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

pls remove

@cadenmyers13
Copy link
Contributor Author

@sbillinge ready for review. Heres the temp tree structure. temp0 is somewhat of a "hidden" dir and each caseX dir gets treated as the top level during each test.

temp0/
├── case1/
│   ├── docs/
│   │   └── examples/
│   │       └── empty_pack/
│   └── requirements/
│       └── packs/
│
├── case2/
│   ├── docs/
│   │   └── examples/
│   │       └── full_pack/
│   │           ├── ex1/
│   │           │   └── solution/
│   │           │       └── diffpy-cmi/
│   │           │           └── script1.py
│   │           └── ex2/
│   │               └── random/
│   │                   └── path/
│   │                       ├── script1.py
│   │                       └── script2.py
│   └── requirements/
│       └── packs/
│
├── case3/
│   ├── docs/
│   │   └── examples/
│   │       ├── packA/
│   │       │   ├── ex1/
│   │       │   │   └── script1.py
│   │       │   └── ex2/
│   │       │       └── solutions/
│   │       │           └── script2.py
│   │       └── packB/
│   │           └── ex3/
│   │               └── more/
│   │                   └── random/
│   │                       └── path/
│   │                           ├── script3.py
│   │                           └── script4.py
│   └── requirements/
│       └── packs/
│
├── case4/
│   ├── docs/
│   │   └── examples/        
│   └── requirements/
│       └── packs/
│
└── case5/
    ├── docs/
    │   └── examples/
    │       ├── packA/
    │       │   └── ex1/
    │       │       └── path1/
    │       │           └── script1.py
    │       └── packB/
    │           └── ex1/
    │               └── path2/
    │                   └── script2.py
    └── requirements/
        └── packs/

@sbillinge
Copy link
Contributor

This directory structure looks great, thanks.

This is getting a bit hard to review. Please could we make a few separate PRs? Let's make one PR that creates the directory structure fixture. It will need a news and should pass tests (it may fail codecov, that is ok).

THen make one PR per test and we work to get everything green.

Makes these PRs from a clean PR, though where necessary you can copy over code from here.

This needs a news.

@cadenmyers13 cadenmyers13 changed the title feat: update cli feat: build examples dict for cli commands Oct 10, 2025
@cadenmyers13
Copy link
Contributor Author

@sbillinge good idea yeah i can do that right now. Do you want to keep this PR or should we just close it so the commit history is clean?

@sbillinge
Copy link
Contributor

This can be closed. Let's only reuse code judiciously from it but treat it more as a learning/design exercise. I think we came up with a good design now, though the code and the commit history itself could be cleaner. Working of off clean PRs from main can fix that.

Please make sure PRs are passing everything before reesting a review, but have one function tested per PR, sthg like that.

@cadenmyers13
Copy link
Contributor Author

@sbillinge okay I've made two PRs, one with the temp dir fixture and the other with the function and test

temp dir fixture: #54 (ready for review)

function and test: #55

@cadenmyers13
Copy link
Contributor Author

closing to keep PR list clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants