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

Add regression tests for builtins.pow and object.__reduce__ #7663

Merged
merged 17 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"typeshedPath": ".",
"include": [
"stdlib",
"stubs"
"stubs",
"test_cases"
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • Do we really want this to be in both pyrightconfigs?
  • Does mypy support assert_type() yet? If it does, we could instead point mypy_primer at typeshed's test_cases. That way we would know if a PR to mypy breaks the tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

  • I don't really understand how the two pyrightconfigs work with each other ¯\_(ツ)_/¯. Happy to have it in only one if that's better.
  • No, mypy doesn't support assert_type yet. The PR was recently merged, and might make it into 0.950, but also might not. See Release 0.950 planning mypy#12579 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

Mypy supports assert_type() on master (I added it last week). I am hoping to convince Jukka to put it in the upcoming 0.950 release. We should make mypy run on this directory too as soon as 0.950 is released, or use master if we're feeling brave.

Copy link
Member Author

Choose a reason for hiding this comment

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

Update: assert_type has been cherry-picked onto the 0.950 branch, so we should be able to point mypy_primer at this directory as soon as 0.950 is released.

Copy link
Member

Choose a reason for hiding this comment

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

Hopefully we can just run mypy regularly, no need to use mypy-primer.

],
"exclude": [
"**/@python2"
Expand Down
3 changes: 2 additions & 1 deletion pyrightconfig.stricter.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"typeshedPath": ".",
"include": [
"stdlib",
"stubs"
"stubs",
"test_cases"
],
"exclude": [
"**/@python2",
Expand Down
12 changes: 0 additions & 12 deletions stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ class object:
def __format__(self, __format_spec: str) -> str: ...
def __getattribute__(self, __name: str) -> Any: ...
def __sizeof__(self) -> int: ...
# return type of pickle methods is rather hard to express in the current type system
# see #6661 and https://docs.python.org/3/library/pickle.html#object.__reduce__
def __reduce__(self) -> str | tuple[Any, ...]: ...
if sys.version_info >= (3, 8):
def __reduce_ex__(self, __protocol: SupportsIndex) -> str | tuple[Any, ...]: ...
Expand Down Expand Up @@ -260,8 +258,6 @@ class int:
def __pow__(self, __x: _PositiveInteger, __modulo: None = ...) -> int: ...
@overload
def __pow__(self, __x: _NegativeInteger, __modulo: None = ...) -> float: ...
# positive x -> int; negative x -> float
# return type must be Any as `int | float` causes too many false-positive errors
@overload
def __pow__(self, __x: int, __modulo: None = ...) -> Any: ...
def __rpow__(self, __x: int, __mod: int | None = ...) -> Any: ...
Expand Down Expand Up @@ -317,8 +313,6 @@ class float:
def __divmod__(self, __x: float) -> tuple[float, float]: ...
@overload
def __pow__(self, __x: int, __mod: None = ...) -> float: ...
# positive x -> float; negative x -> complex
# return type must be Any as `float | complex` causes too many false-positive errors
@overload
def __pow__(self, __x: float, __mod: None = ...) -> Any: ...
def __radd__(self, __x: float) -> float: ...
Expand All @@ -328,7 +322,6 @@ class float:
def __rtruediv__(self, __x: float) -> float: ...
def __rmod__(self, __x: float) -> float: ...
def __rdivmod__(self, __x: float) -> tuple[float, float]: ...
# Returns complex if the argument is negative.
def __rpow__(self, __x: float, __mod: None = ...) -> Any: ...
def __getnewargs__(self) -> tuple[float]: ...
def __trunc__(self) -> int: ...
Expand Down Expand Up @@ -1418,15 +1411,10 @@ if sys.version_info >= (3, 8):
def pow(base: int, exp: _PositiveInteger, mod: None = ...) -> int: ... # type: ignore[misc]
@overload
def pow(base: int, exp: _NegativeInteger, mod: None = ...) -> float: ... # type: ignore[misc]
# int base & positive-int exp -> int; int base & negative-int exp -> float
# return type must be Any as `int | float` causes too many false-positive errors
@overload
def pow(base: int, exp: int, mod: None = ...) -> Any: ...
@overload
def pow(base: float, exp: int, mod: None = ...) -> float: ...
# float base & float exp could return float or complex
# return type must be Any (same as complex base, complex exp),
# as `float | complex` causes too many false-positive errors
@overload
def pow(base: float, exp: complex | _SupportsSomeKindOfPow, mod: None = ...) -> Any: ...
@overload
Expand Down
27 changes: 27 additions & 0 deletions test_cases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Regression tests for typeshed

This directory contains regression tests for the stubs found elsewhere in the
typeshed repo. Each file contains a number of test cases, all of which should
pass a type checker without error.
Comment on lines +3 to +5
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the long term we should probably also allow test that fail a type checker to be able to test the negative case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup. Once we're running mypy on this directory, we can do that by # type: ignoring (with specific error codes) lines that we want to fail and running mypy with --warn-unused-ignores. We can't use that flag globally when running mypy on typeshed, but we should be able to use it for this directory.


The internal structure of this directory should mimic typeshed as a whole.
However, unlike the rest of typeshed, this directory largely contains `.py`
files. This is because the purpose of this folder is to test the implications
of typeshed changes for end users. For example, the stub for `builtins.pow` is
found in `stdlib/builtins.pyi`, and the regression tests for `pow` are found in
`test_cases/stdlib/test_builtins.py`.

100% test coverage for typeshed is neither necessary nor desirable, as it would
lead to unnecessary code duplication. Moreover, typeshed has multiple other
mechanisms for spotting errors in the stubs. As such, this directory should
only contain tests for functions and classes which are known to have caused
problems in the past, where the stubs are difficult to get right.
Copy link
Collaborator

Choose a reason for hiding this comment

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

While this is correct, it might need a tl;dr in bold. As discussed, it's difficult to convince some people that they don't need to write tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

How's it look after b2e59b5?


Note that, unlike elsewhere in typeshed, the test cases in this directory
cannot always use modern syntax for type hints. For example, PEP 604 syntax
(unions with a pipe `|` operator) is new in Python 3.10. While this syntax can
be used on older Python versions in a `.pyi` file, code using this syntax will
fail at runtime on Python <=3.9. Since the test cases all use `.py` extensions,
and since the tests need to pass on all Python versions >=3.6, PEP 604 syntax
cannot be used in a test case. Use `typing.Union` and `typing.Optional`
instead.
56 changes: 56 additions & 0 deletions test_cases/stdlib/test_builtins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from decimal import Decimal
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
from fractions import Fraction
from typing import Any, NoReturn, Tuple, Union
from typing_extensions import Literal, assert_type


#
# REGRESSION TESTS FOR OBJECT.__REDUCE__
#
# The following should pass without error (see #6661):
class Diagnostic:
def __reduce__(self) -> Union[str, Tuple[Any, ...]]:
res = super().__reduce__()
if isinstance(res, tuple) and len(res) >= 3:
res[2]["_info"] = 42

return res


#
# REGRESSION TESTS FOR POW()
#
assert_type(pow(1, 0), Literal[1]) # See #7163
assert_type(pow(1, 0, None), Literal[1]) # See #7163
assert_type(pow(2, 4, 0), NoReturn)
assert_type(pow(2, 4), int)
assert_type(pow(5, -7), float)
assert_type(pow(2, 4, 5), int) # pow(<smallint>, <smallint>, <smallint>)
assert_type(pow(2, 35, 3), int) # pow(<smallint>, <bigint>, <smallint>)
assert_type(pow(4.6, 8), float)
assert_type(pow(5.1, 4, None), float)
assert_type(pow(complex(6), 6.2), complex)
assert_type(pow(complex(9), 7.3, None), complex)
assert_type(pow(Fraction(), 4, None), Fraction)
assert_type(pow(Fraction(3, 7), complex(1, 8)), complex)
assert_type(pow(complex(4, -8), Fraction(2, 3)), complex)
assert_type(pow(Decimal("1.0"), Decimal("1.6")), Decimal)
assert_type(pow(Decimal("1.0"), Decimal("1.0"), Decimal("1.0")), Decimal)
assert_type(pow(Decimal("4.6"), 7, None), Decimal)
assert_type((4).__pow__(7, 4), int)
assert_type((4).__pow__(6, None), int)
assert_type(complex(9).__pow__(3.1, None), complex)
assert_type(Decimal("2.6").__pow__(5, None), Decimal)

# These would ideally be more precise, but `Any` is acceptable
# They have to be `Any` due to the fact that type-checkers can't distinguish between positive and negative numbers for the second argument to `pow()`
#
# int for positive 2nd-arg, float otherwise
assert_type(pow(4, 65), Any)
assert_type(pow(3, 57, None), Any)
# float for positive 2nd-arg, complex otherwise
assert_type(pow(4.7, 7.4), Any)
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
assert_type(pow(4.7, 9.2, None), Any)
assert_type((6.2).__pow__(5.2, None), Any)
# See #7046 -- float for a positive 1st arg, complex otherwise
assert_type((-2) ** 0.5, Any)