Skip to content
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

Use of ParamSpec introduced in #3396 only works in Python 3.10+ #3397

Closed
rsokl opened this issue Jul 5, 2022 · 15 comments · Fixed by #3444
Closed

Use of ParamSpec introduced in #3396 only works in Python 3.10+ #3397

rsokl opened this issue Jul 5, 2022 · 15 comments · Fixed by #3444
Labels
bug something is clearly wrong here legibility make errors helpful and Hypothesis grokable tests/build/CI about testing or deployment *of* Hypothesis

Comments

@rsokl
Copy link
Contributor

rsokl commented Jul 5, 2022

The pattern used by #3396 to leverage ParamSpec

try:
from typing import Concatenate, ParamSpec
except ImportError:
try:
from typing_extensions import Concatenate, ParamSpec
except ImportError:
ParamSpec = None # type: ignore

does not actually work with the typing_extensions backport:

# contents of pspec.py

from hypothesis.strategies import composite, DrawFn
from typing_extensions import ParamSpec  # note: typing-extensions is installed

@composite
def comp(draw: DrawFn, x: int) -> int:
    return x

reveal_type(comp)
$ pip freeze | grep typing_extensions
typing_extensions==4.3.0

$ mypy --version
mypy 0.961 (compiled: yes)

$ mypy pspec.py --python-version=3.10
pspec.py:11: note: Revealed type is "def (x: builtins.int) -> hypothesis.strategies._internal.strategies.SearchStrategy[builtins.int]"
Success: no issues found in 1 source file

$ mypy pspec.py --python-version=3.9
pspec.py:11: note: Revealed type is "Any"
Success: no issues found in 1 source file
pyright repro
$ pyright --version
pyright 1.1.257

$ pyright pspec.py --pythonversion=3.10
No configuration file found.
No pyproject.toml file found.
stubPath C:\Users\Ryan Soklaski\hypothesis\scratch\typings is not a valid directory.
Assuming Python platform Windows
Searching for source files
Found 1 source file
pyright 1.1.257
C:\Users\Ryan Soklaski\hypothesis\scratch\pspec.py
  C:\Users\Ryan Soklaski\hypothesis\scratch\pspec.py:11:13 - information: Type of "comp" is "(x: int) -> SearchStrategy[int]"
0 errors, 0 warnings, 1 information

$ pyright pspec.py --pythonversion=3.9
No configuration file found.
No pyproject.toml file found.
stubPath C:\Users\Ryan Soklaski\hypothesis\scratch\typings is not a valid directory.
Assuming Python platform Windows
Searching for source files
Found 1 source file
pyright 1.1.257
C:\Users\Ryan Soklaski\hypothesis\scratch\pspec.py
  C:\Users\Ryan Soklaski\hypothesis\scratch\pspec.py:7:2 - error: Argument of type "(draw: DrawFn, x: int) -> int" cannot be assigned to parameter "f" of type "() -> Ex@composite" in function "composite"
    Type "(draw: DrawFn, x: int) -> int" cannot be assigned to type "() -> Ex@composite"
      Keyword parameter "draw" is missing in destination
      Keyword parameter "x" is missing in destination (reportGeneralTypeIssues)
  C:\Users\Ryan Soklaski\hypothesis\scratch\pspec.py:11:13 - information: Type of "comp" is "() -> SearchStrategy[int]"
1 error, 0 warnings, 1 information

It seems like nested try-excepts are not supported by mypy:

# contents of scratch.py

import typing

try:
    from typing import Concatenate, ParamSpec
except ImportError:
    try:
        from typing_extensions import Concatenate, ParamSpec
    except ImportError:
        ParamSpec = None  # type: ignore


if typing.TYPE_CHECKING or ParamSpec is not None:
    reveal_type(ParamSpec)
