Skip to content

pygmt.which: Fix the bug when passing multiple files #2726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 15, 2023
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
6 changes: 4 additions & 2 deletions pygmt/src/which.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
GMTTempFile,
build_arg_string,
fmt_docstring,
kwargs_to_strings,
is_nonstr_iter,
use_alias,
)


@fmt_docstring
@use_alias(G="download", V="verbose")
@kwargs_to_strings(fname="sequence_space")
def which(fname, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Reading up on https://peps.python.org/pep-0570/#positional-or-keyword-arguments and https://stackoverflow.com/questions/9450656/positional-argument-vs-keyword-argument (see also Max's previous comment at #731 (comment)), should we explicitly mark fname as positional_or_keyword like this (unsure if syntax is ok)?

Suggested change
def which(fname, **kwargs):
def which(/, fname, *, **kwargs):

Or just stick with the current one (which works also I think).

Copy link
Member Author

Choose a reason for hiding this comment

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

Better to open a separate issue so that we can have more discussions about the behavior and also have consistent function definitions in the whole project.

Copy link
Member

@weiji14 weiji14 Oct 15, 2023

Choose a reason for hiding this comment

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

On second thought, maybe we should enforce using keyword-only arguments like so:

Suggested change
def which(fname, **kwargs):
def which(*, fname, **kwargs):

Then the gmtwhich [ERROR] you mentioned at #2361 (comment) would turn into a slightly more useful error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 cached_files = which([f"@{fname}" for fname in filenames], download="c")

File ~/pygmt/pygmt/helpers/decorators.py:598, in use_alias.<locals>.alias_decorator.<locals>.new_module(*args, **kwargs)
    591     msg = (
    592         "Parameters 'Y' and 'yshift' are deprecated since v0.8.0. "
    593         "and will be removed in v0.12.0. "
    594         "Use Figure.shift_origin(yshift=...) instead."
    595     )
    596     warnings.warn(msg, category=SyntaxWarning, stacklevel=2)
--> 598 return module_func(*args, **kwargs)

TypeError: which() takes 0 positional arguments but 1 was given

Well, actually not that useful (it doesn't say that the fname keyword should be used. But something to consider - if we want to allow 1) positional_or_keyword (current), or 2) only keyword.

Copy link
Member Author

Choose a reason for hiding this comment

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

Instead, I prefer to def which(fname, *, **kwargs). It allows which("file.txt") which is much simpler than which(fname="file.txt").

Copy link
Member

Choose a reason for hiding this comment

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

Ok, let's go with that. I'll approve this PR, but you can apply the change afterwards.

Copy link
Member Author

Choose a reason for hiding this comment

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

def which(fname, *, **kwargs)

gives a syntax error:

'named arguments must follow bare *

See https://stackoverflow.com/questions/14301967/what-does-a-bare-asterisk-do-in-a-parameter-list-what-are-keyword-only-parame.

I'll leave the function definition as it is now.

r"""
Find the full path to specified files.
Expand Down Expand Up @@ -63,6 +62,9 @@ def which(fname, **kwargs):
FileNotFoundError
If the file is not found.
"""
if is_nonstr_iter(fname): # Got a list of files
fname = " ".join(fname)

with GMTTempFile() as tmpfile:
with Session() as lib:
lib.call_module(
Expand Down
14 changes: 7 additions & 7 deletions pygmt/tests/test_which.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Test pygmt.which.
"""
import os
from pathlib import Path

import pytest
from pygmt import which
Expand All @@ -13,20 +13,20 @@ def test_which():
Make sure `which` returns file paths for @files correctly without errors.
"""
for fname in ["tut_quakes.ngdc", "tut_bathy.nc"]:
cached_file = which(f"@{fname}", download="c")
assert os.path.exists(cached_file)
assert os.path.basename(cached_file) == fname
cached_file = which(fname=f"@{fname}", download="c")
assert Path(cached_file).exists()
assert Path(cached_file).name == fname


def test_which_multiple():
"""
Make sure `which` returns file paths for multiple @files correctly.
"""
filenames = ["ridge.txt", "tut_ship.xyz"]
cached_files = which(fname=[f"@{fname}" for fname in filenames], download="c")
cached_files = which([f"@{fname}" for fname in filenames], download="c")
Copy link
Member Author

Choose a reason for hiding this comment

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

Now we can pass a list of files without having to use fname=

for cached_file in cached_files:
assert os.path.exists(cached_file)
assert os.path.basename(cached_file) in filenames
assert Path(cached_file).exists()
assert Path(cached_file).name in filenames


def test_which_fails():
Expand Down