Skip to content

Commit

Permalink
Fix PEP 570 pos-only args in stubs (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Mar 16, 2023
1 parent cbdd90a commit 0e8482f
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

## Unreleased

- Fix handling of positional-only arguments using `/` syntax in stubs (#601)
- Fix bug where objects with a `__call__` method that takes `*args` instead
of `self` was not considered callable (#600)

Expand Down
3 changes: 3 additions & 0 deletions pyanalyze/stubs/_pyanalyze_tests-stubs/posonly.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def f(x: int, /, y: int, z: int = ...) -> None: ...
def g(x: int = ..., /, y: int = ...) -> None: ...
def h(x: int, /, y: int = ..., z: int = ...) -> None: ...
24 changes: 24 additions & 0 deletions pyanalyze/test_typeshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,30 @@ def test_overloaded(self):
assert isinstance(val, CallableValue)
assert isinstance(val.signature, OverloadedSignature)

@skip_before((3, 8))
@assert_passes()
def test_pos_only(self):
def capybara():
from _pyanalyze_tests.posonly import f, g, h

f(1, 2)
f(1, 2, 3)
f(1, y=2, z=3)
f(x=1, y=2) # E: incompatible_call
f() # E: incompatible_call

g()
g(1)
g(1, 2)
g(1, y=2)
g(x=1) # E: incompatible_call

h() # E: incompatible_call
h(1)
h(x=1) # E: incompatible_call
h(1, y=2)
h(1, 2, 3)

def test_typeddict(self):
tsf = TypeshedFinder.make(Checker(), TEST_OPTIONS, verbose=True)
mod = "_pyanalyze_tests.typeddict"
Expand Down
33 changes: 27 additions & 6 deletions pyanalyze/typeshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,13 +840,34 @@ def _get_signature_from_func_def(
if node.decorator_list:
objclass = None
args = node.args

num_without_defaults = len(args.args) - len(args.defaults)
defaults = [None] * num_without_defaults + args.defaults
arguments = list(
self._parse_param_list(
args.args, defaults, mod, ParameterKind.POSITIONAL_OR_KEYWORD, objclass
arguments: List[SigParameter] = []
if sys.version_info >= (3, 8):
assert hasattr(args, "posonlyargs")
num_pos_only_args = len(args.posonlyargs)
defaults = args.defaults
num_pos_only_defaults = len(defaults) - len(args.args)
if num_pos_only_defaults > 0:
num_without_default = num_pos_only_args - num_pos_only_defaults
pos_only_defaults = [None] * num_without_default + defaults[
num_pos_only_defaults:
]
defaults = defaults[num_pos_only_defaults:]
else:
pos_only_defaults = [None for _ in args.posonlyargs]
arguments += self._parse_param_list(
args.posonlyargs,
pos_only_defaults,
mod,
ParameterKind.POSITIONAL_ONLY,
objclass,
)
else:
defaults = args.defaults

num_without_defaults = len(args.args) - len(defaults)
defaults = [None] * num_without_defaults + defaults
arguments += self._parse_param_list(
args.args, defaults, mod, ParameterKind.POSITIONAL_OR_KEYWORD, objclass
)
if autobind:
if is_classmethod or not is_staticmethod:
Expand Down

0 comments on commit 0e8482f

Please sign in to comment.