diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd5ee5108..52954b14e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.11"] #, "3.12-dev"] + python-version: ["3.11", "3.12"] go-version: ["1.18", "1.19", "1.20"] steps: - name: Checkout awpy library @@ -65,12 +65,12 @@ jobs: - name: Lint with ruff uses: chartboost/ruff-action@v1 with: - version: 0.0.291 + version: 0.1.11 - name: Typecheck with pyright uses: jakebailey/pyright-action@v1 with: - version: 1.1.329 + version: 1.1.344 - name: Thorough check with pylint run: pylint awpy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa3f07016..d3d93f25d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,13 @@ exclude: docs/ repos: + - repo: https://github.com/JanEricNitschke/pymend + rev: "v1.0.10" + hooks: + - id: pymend + language: python + args: ["--write", "--check", "--output-style=google"] - repo: 'https://github.com/pre-commit/pre-commit-hooks' - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml language: python @@ -26,19 +32,19 @@ repos: - id: check-builtin-literals language: python - repo: 'https://github.com/charliermarsh/ruff-pre-commit' - rev: v0.0.291 + rev: v0.1.11 hooks: - id: ruff args: - '--fix' - '--exit-non-zero-on-fix' - repo: 'https://github.com/psf/black' - rev: 23.9.1 + rev: 23.12.1 hooks: - id: black language: python - repo: https://github.com/crate-ci/typos - rev: v1.16.13 + rev: v1.17.0 hooks: - id: typos args: [] @@ -64,7 +70,7 @@ repos: "-sn", # Don't display the score ] - repo: https://github.com/golangci/golangci-lint - rev: v1.54.2 + rev: v1.55.2 hooks: - id: golangci-lint entry: bash -c 'cd awpy/parser && golangci-lint run --new-from-rev HEAD --fix' diff --git a/awpy/analytics/map_control.py b/awpy/analytics/map_control.py index 7b86b7bf8..3622f6c18 100644 --- a/awpy/analytics/map_control.py +++ b/awpy/analytics/map_control.py @@ -1,11 +1,11 @@ """Functions for calculating map control values and metrics. - A team's map control can be thought of as the sum of it's - individual player's control. +A team's map control can be thought of as the sum of it's +individual player's control. - Example notebook: +Example notebook: - https://github.com/pnxenopoulos/awpy/blob/main/examples/05_Map_Control_Calculations_And_Visualizations.ipynb +https://github.com/pnxenopoulos/awpy/blob/main/examples/05_Map_Control_Calculations_And_Visualizations.ipynb """ from collections import defaultdict, deque @@ -45,13 +45,14 @@ def _approximate_neighbors( map_name (str): Map for source_tile_id source_tile_id (TileId): TileId for source tile n_neighbors (int): Number of closest tiles/approximated neighbors wanted + (Default value = 5) Returns: - List of TileDistanceObjects for n_neighbors closest tiles + list[DistanceObject]: List of TileDistanceObjects for n_neighbors closest tiles Raises: ValueError: If source_tile_id is not in awpy.data.NAV[map_name] - If n_neighbors <= 0 + ValueError: If n_neighbors <= 0 """ if source_tile_id not in NAV[map_name]: msg = "Tile ID not found." @@ -85,13 +86,13 @@ def _bfs( Args: map_name (str): Map for current_tiles - current_tiles (TileId): List of source tiles for bfs iteration(s) + current_tiles (list[TileId]): List of source tiles for bfs iteration(s) area_threshold (float): Percentage representing amount of map's total - navigable area which is the max cumulative tile - area for each bfs algorithm + navigable area which is the max cumulative tile + area for each bfs algorithm (Default value = 1 / 20) Returns: - TeamMapControlValues containing map control values + TeamMapControlValues: Map control values Raises: ValueError: If area_threshold <= 0 @@ -158,11 +159,11 @@ def _calc_frame_map_control_tile_values( Args: map_name (str): Map for other arguments - ct_tiles (list): List of CT-occupied tiles - t_tiles (list): List of T-occupied tiles + ct_tiles (list[TileId]): List of CT-occupied tiles + t_tiles (list[TileId]): List of T-occupied tiles Returns: - FrameMapControlValues object containing each team's map control values + FrameMapControlValues: Each team's map control values """ return FrameMapControlValues( t_values=_bfs(map_name, t_tiles), @@ -189,7 +190,7 @@ def calc_parsed_frame_map_control_values( (player positions, etc.). Expects extract_team_metadata output format Returns: - FrameMapControlValues object containing each team's map control values + FrameMapControlValues: Each team's map control values Raises: ValueError: If map_name is not in awpy.data.NAV @@ -228,7 +229,7 @@ def calc_frame_map_control_values( frame (GameFrame): Awpy frame object for map control calculations Returns: - FrameMapControlValues object containing each team's map control values + FrameMapControlValues: Each team's map control values Raises: ValueError: If map_name is not in awpy.data.NAV @@ -252,7 +253,7 @@ def _extract_team_metadata( side_data (TeamFrameInfo): Object with metadata for side's players. Returns: - TeamMetadata with metadata on team's players + TeamMetadata: Metadata on team's players """ alive_players: list[PlayerPosition] = [ (player["x"], player["y"], player["z"]) @@ -273,8 +274,7 @@ def extract_teams_metadata( containing relevant data for both sides Returns: - FrameTeamMetadata containing team metadata (player - positions, etc.) + FrameTeamMetadata: Team metadata (player positions, etc.) """ return FrameTeamMetadata( t_metadata=_extract_team_metadata(frame["t"]), @@ -304,7 +304,7 @@ def _calc_map_control_metric_from_dict( Expected format that of calc_frame_map_control_values output Returns: - Map Control Metric + float: Map Control Metric """ current_map_control_value: list[float] = [] tile_areas: list[float] = [] @@ -342,7 +342,7 @@ def calc_frame_map_control_metric( frame (GameFrame): awpy frame to calculate map control metric for Returns: - Map Control metric for given frame + float: Map Control metric for given frame Raises: ValueError: If map_name is not in awpy.data.NAV @@ -376,7 +376,7 @@ def calculate_round_map_control_metrics( round_data (GameRound): awpy round to calculate map control metrics Returns: - List of map control metric values for given round + list[float]: List of map control metric values for given round Raises: ValueError: If map_name is not in awpy.data.NAV diff --git a/awpy/analytics/nav.py b/awpy/analytics/nav.py index 661b8f9ba..d66f8fe7c 100644 --- a/awpy/analytics/nav.py +++ b/awpy/analytics/nav.py @@ -68,17 +68,17 @@ def point_in_area(map_name: str, area_id: TileId, point: PlayerPosition) -> bool """Returns if the point is within a nav area for a map. Args: - map_name (string): Map to consider + map_name (str): Map to consider area_id (TileId): Area ID as an integer - point (list): Point as a list [x,y,z] + point (PlayerPosition): Point as a list [x,y,z] Returns: - True if area contains the point, false if not + bool: True if area contains the point, false if not Raises: ValueError: If map_name is not in awpy.data.NAV - If area_id is not in awpy.data.NAV[map_name] - If the length of point is not 3 + ValueError: If area_id is not in awpy.data.NAV[map_name] + ValueError: If the length of point is not 3 """ if map_name not in NAV: msg = "Map not found." @@ -129,16 +129,17 @@ def find_closest_area( Searches through all the areas by comparing point to area centerpoint. Args: - map_name (string): Map to search - point (tuple): Point as a tuple (x,y,z) or (x, y) - flat (Boolean): Whether z should be ignored. + map_name (str): Map to search + point (PlayerPosition | PlayerPosition2D): Point as a tuple (x,y,z) or (x, y) + flat (bool): Whether z should be ignored. (Default value = False) Returns: - A dict containing info on the closest area + ClosestArea: A dict containing info on the closest area Raises: ValueError: If map_name is not in awpy.data.NAV - If the length of point is not 3 + ValueError: If the length of point is not 3 when flat is False + ValueError: If the length of point is not 2 when flat is True """ if map_name not in NAV: msg = "Map not found." @@ -185,19 +186,16 @@ def _check_arguments_area_distance( Dist type can be [graph, geodesic, euclidean]. Args: - map_name (string): Map to consider + map_name (str): Map to consider area_a (TileId): Area id area_b (TileId): Area id - dist_type (string, optional): String indicating the type of distance to use + dist_type (DistanceType): String indicating the type of distance to use (graph, geodesic or euclidean). Defaults to 'graph' - Returns: - A dict containing info on the path between two areas. - Raises: ValueError: If map_name is not in awpy.data.NAV - If either area_a or area_b is not in awpy.data.NAV[map_name] - If the dist_type is not one of ["graph", "geodesic", "euclidean"] + ValueError: If either area_a or area_b is not in awpy.data.NAV[map_name] + ValueError: If the dist_type is not one of ["graph", "geodesic", "euclidean"] """ if map_name not in NAV: msg = "Map not found." @@ -214,11 +212,11 @@ def _get_area_center(map_name: str, area: TileId) -> tuple[float, float, float]: """Get the coordinates of the center of an area. Args: - map_name (string): Map to consider + map_name (str): Map to consider area (TileId): Area id Returns: - Tuple of x, y, z coordinates of the area center. + tuple[float, float, float]: Tuple of x, y, z coordinates of the area center. """ area_x = (NAV[map_name][area]["southEastX"] + NAV[map_name][area]["northWestX"]) / 2 area_y = (NAV[map_name][area]["southEastY"] + NAV[map_name][area]["northWestY"]) / 2 @@ -234,12 +232,12 @@ def _get_euclidean_area_distance( """Returns the euclidean distance the centers of two areas. Args: - map_name (string): Map to search + map_name (str): Map to search area_a (TileId): Area id area_b (TileId): Area id Returns: - Distance between the centers of the two areas. + float: Distance between the centers of the two areas. """ area_a_x, area_a_y, area_a_z = _get_area_center(map_name, area_a) area_b_x, area_b_y, area_b_z = _get_area_center(map_name, area_b) @@ -263,9 +261,8 @@ def _get_graph_area_distance( area_b (TileId): Area id Returns: - tuple containing - - Distance between two areas as length of the path - - Path between the two areas as list of (TileId) nodes + length (float): Distance between two areas as length of the path + discovered_path (list[TileId]): Path between the two areas as list of nodes """ try: discovered_path = nx.bidirectional_shortest_path(map_graph, area_a, area_b) @@ -283,26 +280,33 @@ def _get_geodesic_area_distance( Args: map_graph (nx.DiGraph): DiGraph for the considered map - area_a (int): Area id - area_b (int): Area id + area_a (TileId): Area id + area_b (TileId): Area id Returns: - tuple containing - - Distance between two areas as geodesic cost - - Path between the two areas as list of (TileId) nodes + geodesic_cost (float): Distance between two areas as geodesic cost + geodesic_path (list[TileId]): Path between the two areas as list of nodes """ def dist_heuristic(node_a: TileId, node_b: TileId) -> float: + """Distance heuristic by taking euclidean distance between tile centers. + + Returns: + float: Distance between tile centers. + """ return distance.euclidean( map_graph.nodes[node_a]["center"], map_graph.nodes[node_b]["center"] ) try: - geodesic_path = nx.astar_path( + geodesic_path: list[TileId] = nx.astar_path( map_graph, area_a, area_b, heuristic=dist_heuristic, weight="weight" ) geodesic_cost = sum( - map_graph[u][v]["weight"] for u, v in pairwise(geodesic_path) + map_graph[u][v]["weight"] + for u, v in pairwise( + geodesic_path + ) # pyright: ignore [reportGeneralTypeIssues] ) except nx.NetworkXNoPath: return float("inf"), [] @@ -320,19 +324,19 @@ def area_distance( Dist type can be [graph, geodesic, euclidean]. Args: - map_name (string): Map to consider + map_name (str): Map to consider area_a (TileId): Source area id area_b (TileId): Destination area id - dist_type (string, optional): String indicating the type of distance to use + dist_type (DistanceType): String indicating the type of distance to use (graph, geodesic or euclidean). Defaults to 'graph' Returns: - A dict containing info on the path between two areas. + DistanceObject: A dict containing info on the path between two areas. Raises: ValueError: If map_name is not in awpy.data.NAV - If either area_a or area_b is not in awpy.data.NAV[map_name] - If the dist_type is not one of ["graph", "geodesic", "euclidean"] + ValueError: If either area_a or area_b is not in awpy.data.NAV[map_name] + ValueError: If the dist_type is not one of ["graph", "geodesic", "euclidean"] """ _check_arguments_area_distance(map_name, area_a, area_b, dist_type) map_graph = NAV_GRAPHS[map_name] @@ -365,21 +369,21 @@ def point_distance( """Returns the distance between two points. Args: - map_name (string): Map to consider - point_a (list): Point as a list (x,y,z) - point_b (list): Point as a list (x,y,z) - dist_type (string, optional): String indicating the type of distance to use. + map_name (str): Map to consider + point_a (PlayerPosition): Point as a list (x,y,z) + point_b (PlayerPosition): Point as a list (x,y,z) + dist_type (PointDistanceType): String indicating the type of distance to use. Can be graph, geodesic, euclidean, manhattan, canberra or cosine. Defaults to 'graph' Returns: - A dict containing info on the distance between two points. + DistanceObject: A dict containing info on the distance between two points. Raises: ValueError: If map_name is not in awpy.data.NAV: - if dist_type is "graph" or "geodesic" - If either point_a or point_b does not have a length of 3 - (for "graph" or "geodesic" dist_type) + ValueError: If dist_type is "graph" or "geodesic" + ValueError: If either point_a or point_b does not have a length of 3 + (for "graph" or "geodesic" dist_type) """ if dist_type not in get_args(PointDistanceType): msg = ( @@ -424,15 +428,15 @@ def generate_position_token(map_name: str, frame: GameFrame) -> Token: """Generates the position token for a game frame. Args: - map_name (string): Map to consider - frame (dict): A game frame + map_name (str): Map to consider + frame (GameFrame): A game frame Returns: - A dict containing the T token, CT token and combined token (T + CT concatenated) + Token: A dict containing the T token, CT token and combined token. Raises: ValueError: If map_name is not in awpy.data.NAV - If either side ("ct" or "t") in the frame has no players + ValueError: If either side ("ct" or "t") in the frame has no players """ if map_name not in NAV: msg = "Map not found." @@ -491,14 +495,14 @@ def generate_position_token(map_name: str, frame: GameFrame) -> Token: def tree() -> dict: """Builds tree data structure from nested defaultdicts. - Args: - None + https://stackoverflow.com/a/19189781/7895542 Returns: - An empty tree + dict: An empty tree """ def the_tree() -> dict: + """Inner function avoid issues if `tree` is later rebound.""" return defaultdict(the_tree) return the_tree() @@ -514,8 +518,11 @@ def _save_matrix_to_file( Args: map_name (str): Name of the map corresponding to the matrix dist_matrix (AreaMatrix | PlaceMatrix): The nested dict to save to file. - matrix_type (Literal["area", "place"]): Whether an area or place matrix + matrix_type (Literal['area', 'place']): Whether an area or place matrix is being saved + + Raises: + ValueError: If matrix type is not valid. """ if matrix_type not in ("area", "place"): msg = f"Matrix type has to be one of ('area', 'place') but was {matrix_type}!" @@ -538,11 +545,11 @@ def generate_area_distance_matrix(map_name: str, *, save: bool = False) -> AreaM later reuse make sure to set 'save=True'! Args: - map_name (string): Map to generate the place matrix for - save (bool, optional): Whether to save the matrix to file Defaults to 'False' + map_name (str): Map to generate the place matrix for + save (bool): Whether to save the matrix to file Defaults to 'False' Returns: - Tree structure containing distances for all area pairs on all maps + AreaMatrix: Tree structure containing distances for all area pairs on all maps Raises: ValueError: Raises a ValueError if map_name is not in awpy.data.NAV @@ -622,9 +629,10 @@ def _get_area_place_mapping(map_name: str) -> dict[str, list[TileId]]: map_name (str): Name of the map to get the mapping for. Returns: - The mapping "areaName": [areas that have this area name] for each "areaName" + dict[str, list[TileId]]: The mapping + "areaName": [areas that have this area name] for each "areaName" """ - area_mapping = defaultdict(list) + area_mapping: dict[str, list[TileId]] = defaultdict(list) # Get the mapping "areaName": [areas that have this area name] for area in NAV[map_name]: area_mapping[NAV[map_name][area]["areaName"]].append(area) @@ -649,7 +657,7 @@ def _get_median_place_distance( dist_type (DistanceType): Distance type to consider. Returns: - Median distance between all areas in two places. + float: Median distance between all areas in two places. """ connections = [] for sub_area1 in area_mapping[place1]: @@ -668,11 +676,11 @@ def generate_place_distance_matrix(map_name: str, *, save: bool = False) -> Plac [reference_point(centroid,representative_point,median_dist)] Args: - map_name (string): Map to generate the place matrix for - save (bool, optional): Whether to save the matrix to file. Defaults to 'False' + map_name (str): Map to generate the place matrix for + save (bool): Whether to save the matrix to file. Defaults to 'False' Returns: - Tree structure containing distances for all place pairs on all maps + PlaceMatrix: Tree structure containing distances for all place pairs on all maps Raises: ValueError: Raises a ValueError if map_name is not in awpy.data.NAV @@ -752,7 +760,7 @@ def _get_area_points_z_s( Returns: area_points (dict[str, list[tuple[float, float]]]): Dict mapping each place to the x and y coordinates of each area inside it. - z_s (dict[str, list]): Dict mapping each place to the z coordinates of each + z_s (dict[str, list]): Dict mapping each place to the z coordinates of each area inside it. """ area_points: dict[str, list[tuple[float, float]]] = defaultdict(list) @@ -778,11 +786,12 @@ def generate_centroids( Also finds the closest tile for each. Args: - map_name (string): Name of the map for which to calculate the centroids + map_name (str): Name of the map for which to calculate the centroids Returns: - Tuple of dictionaries containing the centroid and representative tiles - for each region of the map + area_ids_cent (dict[str, int]): Dict mapping each region to its centroid tile + area_ids_rep (dict[str, int]): Dict mapping each region + to its representative tile Raises: ValueError: If map_name is not in awpy.data.NAV @@ -830,11 +839,11 @@ def stepped_hull(points: list[tuple[float, float]]) -> list[tuple[float, float]] """Produces an approximation of the orthogonal convex hull. Args: - points (list): A list of points given as tuples (x, y) + points (list[tuple[float, float]]): A list of points given as tuples (x, y) Returns: - A list of points making up the hull or - four lists of points making up the four quadrants of the hull + list[tuple[float, float]]: A list of points making up the hull or + four lists of points making up the four quadrants of the hull """ # May be equivalent to the orthogonal convex hull @@ -886,7 +895,7 @@ def build_stepped_upper( max_y (tuple[float, float]): The point with the highest y Returns: - A list of points making up the upper part of the hull + list[tuple[float, float]]: A list of points making up the upper part of the hull """ # Steps towards the highest y point @@ -914,7 +923,7 @@ def build_stepped_lower( min_y (tuple[float, float]): The point with the lowest y Returns: - A list of points making up the lower part of the hull + list[tuple[float, float]]: A list of points making up the lower part of the hull """ # Steps towards the lowest y point @@ -943,29 +952,29 @@ def _check_arguments_position_distance( fewer alive players than position_array_2. Args: - map_name (string): Map to consider - position_array_1 (numpy array): Numpy array with shape (2|1, 5, 3) + map_name (str): Map to consider + position_array_1 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the player and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - position_array_2 (numpy array): Numpy array with shape (2|1, 5, 3) + position_array_2 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the playe and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - distance_type (string, optional): String indicating how the distance between - two player positions should be calculated. + distance_type (DistanceType): String indicating how the distance between + two player positions should be calculated. (Default value = 'geodesic') + + Returns: + tuple[npt.NDArray, npt.NDArray]: Potentially reordered position arrays. Raises: ValueError: If map_name is not in awpy.data.NAV. ValueError: If distance_type is not one of ["graph", "geodesic", "euclidean"]. ValueError: If the 0th (number of teams) and 2nd (number of features) dimensions - of the inputs do not have the same size. + of the inputs do not have the same size. ValueError: If number of features is not 3 for euclidean distance_type - - Returns: - tuple[npt.NDArray, npt.NDArray]: Potentially reordered position arrays. """ if map_name not in NAV: msg = "Map not found." @@ -1006,20 +1015,20 @@ def _precompute_area_names( """Precompute the area names for each player position. Args: - map_name (string): Map to consider - position_array_1 (numpy array): Numpy array with shape (2|1, 5, 3) + map_name (str): Map to consider + position_array_1 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the player and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - position_array_2 (numpy array): Numpy array with shape (2|1, 5, 3) + position_array_2 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the playe and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance Returns: - dict[int, defaultdict[int, dict]]: Mapping for each team + dict[int, dict[int, dict[int, TileId]]]: Mapping for each team containing the areaId for each player. """ areas: dict[int, dict[int, dict[int, TileId]]] = { @@ -1062,12 +1071,12 @@ def _euclidean_position_distance( Fast but ignores walls. Args: - position_array_1 (numpy array): Numpy array with shape (2|1, 5, 3) + position_array_1 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the player and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - position_array_2 (numpy array): Numpy array with shape (2|1, 5, 3) + position_array_2 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the playe and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value @@ -1102,10 +1111,10 @@ def _graph_based_position_distance( that the distance between two states/trajectories is commutative Args: - map_name (string): Map to consider - area1 (int): First area in distance calculation - area2 (int): Second area in distance calculation - distance_type (string, optional): String indicating how the distance between + map_name (str): Map to consider + area1 (TileId): First area in distance calculation + area2 (TileId): Second area in distance calculation + distance_type (DistanceType): String indicating how the distance between two player positions should be calculated. Options are "geodesic", "graph". @@ -1146,29 +1155,29 @@ def position_state_distance( """Calculates a distance between two game states based on player positions. Args: - map_name (string): Map to consider - position_array_1 (numpy array): Numpy array with shape (2|1, 5, 3) + map_name (str): Map to consider + position_array_1 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the player and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - position_array_2 (numpy array): Numpy array with shape (2|1, 5, 3) + position_array_2 (npt.NDArray): Numpy array with shape (2|1, 5, 3) with the first index indicating the team, the second the playe and the third the coordinate. Alternatively the array can have shape (2|1, 5, 1) where the last value gives the area_id. Used only with geodesic and graph distance - distance_type (string, optional): String indicating how the distance between + distance_type (DistanceType): String indicating how the distance between two player positions should be calculated. Options are "geodesic", "graph" and "euclidean". Defaults to 'geodesic' Returns: - A float representing the distance between these two game states + float: A float representing the distance between these two game states Raises: ValueError: If map_name is not in awpy.data.NAV. ValueError: If distance_type is not one of ["graph", "geodesic", "euclidean"]. ValueError: If the 0th (number of teams) and 2nd (number of features) dimensions - of the inputs do not have the same size. + of the inputs do not have the same size. ValueError: If number of features is not 3 for euclidean distance_type """ position_array_1, position_array_2 = _check_arguments_position_distance( @@ -1221,23 +1230,24 @@ def _check_arguments_token_distance( Checks if arguments are valid and raises ValueErrors if not. Args: - map_name (string): Map to consider - token_array_1 (numpy array): 1-D numpy array of a position token - token_array_2 (numpy array): 1-D numpy array of a position token - distance_type (string, optional): String indicating how the distance - between two player positions should be calculated. + map_name (str): Map to consider + token_array_1 (npt.NDArray): 1-D numpy array of a position token + token_array_2 (npt.NDArray): 1-D numpy array of a position token + distance_type (Literal[DistanceType, 'edit_distance']): String indicating + how the distance between two player positions should be calculated. Options are "geodesic", "graph", "euclidean" and "edit_distance". Defaults to 'geodesic' - reference_point (string, optional): String indicating which reference point - to use to determine area distance. + reference_point (Literal['centroid', 'representative_point']): String indicating + which reference point to use to determine area distance. Options are "centroid" and "representative_point". Defaults to 'centroid' + Raises: ValueError: If map_name is not in awpy.data.NAV. ValueError: If distance_type is not one of: - ["graph", "geodesic", "euclidean", "edit_distance"] + ["graph", "geodesic", "euclidean", "edit_distance"] ValueError: If reference_point is not one of: - ["centroid", "representative_point"] + ["centroid", "representative_point"] ValueError: If the input token arrays do not have the same length. """ if map_name not in NAV: @@ -1260,8 +1270,7 @@ def _get_map_area_names(map_name: str) -> list[str]: Needed to translate back from token position to area name. Args: - map_name (string): Map to consider - token_array_1 (numpy array): 1-D numpy array of a position token + map_name (str): Map to consider Returns: list[str]: Sorted list of named areas on the map. @@ -1283,7 +1292,7 @@ def _check_proper_token_length( Raises: ValueError: If the length of the token arrays do not match - the expected length for that map. + the expected length for that map. """ if len(token_array) not in [len(map_area_names), len(map_area_names) * 2]: msg = ( @@ -1307,8 +1316,8 @@ def _edit_distance_tokens( and then one to get the second from 0 to 1 Args: - token_array_1 (numpy array): 1-D numpy array of a position token - token_array_2 (numpy array): 1-D numpy array of a position token + token_array_1 (npt.NDArray[np.int_]): 1-D numpy array of a position token + token_array_2 (npt.NDArray[np.int_]): 1-D numpy array of a position token nom_token_length (int): Length of a position token for one side Returns: @@ -1331,14 +1340,15 @@ def _get_index_differences( Each index ends up in list as many times are the difference of values. Args: - token_array_1 (numpy array): 1-D numpy array of a position token - token_array_2 (numpy array): 1-D numpy array of a position token + token_array_1 (npt.NDArray[np.int_]): 1-D numpy array of a position token + token_array_2 (npt.NDArray[np.int_]): 1-D numpy array of a position token map_area_names (list[str]): Sorted list of named areas on the map. team_index (int): Which team is currently being considered. First or second. Returns: - tuple[list[int], list[int], int]: Lists of differing indices and - Sum of the smaller array. + pos_indices (list[int]): List of indices where array1 has a larger value + neg_indices (list[int]): List of indices where array2 has a larger value + size (int): Sum of the smaller array. """ array1, array2, size = _clean_token_arrays( token_array_1, token_array_2, map_area_names, team_index=team_index @@ -1373,14 +1383,14 @@ def _clean_token_arrays( and the distance per player pair is desired. Args: - token_array_1 (numpy array): 1-D numpy array of a position token - token_array_2 (numpy array): 1-D numpy array of a position token + token_array_1 (npt.NDArray[np.int_]): 1-D numpy array of a position token + token_array_2 (npt.NDArray[np.int_]): 1-D numpy array of a position token map_area_names (list[str]): Sorted list of named areas on the map. team_index (int): Which team is currently being considered. First or second. Returns: - array1 (numpy array): 1-D numpy array of a position sub token. - array2 (numpy array): 1-D numpy array of a position sub token. + array1 (npt.NDArray[np.int_]): 1-D numpy array of a position sub token. + array2 (npt.NDArray[np.int_]): 1-D numpy array of a position sub token. size (int): Sum of the smaller array. """ array1, array2 = ( @@ -1410,30 +1420,30 @@ def token_state_distance( """Calculates a distance between two game states based on player positions. Args: - map_name (string): Map to consider - token_array_1 (numpy array): 1-D numpy array of a position token - token_array_2 (numpy array): 1-D numpy array of a position token - distance_type (string, optional): String indicating how the distance - between two player positions should be calculated. + map_name (str): Map to consider + token_array_1 (npt.NDArray[np.int_]): 1-D numpy array of a position token + token_array_2 (npt.NDArray[np.int_]): 1-D numpy array of a position token + distance_type (Literal[DistanceType, 'edit_distance']): String indicating + how the distance between two player positions should be calculated. Options are "geodesic", "graph", "euclidean" and "edit_distance". Defaults to 'geodesic' - reference_point (string, optional): String indicating which reference point - to use to determine area distance. + reference_point (Literal['centroid', 'representative_point']): String indicating + which reference point to use to determine area distance. Options are "centroid" and "representative_point". Defaults to 'centroid' Returns: - A float representing the distance between these two game states + float: A float representing the distance between these two game states Raises: ValueError: If map_name is not in awpy.data.NAV. ValueError: If distance_type is not one of: - ["graph", "geodesic", "euclidean", "edit_distance"] + ["graph", "geodesic", "euclidean", "edit_distance"] ValueError: If reference_point is not one of: - ["centroid", "representative_point"] + ["centroid", "representative_point"] ValueError: If the input token arrays do not have the same length. ValueError: If the length of the token arrays do not match - the expected length for that map. + the expected length for that map. """ _check_arguments_token_distance( map_name, token_array_1, token_array_2, distance_type, reference_point @@ -1512,7 +1522,7 @@ def get_array_for_frame(frame: GameFrame) -> npt.NDArray: frame (GameFrame): A game frame Returns: - numpy array for that frame + npt.NDArray: numpy array for that frame """ pos_array = np.zeros( ( @@ -1539,23 +1549,23 @@ def frame_distance( """Calculates a distance between two frames based on player positions. Args: - map_name (string): Map to consider + map_name (str): Map to consider frame1 (GameFrame): A game frame frame2 (GameFrame): A game frame - distance_type (string, optional): String indicating how the distance between + distance_type (DistanceType): String indicating how the distance between two player positions should be calculated. Options are "geodesic", "graph" and "euclidean" Defaults to 'geodesic' Returns: - A float representing the distance between these two game states + float: A float representing the distance between these two game states Raises: ValueError: Raises a ValueError if there is a discrepancy between - the frames regarding which sides are filled. - If the ct side of frame1 contains players - while that of frame2 is empty or None the error will be raised. - The same happens for the t sides. + the frames regarding which sides are filled. + If the ct side of frame1 contains players + while that of frame2 is empty or None the error will be raised. + The same happens for the t sides. """ if ( (len(frame1["ct"]["players"] or []) > 0) @@ -1599,20 +1609,20 @@ def token_distance( """Calculates a distance between two game states based on position tokens. Args: - map_name (string): Map to consider - token1 (string): A team position token - token2 (string): A team position token - distance_type (string, optional): String indicating how the distance between - two player positions should be calculated. + map_name (str): Map to consider + token1 (str): A team position token + token2 (str): A team position token + distance_type (Literal[DistanceType, 'edit_distance']): String indicating + how the distance between two player positions should be calculated. Options are "geodesic", "graph", "euclidean" and "edit_distance". Defaults to 'geodesic' - reference_point (string, optional): String indicating which reference point - to use to determine area distance. + reference_point (Literal['centroid', 'representative_point']): String indicating + which reference point to use to determine area distance. Options are "centroid" and "representative_point". Defaults to 'centroid' Returns: - A float representing the distance between these two game states + float: A float representing the distance between these two game states """ return token_state_distance( map_name, @@ -1630,15 +1640,15 @@ def calculate_tile_area( """Calculates area of a given tile in a given map. Args: - map_name (string): Map for tile + map_name (str): Map for tile tile_id (TileId): Id for tile Returns: - A float representing the area of the tile + float: A float representing the area of the tile Raises: ValueError: If map_name is not in awpy.data.NAV - If area_id is not in awpy.data.NAV[map_name] + ValueError: If area_id is not in awpy.data.NAV[map_name] """ if map_name not in NAV: msg = "Map not found." @@ -1661,10 +1671,10 @@ def calculate_map_area( """Calculates total area of all nav tiles in a given map. Args: - map_name (string): Map for area calculations + map_name (str): Map for area calculations Returns: - A float representing the area of the map + float: A float representing the area of the map Raises: ValueError: If map_name is not in awpy.data.NAV diff --git a/awpy/analytics/states.py b/awpy/analytics/states.py index eb70fa06d..df4e0404e 100644 --- a/awpy/analytics/states.py +++ b/awpy/analytics/states.py @@ -1,19 +1,21 @@ """Functions to generate game stats based on snapshots from a demofile.""" # pylint: disable=unused-argument +from typing import Any + from awpy.types import GameFrame -def generate_vector_state(frame: GameFrame, map_name: str) -> dict: +def generate_vector_state(frame: GameFrame, map_name: str) -> dict[str, Any]: """Returns a game state in a dictionary format. Args: - frame (GameFrame) : Dict output of a frame generated from the DemoParser class - map_name (string): String indicating the map name + frame (GameFrame): Dict output of a frame generated from the DemoParser class + map_name (str): String indicating the map name Returns: - A dict with keys for each feature. + dict[str, Any]: With keys for each feature. """ - game_state: dict = { + game_state: dict[str, Any] = { "mapName": map_name, "secondsSincePhaseStart": frame["seconds"], "bombPlanted": frame["bombPlanted"], @@ -72,27 +74,27 @@ def generate_vector_state(frame: GameFrame, map_name: str) -> dict: return game_state -def generate_graph_state(frame: GameFrame) -> dict: +def generate_graph_state(frame: GameFrame) -> dict[str, Any]: """Returns a game state as a graph. Args: - frame (GameFrame) : Dict output of a frame generated from the DemoParser class + frame (GameFrame): Dict output of a frame generated from the DemoParser class Returns: - A dict with keys "T", "CT" and "Global", - where each entry is a vector. Global vector is CT + T concatenated + dict[str, Any]: With keys "T", "CT" and "Global", + where each entry is a vector. Global vector is CT + T concatenated """ return {"ct": [], "t": [], "global": []} -def generate_set_state(frame: GameFrame) -> dict: +def generate_set_state(frame: GameFrame) -> dict[str, Any]: """Returns a game state as a set. Args: - frame (GameFrame) : Dict output of a frame generated from the DemoParser class + frame (GameFrame): Dict output of a frame generated from the DemoParser class Returns: - A dict with keys "T", "CT" and "Global", - where each entry is a vector. Global vector is CT + T concatenated + dict[str, Any]: With keys "T", "CT" and "Global", + where each entry is a vector. Global vector is CT + T concatenated """ return {"ct": [], "t": [], "global": []} diff --git a/awpy/analytics/stats.py b/awpy/analytics/stats.py index bef0bb3cf..0b93c6c31 100644 --- a/awpy/analytics/stats.py +++ b/awpy/analytics/stats.py @@ -59,15 +59,13 @@ def initialize_round( Args: cur_round (GameRound): Current CSGO round to initialize for. - player_statistics (dict[str, PlayerStatistics]): - Dict storing player statistics for a given match. - active_sides (set[Literal["CT", "T"]]): Set of the currently active sides. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + active_sides (set[Literal['CT', 'T']]): Set of the currently active sides. Returns: - dict[str, KAST]: Initialized KAST dict for each player. - dict[str, int]: Number of kills in the round for each player. - set[str]]: Players to track for the given round. - + RoundStatistics: Initialized KAST dict for each player. + dict[str, int]: Number of kills in the round for each player. + set[str]]: Players to track for the given round. """ kast: dict[str, KAST] = {} round_kills: dict[str, int] = {} @@ -247,6 +245,18 @@ def _get_actor_key( actor: Any, game_action: Any, ) -> str: + """Get the key for a specific actor.. + + Args: + actor (Any): Actor to get the key for. + game_action (Any): Action that the actor is performing. + + Returns: + str: Key for the actor. + + Raises: + KeyError: If the actor is not a valid actor for the given game action. + """ actor_name = actor + "Name" actor_steamid = actor + "SteamID" if (actor_name) not in game_action or (actor_steamid) not in game_action: @@ -268,7 +278,14 @@ def _handle_pure_killer_stats( round_statistics: RoundStatistics, kill_action: KillAction, ) -> None: - # Purely attacker related stats + """Purely attacker related stats. + + Args: + killer_key (str): The key of the killer. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + kill_action (KillAction): The kill action to handle. + """ if ( killer_key in round_statistics["active_players"] and kill_action["attackerSteamID"] @@ -288,6 +305,17 @@ def _is_clutch( game_round: GameRound, round_statistics: RoundStatistics, ) -> bool: + """Determine whether the current game state is in a clutch. + + Args: + victim_side (Literal['CT', 'T']): Which side the victim of the last + kill was on. + game_round (GameRound): The current game round that is being analyzed. + round_statistics (RoundStatistics): Player statistics for the current round. + + Returns: + bool: True if the current game state is a clutch. + """ total_players_victim_side = game_round[lower_side(victim_side) + "Side"]["players"] if total_players_victim_side is None: return False @@ -303,6 +331,17 @@ def _find_clutcher( victim_side: Literal["CT", "T"], round_statistics: RoundStatistics, ) -> str | None: + """Try to find the key of a currently clutching player. + + Args: + victim_side_players (list[Players]): Player on the side of the most + recent victim. + victim_side (Literal['CT', 'T']): Which side the victim was one. + round_statistics (RoundStatistics): Player statistics for the current round. + + Returns: + str | None: They key of the clutcher or None if no clutcher was found. + """ for player in victim_side_players: clutcher_key = ( str(player["playerName"]) @@ -324,7 +363,14 @@ def _handle_clutching( round_statistics: RoundStatistics, player_statistics: dict[str, PlayerStatistics], ) -> None: - # Clutch logic + """Clutch logic. + + Args: + kill_action (KillAction): The most recent kill action. + game_round (GameRound): Information about the current game round. + round_statistics (RoundStatistics): Player statistics for the current round. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + """ victim_side = kill_action["victimSide"] if victim_side is None or not is_valid_side(victim_side): return @@ -368,7 +414,15 @@ def _handle_pure_victim_stats( kill_action: KillAction, game_round: GameRound, ) -> None: - # Purely victim related stats: + """Purely victim related stats:. + + Args: + victim_key (str): Key of the current victim. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): _Player statistics for the current round. + kill_action (KillAction): Kill action to handle. + game_round (GameRound): Current game round being analyzed. + """ if victim_key in round_statistics["active_players"]: player_statistics[victim_key]["deaths"] += 1 round_statistics["kast"][victim_key]["s"] = False @@ -383,6 +437,14 @@ def _handle_trade_stats( round_statistics: RoundStatistics, kill_action: KillAction, ) -> None: + """Logic to handle whether a kill counts as a trade. + + Args: + killer_key (str): Key of the killer. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + kill_action (KillAction): The kill action that might be a trade. + """ if kill_action["isTrade"]: # A trade is always onto an enemy # If your teammate kills someone and then you kill them @@ -421,6 +483,15 @@ def _handle_assists( round_statistics: RoundStatistics, kill_action: KillAction, ) -> None: + """Handle whether a kill was an assist.. + + Args: + assister_key (str): Player that might get the assits. + flashthrower_key (str): Player that might get the flash assist. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + kill_action (KillAction): The kill action at question. + """ if ( kill_action["assisterSteamID"] and kill_action["assisterSide"] != kill_action["victimSide"] @@ -444,6 +515,15 @@ def _handle_first_kill( round_statistics: RoundStatistics, kill_action: KillAction, ) -> None: + """Check who gets credit for first kill/death. + + Args: + killer_key (str): Player that got the kill. + victim_key (str): Player that died. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + kill_action (KillAction): The kill action that might be a trade. + """ if kill_action["isFirstKill"] and kill_action["attackerSteamID"]: if killer_key in round_statistics["active_players"]: player_statistics[killer_key]["firstKills"] += 1 @@ -456,6 +536,13 @@ def _handle_kills( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle all the kills in a round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for k in game_round["kills"] or []: killer_key = _get_actor_key("attacker", k) victim_key = _get_actor_key("victim", k) @@ -496,6 +583,13 @@ def _handle_damages( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle all the damage events in a round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for damage_action in game_round["damages"] or []: attacker_key = _get_actor_key("attacker", damage_action) victim_key = _get_actor_key("victim", damage_action) @@ -532,6 +626,13 @@ def _handle_weapon_fires( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle all weapon fires in the round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for weapon_fire in game_round["weaponFires"] or []: fire_key = _get_actor_key("player", weapon_fire) if fire_key in round_statistics["active_players"]: @@ -543,6 +644,13 @@ def _handle_flashes( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle all flashes in the round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for flash_action in game_round["flashes"] or []: flasher_key = _get_actor_key("attacker", flash_action) if ( @@ -565,6 +673,13 @@ def _handle_grenades( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """_Handle all grenades in the round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for grenade_action in game_round["grenades"] or []: thrower_key = _get_actor_key("thrower", grenade_action) if ( @@ -586,6 +701,13 @@ def _handle_bomb( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle all bomb related events in the round. + + Args: + game_round (GameRound): Game round to handle kills for. + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for bomb_event in game_round["bombEvents"] or []: player_key = ( bomb_event["playerName"] @@ -606,6 +728,12 @@ def _handle_kast( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Check which players earned a point for their KAST. + + Args: + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for player, components in round_statistics["kast"].items(): if player in round_statistics["active_players"] and any(components.values()): player_statistics[player]["kast"] += 1 @@ -615,6 +743,12 @@ def _handle_multi_kills( player_statistics: dict[str, PlayerStatistics], round_statistics: RoundStatistics, ) -> None: + """Handle which players earned multikills this round. + + Args: + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + round_statistics (RoundStatistics): Player statistics for the current round. + """ for player, n_kills in round_statistics["round_kills"].items(): if player in round_statistics["active_players"]: _increment_statistic(player_statistics, player, n_kills) @@ -623,6 +757,13 @@ def _handle_multi_kills( def _increment_statistic( player_statistics: dict[str, PlayerStatistics], player: str, n_kills: int ) -> None: + """Increment the appropriate multi kill statistic. + + Args: + player_statistics (dict[str, PlayerStatistics]): Overall stats for the match. + player (str): Player that got the multi kill + n_kills (int): How many kills the player got + """ if not proper_player_number(n_kills): # 0, 1, 2, 3, 4, 5 return kills_string = "kills" + int_to_string_n_players(n_kills) @@ -636,13 +777,12 @@ def player_stats( Args: game_rounds (list[GameRound]): List of game rounds as produced by the DemoParser - return_type (str, optional): Return format ("json" or "df"). Defaults to "json". - selected_side (str, optional): Which side(s) to consider. Defaults to "all". + return_type (str): Return format ("json" or "df"). Defaults to "json". + selected_side (str): Which side(s) to consider. Defaults to "all". Other options are "CT" and "T" Returns: - Union[dict[str, PlayerStatistics],pd.Dataframe]: - Dictionary or Dataframe containing player information + dict[str, PlayerStatistics] | pd.DataFrame: Player information """ player_statistics: dict[str, PlayerStatistics] = {} diff --git a/awpy/analytics/wpa.py b/awpy/analytics/wpa.py index 6a8975ee7..b1a30d839 100644 --- a/awpy/analytics/wpa.py +++ b/awpy/analytics/wpa.py @@ -15,11 +15,14 @@ def state_win_probability(frame: GameFrame, model: Any) -> dict: # noqa: ANN401 """Predicts the win probability of a given frame. Args: - frame (dict): Dict output of a frame generated from the DemoParser class - model (Unknown): Model to predict win probabability from. + frame (GameFrame): Dict output of a frame generated from the DemoParser class + model (Any): Model to predict win probabability from. Returns: - A dictionary containing the CT and T round win probabilities + dict: A dictionary containing the CT and T round win probabilities + + Raises: + NotImplementedError: Not yet implemented. """ raise NotImplementedError @@ -33,6 +36,9 @@ def round_win_probability(ct_score: int, t_score: int, map_name: str) -> dict: map_name (str): Map the demo is from Returns: - A dictionary containing the CT game win, T game win and Draw probabilities + dict: A dictionary containing the CT game win, T game win and Draw probabilities + + Raises: + NotImplementedError: Not yet implemented. """ raise NotImplementedError diff --git a/awpy/data/__init__.py b/awpy/data/__init__.py index 587c98574..b86d15895 100644 --- a/awpy/data/__init__.py +++ b/awpy/data/__init__.py @@ -26,11 +26,11 @@ def create_nav_graphs( """Function to create a dict of DiGraphs from dict of areas and edge_list file. Args: - nav (dict): Dictionary containing information about each area of each map + nav (dict[str, dict[TileId, Area]]): Information about each area of each map data_path (str): Path to the awpy.data folder containing navigation and map data Returns: - A dictionary mapping each map (str) to an nx.DiGraph of its traversible areas + dict[str, nx.DiGraph]: Each map to an nx.DiGraph of its traversible areas """ nav_graphs: dict[str, nx.DiGraph] = {} for map_name in nav: @@ -91,6 +91,11 @@ def create_nav_graphs( def _get_dist_matrices() -> tuple[dict[str, PlaceMatrix], dict[str, AreaMatrix]]: + """Grab the distance matrices from files and combine them. + + Returns: + tuple[dict[str, PlaceMatrix], dict[str, AreaMatrix]]: Dist matrices. + """ place_dist_matrix: dict[str, PlaceMatrix] = {} area_dist_matrix: dict[str, AreaMatrix] = {} for file in os.listdir(PATH + "nav/"): diff --git a/awpy/parser/cleaning.py b/awpy/parser/cleaning.py index 7501b4e5f..a8e9f0d50 100644 --- a/awpy/parser/cleaning.py +++ b/awpy/parser/cleaning.py @@ -17,6 +17,9 @@ def __call__(self, *sequences: Sequence[object]) -> float: Take a sequence of objects and return a distance. + Args: + *sequences (Sequence[object]): Sequence to calculate distance between. + Returns: float: Distance between object. """ @@ -24,6 +27,17 @@ def __call__(self, *sequences: Sequence[object]) -> float: def _set_distance_metric(metric: str) -> DistMetricCallable: + """Get the distance metric callable. + + Args: + metric (str): Identifier for which metric to use + + Returns: + DistMetricCallable: Desired callable + + Raises: + ValueError: If an invalid metric is chosen + """ if metric == "lcss": return textdistance.lcsseq.distance if metric == "hamming": @@ -44,16 +58,16 @@ def associate_entities( """A function to return a dict of associated entities. Accepts. Args: - game_names (list, optional): A list of names generated by the demofile. + game_names (list[str | None] | None): A list of names generated by the demofile. Defaults to [] - entity_names (list, optional): A list of names: Defaults to [] - metric (string, optional): A string indicating distance metric, - one of lcss, hamming, levenshtein, jaro, difflib. - Defaults to 'lcss' + entity_names (list[str] | None): A list of names: Defaults to [] + metric (Literal['lcss', 'hamming', 'levenshtein', 'jaro', 'difflib']): Distance + metric, one of lcss, hamming, levenshtein, jaro, difflib. + Defaults to 'lcss'. Returns: - A dictionary where the keys are entries in game_names, - values are the matched entity names. + dict: A dictionary where the keys are entries in game_names, + values are the matched entity names. Raises: ValueError: If metric is not in: @@ -103,12 +117,15 @@ def replace_entities( entity_dict as created in associate_entities(). Args: - dataframe (DataFrame) : A Pandas DataFrame - col_name (string) : A column in the Pandas DataFrame - entity_dict (dict) : A dictionary as created in associate_entities() + dataframe (pd.DataFrame): A Pandas DataFrame + col_name (str): A column in the Pandas DataFrame + entity_dict (dict): A dictionary as created in associate_entities() Returns: - A dataframe with replaced names. + pd.DataFrame: A dataframe with replaced names. + + Raises: + KeyError: If the requested column does not exist. """ if col_name not in dataframe.columns: msg = "Column does not exist!" diff --git a/awpy/parser/demoparser.py b/awpy/parser/demoparser.py index cb07de1d0..817aa6053 100644 --- a/awpy/parser/demoparser.py +++ b/awpy/parser/demoparser.py @@ -62,14 +62,14 @@ class DemoParser: demo_id (string): A unique demo name/game id. Default is inferred from demofile name output_file (str): The output file name. Default is 'demoid'+".json" - parse_rate (int, optional): One of 128, 64, 32, 16, 8, 4, 2, or 1. + parse_rate (int?): One of 128, 64, 32, 16, 8, 4, 2, or 1. The lower the value, the more frames are collected. Indicates spacing between parsed demo frames in ticks. Default is 128. parse_frames (bool): Flag if you want to parse frames (trajectory data) or not. Default is True parse_kill_frames (bool): Flag if you want to parse frames on kills. Default is False - trade_time (int, optional): Length of the window for a trade (in seconds). + trade_time (int?): Length of the window for a trade (in seconds). Default is 5. dmg_rolled (bool): Boolean if you want damages rolled up. As multiple damages for a player can happen in 1 tick from the same weapon. @@ -100,22 +100,17 @@ def __init__( """Instantiate a DemoParser. Args: - demofile (string): - A string denoting the path to the demo file, + demofile (str): A string denoting the path to the demo file, which ends in .dem. Defaults to '' - outpath (string): - Path where to save the outputfile to. + outpath (str | None): Path where to save the outputfile to. Default is current directory - demo_id (string): - A unique demo name/game id. + demo_id (str | None): A unique demo name/game id. Default is inferred from demofile name - log (bool, optional): - A boolean indicating if the log should print to stdout. + log (bool): A boolean indicating if the log should print to stdout. Default is False - debug (bool, optional): - A boolean indicating if debug output should be used. + debug (bool): A boolean indicating if debug output should be used. Default is False - **parser_args (ParserArgs): Further keyword args: + **parser_args (Unpack[ParserArgs]): Further keyword args: parse_rate (int, optional): One of 128, 64, 32, 16, 8, 4, 2, or 1. The lower the value, the more frames are collected. @@ -211,7 +206,7 @@ def json(self) -> Game | None: """Json getter. Returns: - Game: Parsed demo information in json format + Game | None: Parsed demo information in json format """ return self._json @@ -384,7 +379,7 @@ def parse_rate(self) -> ParseRate: """Parse rate getter. Returns: - int: Current parse rate. + ParseRate: Current parse rate. """ return self.parser_args["parse_rate"] @@ -393,7 +388,7 @@ def parse_rate(self, parse_rate: ParseRate) -> None: """Parse rate setter. Args: - parse_rate (int): Parse rate to use. + parse_rate (ParseRate): Parse rate to use. """ self.parser_args["parse_rate"] = parse_rate @@ -458,7 +453,7 @@ def parse_demo(self) -> None: and the file needs to exist. Returns: - Outputs a JSON file to current working directory. + None: Outputs a JSON file to current working directory. Raises: ValueError: Raises a ValueError if the Golang version is lower than 1.18 @@ -536,13 +531,13 @@ def read_json(self, json_path: str) -> Game: Can be used to read in already processed demofiles. Args: - json_path (string): Path to JSON file + json_path (str): Path to JSON file - Returns (Game): - JSON in Python dictionary form + Returns: + Game: The game object represented by the read json. Raises: - FileNotFoundError: Raises a FileNotFoundError if the JSON path doesn't exist + FileNotFoundError: If the JSON path doesn't exist """ # Check if JSON exists if not os.path.exists(json_path): @@ -576,13 +571,13 @@ def parse( """Wrapper for parse_demo() and read_json(). Use to parse a demo. Args: - return_type (string, optional): Either "json" or "df". Default is "json" - clean (bool, optional): True to run clean_rounds. + return_type (RoundReturnType): Either "json" or "df". Default is "json" + clean (bool): True to run clean_rounds. Otherwise, uncleaned data is returned. Defaults to True. Returns: - A dictionary of output which - is parsed to a JSON file in the working directory. + Game | dict[str, Any]: A dictionary of output which + is parsed to a JSON file in the working directory. Raises: ValueError: Raises a ValueError if the return_type is not "json" or "df" @@ -612,7 +607,7 @@ def parse_json_to_df(self) -> dict[str, Any]: """Returns JSON into dictionary where keys correspond to data frames. Returns: - A dictionary of output + dict[str, Any]: A dictionary of output Raises: AttributeError: Raises an AttributeError if the .json attribute is None @@ -653,8 +648,8 @@ def _parse_frames(self) -> pd.DataFrame: # sourcery skip: extract-method """Returns frames as a Pandas dataframe. Returns: - A Pandas dataframe where each row is a frame (game state) in the demo, - which is a discrete point of time. + pd.DataFrame: Each row is a frame (game state) in the demo, + which is a discrete point of time. Raises: AttributeError: Raises an AttributeError if the .json attribute is None @@ -704,8 +699,8 @@ def _parse_player_frames(self) -> pd.DataFrame: """Returns player frames as a Pandas dataframe. Returns: - A Pandas dataframe where each row is a player's attributes - at a given frame (game state). + pd.DataFrame: A Pandas dataframe where each row is a player's attributes + at a given frame (game state). Raises: AttributeError: Raises an AttributeError if the .json attribute is None @@ -740,7 +735,7 @@ def _parse_rounds(self) -> pd.DataFrame: """Returns rounds as a Pandas dataframe. Returns: - A Pandas dataframe where each row is a round + pd.DataFrame: A Pandas dataframe where each row is a round Raises: AttributeError: Raises an AttributeError if the .json attribute is None @@ -788,10 +783,10 @@ def _parse_action(self, action: GameActionKey) -> pd.DataFrame: """Returns action as a Pandas dataframe. Args: - action (str): Action dict to convert to dataframe. + action (GameActionKey): Action dict to convert to dataframe. Returns: - A Pandas dataframe where each row is a damage event. + pd.DataFrame: A Pandas dataframe where each row is a damage event. Raises: AttributeError: Raises an AttributeError if the .json attribute is None @@ -865,30 +860,30 @@ def clean_rounds( """Cleans a parsed demofile JSON. Args: - remove_no_frames (bool, optional): Remove rounds where there are no frames. + remove_no_frames (bool): Remove rounds where there are no frames. Default to True. - remove_warmups (bool, optional): Remove warmup rounds. Defaults to True. - remove_knifes (bool, optional): Remove knife rounds. Defaults to True. - remove_bad_timings (bool, optional): Remove bad timings. Defaults to True. - remove_excess_players (bool, optional): - Remove rounds with more than 5 players. Defaults to True. - remove_excess_kills (bool, optional): Remove rounds with more than 10 kills. + remove_warmups (bool): Remove warmup rounds. Defaults to True. + remove_knifes (bool): Remove knife rounds. Defaults to True. + remove_bad_timings (bool): Remove bad timings. Defaults to True. + remove_excess_players (bool): Remove rounds with more than 5 players. + Defaults to True. + remove_excess_kills (bool): Remove rounds with more than 10 kills. + Defaults to True. + remove_bad_endings (bool): Remove rounds with bad round end reasons. Defaults to True. - remove_bad_endings (bool, optional): - Remove rounds with bad round end reasons. Defaults to True. - remove_bad_scoring (bool, optional): Remove rounds where the scoring is off + remove_bad_scoring (bool): Remove rounds where the scoring is off Like scores going below the previous round's. Defaults to False. - return_type (str, optional): Return JSON or DataFrame. Defaults to "json". - save_to_json (bool, optional): Whether to write the JSON to a file. + return_type (RoundReturnType): Return JSON or DataFrame. Defaults to "json". + save_to_json (bool): Whether to write the JSON to a file. Defaults to True. + Returns: + Game | dict[str, Any]: A dictionary of the cleaned demo. + Raises: AttributeError: Raises an AttributeError if the .json attribute is None ValueError: Raises a ValueError if the return type is neither 'json' nor 'df' - - Returns: - dict: A dictionary of the cleaned demo. """ if self.json: if remove_no_frames: @@ -1019,6 +1014,14 @@ def rescore_rounds(self) -> None: raise AttributeError(msg) def _has_winner_and_not_winner(self, game_round: GameRound) -> bool: + """Check whether the game round end has a winner of the game and a loser. + + Args: + game_round (GameRound): Game round to check + + Returns: + bool: True if the game round ends with one side winning. + """ tie_score = 15 ot_tie_score = 3 regular_valid_t_win = (game_round["endTScore"] == tie_score + 1) and ( @@ -1199,7 +1202,7 @@ def remove_end_round(self, bad_endings: list[str] | None = None) -> None: """Removes rounds with bad end reason. Args: - bad_endings (list, optional): List of bad round end reasons. + bad_endings (list[str] | None): List of bad round end reasons. Defaults to ["Draw", "Unknown", ""]. Raises: diff --git a/awpy/types.py b/awpy/types.py index ec2de03c9..db5c4f8a1 100644 --- a/awpy/types.py +++ b/awpy/types.py @@ -783,10 +783,10 @@ def other_side(side: Literal["CT", "T"]) -> Literal["T", "CT"]: """Takes a csgo side as input and returns the opposite side in the same formatting. Args: - side (string): A csgo team side (t or ct all upper or all lower case) + side (Literal['CT', 'T']): A csgo team side (CT or T) Returns: - A string of the opposite team side in the same formatting as the input + Literal['T', 'CT']: Opposite team side. Raises: ValueError: Raises a ValueError if side not neither 'CT' nor 'T' @@ -813,10 +813,10 @@ def lower_side(side: Literal["CT", "T"]) -> Literal["ct", "t"]: """Takes a csgo side as input and returns lower cased version. Args: - side (string): A csgo team side (T or CT ) + side (Literal['CT', 'T']): A csgo team side (T or CT ) Returns: - The lower cased string. + Literal['ct', 't']: The lower cased string. Raises: ValueError: Raises a ValueError if side not neither 'CT' nor 'T' @@ -843,10 +843,10 @@ def upper_side(side: Literal["ct", "t"]) -> Literal["CT", "T"]: """Takes a csgo side as input and returns upper cased version. Args: - side (string): A csgo team side (t or ct ) + side (Literal['ct', 't']): A csgo team side (t or ct ) Returns: - The upper cased string. + Literal['CT', 'T']: The upper cased string. Raises: ValueError: Raises a ValueError if side not neither 'ct' nor 't' @@ -866,7 +866,7 @@ def is_valid_side(side: str) -> TypeGuard[Literal["CT", "T"]]: side (str): String to type guard Returns: - Whether it is CT or T + TypeGuard[Literal['CT', 'T']]: Whether it is CT or T """ return side in {"CT", "T"} @@ -878,7 +878,7 @@ def proper_player_number(n_players: int, /) -> TypeGuard[Literal[0, 1, 2, 3, 4, n_players (int): Int to type guard Returns: - Whether the int is in range(6) + TypeGuard[Literal[0, 1, 2, 3, 4, 5]]: Whether the int is in range(6) """ return n_players in range(6) @@ -921,11 +921,11 @@ def int_to_string_n_players( Args: n_players (Literal[0, 1, 2, 3, 4, 5]): Int to convert to string. + Returns: + Literal['0', '1', '2', '3', '4', '5']: str(n_players) + Raises: ValueError: If the int is not in range(6) - - Returns: - str(n_players) """ if n_players == 0: return "0" diff --git a/awpy/utils.py b/awpy/utils.py index 8bef14154..a4860b083 100644 --- a/awpy/utils.py +++ b/awpy/utils.py @@ -2,21 +2,26 @@ import re import subprocess -from typing import Any +from typing import TypeVar import pandas as pd +from typing_extensions import override from awpy.types import Area, TileId +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") -class AutoVivification(dict): + +class AutoVivification(dict[_KT, _VT | "AutoVivification"]): """Implementation of perl's autovivification feature. Stolen from: https://stackoverflow.com/questions/651794/whats-the-best-way-to-initialize-a-dict-of-dicts-in-python """ - def __getitem__(self, item: Any) -> Any: # noqa: ANN401 + @override + def __getitem__(self, item: _KT) -> _VT | "AutoVivification": """Autovivified get item from dict. Tries to get the item as normal. @@ -24,13 +29,13 @@ def __getitem__(self, item: Any) -> Any: # noqa: ANN401 AutoVivification dict is added instead. Args: - item (Any): Item to retrieve the value for. + item (_KT): Item to retrieve the value for. Returns: - Any: Retrieved value. + _VT | 'AutoVivification': Retrieved value. """ try: - return dict.__getitem__(self, item) + return super().__getitem__(item) except KeyError: value = self[item] = type(self)() return value @@ -42,10 +47,24 @@ def check_go_version() -> bool: Returns True if greater than 1.18.0 Returns: - bool whether the found go version is recent enough + bool: bool whether the found go version is recent enough + + Raises: + ValueError: If the go version could not be retrieved """ def parse_go_version(parsed_resp: list[bytes] | None) -> list[str]: + """Parse the go version from a list of bytes. + + Args: + parsed_resp (list[bytes] | None): Raw input to extract version from. + + Returns: + list[str]: List representation of the version. + + Raises: + ValueError: If the go version could not be retrieved + """ if parsed_resp is None or len(parsed_resp) != 1: msg = "Error finding Go version" raise ValueError(msg) @@ -71,9 +90,12 @@ def is_in_range(value: float, minimum: float, maximum: float) -> bool: """Checks if a value is in the range of two others inclusive. Args: - value (Any): Value to check whether it is in range - minimum (Any): Lower inclusive bound of the range check - maximum (Any): Upper inclusive bound of the range check + value (float): Value to check whether it is in range + minimum (float): Lower inclusive bound of the range check + maximum (float): Upper inclusive bound of the range check + + Returns: + bool: Whether the value us between min and max. """ return minimum <= value <= maximum @@ -82,11 +104,10 @@ def transform_csv_to_json(sample_csv: pd.DataFrame) -> dict[str, dict[TileId, Ar """From Adi. Used to transform a nav file CSV to JSON. Args: - sample_csv (pd.DataFrame): - Dataframe containing information about areas of each map + sample_csv (pd.DataFrame): Information about areas of each map Returns: - dict[str, dict[int, Area]] containing information about each area of each map + dict[str, dict[TileId, Area]]: Information about each area of each map """ final_dic: dict[str, dict[TileId, Area]] = {} for cur_map in sample_csv["mapName"].unique(): diff --git a/awpy/visualization/plot.py b/awpy/visualization/plot.py index 8512db15f..b08ba7965 100644 --- a/awpy/visualization/plot.py +++ b/awpy/visualization/plot.py @@ -51,7 +51,11 @@ @contextmanager def with_tmp_dir() -> Generator[None, None, None]: - """Create and finally delete tmp dir.""" + """Create and finally delete tmp dir. + + Yields: + None: Nothing, just returns control. + """ # Raises an exception if the folder already exists. # This is intended. We do not want to delete this folder if the user # already has one with that name. @@ -68,14 +72,14 @@ def plot_map( """Plots a blank map. Args: - map_name (str, optional): Map to search. Defaults to "de_dust2" - map_type (str, optional): "original" or "simpleradar". Defaults to "original" - dark (bool, optional): Only for use with map_type="simpleradar". + map_name (str): Map to search. Defaults to "de_dust2" + map_type (str): "original" or "simpleradar". Defaults to "original" + dark (bool): Only for use with map_type="simpleradar". Indicates if you want to use the SimpleRadar dark map type Defaults to False Returns: - matplotlib fig and ax + tuple[Figure, Axes]: matplotlib fig and ax """ base_path = os.path.join(os.path.dirname(__file__), f"""../data/map/{map_name}""") if map_type == "original": @@ -109,10 +113,10 @@ def position_transform( Args: map_name (str): Map to search position (float): X or Y coordinate - axis (str): Either "x" or "y" (lowercase) + axis (Literal['x', 'y']): Either "x" or "y" (lowercase) Returns: - float + float: float Raises: ValueError: Raises a ValueError if axis not 'x' or 'y' @@ -140,10 +144,10 @@ def position_transform_all( Args: map_name (str): Map to search - position (tuple): (X,Y,Z) coordinates + position (tuple[float, float, float]): (X,Y,Z) coordinates Returns: - tuple + tuple[float, float, float]: tuple """ current_map_data = MAP_DATA[map_name] start_x = current_map_data["pos_x"] @@ -176,17 +180,17 @@ def plot_positions( marker (str): Marker for the position alpha (float): Alpha value for the position sizes (float): Size for the position - map_name (str, optional): Map to search. Defaults to "de_ancient" - map_type (str, optional): "original" or "simpleradar". Defaults to "original" - dark (bool, optional): Only for use with map_type="simpleradar". + map_name (str): Map to search. Defaults to "de_ancient" + map_type (str): "original" or "simpleradar". Defaults to "original" + dark (bool): Only for use with map_type="simpleradar". Indicates if you want to use the SimpleRadar dark map type Defaults to False - apply_transformation (bool, optional): Indicates if you need to also use + apply_transformation (bool): Indicates if you need to also use position_transform() for the X/Y coordinates, Defaults to False Returns: - matplotlib fig and ax + tuple[Figure, Axes]: matplotlib fig and ax """ figure, axes = plot_map(map_name=map_name, map_type=map_type, dark=dark) for position in positions: @@ -217,7 +221,7 @@ def _get_plot_position_for_player( Args: player (PlayerInfo): Information about a player at a point in time. - side (Literal["ct", "t"]): Side that the player is playing on. + side (Literal['ct', 't']): Side that the player is playing on. map_name (str): Map that the player is playing on. Returns: @@ -268,17 +272,17 @@ def plot_round( Args: filename (str): Filename to save the gif - frames (list): List of frames from a parsed demo - map_name (str, optional): Map to search. Defaults to "de_ancient" - map_type (str, optional): "original" or "simpleradar". Defaults to "original - dark (bool, optional): Only for use with map_type="simpleradar". + frames (list[GameFrame]): List of frames from a parsed demo + map_name (str): Map to search. Defaults to "de_ancient" + map_type (str): "original" or "simpleradar". Defaults to "original + dark (bool): Only for use with map_type="simpleradar". Indicates if you want to use the SimpleRadar dark map type Defaults to False - fps (int, optional): Number of frames per second in the gif + fps (int): Number of frames per second in the gif Defaults to 10 Returns: - True, saves .gif + Literal[True]: True, saves .gif """ image_files: list[str] = [] for i, game_frame in tqdm(enumerate(frames)): @@ -315,19 +319,19 @@ def plot_nades( """Plots grenade trajectories. Args: - rounds (list): List of round objects from a parsed demo - nades (list, optional): List of grenade types to plot + rounds (list[GameRound]): List of round objects from a parsed demo + nades (list[str] | None): List of grenade types to plot Defaults to [] - side (str, optional): Specify side to plot grenades. Either "CT" or "T". + side (str): Specify side to plot grenades. Either "CT" or "T". Defaults to "CT" - map_name (str, optional): Map to search. Defaults to "de_ancient" - map_type (str, optional): "original" or "simpleradar". Defaults to "original" - dark (bool, optional): Only for use with map_type="simpleradar". + map_name (str): Map to search. Defaults to "de_ancient" + map_type (str): "original" or "simpleradar". Defaults to "original" + dark (bool): Only for use with map_type="simpleradar". Indicates if you want to use the SimpleRadar dark map type. Defaults to False Returns: - matplotlib fig and ax + tuple[Figure, Axes]: matplotlib fig and ax """ if nades is None: nades = [] @@ -363,19 +367,19 @@ def _plot_frame_team_player_positions( map_name: str, side: Literal["CT", "T"], player_data: TeamMetadata, - axes: plt.Axes, + axes: Axes, ) -> None: """Helper function to team's alive player positions. Args: map_name (str): Map used position_transform call - side (Literal): Side used to determine player scatterplot color + side (Literal['CT', 'T']): Side used to determine player scatterplot color player_data (TeamMetadata): Team's metadata dictionary. Expected format same as output of extract_player_positions - axes (plt.axes): axes object for plotting + axes (Axes): axes object for plotting Returns: - Nothing, all plotting is done on ax object + None: Nothing, all plotting is done on ax object """ transformed_x = [ position_transform(map_name, loc[0], "x") @@ -393,20 +397,21 @@ def _plot_frame_team_player_positions( def _plot_map_control_from_dict( map_name: str, occupied_tiles: FrameMapControlValues, - axes: plt.Axes, + axes: Axes, player_data: FrameTeamMetadata | None = None, ) -> None: """Helper function to plot map control nav tile plot. Args: map_name (str): Map used position_transform call - occupied_tiles (TeamMapControlValues): Map control values for occupied tiles - axes (plt.axes): axes object for plotting - player_data (FrameTeamMetadata): Dictionary of player positions - for each team. Expected format same as output of extract_player_positions + occupied_tiles (FrameMapControlValues): Map control values for occupied tiles + axes (Axes): axes object for plotting + player_data (FrameTeamMetadata | None): Player positions for each team. + Expected format same as output of extract_player_positions + (Default value = None) Returns: - Nothing, all plotting is done on ax object + None: Nothing, all plotting is done on ax object """ ct_tiles, t_tiles = occupied_tiles.ct_values, occupied_tiles.t_values @@ -462,7 +467,7 @@ def plot_frame_map_control( map_name: str, frame: GameFrame, plot_type: MapControlPlotType = "default", - given_fig_ax: tuple[plt.Figure, plt.Axes] | tuple[None, None] = (None, None), + given_fig_ax: tuple[Figure, Axes] | tuple[None, None] = (None, None), ) -> tuple[Figure, Axes]: """Visualize map control for awpy frame. @@ -471,13 +476,15 @@ def plot_frame_map_control( frame (GameFrame): awpy frame to calculate map control for plot_type (MapControlPlotType): Determines which type of plot is created (either default or with players) - given_fig_ax: Optional tuple containing figure and ax objects for plotting + given_fig_ax (tuple[Figure, Axes] | tuple[None, None]): Figure and ax + objects for plotting (Default value = (None, None)) Returns: - matplotlib fig and ax + tuple[Figure, Axes]: matplotlib fig and ax Raises: ValueError: If map_name is not in awpy.data.NAV + ValueError: If the dist_type is incorrect. """ if map_name not in NAV: msg = "Map not found." @@ -523,14 +530,13 @@ def plot_round_map_control( round_data (GameRound): Round whose map control will be animated. Expected format that of awpy round plot_type (MapControlPlotType): Determines which type of plot is created - (either with or without players) + (either with or without players) (Default value = 'default') Returns: - True, ensuring function has completed + Literal[True]: True, ensuring function has completed Raises: ValueError: If map_name is not in awpy.data.NAV - ] """ if map_name not in NAV: msg = "Map not found." @@ -559,16 +565,16 @@ def plot_round_map_control( def _plot_map_control_metrics( metrics: list[float], - axes: plt.Axes, + axes: Axes, ) -> None: """Helper function to plot map control metrics. Args: - metrics (list): List containing map control values to plot - axes (axes): axes object for plotting + metrics (list[float]): List containing map control values to plot + axes (Axes): axes object for plotting Returns: - Nothing, all plotting is done on ax_object + None: Nothing, all plotting is done on ax_object """ x = list(range(1, len(metrics) + 1)) axes.plot(x, metrics) @@ -609,10 +615,10 @@ def plot_map_control_metrics( """Function to plot given map control metrics. Args: - metric_arr (list): List containing map control values to plot + metric_arr (list[float]): List containing map control values to plot Returns: - matplotlib fig and ax with map control metric plot + tuple[Figure, Axes]: matplotlib fig and ax with map control metric plot Raises: ValueError: If metrics is empty diff --git a/pyproject.toml b/pyproject.toml index 9f14118c4..18db9ab7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,19 +17,19 @@ classifiers = [ "Programming Language :: Python :: 3", ] dependencies = [ - "imageio~=2.28.0", - "matplotlib~=3.7.0", + "imageio~=2.33.1", + "matplotlib~=3.8.0", "networkx~=3.1", - "numpy>=1.20,<=1.25", - "pandas~=2.0.1", - "pydantic~=2.0.1", - "scipy~=1.10.0", - "Shapely~=2.0.0", - "sphinx-rtd-theme==1.2", - "sympy==1.11", - "textdistance~=4.5.0", - "tqdm~=4.65.0", - "typing_extensions~=4.7.0", + "numpy>=1.26,<=1.27", + "pandas~=2.1.1", + "pydantic~=2.5.2", + "scipy~=1.11.4", + "Shapely~=2.0.2", + "sphinx-rtd-theme==1.3", + "sympy==1.12", + "textdistance~=4.6.1", + "tqdm~=4.66.1", + "typing_extensions>=4.7.0,<5.0", ] dynamic = ["version"] @@ -48,57 +48,29 @@ include-package-data = true "*.sum", ] +[tool.pymend] +extend-exclude = "docs/|tests/|setup.py" +output-style = "google" +input-style = "google" +check = true +force-params-min-n-params = 2 +force-meta-min-func-length = 3 + [tool.ruff] # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. -select = [ - "E", - "F", - "B", - "W", - "I", - "N", - "D", - "UP", - "YTT", - "ANN", - "S", - "BLE", - "FBT", - "A", - "C4", - "DTZ", - "T10", - "EXE", - "ISC", - "ICN", - "G", - "INP", - "PIE", - "PYI", - "PT", - "Q", - "RSE", - "RET", - "SLF", - "SIM", - "TID", - "TCH", - "INT", - "ARG", - "ERA", - "PD", +select = ["ALL"] +ignore = [ + "D208", + "ANN101", + "T20", + "PTH", + "TRY003", + "COM", + "FA", + "PTH", + "C901", "PERF", - "PGH", - "PLC", - "PLE", - "PLR", - "PLW", - "TRY", - "NPY", - "RUF", - "EM", -] -ignore = ["D208", "ANN101", "T20", "PTH", "TRY003"] # , "PLR0912", "PLR0915", "PLR0913" +] # , "PLR0912", "PLR0915", "PLR0913" # Exclude a variety of commonly ignored directories. exclude = [ @@ -157,7 +129,7 @@ strictDictionaryInference = true strictSetInference = true useLibraryCodeForTypes = false reportPropertyTypeMismatch = "error" -reportFunctionMemberAccess = "warning" +reportFunctionMemberAccess = "error" reportMissingTypeStubs = "none" reportUntypedFunctionDecorator = "error" reportUntypedClassDecorator = "error" @@ -166,14 +138,17 @@ reportUntypedNamedTuple = "error" reportPrivateUsage = "error" reportConstantRedefinition = "error" reportOverlappingOverload = "error" -reportMissingParameterType = "warning" -reportUnnecessaryIsInstance = "none" +reportMissingParameterType = "error" +# Because this is externally visible +# and we want to have the check for users +# that do not use type checkers. +reportUnnecessaryIsInstance = "warning" reportUnnecessaryCast = "error" reportUnnecessaryComparison = "error" reportUnnecessaryContains = "error" reportAssertAlwaysTrue = "error" reportUnnecessaryTypeIgnoreComment = "error" -reportImplicitOverride = "none" +reportImplicitOverride = "error" reportShadowedImports = "error" @@ -190,7 +165,7 @@ fail-under = 10.0 [tool.pylint.basic] # Good variable names which should always be accepted, separated by a comma. -good-names = ["i", "j", "k", "ex", "Run", "_", "x", "y", "z", "e", "PlayerPosition2D"] +good-names = ["i", "j", "k", "ex", "Run", "_", "x", "y", "z", "e", "PlayerPosition2D"] include-naming-hint = true [tool.pylint.design] @@ -223,6 +198,11 @@ max-statements = 50 # Minimum number of public methods for a class (see R0903). min-public-methods = 1 +# Maximum number of allowed ancestors (see R0901). +# Inheriting from typing_extensions.TypedDict alones gives that +# Tracked here: https://github.com/pylint-dev/pylint/issues/9101 +max-parents = 30 + [tool.pylint.exceptions] # Exceptions that will emit a warning when caught. overgeneral-exceptions = ["builtins.BaseException"] diff --git a/requirements.txt b/requirements.txt index 6296017de..8002b3eb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -imageio~=2.28.0 -matplotlib~=3.7.0 -networkx~=3.1 -numpy>=1.20,<=1.25 -pandas~=2.0.1 -pydantic~=2.1.1 -scipy~=1.10.0 -Shapely~=2.0.0 -sphinx-rtd-theme==1.2 -sympy==1.11 -textdistance~=4.5.0 -tqdm~=4.65.0 -typing_extensions~=4.7.0 +imageio~=2.33.1 +matplotlib~=3.8.0 +networkx~=3.2 +numpy>=1.26,<=1.27 +pandas~=2.1.1 +pydantic~=2.5.2 +scipy~=1.11.4 +Shapely~=2.0.2 +sphinx-rtd-theme==1.3 +sympy==1.12 +textdistance~=4.6.1 +tqdm~=4.66.1 +typing_extensions>=4.7.0,<5.0 diff --git a/setup.py b/setup.py index 0a1168e65..5b8f592e0 100644 --- a/setup.py +++ b/setup.py @@ -12,19 +12,19 @@ # Project uses reStructuredText, so ensure that the docutils get # installed or upgraded on the target machine install_requires=[ - "imageio~=2.28.0", - "matplotlib~=3.7.0", + "imageio~=2.33.1", + "matplotlib~=3.8.0", "networkx~=3.1", - "numpy>=1.20,<=1.25", - "pandas~=2.0.1", - "pydantic~=2.0.1", - "scipy~=1.10.0", - "Shapely~=2.0.0", - "sphinx-rtd-theme==1.2", - "sympy==1.11", - "textdistance~=4.5.0", - "tqdm~=4.65.0", - "typing_extensions~=4.7.0", + "numpy>=1.26,<=1.27", + "pandas~=2.1.1", + "pydantic~=2.5.2", + "scipy~=1.11.4", + "Shapely~=2.0.2", + "sphinx-rtd-theme==1.3", + "sympy==1.12", + "textdistance~=4.6.1", + "tqdm~=4.66.1", + "typing_extensions>=4.7.0,<5.0", ], package_data={ # If any package contains *.txt or *.rst files, include them: diff --git a/tests/requirements.txt b/tests/requirements.txt index 21c433cb4..439a2788f 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ -pre-commit==3.4.0 -pylint==2.17.6 -pyright==1.1.329 -pytest==7.4.2 +pre-commit==3.6.0 +pylint==3.0.3 +pyright==1.1.344 +pytest==7.4.4 pytest-cov==4.1.0 requests==2.31.0 diff --git a/tests/test_nav.py b/tests/test_nav.py index 7a11ab124..24c5d2401 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -721,7 +721,7 @@ def test_position_token(self): assert token["ctToken"] == "000000000000000000000000000000" assert ( token["token"] - == "000000000000000000000000000000000000000000000000100000000000" # noqa: S105,E501 + == "000000000000000000000000000000000000000000000000100000000000" # noqa: S105 ) frame = { "ct": { @@ -754,7 +754,7 @@ def test_position_token(self): assert token["ctToken"] == "000000000000000000100000000000" assert ( token["token"] - == "000000000000000000100000000000000000000000000000000000000000" # noqa: S105,E501 + == "000000000000000000100000000000000000000000000000000000000000" # noqa: S105 ) with pytest.raises(ValueError, match="Map not found."):