python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
17.88k stars 2.74k forks source link

Error suddenly appears from Python 3.10 and above. #17473

Open SH2282000 opened 5 days ago

SH2282000 commented 5 days ago

Bug Report

Hi,

since 3.10 it is not possible to call .get() on a PackageMetadata object without getting an error from mypy.

To Reproduce

Let's consider the following scenario:

   from importlib.metadata import metadata

   package_name = "my_package"
   package_metadata = metadata(package_name)
   package_info = {
       "name": package_metadata.get("Name", package_name),
       "project_url": package_metadata.get("Home-page", "Not found"),
       "commit_id": package_metadata.get("Version", "Not found"),
   }

This was perfectly passing all mypy tests until Python 3.10, but not in 3.10 (and later versions) even though this code is still perfectly working.

Expected Behavior

No errors. Just like in older versions of python (< 3.10).

Actual Behavior

With the code above after 3.10, expect the following output:

$ mypy .
X/example.py:5: error: "PackageMetadata" has no attribute "get" [attr-defined]
X/example.py:6: error: "PackageMetadata" has no attribute "get" [attr-defined]
X/example.py:7: error: "PackageMetadata" has no attribute "get" [attr-defined]

Your Environment

Callek commented 5 days ago

I'm not a mypy contributor, but I found this:

https://github.com/python/typeshed/blob/ea34c97ccca90b5eb18bdaa40a1ac053f0098f67/stdlib/importlib/metadata/__init__.pyi#L264C1-L268C57

Also running this code in mypy playground is not causing any failures because its inferring "Any" https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=e0c50c9078a7d0c2aceaea212accade9

SH2282000 commented 4 days ago

Thank you, this is helpful.

I found a similar solution with dict(vars()), but still do not understand why would this .get() would implicitely get deprecated.

I wait for more explanation from the mypy contributors.

Thanks again!

kojiromike commented 3 days ago

Respectfully, surely the example given has to be wrong, because there's no way the ["_headers"] value here was ever a PackageMetadata object. I might have believed that in one case it was a dict (which would have had .get) and now is a list of tuples (which would not), but never a PackageMetadata object.

To be sure, I checked in Python 3.10 and 3.12:

$ poetry run python -m pkgmeta
sys.version='3.10.13 (main, Dec 11 2023, 09:23:50) [Clang 15.0.0 (clang-1500.0.40.1)]'
type(metadata_object)=<class 'importlib.metadata._adapters.Message'>
type(metadata_vars)=<class 'dict'>
type(metadata_headers)=<class 'list'>
Traceback (most recent call last):
  File "/Users/michael/.pyenv/versions/3.10.13/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Users/michael/.pyenv/versions/3.10.13/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/Users/michael/foo/pkgmeta.py", line 31, in <module>
    "name": metadata_headers.get("Name", package_name),
AttributeError: 'list' object has no attribute 'get'

and again in 3.12:

$ poetry run python -m pkgmeta
sys.version='3.12.4 (main, Jun  6 2024, 18:26:44) [Clang 15.0.0 (clang-1500.3.9.4)]'
Runtime type is 'Message'
type(metadata_object)=<class 'importlib.metadata._adapters.Message'>
Runtime type is 'dict'
type(metadata_vars)=<class 'dict'>
Runtime type is 'list'
type(metadata_headers)=<class 'list'>
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/Users/michael/foo/pkgmeta.py", line 31, in <module>
    "name": metadata_headers.get("Name", package_name),
            ^^^^^^^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'get'

The script, a modified version of the given example:

$ cat pkgmeta.py 
#!/usr/bin/env python3

import sys
from importlib.metadata import metadata

try:
    from typing import reveal_type
except ImportError:

    def reveal_type(*a, **k) -> None:
        pass

print(f"{sys.version=}")

package_name = "foo"
metadata_object = metadata(package_name)
reveal_type(metadata_object)
print(f"{type(metadata_object)=}")

metadata_vars = vars(metadata_object)
reveal_type(metadata_vars)
print(f"{type(metadata_vars)=}")

metadata_headers = metadata_vars["_headers"]
reveal_type(metadata_headers)
print(f"{type(metadata_headers)=}")

package_info = {
    "name": metadata_headers.get("Name", package_name),
    "project_url": metadata_headers.get("Home-page", "Not found"),
    "commit_id": metadata_headers.get("Version", "Not found"),
}

So I think the issue here is invalid, and there's probably something else at play. But I'd be interested to see what @SH2282000 gets if/when they run my modified script above.

SH2282000 commented 1 day ago

No, I'm sorry that I confused you. The snippet was the workaround.

I modified it back to what actually is my problem. That I cannot call .get on PackageMetadata.