Skip to content

Set stacklevel of DeprecationWarning as 2#538

Merged
atztogo merged 2 commits intospglib:developfrom
ktns:develop
Dec 12, 2024
Merged

Set stacklevel of DeprecationWarning as 2#538
atztogo merged 2 commits intospglib:developfrom
ktns:develop

Conversation

@ktns
Copy link
Copy Markdown
Contributor

@ktns ktns commented Nov 21, 2024

Hi.

I want to silence DeprecationWarning from 3rd party packages which is using deprecated interfaces of spglib and show warnings from our 1st party package only.

To do that, I want stacklevel of DeprecationWarning to be set as 2.

Could you accept this PR? Thanks.

@codecov
Copy link
Copy Markdown

codecov bot commented Nov 21, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 84.24%. Comparing base (8a968e4) to head (0d1090e).
Report is 63 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #538   +/-   ##
========================================
  Coverage    84.24%   84.24%           
========================================
  Files           26       26           
  Lines         8209     8209           
  Branches      1697     1702    +5     
========================================
  Hits          6916     6916           
  Misses        1293     1293           
Flag Coverage Δ
c_api 75.52% <ø> (ø)
fortran_api 56.43% <ø> (ø)
python_api 81.56% <ø> (ø)
unit_tests 13.60% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@atztogo
Copy link
Copy Markdown
Collaborator

atztogo commented Nov 21, 2024

@lan496, @LecrisUT, I have no idea. Could you decide?

@lan496
Copy link
Copy Markdown
Member

lan496 commented Nov 21, 2024

I have no concerns to merge.

@LecrisUT What do you think? Unfortunately, Some packages using spglib are slow in migration...

Copy link
Copy Markdown
Collaborator

@LecrisUT LecrisUT left a comment

Choose a reason for hiding this comment

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

@ktns can you show an example of how you are affected by this? I believe there are better solutions to silence the warnings, but maybe we need to set the stack level higher for only some of them.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 21, 2024

