Skip to content

Commit faa5da0

Browse files
committed
Add mypy for static analysis
1 parent dabbfed commit faa5da0

File tree

95 files changed

+610
-276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+610
-276
lines changed

Diff for: bitbots_behavior/bitbots_blackboard/CMakeLists.txt

+12-7
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ if(NOT CMAKE_CXX_STANDARD)
66
set(CMAKE_CXX_STANDARD 17)
77
endif()
88

9-
find_package(bio_ik_msgs REQUIRED)
109
find_package(ament_cmake REQUIRED)
11-
find_package(sensor_msgs REQUIRED)
12-
find_package(rclpy REQUIRED)
13-
find_package(tf2 REQUIRED)
10+
find_package(bio_ik_msgs REQUIRED)
11+
find_package(bitbots_docs REQUIRED)
1412
find_package(bitbots_msgs REQUIRED)
13+
find_package(geometry_msgs REQUIRED)
14+
find_package(rclpy REQUIRED)
15+
find_package(sensor_msgs REQUIRED)
1516
find_package(std_msgs REQUIRED)
16-
find_package(tf2_geometry_msgs REQUIRED)
1717
find_package(std_srvs REQUIRED)
18-
find_package(geometry_msgs REQUIRED)
19-
find_package(bitbots_docs REQUIRED)
18+
find_package(tf2 REQUIRED)
19+
find_package(tf2_geometry_msgs REQUIRED)
2020

