Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ Contributors
- Jinyong Li, `Jinyong-Li@github <https://github.com/Jinyong-Li>`_
- wynneyin, `wynneyin@github <https://github.com/wynneyin>`_
- Arin Aeruva, `arin30@github <https://github.com/arin30>`_
- Cyber Marty, `Cyber-Marty@github <https://github.com/Cyber-Marty>`_
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changelog
Unreleased
----------

- Stop ``partial`` and ``partial_right`` wrappers from accepting a positional iteratee argument in
a slot that a keyword argument already binds, so ``map_`` passes the element instead of an index
into the bound parameter.
- Forward keyword arguments unchanged through ``over_args``.
- Fix bug in ``random`` where float bounds spanning more than the maximum finite float (e.g.
``random(-1e308, 1e308)``) could return ``inf`` or ``nan`` instead of a value within the bounds.
Expand Down
29 changes: 21 additions & 8 deletions src/pydash/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,18 +639,31 @@ def _argcount(self) -> t.Optional[int]:
)
if func_argcount is None:
return None
argcount = func_argcount - len(self.args)
try:
argspec = getfullargspec(self.func)
except TypeError:
bound_keywords = len(self.kwargs)
argcount -= len(self.kwargs)
else:
# Keyword-only parameters and **kwargs do not consume positional slots.
bound_keywords = (
len(self.kwargs)
if argspec.varargs
else sum(key in argspec.args for key in self.kwargs)
)
argcount = func_argcount - len(self.args) - bound_keywords
if argspec.varargs:
argcount -= len(self.kwargs)
else:
# Keyword-only parameters and **kwargs do not consume positional slots.
# `argspec.args` may start with an implicit `self`/`cls` slot that
# `func_argcount` already excludes, so align both before indexing slots.
implicit_slots = len(argspec.args) - func_argcount
first_bound_slot = next(
(
index
for index, name in enumerate(argspec.args[implicit_slots:])
if name in self.kwargs
),
None,
)
if first_bound_slot is not None:
# A positional parameter bound by keyword can't also receive a caller
# argument, so only the slots before it stay positionally reachable.
argcount = min(argcount, first_bound_slot - len(self.args))
return argcount if argcount >= 0 else None


Expand Down
81 changes: 81 additions & 0 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,87 @@ def transform(value, *, increment):
assert _.map_([1, 2], wrapped) == [3, 4]


@parametrize("factory", [_.partial, _.partial_right])
def test_partial_keyword_bound_middle_argument_as_iteratee(factory):
def transform(value, increment=0, scale=1):
return (value + increment) * scale

wrapped = factory(transform, increment=2)

assert wrapped(1) == 3
assert wrapped._argcount == 1
assert _.map_([1, 2], wrapped) == [3, 4]
assert _.chain([1, 2]).map_(wrapped).value() == [3, 4]


@parametrize("factory", [_.partial, _.partial_right])
def test_partial_keyword_bound_argument_argcount(factory):
def transform(value, increment=0, scale=1):
return (value + increment) * scale

assert factory(transform, scale=2)._argcount == 2
assert factory(transform, increment=2)._argcount == 1
assert factory(transform, increment=2, scale=3)._argcount == 1


@parametrize("factory", [_.partial, _.partial_right])
def test_partial_keyword_bound_argument_argcount_with_implicit_first_parameter(factory):
class Parser:
def parse(self, value, base=0, scale=1):
return (value + base) * scale

@classmethod
def parse_class(cls, value, base=0, scale=1):
return (value + base) * scale

def __call__(self, value, base=0, scale=1):
return (value + base) * scale

parser = Parser()

# The implicit `self`/`cls` that `getfullargspec` reports must not shift the bound slot.
assert factory(parser.parse, scale=2)._argcount == 2
assert factory(parser.parse, base=2)._argcount == 1
assert factory(parser, scale=2)._argcount == 2
assert factory(Parser.parse_class, scale=2)._argcount == 2


def test_partial_keyword_bound_method_as_iteratee():
class Parser:
def parse(self, value, base=0, scale=1):
return (value + base) * scale

parser = Parser()

assert _.map_([1, 2], _.partial(parser.parse, scale=2)) == [2, 6]
assert _.map_([1, 2], _.partial(parser.parse, base=2)) == [3, 4]


@parametrize("factory", [_.partial, _.partial_right])
def test_partial_keyword_bound_argument_argcount_with_bound_positional(factory):
def transform(left, right, increment=0, scale=1):
return (left, right, increment, scale)

assert factory(transform, 10, increment=5)._argcount == 1
assert factory(transform, 10, 20, increment=5)._argcount == 0


def test_partial_bound_positional_and_keyword_slot_as_iteratee():
def transform(left, right, increment=0, scale=1):
return (left, right, increment, scale)

# Slots before the keyword-bound parameter still receive the element.
assert _.map_([1, 2], _.partial(transform, 10, increment=5)) == [
(10, 1, 5, 1),
(10, 2, 5, 1),
]
# A zero argument count calls the wrapper without the element or index.
assert _.map_([1, 2], _.partial(transform, 10, 20, increment=5)) == [
(10, 20, 5, 1),
(10, 20, 5, 1),
]


@parametrize("factory", [_.partial, _.partial_right])
def test_partial_variable_keyword_arguments_as_iteratee(factory):
def transform(value, **options):
Expand Down