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 type hints #576

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Add type hints #576

wants to merge 2 commits into from

Conversation

getzze
Copy link

@getzze getzze commented Mar 26, 2024

This is a huge PR, but hopefully it is not too hard to review.

To type check the code I had to setup mypy and I found it was simpler to also add a pyproject.toml file and bump the minimal python version to 3.8 (3.7 is not supported anymore anyway).
I also set up a ruff workflow in the pyproject.toml file, I find it very useful to lint and format the code.

All tests are passing, mypy and ruff give no errors.

The problem to review this PR is that I modified the python code sometimes, to improve type checking. Maybe I can add comments where the code was modified to make it easier to review. Some examples of things that I modified:

  • make no_default an Enum sentinel: using the string "no_default" do not work anymore
  • same for no_pad.
  • split def some_func(*args, ...) in def some_func(func, /, *args, ...) when args needed to be a function followed by its arguments.
  • use an explicit list of keyword arguments instead of **kwargs in function definitions to be able to type check the arguments.
  • add missing returns
  • transform assert to if blocks.

Also, I added a dependency to typing_extensions, that is a backport of typing improvements to older python versions. It can be removed, losing a bit of type checking for functoolz.excepts and functoolz.juxt.

Related PR: #516 #502
Issue: #496

if package == 'tlz':
return ModuleSpec(fullname, self)
return None
Copy link
Author

Choose a reason for hiding this comment

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

Missing return

if package == 'tlz':
return self
return None
Copy link
Author

Choose a reason for hiding this comment

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

missing return


class TlzLoader:

class TlzLoader(Loader):
Copy link
Author

Choose a reason for hiding this comment

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

Make it an explicit subclass of Loader

Comment on lines +622 to +623
if sigspec is None:
return -1
Copy link
Author

Choose a reason for hiding this comment

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

account for sigspec=None

""" Return the names of position-only arguments if func has **kwargs"""
if num_pos_only == 0:
if num_pos_only == 0 or sigspec is None:
Copy link
Author

Choose a reason for hiding this comment

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

account for sigspec=None

@@ -641,7 +673,7 @@ def expand_sig(sig):
if isinstance(sig, tuple):
if len(sig) == 3:
num_pos_only, func, keyword_only = sig
assert isinstance(sig[-1], tuple)
# assert isinstance(sig[-1], tuple)
Copy link
Author

Choose a reason for hiding this comment

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

remove assert, replace with nothing here

@@ -36,6 +35,7 @@
count,
curry,
diff,
excepts,
Copy link
Author

Choose a reason for hiding this comment

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

For the tests to pass

from .exceptions import merge, merge_with

accumulate = toolz.curry(toolz.accumulate)
apply = toolz.curry(toolz.apply)
Copy link
Author

Choose a reason for hiding this comment

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

For the tests to pass, because the signature of apply changed.