2121
set(INCLUDE_DIRS
2222
${bio_ik_msgs_INCLUDE_DIRS}
@@ -76,6 +76,11 @@ ament_export_dependencies(geometry_msgs)
7676
ament_export_dependencies(bitbots_docs)
7777
ament_export_include_directories(${INCLUDE_DIRS})
7878

79+
if(BUILD_TESTING)
80+
find_package(ament_cmake_mypy REQUIRED)
81+
ament_mypy(CONFIG_FILE "${CMAKE_CURRENT_LIST_DIR}/mypy.ini")
82+
endif()
83+
7984
ament_python_install_package(${PROJECT_NAME})
8085

8186
ament_package()

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Optional
1+
from typing import TYPE_CHECKING
22

33
from bitbots_utils.utils import nobeartype
44
from rclpy.node import Node
@@ -11,6 +11,6 @@ class AbstractBlackboardCapsule:
1111
"""Abstract class for blackboard capsules."""
1212

1313
@nobeartype
14-
def __init__(self, node: Node, blackboard: Optional["BodyBlackboard"] = None):
14+
def __init__(self, node: Node, blackboard: "BodyBlackboard"):
1515
self._node = node
1616
self._blackboard = blackboard

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/costmap_capsule.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def publish_costmap(self) -> None:
8181
"""
8282
Publishes the costmap for rviz
8383
"""
84+
assert self.costmap is not None, "Costmap is not initialized"
8485
# Normalize costmap to match the rviz color scheme in a good way
8586
normalized_costmap = (
8687
(255 - ((self.costmap - np.min(self.costmap)) / (np.max(self.costmap) - np.min(self.costmap))) * 255 / 2.1)
@@ -157,6 +158,7 @@ def calc_gradients(self) -> None:
157158
"""
158159
Recalculates the gradient map based on the current costmap.
159160
"""
161+
assert self.base_costmap is not None, "Base costmap is not initialized"
160162
gradient = np.gradient(self.base_costmap)
161163
norms = np.linalg.norm(gradient, axis=0)
162164

@@ -203,8 +205,8 @@ def calc_base_costmap(self) -> None:
203205

204206
# Create Grid
205207
grid_x, grid_y = np.mgrid[
206-
0 : self.field_length + self.map_margin * 2 : (self.field_length + self.map_margin * 2) * 10j,
207-
0 : self.field_width + self.map_margin * 2 : (self.field_width + self.map_margin * 2) * 10j,
208+
0 : self.field_length + self.map_margin * 2 : (self.field_length + self.map_margin * 2) * 10j, # type: ignore[misc]
209+
0 : self.field_width + self.map_margin * 2 : (self.field_width + self.map_margin * 2) * 10j, # type: ignore[misc]
208210
]
209211

210212
fix_points: List[Tuple[Tuple[float, float], float]] = []
@@ -278,7 +280,8 @@ def calc_base_costmap(self) -> None:
278280
)
279281

280282
# Smooth the costmap to get more continuous gradients
281-
self.base_costmap = gaussian_filter(interpolated, self.body_config["base_costmap_smoothing_sigma"])
283+
base_costmap: np.ndarray = gaussian_filter(interpolated, self.body_config["base_costmap_smoothing_sigma"])
284+
self.base_costmap = base_costmap
282285
self.costmap = self.base_costmap.copy()
283286

284287
def get_gradient_at_field_position(self, x: float, y: float) -> Tuple[float, float]:
@@ -287,6 +290,7 @@ def get_gradient_at_field_position(self, x: float, y: float) -> Tuple[float, flo
287290
:param x: Field coordinate in the x direction
288291
:param y: Field coordinate in the y direction
289292
"""
293+
assert self.gradient_map is not None, "Gradient map is not initialized"
290294
idx_x, idx_y = self.field_2_costmap_coord(x, y)
291295
return -self.gradient_map[0][idx_x, idx_y], -self.gradient_map[1][idx_x, idx_y]
292296

@@ -296,6 +300,7 @@ def get_cost_at_field_position(self, x: float, y: float) -> float:
296300
:param x: Field coordinate in the x direction
297301
:param y: Field coordinate in the y direction
298302
"""
303+
assert self.costmap is not None, "Costmap is not initialized"
299304
idx_x, idx_y = self.field_2_costmap_coord(x, y)
300305
return self.costmap[idx_x, idx_y]
301306

@@ -358,6 +363,7 @@ def get_cost_of_kick(self, x: float, y: float, direction: float, kick_length: fl
358363
:param kick_length: The length of the kick
359364
:param angular_range: The angular range of the kick
360365
"""
366+
assert self.costmap is not None, "Costmap is not initialized"
361367

362368
# create a mask in the size of the costmap consisting of 8-bit values initialized as 0
363369
mask = Image.new("L", (self.costmap.shape[1], self.costmap.shape[0]))

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/game_status_capsule.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def __init__(self, node, blackboard=None):
1515
self.gamestate = GameState()
1616
self.last_update: float = 0.0
1717
self.unpenalized_time: float = 0.0
18-
self.last_goal_from_us_time = -86400
19-
self.last_goal_time = -86400
18+
self.last_goal_from_us_time = -86400.0
19+
self.last_goal_time = -86400.0
2020
self.free_kick_kickoff_team: Optional[bool] = None
2121

2222
def get_gamestate(self) -> int:

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/kick_capsule.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class KickCapsule(AbstractBlackboardCapsule):
2323
is_currently_kicking: bool = False
2424

2525
__connected: bool = False
26-
__action_client: ActionClient = None
26+
__action_client: Optional[ActionClient] = None
2727

2828
class WalkKickTargets(Flag):
2929
"""

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/misc_capsule.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
88
from bitbots_msgs.msg import Audio, HeadMode, RobotControlState
99

10-
THeadMode: TypeAlias = Literal[
10+
THeadMode: TypeAlias = Literal[ # type: ignore[valid-type]
1111
HeadMode.BALL_MODE,
1212
HeadMode.FIELD_FEATURES,
1313
HeadMode.LOOK_FORWARD,

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/team_data_capsule.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Literal, Optional, Tuple
1+
from typing import List, Literal, Optional, Tuple
22

33
import numpy as np
44
from bitbots_utils.utils import get_parameters_from_other_node
@@ -27,7 +27,7 @@ def __init__(self, node, blackboard):
2727

2828
# Data
2929
# indexed with one to match robot ids
30-
self.team_data: Dict[int, TeamData] = {}
30+
self.team_data: dict[int, TeamData] = {}
3131
for i in range(1, 7):
3232
self.team_data[i] = TeamData()
3333
self.times_to_ball = dict()
@@ -158,7 +158,8 @@ def get_role(self) -> Tuple[int, float]:
158158
return self.strategy.role, self.role_update
159159

160160
def set_kickoff_strategy(
161-
self, strategy: Literal[Strategy.SIDE_LEFT, Strategy.SIDE_MIDDLE, Strategy.SIDE_RIGHT]
161+
self,
162+
strategy: Literal[Strategy.SIDE_LEFT, Strategy.SIDE_MIDDLE, Strategy.SIDE_RIGHT], # type: ignore[valid-type]
162163
) -> None:
163164
self.strategy.offensive_side = strategy
164165
self.strategy_update = self._node.get_clock().now().nanoseconds / 1e9
@@ -184,7 +185,7 @@ def is_not_goalie(team_data: TeamData) -> bool:
184185

185186
# Remove goalie data if needed
186187
if not count_goalie:
187-
team_data_infos = filter(is_not_goalie, team_data_infos)
188+
team_data_infos = filter(is_not_goalie, team_data_infos) # type: ignore[assignment]
188189

189190
# Count valid team data infos (aka robots with valid team data)
190191
return sum(map(self.is_valid, team_data_infos))
@@ -194,10 +195,10 @@ def is_a_goalie(team_data: TeamData) -> bool:
194195
return team_data.strategy.role == Strategy.ROLE_GOALIE
195196

196197
# Get the team data infos for all robots (ignoring the robot id/name)
197-
team_data_infos = self.team_data.values()
198+
team_data_infos = self.team_data.values() # type: ignore[assignment]
198199

199200
# Remove none goalie Data
200-
team_data_infos = filter(is_a_goalie, team_data_infos)
201+
team_data_infos = filter(is_a_goalie, team_data_infos) # type: ignore[assignment]
201202

202203
# Count valid team data infos (aka robots with valid team data)
203204
return sum(map(self.is_valid, team_data_infos)) == 1
@@ -246,3 +247,4 @@ def get_ball_max_covariance(team_data: TeamData) -> float:
246247
return PointStamped(
247248
header=team_data_with_best_ball.header, point=team_data_with_best_ball.ball_absolute.pose.position
248249
)
250+
return None

Diff for: bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/world_model_capsule.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import math
2-
from typing import Optional, Tuple
2+
from typing import Tuple
33

44
import numpy as np
55
import tf2_ros as tf2
@@ -22,6 +22,18 @@
2222
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
2323

2424

25+
class WorldModelTFError(Exception):
26+
...
27+
28+
29+
class WorldModelPositionTFError(WorldModelTFError):
30+
...
31+
32+
33+
class WorldModelBallTFError(WorldModelTFError):
34+
...
35+
36+
2537
class WorldModelCapsule(AbstractBlackboardCapsule):
2638
"""Provides information about the world model."""
2739

@@ -118,7 +130,7 @@ def get_ball_position_uv(self) -> Tuple[float, float]:
118130
except tf2.ExtrapolationException as e:
119131
self._node.get_logger().warn(str(e))
120132
self._node.get_logger().error("Severe transformation problem concerning the ball!")
121-
return None
133+
raise WorldModelBallTFError("Could not transform ball to base_footprint frame") from e
122134
return ball_bfp.x, ball_bfp.y
123135

124136
def get_ball_distance(self) -> float:
@@ -210,23 +222,21 @@ def get_map_based_opp_goal_right_post_uv(self) -> Tuple[float, float]:
210222
# Pose #
211223
########
212224

213-
def get_current_position(self) -> Optional[Tuple[float, float, float]]:
225+
def get_current_position(self) -> Tuple[float, float, float]:
214226
"""
215227
Returns the current position on the field as determined by the localization.
216228
0,0,0 is the center of the field looking in the direction of the opponent goal.
217229
:returns x,y,theta:
218230
"""
219-
if not (transform := self.get_current_position_transform()):
220-
return None
231+
transform = self.get_current_position_transform()
221232
theta = euler_from_quaternion(numpify(transform.transform.rotation))[2]
222233
return transform.transform.translation.x, transform.transform.translation.y, theta
223234

224-
def get_current_position_pose_stamped(self) -> Optional[PoseStamped]:
235+
def get_current_position_pose_stamped(self) -> PoseStamped:
225236
"""
226237
Returns the current position as determined by the localization as a PoseStamped
227238
"""
228-
if not (transform := self.get_current_position_transform()):
229-
return None
239+
transform = self.get_current_position_transform()
230240
return PoseStamped(
231241
header=transform.header,
232242
pose=Pose(
@@ -245,7 +255,7 @@ def get_current_position_transform(self) -> TransformStamped:
245255
)
246256
except (tf2.LookupException, tf2.ConnectivityException, tf2.ExtrapolationException) as e:
247257
self._node.get_logger().warn(str(e))
248-
return None
258+
raise WorldModelPositionTFError("Could not get current position transform") from e
249259

250260
##########
251261
# Common #

Diff for: bitbots_behavior/bitbots_blackboard/mypy.ini

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Global options:
2+
3+
[mypy]
4+
check_untyped_defs = True
5+
ignore_missing_imports = True

Diff for: bitbots_behavior/bitbots_blackboard/package.xml

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
<exec_depend>std_srvs</exec_depend>
3838
<exec_depend>tf2_geometry_msgs</exec_depend>
3939
<exec_depend>tf2</exec_depend>
40+
<test_depend>ament_mypy</test_depend>
41+
<test_depend>ament_cmake_mypy</test_depend>
42+
43+
<buildtool_depend>ament_cmake</buildtool_depend>
4044

4145
<export>
4246
<bitbots_documentation>

Diff for: bitbots_behavior/bitbots_body_behavior/bitbots_body_behavior/behavior_dsd/actions/go_to.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class GoToRelativePosition(AbstractActionElement):
1313
blackboard: BodyBlackboard
1414

15-
def __init__(self, blackboard, dsd, parameters: dict = None):
15+
def __init__(self, blackboard, dsd, parameters):
1616
super().__init__(blackboard, dsd, parameters)
1717
self.point = float(parameters.get("x", 0)), float(parameters.get("y", 0)), float(parameters.get("t", 0))
1818
self.threshold = float(parameters.get("threshold", 0.1))

Diff for: bitbots_behavior/bitbots_body_behavior/bitbots_body_behavior/behavior_dsd/decisions/aligned_to_goal.py

-4
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ def perform(self, reevaluate=False):
2626
# Get our position
2727
current_pose = self.blackboard.world_model.get_current_position()
2828

29-
# Check if we know our position
30-
if current_pose is None:
31-
return "NO"
32-
3329
# Get maximum kick angle relative to our base footprint
3430
angle_difference_right = current_pose[2] - self.max_kick_angle
3531
angle_difference_left = current_pose[2] + self.max_kick_angle

Diff for: bitbots_behavior/bitbots_body_behavior/package.xml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<depend>soccer_vision_3d_msgs</depend>
3131
<depend>tf_transformations</depend>
3232
<depend>tf2</depend>
33+
<test_depend>ament_mypy</test_depend>
3334

3435

3536
<export>

Diff for: bitbots_behavior/bitbots_body_behavior/test/mypy.ini

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Global options:
2+
3+
[mypy]
4+
check_untyped_defs = True
5+
ignore_missing_imports = True
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pathlib import Path
2+
3+
from ament_mypy.main import main
4+
5+
import pytest
6+
7+
8+
@pytest.mark.mypy
9+
def test_mypy():
10+
rc = main(argv=["--config", str((Path(__file__).parent / "mypy.ini").resolve())])
11+
assert rc == 0, 'Found code style errors / warnings'

0 commit comments

Comments
 (0)