Specifically, I was using pytest and flooded by lines like below.

  /usr/local/lib/python3.11/site-packages/spglib/spglib.py:115: DeprecationWarning: dict interface (SpglibDataset['number']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead

I think this is because pyxtal is using the old API.

I wrote ignore::DeprecationWarning:spglib.* in filterwarning section of pytest.ini to silence these for now, but in case I accidentally write some code using the old API, I want to set stacklevel as 2 and rewrite filterwarning as ignore::DeprecationWarning:pyxtal.

I believe DeprecationWarning generally should be emitted from where deprecated code is used rather than from deprecated code itself so I would like to set stacklevel to all of them, but if there's any concern I will follow your decision.

@LecrisUT
Copy link
Copy Markdown
Collaborator

LecrisUT commented Nov 21, 2024

@ktns can you check on how to make sure the warning is emitted only once. That would be a reasonable thing to add at the very least. It seems that the intended filter did not work as I thought:

warnings.filterwarnings(
"module", category=DeprecationWarning, message=r"dict interface.*"
)

The deprecation in pyxtal can be seen here:
https://github.com/MaterSim/PyXtal/blob/e086219624181a1b9ac9c44aa291808a54887171/pyxtal/miscellaneous/gulp_c.py#L19-L22
where it should change to

         info = get_symmetry_dataset(s, symprec=1e-1)
         s.write("1.vasp", format="vasp", vasp5=True, direct=True)
         os.system("cat 1.vasp >> " + file)
-        print("{:4d} {:8.3f} {:s}".format(i, calc.energy / 8, info["international"]))
+        print("{:4d} {:8.3f} {:s}".format(i, calc.energy / 8, info.international))

If you could make a PR change there it would be awesome. Either bump the minimum spglib version and use the new interface, or add filterwarning scoped as specific as possible and add a tracker for when the interface would be changed.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 21, 2024

I think why many warning lines are emitted, not once, is because warning messages include different keys.

PR to pyxtal is right way to go of course but I can't send PRs (and wait them to be merged) to every library I come across, so I would like a way to filter warnings per caller basis.

@LecrisUT
Copy link
Copy Markdown
Collaborator

I think why many warning lines are emitted, not once, is because warning messages include different keys.

That should not be the case if I read the usage of module filter correct. Maybe you can change it to module+lineno instead of message filter, but that seems brittle. Another option could be to add a filterwanings("ignore") after

warnings.warn(
f"dict interface ({self.__class__.__name__}['{key}']) is deprecated."
f"Use attribute interface ({self.__class__.__name__}.{key}) instead",
DeprecationWarning,
)

PR to pyxtal is right way to go of course but I can't send PRs (and wait them to be merged) to every library I come across, so I would like a way to filter warnings per caller basis.

Not sure I got what you mean. But I think a good design is to add the filterwarning as close to the code you have control in this case.

Overall if I read the stacklevel part correctly, setting it to 2 would mean that the user of pyxtal will get the warning, but not pyxtal project itself, which would be really bad wouldn't it? Did I read that correctly?

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 21, 2024

That should not be the case if I read the usage of module filter correct. Maybe you can change it to module+lineno instead of message filter, but that seems brittle.

"module" filter suppresses only warnings with identical message and category.

import warnings
warnings.filterwarnings("module", category=UserWarning)

for i in range(10):
  warnings.warn(f"i={i}", UserWarning)
for i in range(10):
  warnings.warn(f"i={i}", UserWarning)

will emit 10 warning lines.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 21, 2024

Overall if I read the stacklevel part correctly, setting it to 2 would mean that the user of pyxtal will get the warning, but not pyxtal project itself, which would be really bad wouldn't it? Did I read that correctly?

I don't really understand this part. Could you elaborate why pyxtal project itself get effected by stacklevel=2?

@LecrisUT
Copy link
Copy Markdown
Collaborator

LecrisUT commented Nov 22, 2024

"module" filter suppresses only warnings with identical message and category.

Even if you include "i=" or r"i=.*" in the message argument?

We can simplify the warning message than.I thought I've also added @deprecated for mypy support. Any feedback on a useful message?

I don't really understand this part. Could you elaborate why pyxtal project itself get effected by stacklevel=2?

Quoting from the python doc:

The stacklevel argument can be used by wrapper functions written in Python, like this:

def deprecated_api(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)

This makes the warning refer to deprecated_api’s caller, rather than to the source of deprecated_api itself (since the latter would defeat the purpose of the warning message).

...

The stacklevel determines where the warning is emitted. If it is 1 (the default), the warning is emitted at the direct caller of the deprecated object; if it is higher, it is emitted further up the stack. Static type checker behavior is not affected by the category and stacklevel arguments.

To my reading we should be emitting the warning when it calls __getitem__ and not when someone else calls the pyxtal api. My concern is that if you call a foo function with high stacklevel the warning would not be emitted (will test it myself later) or it would take the context of the caller way later in the stack defeating the filterwarnings control.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 22, 2024

Even if you include "i=" or r"i=.*" in the message argument?

Yes, no matter filterwarnings have message argument, warnings with different messages will be treated as different warnings and get suppressed by filters independently.

python3 -c '   
import warnings                                                         
warnings.filterwarnings("module", category=UserWarning, message=r"i=.*")

for i in range(10):
  warnings.warn(f"i={i}", UserWarning)
for i in range(10):
  warnings.warn(f"i={i}", UserWarning)
'
<string>:6: UserWarning: i=0
<string>:6: UserWarning: i=1
<string>:6: UserWarning: i=2
<string>:6: UserWarning: i=3
<string>:6: UserWarning: i=4
<string>:6: UserWarning: i=5
<string>:6: UserWarning: i=6
<string>:6: UserWarning: i=7
<string>:6: UserWarning: i=8
<string>:6: UserWarning: i=9

My concern is that if you call a foo function with high stacklevel the warning would not be emitted

What do you mean by call a foo function with high stacklevel? I don't think there is a way to change stacklevel from the caller.

FWIW, if stacklevel is larger than the size of the actual stack trace, it will be emitted like this.

sys:1: UserWarning: stacklevel=99

or it would take the context of the caller way later in the stack defeating the filterwarnings control.

I'm not sure what you mean but setting stacklevel does not delay the timing when the warning is emitted. It just refer the stack trace and emit the warning immediately as if it is emitted from the module/line of specified level of stack.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 22, 2024

I'm afraid my intension was not clear so let me clarify my goal.

Suppose 1st party and 3rd party library using deprecated methods from 2nd party library like below.

# second.py
def deprecated():
    warnings.warn('bla', DeprecationWarning)

# third.py
def foo():
    second.deprecated()

# first.py
def bar():
    second.deprecated()

def baz():
    third.foo()

My goal is to treat warnings only from first.py as errors and not from third.py. To do that, warnings.warn in deprecated() should have stacklevel=2 and filterwarnings should be like below.

ignore::DeprecationWarning
error::DeprecationWarning::first

Side note, if stacklevel=2 is passed, warning lines will include caller modules' names, not spglib itself. So it will be clear to viewers that the problem is in the caller, not in spglib.

@LecrisUT
Copy link
Copy Markdown
Collaborator

My goal is to treat warnings only from first.py as errors and not from third.py

Yes, but I don't think it's the appropriate way for that. I need to think this a bit and look at some other references. There is a reason why the default is stacklevel=1.

What about using @warnings.deprecated and mypy checking. Isn't that a better tool for that? Let me see how the filterwarnings work with the stacklevel.

Another design in your example is to use with warnings.catch_warnings in baz(), independent of you having the bar() function. Because you are already aware that foo() function is using deprecated api, so you report to third to request a change and on your end you disable the warning in that context and possibly try...catch it so that your users are not affected.

@LecrisUT
Copy link
Copy Markdown
Collaborator

Ok I've done some experimentation and the stacklevel behaves very "interesting", check this gist

  • stacklevel=0 -> no warnings
  • stacklevel=1 -> warnings on bar['val']
    Weird thing is that foo['val'] does not trigger the warning
  • stacklevel=2 -> warnings on call_foo and call_bar
  • stacklevel=3 -> warnings on case_1/case_2

Using the warnings.warn manually has the exact same effect, but offset by one stacklevel. At firthst glance that would seem to be resolving the issue, but then, consider moving the call_bar function to another module, move case_1 as well, etc., then the warnings are again not reliably triggered. But this seems to be an issue with the defaults of filterwanings default or my IDE, because when I override the filterwarnings(category=DeprecationWarning), I get more reliable replication.


As for my hunch that stacklevel affects the context for the filterwarnings, that is indeed correct, so the question is which stacklevel is correct to use here.

  1. warn(stacklevel=1): We can guarantee that only one warning due to DictInterface is triggered, but depending on the order it can come from either first-party or third-party and it does not show reliably where it comes from
    This is not achievable with warnings.deprecated
  2. deprecated(stacklevel=1)/warn(stacklevel=2): The context is from the caller of the deprecated api
  3. higher stacklevel: undefined behavior

Overall I think you are right with the approach here and setting the stacklevel=2 for the DictInterface, but we need to check more carefully for the other ones. @ktns if it's ok with you I will re-implement this with warnings.deprecated instead, and change the warning message so that it can be filtered more reliably.

@ktns
Copy link
Copy Markdown
Contributor Author

ktns commented Nov 24, 2024

Please let me take time to understand your concerns...

Weird thing is that foo['val'] does not trigger the warning

This is because by default python suppresses warnings with identical message/category/module/line second time or later.

@DanielYang59
Copy link
Copy Markdown
Contributor

DanielYang59 commented Nov 29, 2024

Please coordinate with #538.

@LecrisUT Sorry I didn't see this PR (only searched issues)


I agree with this change, but my suggestion is to go with warnings.deprecated. I plan to work on it, but not sure if @ktns would want to research the issue more on their side.

warnings.deprecated is clean, but it's added in Python 3.13, so I don't think we could use it right now.

Added in version 3.13: See PEP 702.

spglib/pyproject.toml

Lines 39 to 43 in 8a968e4

"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",

@LecrisUT
Copy link
Copy Markdown
Collaborator

LecrisUT commented Nov 29, 2024

warnings.deprecated is clean, but it's added in Python 3.13, so I don't think we could use it right now.

Added in version 3.13: See PEP 702.

This is backported in typing-extensions 4.5.0 for mypy support and 4.9.0 for the current version in warnings.deprecated

https://typing-extensions.readthedocs.io/en/latest/#typing_extensions.deprecated

@DanielYang59
Copy link
Copy Markdown
Contributor

This is backported in typing-extensions 4.5.0 for mypy support and 4.9.0 for the current version in warnings.deprecated

I guess that means we would need an additional dependency for 3.10 <= Python < 3.13 (typing-extensions is not stdlib):

"typing-extensions; python_version<'3.10'",

Would that be a very high ask to install an additional dependency just to use typing_extensions.deprecated?

@LecrisUT
Copy link
Copy Markdown
Collaborator

LecrisUT commented Nov 29, 2024

This is backported in typing-extensions 4.5.0 for mypy support and 4.9.0 for the current version in warnings.deprecated

I guess that means we would need an additional dependency for 3.10 <= Python < 3.13 (typing-extensions is not stdlib):

"typing-extensions; python_version<'3.10'",

Would that be a very high ask to install an additional dependency just to use typing_extensions.deprecated?

Not at all and that is good practice (that is how these packages are meant to be used), we just need to update the dependencies. What I strive for is to make sure at least the LTS distros have all the required dependencies necessary, but so far I only have access to Fedora based ones through EPEL, so those are my targets on deciding if these can be added. Other environments like conda, pypi, spack, homebrew, have more freedom on getting the required version of dependencies so it's not much of a worry there

@janosh
Copy link
Copy Markdown
Contributor

janosh commented Dec 10, 2024

would also appreciate this PR being merged. didn't read the entire thread so apologies if this is redundant but let me link https://docs.astral.sh/ruff/rules/no-explicit-stacklevel for why passing explicit stacklevel>1 is almost always advisable

@atztogo
Copy link
Copy Markdown
Collaborator

atztogo commented Dec 10, 2024

@LecrisUT, is it possible to merge this PR and then make another PR to improve it?

@DanielYang59
Copy link
Copy Markdown
Contributor

DanielYang59 commented Dec 11, 2024

link https://docs.astral.sh/ruff/rules/no-explicit-stacklevel for why passing explicit stacklevel>1 is almost always advisable

This is worth looking and link two more detailed explanation on the stacktrace behaviour related to warnings in case they would help make the decision (this is a great and informative channel recommended by Janosh BTW):


In my opinion though two stacks up should suffice for a deprecated API, see https://docs.python.org/3/library/warnings.html#warnings.warn:

The stacklevel argument can be used by wrapper functions written in Python, like this:

def deprecated_api(message):
warnings.warn(message, DeprecationWarning, stacklevel=2)

Copy link
Copy Markdown
Collaborator

@LecrisUT LecrisUT left a comment

Choose a reason for hiding this comment

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

I can't properly review this week until I get furniture to work at, but I found 1 of them that should have stacklevel=1. Other than double checking for that, no issues with the current approach, but I will convert all of these to warnings.deprecated before release and fully support mypy. Happy if anyone wants to tackle these (in a separate PR) before I can get to them, they're super easy PRs.

warnings.warn(
"Use get_magnetic_symmetry() for cell with magnetic moments.",
DeprecationWarning,
stacklevel=2,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This one should be stacklevel=1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed. But can I ask you why?

Copy link
Copy Markdown
Contributor

@DanielYang59 DanielYang59 Dec 12, 2024

Choose a reason for hiding this comment

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

I'm curious why using one stack here too, IMO stacklevel=1 is useful when you want to point to the implementation itself instead of the API. But this deprecation is pointing to the get_symmetry interface?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Because the usage of get_symmetry is not deprecated, it's just one mode of operation is deprecated. When we change to warnings.deprecated we can move it to the typing part and distinguish between those.

Thinks about it more I think stacklevel=2 could also work in some sense, but I would want it to show up as any usage of get_symmetry is deprecated. Probably we can change it once the mypy part is also added.

@@ -116,6 +116,7 @@ def __getitem__(self, key: str) -> Any:
f"dict interface ({self.__class__.__name__}['{key}']) is deprecated."
f"Use attribute interface ({self.__class__.__name__}.{key}) instead",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@ktns do we need to drop this expansion?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have no strong intention to drop that since we will be able to filter it via module.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

But I thought you found that if the message differs the filter doesn't work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I said that if messages differ they will independently filtered and it cannot be warned only once. I don't want warnings to show up at all during pytest if they are called in a 3rd party module, so I don't care if there's a way to filter them based on caller module.

@atztogo atztogo merged commit f0db532 into spglib:develop Dec 12, 2024
@lan496 lan496 added this to the 2.6 milestone Jan 19, 2025
@LecrisUT LecrisUT mentioned this pull request Jan 19, 2025
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.

6 participants