def merge(*dicts, **kwargs):
def merge(
*dicts: Mapping[_S, _T],
factory: type[_DictType] = dict,
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add a factory keyword argument

@eriknw
Copy link
Member

eriknw commented Mar 26, 2024

wow, nice! Thanks @getzze! I'm in the process of moving so it may take me some time to properly review this, but know that this is appreciated and likely to go in :)

def merge_with(
func: Callable[[Sequence[_T]], _U],
*dicts: Mapping[_S, _T],
factory: type[_DictType] = dict,
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add a factory keyword argument


values = collections.defaultdict(lambda: [].append)
values = defaultdict(list)
Copy link
Author

Choose a reason for hiding this comment

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

use defaultdict(list) instead of a lambda.

def dissoc(
d: Mapping[_S, _T],
*keys: _S,
factory: type[_DictType] = dict,
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add a factory keyword argument

if self.key == self._default_hashkey:
val = self.key
def __hash__(self) -> int:
if not callable(self.key):
Copy link
Author

Choose a reason for hiding this comment

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

check if the key is callable instead of a specific string.

@@ -50,7 +70,8 @@ def fold(binop, seq, default=no_default, map=map, chunksize=128, combine=None):
>>> fold(add, [1, 2, 3, 4], chunksize=2, map=map)
10
"""
assert chunksize > 1
# assert chunksize > 1
chunksize = max(chunksize, 1)
Copy link
Author

Choose a reason for hiding this comment

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

update the variable instead of using an assert

Comment on lines +15 to +32
class NoDefaultType(Enum):
no_default = '__no_default__'


no_default = NoDefaultType.no_default

# no_default = '__no__default__'
# NoDefaultType = Literal['__no__default__']


class NoPadType(Enum):
no_pad = '__no_pad__'


no_pad = NoPadType.no_pad

# no_pad = '__no__pad__'
# NoPadType = Literal['__no__pad__']
Copy link
Author

Choose a reason for hiding this comment

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

use an Enum for no_default and no_pad instead of a string

Comment on lines +151 to +155
d: dict[_S, list[_T]] = collections.defaultdict(list)
for item in seq:
d[key(item)](item)
rv = {}
for k, v in d.items():
rv[k] = v.__self__
return rv
vals = d[key(item)]
vals.append(item)
return d
Copy link
Author

Choose a reason for hiding this comment

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

use defaultdict(list) instead of a lambda and loop only once over items

def merge_sorted(*seqs, **kwargs):
def merge_sorted(
*seqs: Iterable[_CT],
key: UnaryOp[_CT] | None = None,
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add a key keyword argument

@@ -259,21 +305,20 @@ def unique(seq, key=None):
('cat', 'mouse')
"""
seen = set()
seen_add = seen.add
Copy link
Author

Choose a reason for hiding this comment

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

do not cache the set.add function

seen_add(item)
return True
else:
if isinstance(seq, Sequence):
Copy link
Author

Choose a reason for hiding this comment

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

use isinstance(seq, Sequence) instead of not (iter(seq) is seq)

Comment on lines +1047 to +1048
default: _T | NoDefaultType = no_default,
key: TransformOp[_T, Any] | None = None,
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add a default and key keyword arguments

@@ -26,7 +68,7 @@ def identity(x):
return x


def apply(*func_and_args, **kwargs):
def apply(func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T:
Copy link
Author

Choose a reason for hiding this comment

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

explicitly add func first positional only argument.

args = (val,) + args
return func(*args)
return reduce(evalform_front, forms, val)
# if isinstance(form, tuple):
Copy link
Author

Choose a reason for hiding this comment

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

according to the types, no need to check for tuple type, but it would be good to check anyway.

) -> _T:
if callable(form):
return form(val)
# if isinstance(form, tuple):
Copy link
Author

Choose a reason for hiding this comment

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

according to the types, no need to check for tuple type, but it would be good to check anyway.

def __init__(
self,
# TODO: type hint that the returned value of a `partial` is _T
func: curry[_T] | partial | Callable[..., _T],
Copy link
Author

Choose a reason for hiding this comment

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

add an explicit func first position only argument.

func = obj.func
obj = cls(func, *args, **(kwargs or {}))
# TODO: check that func is callable
Copy link
Author

Choose a reason for hiding this comment

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

TODO: check that func is really callable

@allenlawrence94
Copy link

What's preventing this from going through? Would love better typehints in toolz

@Diegovsky
Copy link

@getzze have you lost interest in this? If so, I wouldn't mind continuing your work

@getzze
Copy link
Author

getzze commented Dec 5, 2024

I can do it but there is nothing more to do, no?

@Diegovsky
Copy link

Diegovsky commented Dec 6, 2024

I thought it had become stale due to lack of activity. In that case, what is preventing this from being merged? Maybe we can @ some maintainer to take a look :)

@0zitro
Copy link

0zitro commented Dec 20, 2024

Agreed, this PR seems very cool! But what's up with the deleted versioneer and move to the new pyproject.toml configuration? Don't really think that it fits the PR's goal, does it? Would create a separate PR for that really.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants