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

fixing mypy errors with loosest rules #2040

Merged
merged 3 commits into from
Mar 8, 2022
Merged

Conversation

mgor
Copy link
Contributor

@mgor mgor commented Mar 4, 2022

tried to update locust to 2.8 in a project that uses mypy for static type checking and ran into problems.

noticed that locust/py.typed had been added (TIL). i've tried to cleanup the typing in locust by running mypy with default settings and fixing the errors.

➜ mypy --ignore-missing-imports locust/ --exclude locust/test
locust/clients.py:4: error: Library stubs not installed for "requests" (or incompatible with Python 3.8)
locust/clients.py:4: note: Hint: "python3 -m pip install types-requests"
locust/clients.py:4: note: (or run "mypy --install-types" to install all missing stub packages)
locust/clients.py:4: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
locust/clients.py:6: error: Library stubs not installed for "requests.auth" (or incompatible with Python 3.8)
locust/clients.py:7: error: Library stubs not installed for "requests.exceptions" (or incompatible with Python 3.8)
locust/stats.py:243: error: Need type annotation for "num_reqs_per_sec" (hint: "num_reqs_per_sec: Dict[<type>, <type>] = ...")
locust/stats.py:245: error: Need type annotation for "num_fail_per_sec" (hint: "num_fail_per_sec: Dict[<type>, <type>] = ...")
locust/stats.py:247: error: Need type annotation for "response_times" (hint: "response_times: Dict[<type>, <type>] = ...")
locust/input_events.py:91: error: Function "builtins.callable" is not valid as a type
locust/input_events.py:91: note: Perhaps you need "Callable[...]" or a callback protocol?
locust/user/task.py:250: error: Need type annotation for "_task_queue" (hint: "_task_queue: List[<type>] = ...")
locust/user/task.py:261: error: Cannot determine type of "min_wait"
locust/user/task.py:262: error: Cannot determine type of "min_wait"
locust/user/task.py:271: error: Incompatible return value type (got "Optional[User]", expected "User")
locust/util/deprecation.py:33: error: Dynamic metaclass not supported for "DeprecatedLocustClass"
locust/util/deprecation.py:42: error: Dynamic metaclass not supported for "DeprecatedHttpLocustClass"
locust/util/deprecation.py:51: error: Dynamic metaclass not supported for "DeprecatedFastHttpLocustClass"
locust/user/users.py:52: error: Incompatible types in assignment (expression has type "None", variable has type "str")
locust/web.py:64: error: Incompatible types in assignment (expression has type "None", variable has type "Dict[Any, Any]")
locust/shape.py:12: error: Incompatible types in assignment (expression has type "None", variable has type "Runner")
locust/runners.py:242: error: Need type annotation for "to_stop" (hint: "to_stop: List[<type>] = ...")
locust/runners.py:1005: error: Need type annotation for "reported_user_classes_count"
locust/env.py:43: error: Incompatible types in assignment (expression has type "None", variable has type "Runner")
locust/env.py:46: error: Incompatible types in assignment (expression has type "None", variable has type "WebUI")
locust/env.py:49: error: Incompatible types in assignment (expression has type "None", variable has type "int")
locust/env.py:115: error: Attribute "runner" already defined on line 43
locust/dispatch.py:80: error: Argument 1 to "map" has incompatible type overloaded function; expected "Callable[[dict_values[_KT, _VT]], Union[_T, int]]"
locust/dispatch.py:80: error: Argument 1 to "map" has incompatible type "Callable[[Dict[_KT, _VT]], dict_values[_KT, _VT]]"; expected "Callable[[Dict[str, int]], dict_values[_KT, _VT]]"
locust/dispatch.py:89: error: Need type annotation for "_dispatch_iteration_durations" (hint: "_dispatch_iteration_durations: List[<type>] = ...")
locust/dispatch.py:91: error: Need type annotation for "_active_users" (hint: "_active_users: List[<type>] = ...")
locust/dispatch.py:111: error: No overload variant of "next" matches argument type "None"
locust/dispatch.py:111: note: Possible overload variants:
locust/dispatch.py:111: note:     def [_T] next(SupportsNext[_T]) -> _T
locust/dispatch.py:111: note:     def [_T, _VT] next(SupportsNext[_T], _VT) -> Union[_T, _VT]
locust/dispatch.py:144: error: Unsupported left operand type for < ("_T")
locust/dispatch.py:144: error: Unsupported operand types for < ("int" and "None")
locust/dispatch.py:144: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:154: error: Unsupported left operand type for > ("_T")
locust/dispatch.py:154: error: Unsupported operand types for > ("int" and "None")
locust/dispatch.py:154: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:170: error: Incompatible types in assignment (expression has type "int", variable has type "None")
locust/dispatch.py:172: error: Incompatible types in assignment (expression has type "float", variable has type "None")
locust/dispatch.py:174: error: Incompatible types in assignment (expression has type "int", variable has type "None")
locust/dispatch.py:174: error: Argument 1 to "floor" has incompatible type "None"; expected "Union[SupportsFloat, SupportsIndex]"
locust/dispatch.py:176: error: Unsupported left operand type for / ("None")
locust/dispatch.py:182: error: Argument 1 to "map" has incompatible type overloaded function; expected "Callable[[dict_values[_KT, _VT]], Union[_T, int]]"
locust/dispatch.py:182: error: Argument 1 to "map" has incompatible type "Callable[[Dict[_KT, _VT]], dict_values[_KT, _VT]]"; expected "Callable[[Dict[str, int]], dict_values[_KT, _VT]]"
locust/dispatch.py:184: error: Incompatible types in assignment (expression has type "Generator[Dict[str, Dict[str, int]], None, None]", variable has type "None")
locust/dispatch.py:221: error: Argument 1 to "_distribute_users" of "UsersDispatcher" has incompatible type "Union[_T, int]"; expected "int"
locust/dispatch.py:229: error: Incompatible types in assignment (expression has type "Iterator[WorkerNode]", variable has type "cycle[WorkerNode]")
locust/dispatch.py:233: error: Argument 1 to "contextmanager" has incompatible type "Callable[[UsersDispatcher], None]"; expected "Callable[[UsersDispatcher], Iterator[<nothing>]]"
locust/dispatch.py:234: error: The return type of a generator function should be "Generator" or one of its supertypes
locust/dispatch.py:251: error: Unsupported operand types for - ("None" and "float")
locust/dispatch.py:259: error: No overload variant of "min" matches argument types "Union[Any, int]", "None"
locust/dispatch.py:259: note: Possible overload variants:
locust/dispatch.py:259: note:     def [SupportsRichComparisonT] min(SupportsRichComparisonT, SupportsRichComparisonT, *_args: SupportsRichComparisonT, key: None = ...) -> SupportsRichComparisonT
locust/dispatch.py:259: note:     def [_T] min(_T, _T, *_args: _T, key: Callable[[_T], Union[SupportsDunderLT, SupportsDunderGT]]) -> _T
locust/dispatch.py:259: note:     def [SupportsRichComparisonT] min(Iterable[SupportsRichComparisonT], *, key: None = ...) -> SupportsRichComparisonT
locust/dispatch.py:259: note:     def [_T] min(Iterable[_T], *, key: Callable[[_T], Union[SupportsDunderLT, SupportsDunderGT]]) -> _T
locust/dispatch.py:259: note:     def [SupportsRichComparisonT, _T] min(Iterable[SupportsRichComparisonT], *, key: None = ..., default: _T) -> Union[SupportsRichComparisonT, _T]
locust/dispatch.py:259: note:     def [_T1, _T2] min(Iterable[_T1], *, key: Callable[[_T1], Union[SupportsDunderLT, SupportsDunderGT]], default: _T2) -> Union[_T1, _T2]
locust/dispatch.py:260: error: Unsupported left operand type for + ("_T")
locust/dispatch.py:260: error: Unsupported operand types for + ("int" and "None")
locust/dispatch.py:260: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:269: error: Unsupported operand types for + ("_T" and "int")
locust/dispatch.py:269: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:281: error: No overload variant of "max" matches argument types "Union[Any, int]", "None"
locust/dispatch.py:281: note: Possible overload variants:
locust/dispatch.py:281: note:     def [SupportsRichComparisonT] max(SupportsRichComparisonT, SupportsRichComparisonT, *_args: SupportsRichComparisonT, key: None = ...) -> SupportsRichComparisonT
locust/dispatch.py:281: note:     def [_T] max(_T, _T, *_args: _T, key: Callable[[_T], Union[SupportsDunderLT, SupportsDunderGT]]) -> _T
locust/dispatch.py:281: note:     def [SupportsRichComparisonT] max(Iterable[SupportsRichComparisonT], *, key: None = ...) -> SupportsRichComparisonT
locust/dispatch.py:281: note:     def [_T] max(Iterable[_T], *, key: Callable[[_T], Union[SupportsDunderLT, SupportsDunderGT]]) -> _T
locust/dispatch.py:281: note:     def [SupportsRichComparisonT, _T] max(Iterable[SupportsRichComparisonT], *, key: None = ..., default: _T) -> Union[SupportsRichComparisonT, _T]
locust/dispatch.py:281: note:     def [_T1, _T2] max(Iterable[_T1], *, key: Callable[[_T1], Union[SupportsDunderLT, SupportsDunderGT]], default: _T2) -> Union[_T1, _T2]
locust/dispatch.py:282: error: Unsupported left operand type for - ("_T")
locust/dispatch.py:282: error: Unsupported operand types for - ("int" and "None")
locust/dispatch.py:282: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:290: error: Unsupported operand types for - ("_T" and "int")
locust/dispatch.py:290: note: Left operand is of type "Union[_T, int]"
locust/dispatch.py:347: error: Incompatible return value type (got "cycle[None]", expected "Generator[Optional[str], None, None]")
locust/dispatch.py:357: error: "User" has no attribute "__name__"
locust/dispatch.py:373: error: Incompatible return value type (got "cycle[Any]", expected "Generator[Optional[str], None, None]")
locust/dispatch.py:377: error: List comprehension has incompatible type List[Tuple[Type[User], int]]; expected List[Tuple[User, int]]
locust/dispatch.py:378: error: List comprehension has incompatible type List[Tuple[Type[User], int]]; expected List[Tuple[User, int]]
locust/dispatch.py:385: error: Need type annotation for "spawned_classes" (hint: "spawned_classes: Set[<type>] = ...")
locust/dispatch.py:403: error: Incompatible types in "yield" (actual type "Optional[str]", expected type "str")
locust/dispatch.py:412: error: Argument 1 to "map" has incompatible type "Callable[[Dict[_KT, _VT]], Dict[_KT, _VT]]"; expected "Callable[[Dict[str, int]], Dict[_KT, _VT]]"
locust/debug.py:66: error: Incompatible types in assignment (expression has type "str", variable has type "Dict[Any, Any]")
locust/debug.py:96: error: Incompatible types in assignment (expression has type "None", variable has type "Environment")
locust/debug.py:145: error: "Environment" has no attribute "single_user_instance"
locust/contrib/fasthttp.py:88: error: Unsupported left operand type for + ("None")
locust/contrib/fasthttp.py:88: note: Left operand is of type "Optional[str]"
locust/contrib/fasthttp.py:331: error: Incompatible types in assignment (expression has type "None", variable has type "str")
locust/contrib/fasthttp.py:340: error: Incompatible return value type (got "None", expected "str")
Found 67 errors in 13 files (checked 35 source files)