$ mypy scratch.py --python-version=3.10
scratch.py:13: note: Revealed type is "def (name: builtins.str, *, bound: Union[Any, None] =, contravariant: builtins.bool =, covariant: builtins.bool =) -> typing.ParamSpec"
Success: no issues found in 1 source file

$ mypy scratch.py --python-version=3.9
scratch.py:4: error: Module "typing" has no attribute "Concatenate"
scratch.py:4: error: Module "typing" has no attribute "ParamSpec"; maybe "_ParamSpec"?
scratch.py:13: note: Revealed type is "Any"
Found 2 errors in 1 file (checked 1 source file)

The following works for mypy for both Python 3.10 and the typing-extensions backport, but only works for pyright for Python 3.10 (potentially relevant comment from Eric Traut):

if sys.version_info >= (3, 10):
    from typing import Concatenate, ParamSpec
elif typing.TYPE_CHECKING:
    try:
        from typing_extensions import Concatenate, ParamSpec
    except ImportError:
        from typing import Any as ParamSpec
else:
    ParamSpec = None

Whereas this works for pyright under all circumstances, but fails in mypy when typing-extensions is not installed:

if sys.version_info >= (3, 10):
    from typing import Concatenate, ParamSpec
elif typing.TYPE_CHECKING:
    from typing_extensions import Concatenate, ParamSpec
else:
    ParamSpec = None

I've gotta go to bed.. @sobolevn I'm wondering if you have any recommendations here.

@rsokl rsokl added bug something is clearly wrong here legibility make errors helpful and Hypothesis grokable labels Jul 5, 2022
@Zac-HD Zac-HD added the tests/build/CI about testing or deployment *of* Hypothesis label Jul 5, 2022
@Zac-HD
Copy link
Member

Zac-HD commented Jul 5, 2022

Thanks for the report! We also have an inadequate-tests problem then...

@rsokl
Copy link
Contributor Author

rsokl commented Jul 5, 2022

We do have a (recent) precedent for parameterizing type-checker tests over python versions 😄

Tests that I can think of:

  • Python 3.10+ mypy/pyright should pass current tests
  • Python 3.10< mypy/pyright & no backport: @composite should mask decorated function's signatures, but express the accurate return type SearchStrategy[Ex]
  • (Add typing_extensions to CI matrix.) With backport: All Python versions should pass current tests.

I was trying to remember: what is the reason why we avoid adding typing_extensions to our dependencies?

@Zac-HD
Copy link
Member

Zac-HD commented Jul 5, 2022

I was trying to remember: what is the reason why we avoid adding typing_extensions to our dependencies?

Ensuring that we don't pick up an accidental hard-dependency; it is in our tools and coverage deps, just not the minimal test deps, and I think that's the right balance.

@sobolevn
Copy link
Member

sobolevn commented Jul 5, 2022

In my opinion, depending on typing_extensions is not a big deal.
Almost every typed package depend on it.

It will simplify a lot of things for us.

@Zac-HD
Copy link
Member

Zac-HD commented Jul 5, 2022

The main argument against is that minimising dependencies is a big deal for PyPy, CPython, and other foundational projects. Maybe it's worth it anyway; I'd need to look into the compatibility story with prerelease and nightly builds.

@Mec-iS

This comment was marked as resolved.

@rsokl

This comment was marked as resolved.

@Zac-HD

This comment was marked as resolved.

@Zac-HD
Copy link
Member

Zac-HD commented Jul 8, 2022

Quick untested hack: 854e8ab

@hauntsaninja
Copy link
Contributor

Your hack should work. Type checkers are special cased to always know what typing_extensions is even if it isn't installed (it's treated like part of the standard library).

@Zac-HD
Copy link
Member

Zac-HD commented Aug 18, 2022

Ooh, thanks for dropping by! I'm a little concerned that the hack will fail in some environments, so what about:

if typing.TYPE_CHECKING or sys.version_info[:2] >= (3, 10):
    from typing import Concatenate, ParamSpec
