diff --git a/axelrod/deterministic_cache.py b/axelrod/deterministic_cache.py index 8199f5ad2..4c2f0ee88 100644 --- a/axelrod/deterministic_cache.py +++ b/axelrod/deterministic_cache.py @@ -14,7 +14,7 @@ import pickle from collections import UserDict -from typing import List, Tuple +from typing import List, Optional, Tuple from axelrod import Classifiers @@ -104,7 +104,7 @@ class DeterministicCache(UserDict): methods to save/load the cache to/from a file. """ - def __init__(self, file_name: str = None) -> None: + def __init__(self, file_name: Optional[str] = None) -> None: """Initialize a new cache. Parameters diff --git a/axelrod/ecosystem.py b/axelrod/ecosystem.py index aa2ad9a93..86f4fbd0d 100644 --- a/axelrod/ecosystem.py +++ b/axelrod/ecosystem.py @@ -12,7 +12,7 @@ """ import random -from typing import Callable, List +from typing import Callable, List, Optional from axelrod.result_set import ResultSet @@ -29,8 +29,8 @@ class Ecosystem(object): def __init__( self, results: ResultSet, - fitness: Callable[[float], float] = None, - population: List[int] = None, + fitness: Optional[Callable[[float], float]] = None, + population: Optional[List[int]] = None, ) -> None: """Create a new ecosystem. diff --git a/axelrod/fingerprint.py b/axelrod/fingerprint.py index dec12ae05..140e88604 100644 --- a/axelrod/fingerprint.py +++ b/axelrod/fingerprint.py @@ -1,7 +1,7 @@ import os from collections import namedtuple from tempfile import mkstemp -from typing import Any, List, Union +from typing import Any, List, Optional, Union import dask.dataframe as dd import matplotlib.pyplot as plt @@ -280,10 +280,10 @@ def fingerprint( turns: int = 50, repetitions: int = 10, step: float = 0.01, - processes: int = None, - filename: str = None, + processes: Optional[int] = None, + filename: Optional[str] = None, progress_bar: bool = True, - seed: int = None, + seed: Optional[int] = None, ) -> dict: """Build and play the spatial tournament. @@ -358,7 +358,7 @@ def plot( self, cmap: str = "seismic", interpolation: str = "none", - title: str = None, + title: Optional[str] = None, colorbar: bool = True, labels: bool = True, ) -> plt.Figure: @@ -437,11 +437,11 @@ def fingerprint( self, turns: int = 50, repetitions: int = 1000, - noise: float = None, - processes: int = None, - filename: str = None, + noise: Optional[float] = None, + processes: Optional[int] = None, + filename: Optional[str] = None, progress_bar: bool = True, - seed: int = None, + seed: Optional[int] = None, ) -> np.ndarray: """Creates a spatial tournament to run the necessary matches to obtain fingerprint data. @@ -556,7 +556,7 @@ def plot( self, cmap: str = "viridis", interpolation: str = "none", - title: str = None, + title: Optional[str] = None, colorbar: bool = True, labels: bool = True, display_names: bool = False, diff --git a/axelrod/game.py b/axelrod/game.py index 0d8f94380..c2b4ffc44 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -2,6 +2,7 @@ from typing import Tuple, Union import numpy as np +import numpy.typing as npt from axelrod import Action @@ -20,7 +21,7 @@ class AsymmetricGame(object): """ # pylint: disable=invalid-name - def __init__(self, A: np.array, B: np.array) -> None: + def __init__(self, A: npt.NDArray, B: npt.NDArray) -> None: """ Creates an asymmetric game from two matrices. diff --git a/axelrod/load_data_.py b/axelrod/load_data_.py index e55f5f395..58ab40c46 100644 --- a/axelrod/load_data_.py +++ b/axelrod/load_data_.py @@ -1,6 +1,6 @@ import pathlib import pkgutil -from typing import Dict, List, Text, Tuple +from typing import Callable, Dict, List, Optional, Tuple def axl_filename(path: pathlib.Path) -> pathlib.Path: @@ -20,12 +20,18 @@ def axl_filename(path: pathlib.Path) -> pathlib.Path: return axl_path / path -def load_file(filename: str, directory: str) -> List[List[str]]: +def load_file( + filename: str, + directory: str, + get_data: Callable[[str, str], Optional[bytes]] = pkgutil.get_data, +) -> List[List[str]]: """Loads a data file stored in the Axelrod library's data subdirectory, likely for parameters for a strategy.""" path = str(pathlib.Path(directory) / filename) - data_bytes = pkgutil.get_data(__name__, path) + data_bytes = get_data(__name__, path) + if data_bytes is None: + raise FileNotFoundError(f"Some loader issue for path {path}") data = data_bytes.decode("UTF-8", "replace") rows = [] diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index 41ee0de2a..c186c0c25 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -1,5 +1,5 @@ from itertools import cycle -from typing import List +from typing import List, Optional from axelrod.action import Action from axelrod.player import Player @@ -14,7 +14,7 @@ class MockPlayer(Player): name = "Mock Player" - def __init__(self, actions: List[Action] = None) -> None: + def __init__(self, actions: Optional[List[Action]] = None) -> None: super().__init__() if not actions: actions = [] diff --git a/axelrod/moran.py b/axelrod/moran.py index b5e3ffbe8..539d3d5db 100644 --- a/axelrod/moran.py +++ b/axelrod/moran.py @@ -18,7 +18,7 @@ def __init__( self, players: List[Player], turns: int = DEFAULT_TURNS, - prob_end: float = None, + prob_end: Optional[float] = None, noise: float = 0, game: Game = None, deterministic_cache: DeterministicCache = None, @@ -26,7 +26,7 @@ def __init__( mode: str = "bd", interaction_graph: Graph = None, reproduction_graph: Graph = None, - fitness_transformation: Callable = None, + fitness_transformation: Optional[Callable] = None, mutation_method="transition", stop_on_fixation=True, seed=None, @@ -175,7 +175,7 @@ def set_players(self) -> None: self.populations = [self.population_distribution()] def fitness_proportionate_selection( - self, scores: List, fitness_transformation: Callable = None + self, scores: List, fitness_transformation: Optional[Callable] = None ) -> int: """Randomly selects an individual proportionally to score. @@ -229,7 +229,7 @@ def mutate(self, index: int) -> Player: # Just clone the player return self.players[index].clone() - def death(self, index: int = None) -> int: + def death(self, index: Optional[int] = None) -> int: """ Selects the player to be removed. @@ -258,7 +258,7 @@ def death(self, index: int = None) -> int: i = self.index[vertex] return i - def birth(self, index: int = None) -> int: + def birth(self, index: Optional[int] = None) -> int: """The birth event. Parameters @@ -349,7 +349,6 @@ def _matchup_indices(self) -> Set[Tuple[int, int]]: # The other calculations are unnecessary if self.mode == "db": source = self.index[self.dead] - self.dead = None sources = sorted(self.interaction_graph.out_vertices(source)) else: # birth-death is global diff --git a/axelrod/plot.py b/axelrod/plot.py index 48509a1c7..23f5b7c9c 100644 --- a/axelrod/plot.py +++ b/axelrod/plot.py @@ -1,5 +1,5 @@ import pathlib -from typing import List, Union +from typing import Any, Callable, List, Optional, Union import matplotlib import matplotlib.pyplot as plt @@ -10,7 +10,7 @@ from .load_data_ import axl_filename from .result_set import ResultSet -titleType = List[str] +titleType = str namesType = List[str] dataType = List[List[Union[int, float]]] @@ -25,8 +25,11 @@ def _violinplot( self, data: dataType, names: namesType, - title: titleType = None, - ax: matplotlib.axes.SubplotBase = None, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, + get_figure: Callable[ + [matplotlib.axes.Axes], Union[matplotlib.figure.Figure, Any, None] + ] = lambda ax: ax.get_figure(), ) -> matplotlib.figure.Figure: """For making violinplots.""" @@ -35,7 +38,11 @@ def _violinplot( else: ax = ax - figure = ax.get_figure() + figure = get_figure(ax) + if not isinstance(figure, matplotlib.figure.Figure): + raise RuntimeError( + "get_figure unexpectedly returned a non-figure object" + ) width = max(self.num_players / 3, 12) height = width / 2 spacing = 4 @@ -50,7 +57,7 @@ def _violinplot( ) ax.set_xticks(positions) ax.set_xticklabels(names, rotation=90) - ax.set_xlim([0, spacing * (self.num_players + 1)]) + ax.set_xlim((0, spacing * (self.num_players + 1))) ax.tick_params(axis="both", which="both", labelsize=8) if title: ax.set_title(title) @@ -76,7 +83,9 @@ def _boxplot_xticks_labels(self): return [str(n) for n in self.result_set.ranked_names] def boxplot( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """For the specific mean score boxplot.""" data = self._boxplot_dataset @@ -98,7 +107,9 @@ def _winplot_dataset(self): return wins, ranked_names def winplot( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """Plots the distributions for the number of wins for each strategy.""" @@ -126,7 +137,9 @@ def _sdv_plot_dataset(self): return diffs, ranked_names def sdvplot( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """Score difference violin plots to visualize the distributions of how players attain their payoffs.""" @@ -143,7 +156,9 @@ def _lengthplot_dataset(self): ] def lengthplot( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """For the specific match length boxplot.""" data = self._lengthplot_dataset @@ -174,9 +189,12 @@ def _payoff_heatmap( self, data: dataType, names: namesType, - title: titleType = None, - ax: matplotlib.axes.SubplotBase = None, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, cmap: str = "viridis", + get_figure: Callable[ + [matplotlib.axes.Axes], Union[matplotlib.figure.Figure, Any, None] + ] = lambda ax: ax.get_figure(), ) -> matplotlib.figure.Figure: """Generic heatmap plot""" @@ -185,7 +203,11 @@ def _payoff_heatmap( else: ax = ax - figure = ax.get_figure() + figure = get_figure(ax) + if not isinstance(figure, matplotlib.figure.Figure): + raise RuntimeError( + "get_figure unexpectedly returned a non-figure object" + ) width = max(self.num_players / 4, 12) height = width figure.set_size_inches(width, height) @@ -202,7 +224,9 @@ def _payoff_heatmap( return figure def pdplot( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """Payoff difference heatmap to visualize the distributions of how players attain their payoffs.""" @@ -210,7 +234,9 @@ def pdplot( return self._payoff_heatmap(matrix, names, title=title, ax=ax) def payoff( - self, title: titleType = None, ax: matplotlib.axes.SubplotBase = None + self, + title: Optional[titleType] = None, + ax: Optional[matplotlib.axes.Axes] = None, ) -> matplotlib.figure.Figure: """Payoff heatmap to visualize the distributions of how players attain their payoffs.""" @@ -223,9 +249,12 @@ def payoff( def stackplot( self, eco, - title: titleType = None, + title: Optional[titleType] = None, logscale: bool = True, - ax: matplotlib.axes.SubplotBase = None, + ax: Optional[matplotlib.axes.Axes] = None, + get_figure: Callable[ + [matplotlib.axes.Axes], Union[matplotlib.figure.Figure, Any, None] + ] = lambda ax: ax.get_figure(), ) -> matplotlib.figure.Figure: populations = eco.population_sizes @@ -235,7 +264,11 @@ def stackplot( else: ax = ax - figure = ax.get_figure() + figure = get_figure(ax) + if not isinstance(figure, matplotlib.figure.Figure): + raise RuntimeError( + "get_figure unexpectedly returned a non-figure object" + ) turns = range(len(populations)) pops = [ [populations[iturn][ir] for iturn in turns] @@ -247,7 +280,7 @@ def stackplot( ax.yaxis.set_label_position("right") ax.yaxis.labelpad = 25.0 - ax.set_ylim([0.0, 1.0]) + ax.set_ylim((0.0, 1.0)) ax.set_ylabel("Relative population size") ax.set_xlabel("Turn") if title is not None: diff --git a/axelrod/strategies/adaptive.py b/axelrod/strategies/adaptive.py index 5d7480565..039cf0d3f 100644 --- a/axelrod/strategies/adaptive.py +++ b/axelrod/strategies/adaptive.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from axelrod.action import Action from axelrod.player import Player @@ -26,7 +26,7 @@ class Adaptive(Player): "manipulates_state": False, } - def __init__(self, initial_plays: List[Action] = None) -> None: + def __init__(self, initial_plays: Optional[List[Action]] = None) -> None: super().__init__() if not initial_plays: initial_plays = [C] * 6 + [D] * 5 diff --git a/axelrod/strategies/ann.py b/axelrod/strategies/ann.py index e7956450f..4fd764c67 100644 --- a/axelrod/strategies/ann.py +++ b/axelrod/strategies/ann.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Optional, Tuple import numpy as np @@ -198,7 +198,10 @@ class ANN(Player): } def __init__( - self, num_features: int, num_hidden: int, weights: List[float] = None + self, + num_features: int, + num_hidden: int, + weights: Optional[List[float]] = None, ) -> None: Player.__init__(self) self.num_features = num_features @@ -236,10 +239,10 @@ def __init__( self, num_features: int, num_hidden: int, - weights: List[float] = None, - mutation_probability: float = None, + weights: Optional[List[float]] = None, + mutation_probability: Optional[float] = None, mutation_distance: int = 5, - seed: int = None, + seed: Optional[int] = None, ) -> None: EvolvablePlayer.__init__(self, seed=seed) ( diff --git a/axelrod/strategies/calculator.py b/axelrod/strategies/calculator.py index 8b2546592..5d273a3ca 100644 --- a/axelrod/strategies/calculator.py +++ b/axelrod/strategies/calculator.py @@ -1,3 +1,5 @@ +from typing import Optional + from axelrod._strategy_utils import detect_cycle from axelrod.action import Action from axelrod.player import Player @@ -32,7 +34,7 @@ def __init__(self) -> None: self.joss_instance = Joss() super().__init__() - def set_seed(self, seed: int = None): + def set_seed(self, seed: Optional[int] = None): super().set_seed(seed) self.joss_instance.set_seed(seed) diff --git a/axelrod/strategies/cycler.py b/axelrod/strategies/cycler.py index ac2be686a..2eb27f206 100644 --- a/axelrod/strategies/cycler.py +++ b/axelrod/strategies/cycler.py @@ -1,6 +1,6 @@ import copy import itertools -from typing import List, Tuple +from typing import List, Optional, Tuple from axelrod.action import Action, actions_to_str, str_to_actions from axelrod.evolvable_player import ( @@ -109,11 +109,11 @@ class EvolvableCycler(Cycler, EvolvablePlayer): def __init__( self, - cycle: str = None, - cycle_length: int = None, + cycle: Optional[str] = None, + cycle_length: Optional[int] = None, mutation_probability: float = 0.2, mutation_potency: int = 1, - seed: int = None, + seed: Optional[int] = None, ) -> None: EvolvablePlayer.__init__(self, seed=seed) cycle, cycle_length = self._normalize_parameters(cycle, cycle_length) diff --git a/axelrod/strategies/finite_state_machines.py b/axelrod/strategies/finite_state_machines.py index a20e408db..9dce23365 100644 --- a/axelrod/strategies/finite_state_machines.py +++ b/axelrod/strategies/finite_state_machines.py @@ -1,5 +1,5 @@ import itertools -from typing import Any, Dict, List, Sequence, Text, Tuple +from typing import Any, Dict, List, Optional, Sequence, Text, Tuple from axelrod.action import Action from axelrod.evolvable_player import ( @@ -145,12 +145,12 @@ class EvolvableFSMPlayer(FSMPlayer, EvolvablePlayer): def __init__( self, - transitions: tuple = None, - initial_state: int = None, + transitions: Optional[tuple] = None, + initial_state: Optional[int] = None, initial_action: Action = None, - num_states: int = None, + num_states: Optional[int] = None, mutation_probability: float = 0.1, - seed: int = None, + seed: Optional[int] = None, ) -> None: """If transitions, initial_state, and initial_action are None then generate random parameters using num_states.""" @@ -189,10 +189,10 @@ def normalize_transitions( def _normalize_parameters( self, - transitions: Tuple = None, - initial_state: int = None, + transitions: Optional[Tuple] = None, + initial_state: Optional[int] = None, initial_action: Action = None, - num_states: int = None, + num_states: Optional[int] = None, ) -> Tuple[Tuple, int, Action, int]: if not ( (transitions is not None) diff --git a/axelrod/strategies/handshake.py b/axelrod/strategies/handshake.py index 6e24ee1cd..fa248c78e 100644 --- a/axelrod/strategies/handshake.py +++ b/axelrod/strategies/handshake.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from axelrod.action import Action from axelrod.player import Player @@ -25,7 +25,7 @@ class Handshake(Player): "manipulates_state": False, } - def __init__(self, initial_plays: List[Action] = None) -> None: + def __init__(self, initial_plays: Optional[List[Action]] = None) -> None: super().__init__() if not initial_plays: initial_plays = [C, D] diff --git a/axelrod/strategies/hmm.py b/axelrod/strategies/hmm.py index 92038411e..f6989891a 100644 --- a/axelrod/strategies/hmm.py +++ b/axelrod/strategies/hmm.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional from axelrod.action import Action from axelrod.evolvable_player import ( @@ -254,7 +254,7 @@ def __init__( initial_action=C, num_states=None, mutation_probability=None, - seed: int = None, + seed: Optional[int] = None, ) -> None: EvolvablePlayer.__init__(self, seed=seed) ( diff --git a/axelrod/strategies/memoryone.py b/axelrod/strategies/memoryone.py index 22d33bfaa..6fe03c151 100644 --- a/axelrod/strategies/memoryone.py +++ b/axelrod/strategies/memoryone.py @@ -2,7 +2,7 @@ files, including titfortat.py and zero_determinant.py""" import warnings -from typing import Tuple +from typing import Optional, Tuple from axelrod.action import Action from axelrod.player import Player @@ -68,7 +68,7 @@ class MemoryOnePlayer(Player): def __init__( self, - four_vector: Tuple[float, float, float, float] = None, + four_vector: Optional[Tuple[float, float, float, float]] = None, initial: Action = C, ) -> None: """ @@ -179,7 +179,7 @@ class GTFT(MemoryOnePlayer): "manipulates_state": False, } - def __init__(self, p: float = None) -> None: + def __init__(self, p: Optional[float] = None) -> None: """ Parameters diff --git a/axelrod/strategies/prober.py b/axelrod/strategies/prober.py index 78118a91c..c73d82c55 100644 --- a/axelrod/strategies/prober.py +++ b/axelrod/strategies/prober.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from axelrod.action import Action from axelrod.player import Player @@ -67,7 +67,7 @@ class Detective(Player): "manipulates_state": False, } - def __init__(self, initial_actions: List[Action] = None) -> None: + def __init__(self, initial_actions: Optional[List[Action]] = None) -> None: super().__init__() if initial_actions is None: self.initial_actions = [C, D, C, C] diff --git a/axelrod/tests/unit/test_load_data.py b/axelrod/tests/unit/test_load_data.py index 6dd880335..8273b1809 100644 --- a/axelrod/tests/unit/test_load_data.py +++ b/axelrod/tests/unit/test_load_data.py @@ -2,7 +2,7 @@ import pathlib import unittest -from axelrod.load_data_ import axl_filename +from axelrod.load_data_ import axl_filename, load_file class TestLoadData(unittest.TestCase): @@ -15,3 +15,15 @@ def test_axl_filename(self): expected_fn = os.path.join(dirname, "../../strategies/titfortat.py") self.assertTrue(os.path.samefile(actual_fn, expected_fn)) + + def test_raise_error_if_file_empty(self): + path = pathlib.Path("not/a/file.py") + with self.assertRaises(FileNotFoundError): + load_file(path, ".") + + def test_raise_error_if_something(self): + dirname = os.path.dirname(__file__) + path = os.path.join(dirname, "../../strategies/titfortat.py") + bad_loader = lambda _, __: None + with self.assertRaises(FileNotFoundError): + load_file(path, ".", bad_loader) diff --git a/axelrod/tests/unit/test_plot.py b/axelrod/tests/unit/test_plot.py index 0db464361..e34b7fabf 100644 --- a/axelrod/tests/unit/test_plot.py +++ b/axelrod/tests/unit/test_plot.py @@ -255,3 +255,23 @@ def test_all_plots(self): progress_bar=True, ) ) + + def test_figure_generation_failure_violinplot(self): + plot = axl.Plot(self.test_result_set) + with self.assertRaises(RuntimeError): + plot._violinplot( + [0, 0, 0], ["a", "b", "c"], get_figure=lambda _: None + ) + + def test_figure_generation_failure_payoff_heatmap(self): + plot = axl.Plot(self.test_result_set) + with self.assertRaises(RuntimeError): + plot._payoff_heatmap( + [0, 0, 0], ["a", "b", "c"], get_figure=lambda _: None + ) + + def test_figure_generation_failure_stackplot(self): + plot = axl.Plot(self.test_result_set) + eco = axl.Ecosystem(self.test_result_set) + with self.assertRaises(RuntimeError): + plot.stackplot(eco, get_figure=lambda _: None) diff --git a/axelrod/tournament.py b/axelrod/tournament.py index c79775489..05dd6fead 100644 --- a/axelrod/tournament.py +++ b/axelrod/tournament.py @@ -28,13 +28,13 @@ def __init__( players: List[Player], name: str = "axelrod", game: Game = None, - turns: int = None, - prob_end: float = None, + turns: Optional[int] = None, + prob_end: Optional[float] = None, repetitions: int = 10, noise: float = 0, - edges: List[Tuple] = None, - match_attributes: dict = None, - seed: int = None, + edges: Optional[List[Tuple]] = None, + match_attributes: Optional[dict] = None, + seed: Optional[int] = None, ) -> None: """ Parameters @@ -110,8 +110,8 @@ def setup_output(self, filename=None): def play( self, build_results: bool = True, - filename: str = None, - processes: int = None, + filename: Optional[str] = None, + processes: Optional[int] = None, progress_bar: bool = True, ) -> ResultSet: """ diff --git a/run_mypy.py b/run_mypy.py index 94e3d669e..c0952e7e7 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -49,7 +49,6 @@ "axelrod/strategies/rand.py", "axelrod/strategies/titfortat.py", "axelrod/strategies/hmm.py", - "axelrod/strategies/human.py", "axelrod/strategies/finite_state_machines.py", "axelrod/strategies/worse_and_worse.py", ] diff --git a/tox.ini b/tox.ini index e40c8375b..7afca6c9d 100644 --- a/tox.ini +++ b/tox.ini @@ -28,8 +28,11 @@ deps = isort black numpy==1.26.4 + mypy + types-setuptools commands = python -m pytest --cov-report term-missing --cov=axelrod --cov-fail-under=100 . --doctest-glob="*.md" --doctest-glob="*.rst" python -m black -l 80 . --check python -m isort --check-only axelrod/. + python run_mypy.py python run_strategy_indexer.py