with this PR, the tests succeeds, and:

➜ mypy --ignore-missing-imports locust/ --exclude locust/test
Success: no issues found in 35 source files

while testing the changes i had some problems with the packaging when specifying locust dependency from a git repo.
i noticed that setuptools-scm has deprecated using setup.py after version 6.0.1, so i added a constraint for that package in setup.py. (also, the black configuration specified in pyproject.toml caused problems with pip, causing it to think it was going to use that instead of setup.{py,cfg}, unfortunately the black maintainers won't add support for providing it via setup.cfg).

setup.py Show resolved Hide resolved
@cyberw
Copy link
Collaborator

cyberw commented Mar 4, 2022

Cool stuff. Will take a real look next week. Do we need to add mypy to the build or would this already fail the build?

@mgor
Copy link
Contributor Author

mgor commented Mar 4, 2022

There's no need to add mypy as a install_require dependency, it could however be good to add checkinging typing in the tests.
This PR excludes the errors in locust/test and also ignores any missing type stubs for dependencies.

I guess it's up to you/the project to decide if there should be any type checker, and if so which one, and which rules should be used. This is why I ran with minimal rules, since I didn't want to impose too much.

@cyberw
Copy link
Collaborator

cyberw commented Mar 4, 2022

We can do minmal rules but it would be nice to add mypy to tox.ini (otherwise we would just end up breaking your project and others down the line)

@mgor
Copy link
Contributor Author

mgor commented Mar 4, 2022

Cool, I'll add mypy to tox.ini and update the PR! I'll see if I get some time during the weekend, otherwise I'll do it beginning next week.

fixed typing errors in locust/test.
locust/debug.py Show resolved Hide resolved
@@ -301,7 +304,9 @@ def _get_user_current_count(self, user: str) -> int:

def _distribute_users(
self, target_user_count: int
) -> Tuple[dict, Generator[str, None, None], typing.Iterator["WorkerNode"], List[Tuple["WorkerNode", str]]]:
) -> Tuple[
Dict[str, Dict[str, int]], Generator[Optional[str], None, None], itertools.cycle, List[Tuple["WorkerNode", str]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is str really optional? Feels like a different solution could be possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it was due to:

def infinite_cycle_gen(users: List[Tuple[Type[User], int]]) -> itertools.cycle:
            if not users:
                return itertools.cycle([None])

i made quick check, and change it to itertools.cycle([]) and removed Optional and mypy didn't complain. just about to logout, but i'll run the tests tomorrow to make sure they still pass as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one was a bit tricky, itertools.cycle([None]) gives you an endless loop of None values, and where these values are used there's checks, like:

        while user_count < target_user_count:
            user = next(user_gen)
            if not user:
                break

and,

        for user in self._user_generator:
            if not user:
                self._no_user_to_spawn = True
                break

While itertools.cycle([]) gives you nothing, e.g., it would be like for _ in [].

So I would say that yes, it is an Optional[str], otherwise there would have to be quite some logic thas has to be re-written in dispatch.py

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so this one can be typed as str when using --no-strict-optional settings for mypy. I'd like to see it typed as Optional[str] though :) But your call.

locust/clients.py Outdated Show resolved Hide resolved
locust/contrib/fasthttp.py Outdated Show resolved Hide resolved
@@ -62,13 +61,13 @@ def __init__(self, worker_nodes: "List[WorkerNode]", user_classes: List[Type[Use
assert len(user_classes) > 0
assert len(set(self._user_classes)) == len(self._user_classes)

self._target_user_count = None
self._target_user_count: int
Copy link
Collaborator

Choose a reason for hiding this comment

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

This changes self._target_user_count to be undefined (rather than None), right? Maybe that is ok, but I just want to be clear :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah crap... if it would be used before actually being set, it would actually throw AttributeError, e.g. it would not be None.

I see 3 options:

  1. Change them to Optional[..], and then deal with all the typing errors that it might cause if it's used without checking that it's not None, e.g. changing this one to Optional[int] produces:
➜ mypy locust/ --ignore-missing-imports
locust/dispatch.py:147: error: Unsupported operand types for < ("int" and "None")
locust/dispatch.py:147: note: Right operand is of type "Optional[int]"
locust/dispatch.py:157: error: Unsupported operand types for > ("int" and "None")
locust/dispatch.py:157: note: Right operand is of type "Optional[int]"
locust/dispatch.py:262: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[int]"
locust/dispatch.py:274: error: Unsupported operand types for >= ("int" and "None")
locust/dispatch.py:274: note: Right operand is of type "Optional[int]"
locust/dispatch.py:284: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[int]"
locust/dispatch.py:295: error: Unsupported operand types for <= ("int" and "None")
locust/dispatch.py:295: note: Right operand is of type "Optional[int]"
Found 6 errors in 1 file (checked 61 source files)
  1. Set an initial value of e.g. 0
  2. Use setattr to monkey patch them with an initial value of None
  3. # type: ignore

I'd say that the list is ordered by, my, preference. Where 1 would cause most changes.

However, all the tests pass, so internally locust does not seem to try to use the variables before they have actually been set.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps we could use --no-strict-optional? It would simplify things in a lot of places.

Copy link
Collaborator

@cyberw cyberw Mar 8, 2022

Choose a reason for hiding this comment

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

Or maybe... it seems possible to do self._target_user_count: int = None?

It could definitely be argued that it is weird to have a None-value in something that is not marked as an Optional, but in this case I think it may make sense, because it will not be None for outside users.

Having None-checks all over the place just to keep the type checker happy (option 1) seems excessive (and options 2-4 feel even worse :) (although you could argue that this solution is just a variant of option 4)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh man, I was looking for an option to mypy that didn't force marking variables Optional for having a None value. My bad for missing this one.

The combination that worked as self._target_user_count: int = None and --no-strict-optional parameter to mypy.

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 Lets do --no-strict-optional for now, and remove the Optional in declarations (except when it is obvious that it may happen even in normal circumstances and thus need to be handled in user code)

.gitignore Show resolved Hide resolved
@cyberw cyberw merged commit 6bddf09 into locustio:master Mar 8, 2022
@cyberw
Copy link
Collaborator

cyberw commented Mar 8, 2022

Thanks!

@mgor mgor deleted the bug/typing branch March 13, 2022 15:36
@mgor mgor mentioned this pull request Apr 11, 2022
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.

None yet

2 participants