else:
    try:
        from typing_extensions import Concatenate, ParamSpec
    except ImportError:  # pragma: no cover
        ParamSpec = None

@hauntsaninja
Copy link
Contributor

No, type checkers will complain about that when checking targeting Python 3.9. See https://mypy-play.net/?mypy=latest&python=3.9&gist=0495f5e1337cd862c26dbb2de4d05633

I would do:

if typing.TYPE_CHECKING:
    from typing_extensions import Concatenate, ParamSpec
else:
    try:
        from typing import Concatenate, ParamSpec
    except ImportError:
        try:
            from typing_extensions import Concatenate, ParamSpec
        except ImportError:
            Concatenate, ParamSpec = None, None

I guess if you don't fully believe that all type checkers always know about typing_extensions, you could hedge a little more with:

if typing.TYPE_CHECKING:
    if sys.version_info >= (3, 10):
        from typing import Concatenate, ParamSpec
    else:
        from typing_extensions import Concatenate, ParamSpec
else:
    try:
        from typing import Concatenate, ParamSpec
    except ImportError:
        try:
            from typing_extensions import Concatenate, ParamSpec
        except ImportError:
            Concatenate, ParamSpec = None, None

But they really always do. typing_extensions has been in the standard library since Python 2.7, don't you know :-) https://github.com/python/typeshed/blob/2c052651e953109c94ae998f5ccc6d043df060c9/stdlib/VERSIONS#L273

@rsokl
Copy link
Contributor Author

rsokl commented Aug 18, 2022

Option 1 does indeed work with pyright without typing-extensions actually being installed.

Type checkers are special cased to always know what typing_extensions is even if it isn't installed (it's treated like part of the standard library).

In the case of mypy, typing-extensions is installed as a dependency so of course it works in that regard.
Out of curiosity I uninstalled typing-extensions after installing mypy and running it on both options 1 and 2 raises ModuleNotFoundError: No module named 'typing_extensions'.

$ mypy scratch/scratch.py --python-version=3.9
Traceback (most recent call last):
  File "C:\Users\rsokl\miniconda3\envs\hydra-zen\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\rsokl\miniconda3\envs\hydra-zen\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\rsokl\miniconda3\envs\hydra-zen\Scripts\mypy.exe\__main__.py", line 4, in <module>
  File "C:\Users\rsokl\miniconda3\envs\hydra-zen\lib\site-packages\mypy\__main__.py", line 6, in <module>
    from mypy.main import main, process_options
  File "mypy\main.py", line 11, in <module>
ModuleNotFoundError: No module named 'typing_extensions'

@hauntsaninja
Copy link
Contributor

hauntsaninja commented Aug 18, 2022

@rsokl that traceback is mypy itself crashing. The better proof for this is using the --python-executable flag of mypy to point it at an environment where typing_extensions doesn't exist. Something like:

echo $'import typing_extensions\nreveal_type(typing_extensions.ParamSpec)' > mod.py
python -m venv env
pipx run mypy --python-executable env/bin/python mod.py

Or more thoroughly, see that numpy stops being found, but typing_extensions is still found:

python -m venv env1
python -m venv env2
env1/bin/python -m pip install mypy numpy

echo $'import typing_extensions\nreveal_type(typing_extensions.ParamSpec)\nimport numpy\nreveal_type(numpy.uint32)' > mod.py
env1/bin/python -m mypy --python-executable env1/bin/python mod.py
env1/bin/python -m mypy --python-executable env2/bin/python mod.py

@rsokl
Copy link
Contributor Author

rsokl commented Aug 18, 2022

Ah, gotchya. Thanks for clarifying @hauntsaninja . I should have couched my post with "I am sure that you know what you are talking about, whereas I have little experience with mypy" 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something is clearly wrong here legibility make errors helpful and Hypothesis grokable tests/build/CI about testing or deployment *of* Hypothesis
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants