From 341ef14c71750a35c871937ec3f3fd06ba302d6e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Dec 2025 09:07:38 +0000 Subject: [PATCH 1/7] Bump supported Python interval to [3.10, 3.14], and updated ruff version target to 3.10. --- .github/workflows/lint.yml | 4 ++-- .github/workflows/python.yml | 8 ++++---- pyproject.toml | 6 +++--- src/pygambit/levelk.py | 2 +- src/pygambit/qre.py | 11 +++++++---- src/pygambit/util.py | 2 +- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 17ec52a78..05c57459a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - uses: py-actions/flake8@v2 cython-lint: @@ -58,7 +58,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - name: Install Python packages run: python -m pip install cython-lint - name: cython-lint diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 794340dde..fae0f51c9 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,7 +10,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: matrix: - python-version: ['3.9', '3.13'] + python-version: ['3.10', '3.14'] steps: - uses: actions/checkout@v5 @@ -39,7 +39,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] steps: - uses: actions/checkout@v4 @@ -64,7 +64,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] steps: - uses: actions/checkout@v4 @@ -89,7 +89,7 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] steps: - uses: actions/checkout@v5 diff --git a/pyproject.toml b/pyproject.toml index fb4f80b43..0772d87c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pygambit" version = "16.4.0" description = "The package for computation in game theory" readme = "src/README.rst" -requires-python = ">=3.9" +requires-python = ">=3.10" license = "GPL-2.0-or-later" authors = [ {name = "Theodore Turocy", email = "ted.turocy@gmail.com"}, @@ -17,11 +17,11 @@ keywords = ["game theory", "Nash equilibrium"] classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering :: Mathematics" ] @@ -41,7 +41,7 @@ Changelog = "https://github.com/gambitproject/gambit/blob/master/ChangeLog" [tool.ruff] line-length = 99 indent-width = 4 -target-version = "py39" +target-version = "py310" include = ["setup.py", "src/pygambit/**/*.py", "tests/**/*.py, doc/tutorials/*.ipynb"] [tool.ruff.lint] diff --git a/src/pygambit/levelk.py b/src/pygambit/levelk.py index 7203e0784..5391979f8 100644 --- a/src/pygambit/levelk.py +++ b/src/pygambit/levelk.py @@ -97,7 +97,7 @@ def fit_coghier(game, data, min_tau, max_tau, min_lam, max_lam, points, to polish the maximizer. """ def log_like(profile, data): - return sum(math.log(p) * d for (p, d) in zip(profile, data)) + return sum(math.log(p) * d for (p, d) in zip(profile, data, strict=True)) def objective(params, game, data): penalty = 0.0 diff --git a/src/pygambit/qre.py b/src/pygambit/qre.py index 01162f27a..087b3fa65 100644 --- a/src/pygambit/qre.py +++ b/src/pygambit/qre.py @@ -195,14 +195,15 @@ def _empirical_log_logit_probs(lam: float, regrets: list) -> list: math.log(sum([math.exp(lam*r) for r in infoset])) for infoset in regrets ] - return [lam*a - s for (r, s) in zip(regrets, log_sums) for a in r] + return [lam*a - s for (r, s) in zip(regrets, log_sums, strict=True) for a in r] def _empirical_log_like(lam: float, regrets: list, flattened_data: list) -> float: """Given empirical choice regrets and a list of frequencies of choices, compute the log-likelihood of the choices given the regrets and assuming the logit choice model with lambda `lam`.""" - return sum([f*p for (f, p) in zip(flattened_data, _empirical_log_logit_probs(lam, regrets))]) + return sum([f*p for (f, p) in zip(flattened_data, _empirical_log_logit_probs(lam, regrets), + strict=True)]) def _estimate_strategy_empirical( @@ -219,7 +220,8 @@ def _estimate_strategy_empirical( ) profile = data.game.mixed_strategy_profile() for strategy, log_prob in zip(data.game.strategies, - _empirical_log_logit_probs(res.x[0], regrets)): + _empirical_log_logit_probs(res.x[0], regrets), + strict=True): profile[strategy] = math.exp(log_prob) return LogitQREMixedStrategyFitResult( data, "empirical", res.x[0], profile, -res.fun @@ -239,7 +241,8 @@ def _estimate_behavior_empirical( bounds=((0.0, None),) ) profile = data.game.mixed_behavior_profile() - for action, log_prob in zip(data.game.actions, _empirical_log_logit_probs(res.x[0], regrets)): + for action, log_prob in zip(data.game.actions, _empirical_log_logit_probs(res.x[0], regrets), + strict=True): profile[action] = math.exp(log_prob) return LogitQREMixedBehaviorFitResult( data, "empirical", res.x[0], profile, -res.fun diff --git a/src/pygambit/util.py b/src/pygambit/util.py index 714f90fdd..af69586ff 100644 --- a/src/pygambit/util.py +++ b/src/pygambit/util.py @@ -5,7 +5,7 @@ @contextlib.contextmanager -def make_temporary(content: typing.Optional[str] = None) -> pathlib.Path: +def make_temporary(content: str | None = None) -> typing.Generator[pathlib.Path, None, None]: """Context manager to create a temporary file containing `content', and provide the path to the temporary file. From 6eeefbc184b77e579f4b51d4bc8a9a63ca3d668b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 8 Dec 2025 13:28:42 +0000 Subject: [PATCH 2/7] bump rtd version --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index af921b438..411a05302 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ formats: all build: os: ubuntu-22.04 tools: - python: "3.13" + python: "3.14" apt_packages: - libgmp-dev - pandoc From 8ecd415ee02a19b794c9efeacc272960a61e9d0c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 8 Dec 2025 13:31:26 +0000 Subject: [PATCH 3/7] bump Python version to 3.14 in devcontainer configuration --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a082fb340..ff7f366f9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ "features": { "ghcr.io/devcontainers/features/python:1": { "installTools": true, - "version": "3.11" + "version": "3.14" }, "ghcr.io/devcontainers-contrib/features/gdbgui:2": { "version": "latest" From 96685801e7873e019886e5f7297b458d261562ad Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 8 Dec 2025 13:43:22 +0000 Subject: [PATCH 4/7] refactor: update type hints for test functions and enforce strict zip in test functions --- tests/test_extensive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_extensive.py b/tests/test_extensive.py index 2f943ace5..7b8470285 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -1,5 +1,3 @@ -import typing - import numpy as np import pytest @@ -11,10 +9,10 @@ @pytest.mark.parametrize( "players,title", [([], "New game"), (["Alice", "Bob"], "A poker game")] ) -def test_new_tree(players: list, title: typing.Optional[str]): +def test_new_tree(players: list, title: str | None): game = gbt.Game.new_tree(players=players, title=title) assert len(game.players) == len(players) - for player, label in zip(game.players, players): + for player, label in zip(game.players, players, strict=True): assert player.label == label assert game.title == title @@ -40,7 +38,7 @@ def test_game_add_players_label(players: list): game = gbt.Game.new_tree() for player in players: game.add_player(player) - for player, label in zip(game.players, players): + for player, label in zip(game.players, players, strict=True): assert player.label == label @@ -396,7 +394,7 @@ def test_outcome_index_exception_label(): ], ) def test_reduced_strategic_form( - game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: typing.Union[list, None] + game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: list | None ): """ We test two things: From a2ccef0986944144f28ab0d5fe8c6beca5a29f20 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 8 Dec 2025 14:06:44 +0000 Subject: [PATCH 5/7] refactor: enforce strict zip and | type annotations in multiple functions and tests --- src/pygambit/game.pxi | 49 +++++++++++++++++++++++++------------ src/pygambit/stratmixed.pxi | 2 +- tests/test_actions.py | 4 +-- tests/test_behav.py | 46 +++++++++++++++++----------------- tests/test_mixed.py | 35 +++++++++++++------------- tests/test_nash.py | 13 +++++----- tests/test_node.py | 8 +++--- 7 files changed, 89 insertions(+), 68 deletions(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index adb728e5e..8bfad21bb 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -557,7 +557,7 @@ class Game: shape = arrays[0].shape g = Game.new_table(shape) for profile in itertools.product(*(range(s) for s in shape)): - for array, player in zip(arrays, g.players): + for array, player in zip(arrays, g.players, strict=True): g[profile][player] = array[profile] g.title = title return g @@ -628,10 +628,10 @@ class Game: arrays = list(payoffs.values()) shape = arrays[0].shape g = Game.new_table(shape) - for (player, label) in zip(g.players, payoffs): + for (player, label) in zip(g.players, payoffs, strict=True): player.label = label for profile in itertools.product(*(range(s) for s in shape)): - for array, player in zip(arrays, g.players): + for array, player in zip(arrays, g.players, strict=True): g[profile][player] = array[profile] g.title = title return g @@ -916,12 +916,12 @@ class Game: return profile if len(data) != len(self.players): raise ValueError("Number of elements does not match number of players") - for (p, d) in zip(self.players, data): + for (p, d) in zip(self.players, data, strict=True): if len(p.strategies) != len(d): raise ValueError( f"Number of elements does not match number of strategies for {p}" ) - for (s, v) in zip(p.strategies, d): + for (s, v) in zip(p.strategies, d, strict=True): profile[s] = typefunc(v) return profile @@ -1000,9 +1000,12 @@ class Game: for player in self.players: for strategy, prob in zip( player.strategies, - scipy.stats.dirichlet(alpha=[1 for strategy in player.strategies], - seed=gen).rvs(size=1)[0] - ): + scipy.stats.dirichlet( + alpha=[1 for strategy in player.strategies], + seed=gen + ).rvs(size=1)[0], + strict=True + ): profile[strategy] = prob return profile elif denom < 1: @@ -1018,7 +1021,15 @@ class Game: ) + [denom + k] ) - for strategy, (hi, lo) in zip(player.strategies, zip(sample[1:], sample[:-1])): + for strategy, (hi, lo) in zip( + player.strategies, + zip( + sample[1:], + sample[:-1], + strict=True + ), + strict=True + ): profile[strategy] = Rational(hi - lo - 1, denom) return profile @@ -1034,13 +1045,13 @@ class Game: for (p, d) in zip(self.players, data): if len(p.infosets) != len(d): raise ValueError(f"Number of elements does not match number of infosets for {p}") - for (i, v) in zip(p.infosets, d): + for (i, v) in zip(p.infosets, d, strict=True): if len(i.actions) != len(v): raise ValueError( f"Number of elements does not match number of " f"actions for infoset {i} for {p}" ) - for (a, u) in zip(i.actions, v): + for (a, u) in zip(i.actions, v, strict=True): profile[a] = typefunc(u) return profile @@ -1123,7 +1134,8 @@ class Game: for action, prob in zip( infoset.actions, scipy.stats.dirichlet(alpha=[1 for action in infoset.actions], - seed=gen).rvs(size=1)[0] + seed=gen).rvs(size=1)[0], + strict=True ): profile[action] = prob return profile @@ -1140,7 +1152,14 @@ class Game: ) + [denom + k] ) - for action, (hi, lo) in zip(infoset.actions, zip(sample[1:], sample[:-1])): + for action, (hi, lo) in zip( + infoset.actions, + zip( + sample[1:], sample[:-1], + strict=True + ), + strict=True + ): profile[action] = Rational(hi - lo - 1, denom) return profile @@ -1579,7 +1598,7 @@ class Game: resolved_node = cython.cast(Node, resolved_nodes[0]) self.game.deref().AppendMove(resolved_node.node, resolved_player.player, len(actions)) - for label, action in zip(actions, resolved_node.infoset.actions): + for label, action in zip(actions, resolved_node.infoset.actions, strict=True): action.label = label resolved_infoset = cython.cast(Infoset, resolved_node.infoset) for n in resolved_nodes[1:]: @@ -1943,7 +1962,7 @@ class Game: c = Outcome.wrap(self.game.deref().NewOutcome()) if str(label) != "": c.label = str(label) - for player, payoff in zip(self.players, payoffs): + for player, payoff in zip(self.players, payoffs, strict=True): c[player] = payoff return c diff --git a/src/pygambit/stratmixed.pxi b/src/pygambit/stratmixed.pxi index a5ee63707..986ee6388 100644 --- a/src/pygambit/stratmixed.pxi +++ b/src/pygambit/stratmixed.pxi @@ -288,7 +288,7 @@ class MixedStrategyProfile: raise ValueError( "when setting a mixed strategy, must specify exactly one value per strategy" ) - for s, v in zip(player.strategies, value): + for s, v in zip(player.strategies, value, strict=True): self._setprob_strategy(s, v) def __setitem__( diff --git a/tests/test_actions.py b/tests/test_actions.py index 72cb506a5..3942049e8 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -23,7 +23,7 @@ def test_set_action_label(game: gbt.Game, label: str): ) def test_set_chance_valid_probability(game: gbt.Game, inprobs: list, outprobs: list): game.set_chance_probs(game.root.infoset, inprobs) - for (action, prob) in zip(game.root.infoset.actions, outprobs): + for (action, prob) in zip(game.root.infoset.actions, outprobs, strict=True): assert action.prob == prob @@ -120,7 +120,7 @@ def test_action_delete_chance(game: gbt.Game): for p in new_probs: assert p == 1/len(new_probs) else: - for p1, p2 in zip(old_probs[1:], new_probs): + for p1, p2 in zip(old_probs[1:], new_probs, strict=True): if p1 == 0: assert p2 == 0 else: diff --git a/tests/test_behav.py b/tests/test_behav.py index 638a8baf8..fc7a0c9c2 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -34,7 +34,7 @@ def _set_action_probs(profile: gbt.MixedBehaviorProfile, probs: list, rational_f (games.create_stripped_down_poker_efg(), 1, "1/4", True) ] ) -def test_payoff_reference(game: gbt.Game, player_idx: int, payoff: typing.Union[str, float], +def test_payoff_reference(game: gbt.Game, player_idx: int, payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) payoff = gbt.Rational(payoff) if rational_flag else payoff @@ -55,7 +55,7 @@ def test_payoff_reference(game: gbt.Game, player_idx: int, payoff: typing.Union[ (games.create_stripped_down_poker_efg(), "Bob", "1/4", True), ] ) -def test_payoff_by_label_reference(game: gbt.Game, label: str, payoff: typing.Union[str, float], +def test_payoff_by_label_reference(game: gbt.Game, label: str, payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) payoff = gbt.Rational(payoff) if rational_flag else payoff @@ -130,7 +130,7 @@ def test_is_defined_at_by_label(game: gbt.Game, label: str, rational_flag: bool) def test_profile_indexing_by_player_infoset_action_idx_reference(game: gbt.Game, player_idx: int, infoset_idx: int, action_idx: int, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) action = game.players[player_idx].infosets[infoset_idx].actions[action_idx] @@ -157,7 +157,7 @@ def test_profile_indexing_by_player_infoset_action_idx_reference(game: gbt.Game, ] ) def test_profile_indexing_by_action_label_reference(game: gbt.Game, action_label: str, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): """Here we only use the action label, which are all valid""" profile = game.mixed_behavior_profile(rational=rational_flag) @@ -181,7 +181,7 @@ def test_profile_indexing_by_action_label_reference(game: gbt.Game, action_label ) def test_profile_indexing_by_invalid_action_label(game: gbt.Game, action_label: str, rational_flag: bool, - error: typing.Union[ValueError, KeyError]): + error: ValueError | KeyError): """Test that we get a KeyError for a missing label, and a ValueError for an ambigiuous label """ with pytest.raises(error): @@ -225,7 +225,7 @@ def test_profile_indexing_by_invalid_infoset_label(rational_flag: bool): def test_profile_indexing_by_infoset_and_action_labels_reference(game: gbt.Game, infoset_label: str, action_label: str, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): """Here we use the infoset label and action label, with some examples where the action label alone throws a ValueError (checked in a separate test) @@ -255,7 +255,7 @@ def test_profile_indexing_by_player_infoset_action_labels_reference(game: gbt.Ga player_label: str, infoset_label: str, action_label: str, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): """Here we use the infoset label and action label, with some examples where the action label alone throws a ValueError (checked in a separate test) @@ -443,7 +443,7 @@ def test_profile_indexing_by_player_label_reference(game: gbt.Game, player_label (games.create_stripped_down_poker_efg(), 5, "6/10", True), ] ) -def test_set_probabilities_action(game: gbt.Game, action_idx: int, prob: typing.Union[str, float], +def test_set_probabilities_action(game: gbt.Game, action_idx: int, prob: str | float, rational_flag: bool): """Test to set probabilities of actions by action index""" profile = game.mixed_behavior_profile(rational=rational_flag) @@ -472,7 +472,7 @@ def test_set_probabilities_action(game: gbt.Game, action_idx: int, prob: typing. ] ) def test_set_probabilities_action_by_label(game: gbt.Game, label: str, - prob: typing.Union[str, float], rational_flag: bool): + prob: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) prob = gbt.Rational(prob) if rational_flag else prob profile[label] = prob @@ -634,7 +634,7 @@ def test_set_probabilities_player_by_label(game: gbt.Game, player_label: str, be (games.create_stripped_down_poker_efg(), 10, 0.25, False)] ) def test_realiz_prob_nodes_reference(game: gbt.Game, node_idx: int, - realiz_prob: typing.Union[str, float], rational_flag: bool): + realiz_prob: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) realiz_prob = (gbt.Rational(realiz_prob) if rational_flag else realiz_prob) node = list(game.nodes)[node_idx] @@ -658,7 +658,7 @@ def test_realiz_prob_nodes_reference(game: gbt.Game, node_idx: int, ] ) def test_infoset_prob_reference(game: gbt.Game, player_idx: int, infoset_idx: int, - prob: typing.Union[str, float], rational_flag: bool): + prob: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) ip = profile.infoset_prob(game.players[player_idx].infosets[infoset_idx]) assert ip == (gbt.Rational(prob) if rational_flag else prob) @@ -681,7 +681,7 @@ def test_infoset_prob_reference(game: gbt.Game, player_idx: int, infoset_idx: in ] ) def test_infoset_prob_by_label_reference(game: gbt.Game, label: str, - prob: typing.Union[str, float], rational_flag: bool): + prob: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) assert profile.infoset_prob(label) == (gbt.Rational(prob) if rational_flag else prob) @@ -703,7 +703,7 @@ def test_infoset_prob_by_label_reference(game: gbt.Game, label: str, ] ) def test_infoset_payoff_reference(game: gbt.Game, player_idx: int, infoset_idx: int, - payoff: typing.Union[str, float], rational_flag: bool): + payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) iv = profile.infoset_value(game.players[player_idx].infosets[infoset_idx]) assert iv == (gbt.Rational(payoff) if rational_flag else payoff) @@ -726,7 +726,7 @@ def test_infoset_payoff_reference(game: gbt.Game, player_idx: int, infoset_idx: ] ) def test_infoset_payoff_by_label_reference(game: gbt.Game, label: str, - payoff: typing.Union[str, float], rational_flag: bool): + payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) assert profile.infoset_value(label) == (gbt.Rational(payoff) if rational_flag else payoff) @@ -761,7 +761,7 @@ def test_infoset_payoff_by_label_reference(game: gbt.Game, label: str, ] ) def test_action_payoff_reference(game: gbt.Game, player_idx: int, infoset_idx: int, - action_idx: int, payoff: typing.Union[str, float], + action_idx: int, payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) av = profile.action_value(game.players[player_idx].infosets[infoset_idx].actions[action_idx]) @@ -787,7 +787,7 @@ def test_action_payoff_reference(game: gbt.Game, player_idx: int, infoset_idx: i ] ) def test_action_value_by_label_reference(game: gbt.Game, label: str, - payoff: typing.Union[str, float], rational_flag: bool): + payoff: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) assert profile.action_value(label) == (gbt.Rational(payoff) if rational_flag else payoff) @@ -859,8 +859,8 @@ def test_regret_consistency(game: gbt.Game, rational_flag: bool): ] ) def test_regret_reference(game: gbt.Game, player_idx: int, infoset_idx: int, action_idx: int, - action_probs: typing.Union[None, list], rational_flag: bool, - tol: typing.Union[gbt.Rational, float], value: typing.Union[str, float]): + action_probs: None | list, rational_flag: bool, + tol: gbt.Rational | float, value: str | float): action = game.players[player_idx].infosets[infoset_idx].actions[action_idx] profile = game.mixed_behavior_profile(rational=rational_flag) if action_probs: @@ -941,8 +941,8 @@ def test_node_value_consistency(game: gbt.Game, rational_flag: bool): (games.create_stripped_down_poker_efg(), [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, 1.0), ] ) -def test_liap_value_reference(game: gbt.Game, action_probs: typing.Union[None, list], - rational_flag: bool, expected_value: typing.Union[str, float]): +def test_liap_value_reference(game: gbt.Game, action_probs: None | list, + rational_flag: bool, expected_value: str | float): """Tests liap_value under profile given by action_probs (which will be uniform if action_probs is None) """ @@ -985,9 +985,9 @@ def test_liap_value_reference(game: gbt.Game, action_probs: typing.Union[None, l 2, 1, "2/7", True), ] ) -def test_node_belief_reference(game: gbt.Game, tol: typing.Union[gbt.Rational, float], +def test_node_belief_reference(game: gbt.Game, tol: gbt.Rational | float, probs: list, infoset_idx: int, member_idx: int, - value: typing.Union[str, float], rational_flag: bool): + value: str | float, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) _set_action_probs(profile, probs, rational_flag) node = game.infosets[infoset_idx].members[member_idx] @@ -1181,7 +1181,7 @@ def test_specific_profile(game: gbt.Game, rational_flag: bool, data: list): for each player over his actions. """ profile = game.mixed_behavior_profile(rational=rational_flag, data=data) - for (action, prob) in zip(game.actions, [k for i in data for j in i for k in j]): + for (action, prob) in zip(game.actions, [k for i in data for j in i for k in j], strict=True): assert profile[action] == (gbt.Rational(prob) if rational_flag else prob) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 82658bfd1..03d3da50e 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -118,7 +118,7 @@ def test_normalize(game, profile_data, expected_data, rational_flag): ) def test_set_and_get_probability_by_strategy_label(game: gbt.Game, strategy_label: str, rational_flag: bool, - prob: typing.Union[float, str]): + prob: float | str): profile = game.mixed_strategy_profile(rational=rational_flag) prob = gbt.Rational(prob) if rational_flag else prob profile[strategy_label] = prob @@ -179,7 +179,7 @@ def test_set_and_get_probabilities_by_player_label(game: gbt.Game, player_label: ) def test_profile_indexing_by_player_and_strategy_label_reference(game: gbt.Game, player_label: str, strategy_label: str, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag) prob = gbt.Rational(prob) if rational_flag else prob @@ -229,8 +229,8 @@ def test_profile_indexing_by_player_and_invalid_strategy_label(game: gbt.Game, ) def test_profile_indexing_by_invalid_strategy_label(game: gbt.Game, strategy_label: str, rational_flag: bool, - error: typing.Union[ValueError, KeyError], - message: typing.Union[str, None]): + error: ValueError | KeyError, + message: str | None): """Check that we get a ValueError for an ambigious strategy label and a KeyError for one that is neither a player or strategy label in the game """ @@ -275,7 +275,7 @@ def test_profile_indexing_by_player_and_duplicate_strategy_label(): ] ) def test_profile_indexing_by_strategy_label_reference(game: gbt.Game, strategy_label: str, - prob: typing.Union[str, float], + prob: str | float, rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag) prob = gbt.Rational(prob) if rational_flag else prob @@ -366,7 +366,7 @@ def test_profile_indexing_by_player_label_reference(game: gbt.Game, player_label ] ) def test_payoff_by_label_reference(game: gbt.Game, rational_flag: bool, profile_data: list, - label: str, payoff: typing.Union[float, str]): + label: str, payoff: float | str): payoff = gbt.Rational(payoff) if rational_flag else payoff profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) assert profile.payoff(label) == payoff @@ -396,7 +396,7 @@ def test_payoff_by_label_reference(game: gbt.Game, rational_flag: bool, profile_ ] ) def test_strategy_value_by_label_reference(game: gbt.Game, rational_flag: bool, label: str, - value: typing.Union[float, str]): + value: float | str): value = gbt.Rational(value) if rational_flag else value assert game.mixed_strategy_profile(rational=rational_flag).strategy_value(label) == value @@ -456,7 +456,7 @@ def test_as_behavior_error(game: gbt.Game, rational_flag: bool): def test_payoffs_reference(game: gbt.Game, profile_data: list, rational_flag: bool, payoffs: tuple): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) - for payoff, player in zip(payoffs, profile.game.players): + for payoff, player in zip(payoffs, profile.game.players, strict=True): payoff = gbt.Rational(payoff) if rational_flag else payoff assert profile.payoff(player) == payoff @@ -491,7 +491,8 @@ def test_strategy_value_reference(game: gbt.Game, profile_data: list, rational_f strategy_values: list): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) for strategy_values_for_player, player in zip( - strategy_values, profile.game.players + strategy_values, profile.game.players, + strict=True ): for i, s in enumerate(player.strategies): sv = strategy_values_for_player[i] @@ -537,8 +538,8 @@ def test_strategy_value_reference(game: gbt.Game, profile_data: list, rational_f ] ) def test_liapunov_value_reference(game: gbt.Game, profile_data: list, - liap_expected: typing.Union[float, str], - tol: typing.Union[float, gbt.Rational, int], + liap_expected: float | str, + tol: float | gbt.Rational | int, rational_flag: bool): liap_expected = gbt.Rational(liap_expected) if rational_flag else liap_expected profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) @@ -613,7 +614,7 @@ def test_strategy_regret_consistency(game: gbt.Game, rational_flag: bool): ] ) def test_liapunov_value_consistency(game: gbt.Game, profile_data: list, - tol: typing.Union[float, gbt.Rational], + tol: float | gbt.Rational, rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) @@ -665,8 +666,8 @@ def test_liapunov_value_consistency(game: gbt.Game, profile_data: list, ] ) def test_linearity_payoff_property(game: gbt.Game, profile1: list, profile2: list, - alpha: typing.Union[float, gbt.Rational], - tol: typing.Union[float, gbt.Rational], rational_flag: bool): + alpha: float | gbt.Rational, + tol: float | gbt.Rational, rational_flag: bool): profile1 = game.mixed_strategy_profile(rational=rational_flag, data=profile1) profile2 = game.mixed_strategy_profile(rational=rational_flag, data=profile2) @@ -713,7 +714,7 @@ def test_linearity_payoff_property(game: gbt.Game, profile1: list, profile2: lis ] ) def test_payoff_and_strategy_value_consistency(game: gbt.Game, profile_data: list, - tol: typing.Union[float, gbt.Rational], + tol: float | gbt.Rational, rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) for player in game.players: @@ -746,8 +747,8 @@ def test_payoff_and_strategy_value_consistency(game: gbt.Game, profile_data: lis ] ) def test_property_linearity_strategy_value(game: gbt.Game, profile1: list, profile2: list, - alpha: typing.Union[float, str], rational_flag: bool, - tol: typing.Union[float, gbt.Rational]): + alpha: float | str, rational_flag: bool, + tol: float | gbt.Rational): alpha = gbt.Rational(alpha) if rational_flag else alpha diff --git a/tests/test_nash.py b/tests/test_nash.py index 388600157..b436b79a9 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -6,7 +6,6 @@ There is better test coverage for lp_solve, lcp_solve, and enumpoly_solve, all in mixed behaviors. """ -import typing import pytest @@ -77,7 +76,7 @@ def test_enummixed_rational(game: gbt.Game, mixed_strategy_prof_data: list): """ result = gbt.nash.enummixed_solve(game, rational=True) assert len(result.equilibria) == len(mixed_strategy_prof_data) - for eq, exp in zip(result.equilibria, mixed_strategy_prof_data): + for eq, exp in zip(result.equilibria, mixed_strategy_prof_data, strict=True): assert eq.max_regret() == 0 expected = game.mixed_strategy_profile(rational=True, data=exp) assert eq == expected @@ -139,7 +138,7 @@ def test_enummixed_rational(game: gbt.Game, mixed_strategy_prof_data: list): ], ) def test_enumpoly_ordered_behavior( - game: gbt.Game, mixed_behav_prof_data: list, stop_after: typing.Union[None, int] + game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int ): """Test calls of enumpoly for mixed behavior equilibria, using max_regret (internal consistency); and comparison to a set of previously @@ -162,7 +161,7 @@ def test_enumpoly_ordered_behavior( # compute all result = gbt.nash.enumpoly_solve(game, use_strategic=False) assert len(result.equilibria) == len(mixed_behav_prof_data) - for eq, exp in zip(result.equilibria, mixed_behav_prof_data): + for eq, exp in zip(result.equilibria, mixed_behav_prof_data, strict=True): assert abs(eq.max_regret()) <= TOL expected = game.mixed_behavior_profile(rational=True, data=exp) for p in game.players: @@ -195,7 +194,7 @@ def test_enumpoly_ordered_behavior( ], ) def test_enumpoly_unordered_behavior( - game: gbt.Game, mixed_behav_prof_data: list, stop_after: typing.Union[None, int] + game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int ): """Test calls of enumpoly for mixed behavior equilibria, using max_regret (internal consistency); and comparison to a set of previously @@ -308,7 +307,7 @@ def test_lcp_strategy_double(): ] ) def test_lcp_strategy_rational(game: gbt.Game, mixed_strategy_prof_data: list, - stop_after: typing.Union[None, int]): + stop_after: None | int): """Test calls of LCP for mixed strategy equilibria, rational precision using max_regret (internal consistency); and comparison to a sequence of previously computed equilibria using this function (regression test). @@ -325,7 +324,7 @@ def test_lcp_strategy_rational(game: gbt.Game, mixed_strategy_prof_data: list, # compute all result = gbt.nash.lcp_solve(game, use_strategic=True) assert len(result.equilibria) == len(mixed_strategy_prof_data) - for eq, exp in zip(result.equilibria, mixed_strategy_prof_data): + for eq, exp in zip(result.equilibria, mixed_strategy_prof_data, strict=True): assert eq.max_regret() == 0 expected = game.mixed_strategy_profile(rational=True, data=exp) assert eq == expected diff --git a/tests/test_node.py b/tests/test_node.py index 2806013c4..bb3fcc040 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -338,7 +338,7 @@ def test_node_copy_across_games(): def _subtrees_equal( n1: gbt.Node, n2: gbt.Node, - recursion_stop_node: typing.Union[gbt.Node, None] = None + recursion_stop_node: gbt.Node | None = None ) -> bool: if n1 == recursion_stop_node: return n2.is_terminal @@ -355,7 +355,9 @@ def _subtrees_equal( return False return all( - _subtrees_equal(c1, c2, recursion_stop_node) for (c1, c2) in zip(n1.children, n2.children) + _subtrees_equal(c1, c2, recursion_stop_node) for (c1, c2) in zip( + n1.children, n2.children, strict=True + ) ) @@ -485,7 +487,7 @@ def test_append_move_labels_list_of_nodes(): tmp1 = game.root.children[1].children[0].infoset.actions tmp2 = game.root.children[0].children[0].infoset.actions - for (action, action1, action2) in zip(action_list, tmp1, tmp2): + for (action, action1, action2) in zip(action_list, tmp1, tmp2, strict=True): assert action.label == action1.label assert action.label == action2.label From cd9a629fdeb0b4179e0f5c47d33af582b7a329e3 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 8 Dec 2025 14:25:43 +0000 Subject: [PATCH 6/7] refactor: update type hints to remove Union operator and Optional in favour of | --- src/pygambit/action.pxi | 2 +- src/pygambit/behavmixed.pxi | 12 ++-- src/pygambit/gambit.pyx | 12 ++-- src/pygambit/game.pxi | 110 ++++++++++++++++++------------------ src/pygambit/infoset.pxi | 6 +- src/pygambit/nash.pxi | 4 +- src/pygambit/node.pxi | 18 +++--- src/pygambit/outcome.pxi | 8 +-- src/pygambit/player.pxi | 6 +- src/pygambit/strategy.pxi | 2 +- src/pygambit/stratmixed.pxi | 6 +- 11 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/pygambit/action.pxi b/src/pygambit/action.pxi index 576834b54..48243e659 100644 --- a/src/pygambit/action.pxi +++ b/src/pygambit/action.pxi @@ -85,7 +85,7 @@ class Action: return Infoset.wrap(self.action.deref().GetInfoset()) @property - def prob(self) -> typing.Union[decimal.Decimal, Rational]: + def prob(self) -> decimal.Decimal | Rational: """ Get the probability a chance action is played. diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index e37d465f7..97a1ba6a6 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -257,8 +257,8 @@ class MixedBehavior: def __getitem__( self, - index: typing.Union[InfosetReference, ActionReference] - ) -> typing.Union[MixedAction, ProfileDType]: + index: InfosetReference | ActionReference + ) -> MixedAction | ProfileDType: """Access a component of the mixed behavior specified by `index`. Parameters @@ -299,7 +299,7 @@ class MixedBehavior: ) def __setitem__(self, - index: typing.Union[InfosetReference, ActionReference], + index: InfosetReference | ActionReference, value: typing.Any) -> None: """Sets a component of the mixed behavior to `value`. @@ -434,8 +434,8 @@ class MixedBehaviorProfile: def __getitem__( self, - index: typing.Union[PlayerReference, InfosetReference, ActionReference] - ) -> typing.Union[MixedBehavior, MixedAction, ProfileDType]: + index: PlayerReference | InfosetReference | ActionReference + ) -> MixedBehavior | MixedAction | ProfileDType: """Access a component of the mixed behavior specified by `index`. Parameters @@ -506,7 +506,7 @@ class MixedBehaviorProfile: def __setitem__( self, - index: typing.Union[PlayerReference, InfosetReference, ActionReference], + index: PlayerReference | InfosetReference | ActionReference, value: typing.Any ) -> None: """Sets a probability, mixed agent strategy, or mixed behavior strategy to `value`. diff --git a/src/pygambit/gambit.pyx b/src/pygambit/gambit.pyx index 95799a33d..ee31dde27 100644 --- a/src/pygambit/gambit.pyx +++ b/src/pygambit/gambit.pyx @@ -74,14 +74,14 @@ def _to_number(value: typing.Any) -> c_Number: return c_Number(value.encode("ascii")) -PlayerReference = typing.Union[Player, str] -StrategyReference = typing.Union[Strategy, str] -InfosetReference = typing.Union[Infoset, str] -ActionReference = typing.Union[Action, str] -NodeReference = typing.Union[Node, str] +PlayerReference = Player | str +StrategyReference = Strategy | str +InfosetReference = Infoset | str +ActionReference = Action | str +NodeReference = Node | str NodeReferenceSet = typing.Iterable[NodeReference] -ProfileDType = typing.Union[float, Rational] +ProfileDType = float | Rational ###################### diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 8bfad21bb..c20c43aa1 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -35,7 +35,7 @@ ctypedef c_Game (*GameParser)(const string &) except +IOError @cython.cfunc -def read_game(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase], +def read_game(filepath_or_buffer: str | pathlib.Path | io.IOBase, parser: GameParser): g = cython.declare(Game) @@ -53,7 +53,7 @@ def read_game(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase], return g -def read_gbt(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> Game: +def read_gbt(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in a GBT file. Parameters @@ -80,7 +80,7 @@ def read_gbt(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> return read_game(filepath_or_buffer, parser=ParseGbtGame) -def read_efg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> Game: +def read_efg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in an EFG file. Parameters @@ -107,7 +107,7 @@ def read_efg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> return read_game(filepath_or_buffer, parser=ParseEfgGame) -def read_nfg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> Game: +def read_nfg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in a NFG file. Parameters @@ -134,7 +134,7 @@ def read_nfg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> return read_game(filepath_or_buffer, parser=ParseNfgGame) -def read_agg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) -> Game: +def read_agg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in an AGG file. Parameters @@ -255,7 +255,7 @@ class GameOutcomes: for outcome in self.game.deref().GetOutcomes(): yield Outcome.wrap(outcome) - def __getitem__(self, index: typing.Union[int, str]) -> Outcome: + def __getitem__(self, index: int | str) -> Outcome: if isinstance(index, str): if not index.strip(): raise ValueError("Outcome label cannot be empty or all whitespace") @@ -296,7 +296,7 @@ class GamePlayers: for player in self.game.deref().GetPlayers(): yield Player.wrap(player) - def __getitem__(self, index: typing.Union[int, str]) -> Player: + def __getitem__(self, index: int | str) -> Player: if isinstance(index, str): if not index.strip(): raise ValueError("Player label cannot be empty or all whitespace") @@ -341,7 +341,7 @@ class GameActions: for infoset in self.game.infosets: yield from infoset.actions - def __getitem__(self, index: typing.Union[int, str]) -> Action: + def __getitem__(self, index: int | str) -> Action: if isinstance(index, str): if not index.strip(): raise ValueError("Action label cannot be empty or all whitespace") @@ -385,7 +385,7 @@ class GameInfosets: for player in self.game.players: yield from player.infosets - def __getitem__(self, index: typing.Union[int, str]) -> Infoset: + def __getitem__(self, index: int | str) -> Infoset: if isinstance(index, str): if not index.strip(): raise ValueError("Infoset label cannot be empty or all whitespace") @@ -429,7 +429,7 @@ class GameStrategies: for player in self.game.players: yield from player.strategies - def __getitem__(self, index: typing.Union[int, str]) -> Strategy: + def __getitem__(self, index: int | str) -> Strategy: if isinstance(index, str): if not index.strip(): raise ValueError("Strategy label cannot be empty or all whitespace") @@ -468,7 +468,7 @@ class Game: @classmethod def new_tree(cls, - players: typing.Optional[typing.List[str]] = None, + players: typing.List[str] | None = None, title: str = "Untitled extensive game") -> Game: """Create a new ``Game`` consisting of a trivial game tree, with one node, which is both root and terminal. @@ -789,7 +789,7 @@ class Game: return self.game.deref().IsPerfectRecall() @property - def min_payoff(self) -> typing.Union[decimal.Decimal, Rational]: + def min_payoff(self) -> decimal.Decimal | Rational: """The minimum payoff to any player in any play of the game. .. versionchanged:: 16.5.0 @@ -804,7 +804,7 @@ class Game: return rat_to_py(self.game.deref().GetMinPayoff()) @property - def max_payoff(self) -> typing.Union[decimal.Decimal, Rational]: + def max_payoff(self) -> decimal.Decimal | Rational: """The maximum payoff to any player in any play of the game. .. versionchanged:: 16.5.0 @@ -818,7 +818,7 @@ class Game: """ return rat_to_py(self.game.deref().GetMaxPayoff()) - def set_chance_probs(self, infoset: typing.Union[Infoset, str], probs: typing.Sequence): + def set_chance_probs(self, infoset: Infoset | str, probs: typing.Sequence): """Set the action probabilities at chance information set `infoset`. Parameters @@ -909,7 +909,7 @@ class Game: def _fill_strategy_profile(self, profile: MixedStrategyProfile, - data: typing.Optional[list], + data: list | None, typefunc: typing.Callable) -> MixedStrategyProfile: """Utility function to fill a `MixedStrategyProfile` with the data from a nested list.""" if data is None: @@ -970,7 +970,7 @@ class Game: def random_strategy_profile( self, denom: int = None, - gen: typing.Optional[np.random.Generator] = None + gen: np.random.Generator | None = None ) -> MixedStrategyProfile: """Create a `MixedStrategy` on the game, with probabilities drawn from the uniform distribution over the set of mixed strategy profiles. @@ -1035,7 +1035,7 @@ class Game: def _fill_behavior_profile(self, profile: MixedBehaviorProfile, - data: typing.Optional[list], + data: list | None, typefunc: typing.Callable) -> MixedBehaviorProfile: """Utility function to fill a `MixedBehaviorProfile` with the data from a nested list.""" if data is None: @@ -1099,7 +1099,7 @@ class Game: def random_behavior_profile( self, denom: int = None, - gen: typing.Optional[np.random.Generator] = None + gen: np.random.Generator | None = None ) -> MixedBehaviorProfile: """Create a `MixedBehaviorProfile` on the game, with probabilities drawn from the uniform distribution over the set of mixed behavior profiles. @@ -1192,7 +1192,7 @@ class Game: def _to_format( self, writer: GameWriter, - filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase, None] = None + filepath_or_buffer: str | pathlib.Path | io.IOBase | None = None ): serialized_game = writer(self.game) if filepath_or_buffer is None: @@ -1207,8 +1207,8 @@ class Game: def to_efg( self, - filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase, None] = None - ) -> typing.Union[str, None]: + filepath_or_buffer: str | pathlib.Path | io.IOBase | None = None + ) -> str | None: """Save the game to an .efg file or return its serialized representation Parameters @@ -1229,8 +1229,8 @@ class Game: def to_nfg( self, - filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase, None] = None - ) -> typing.Union[str, None]: + filepath_or_buffer: str | pathlib.Path | io.IOBase | None = None + ) -> str | None: """Save the game to a .nfg file or return its serialized representation Parameters @@ -1251,8 +1251,8 @@ class Game: def to_html( self, - filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase, None] = None - ) -> typing.Union[str, None]: + filepath_or_buffer: str | pathlib.Path | io.IOBase | None = None + ) -> str | None: """Export the game to HTML format. Generates a rendering of the strategic form of the game as a @@ -1279,8 +1279,8 @@ class Game: def to_latex( self, - filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase, None] = None - ) -> typing.Union[str, None]: + filepath_or_buffer: str | pathlib.Path | io.IOBase | None = None + ) -> str | None: """Export the game to LaTeX format. Generates a rendering of the strategic form of the game in @@ -1573,8 +1573,8 @@ class Game: f"{funcname}(): {argname} must be Action or str, not {action.__class__.__name__}" ) - def append_move(self, nodes: typing.Union[NodeReference, NodeReferenceSet], - player: typing.Union[Player, str], + def append_move(self, nodes: Node | NodeReferenceSet, + player: Player | str, actions: typing.List[str]) -> None: """Add a move for `player` at terminal `nodes`. All elements of `nodes` become part of a new information set, with actions labeled according to `actions`. @@ -1604,8 +1604,8 @@ class Game: for n in resolved_nodes[1:]: self.game.deref().AppendMove(cython.cast(Node, n).node, resolved_infoset.infoset) - def append_infoset(self, nodes: typing.Union[NodeReference, NodeReferenceSet], - infoset: typing.Union[Infoset, str]) -> None: + def append_infoset(self, nodes: Node | NodeReferenceSet, + infoset: Infoset | str) -> None: """Add a move in information set `infoset` at terminal `nodes`. Raises @@ -1625,8 +1625,8 @@ class Game: for n in resolved_nodes: self.game.deref().AppendMove(cython.cast(Node, n).node, resolved_infoset.infoset) - def insert_move(self, node: typing.Union[Node, str], - player: typing.Union[Player, str], actions: int) -> None: + def insert_move(self, node: Node | str, + player: Player | str, actions: int) -> None: """Insert a move for `player` prior to the node `node`, with `actions` actions. `node` becomes the first child of the newly-inserted node. @@ -1644,8 +1644,8 @@ class Game: raise UndefinedOperationError("insert_move(): `actions` must be a positive number") self.game.deref().InsertMove(resolved_node.node, resolved_player.player, actions) - def insert_infoset(self, node: typing.Union[Node, str], - infoset: typing.Union[Infoset, str]) -> None: + def insert_infoset(self, node: Node | str, + infoset: Infoset | str) -> None: """Insert a move in information set `infoset` prior to the node `node`. `node` becomes the first child of the newly-inserted node. @@ -1659,7 +1659,7 @@ class Game: resolved_infoset = cython.cast(Infoset, self._resolve_infoset(infoset, "insert_infoset")) self.game.deref().InsertMove(resolved_node.node, resolved_infoset.infoset) - def copy_tree(self, src: typing.Union[Node, str], dest: typing.Union[Node, str]) -> None: + def copy_tree(self, src: Node | str, dest: Node | str) -> None: """Copy the subtree rooted at the node `src` to the node `dest`. Each node in the subtree copied to follow `dest` is placed in the same information set @@ -1691,7 +1691,7 @@ class Game: raise UndefinedOperationError("copy_tree(): `dest` must be a terminal node.") self.game.deref().CopyTree(resolved_dest.node, resolved_src.node) - def move_tree(self, src: typing.Union[Node, str], dest: typing.Union[Node, str]) -> None: + def move_tree(self, src: Node | str, dest: Node | str) -> None: """Move the subtree rooted at 'src' to 'dest'. Parameters @@ -1716,7 +1716,7 @@ class Game: raise UndefinedOperationError("move_tree(): `dest` cannot be a successor of `src`.") self.game.deref().MoveTree(resolved_dest.node, resolved_src.node) - def delete_parent(self, node: typing.Union[Node, str]) -> None: + def delete_parent(self, node: Node | str) -> None: """Delete the parent node of `node`. `node` replaces its parent in the tree. All other subtrees rooted at `node`'s parent are deleted. @@ -1735,7 +1735,7 @@ class Game: resolved_node = cython.cast(Node, self._resolve_node(node, "delete_parent")) self.game.deref().DeleteParent(resolved_node.node) - def delete_tree(self, node: typing.Union[Node, str]) -> None: + def delete_tree(self, node: Node | str) -> None: """Truncate the game tree at `node`, deleting the subtree beneath it. Parameters @@ -1753,8 +1753,8 @@ class Game: self.game.deref().DeleteTree(resolved_node.node) def add_action(self, - infoset: typing.Union[typing.Infoset, str], - before: typing.Optional[typing.Union[Action, str]] = None) -> None: + infoset: typing.Infoset | str, + before: Action | str | None = None) -> None: """Add an action at the information set `infoset`. If `before` is not null, the new action is inserted before `before`. @@ -1784,7 +1784,7 @@ class Game: raise MismatchError("add_action(): must specify an action from the same infoset") self.game.deref().InsertAction(resolved_infoset.infoset, resolved_action.action) - def delete_action(self, action: typing.Union[Action, str]) -> None: + def delete_action(self, action: Action | str) -> None: """Deletes `action` from its information set. The subtrees which are rooted at nodes that follow the deleted action are also deleted. If the action is at a chance node then the probabilities of any remaining actions @@ -1805,7 +1805,7 @@ class Game: ) self.game.deref().DeleteAction(resolved_action.action) - def leave_infoset(self, node: typing.Union[Node, str]): + def leave_infoset(self, node: Node | str): """Remove this node from its information set. If this node is the only node in its information set, this operation has no effect. @@ -1818,8 +1818,8 @@ class Game: self.game.deref().LeaveInfoset(resolved_node.node) def set_infoset(self, - node: typing.Union[Node, str], - infoset: typing.Union[Infoset, str]) -> None: + node: Node | str, + infoset: Infoset | str) -> None: """Place `node` in the information set `infoset`. `node` must have the same number of descendants as `infoset` has actions. @@ -1845,8 +1845,8 @@ class Game: self.game.deref().SetInfoset(resolved_node.node, resolved_infoset.infoset) def reveal(self, - infoset: typing.Union[Infoset, str], - player: typing.Union[Player, str]) -> None: + infoset: Infoset | str, + player: Player | str) -> None: """Reveals the move made at `infoset` to `player`. Revealing the move modifies all subsequent information sets for `player` such @@ -1910,8 +1910,8 @@ class Game: p.label = str(label) return p - def set_player(self, infoset: typing.Union[Infoset, str], - player: typing.Union[Player, str]) -> None: + def set_player(self, infoset: Infoset | str, + player: Player | str) -> None: """Set the player at an information set. Parameters @@ -1932,7 +1932,7 @@ class Game: self.game.deref().SetPlayer(resolved_infoset.infoset, resolved_player.player) def add_outcome(self, - payoffs: typing.Optional[typing.List] = None, + payoffs: list | None = None, label: str = "") -> Outcome: """Add a new outcome to the game. @@ -1966,7 +1966,7 @@ class Game: c[player] = payoff return c - def delete_outcome(self, outcome: typing.Union[Outcome, str]) -> None: + def delete_outcome(self, outcome: Outcome | str) -> None: """Delete an outcome from the game. If this game is an extensive game, any @@ -1987,8 +1987,8 @@ class Game: resolved_outcome = cython.cast(Outcome, self._resolve_outcome(outcome, "delete_outcome")) self.game.deref().DeleteOutcome(resolved_outcome.outcome) - def set_outcome(self, node: typing.Union[Node, str], - outcome: typing.Optional[typing.Union[Outcome, str]]) -> None: + def set_outcome(self, node: Node | str, + outcome: Outcome | str | None) -> None: """Set `outcome` to be the outcome at `node`. If `outcome` is None, the outcome at `node` is unset. @@ -2012,7 +2012,7 @@ class Game: resolved_outcome = cython.cast(Outcome, self._resolve_outcome(outcome, "set_outcome")) self.game.deref().SetOutcome(resolved_node.node, resolved_outcome.outcome) - def add_strategy(self, player: typing.Union[Player, str], label: str = None) -> Strategy: + def add_strategy(self, player: Player | str, label: str = None) -> Strategy: """Add a new strategy to the set of strategies for `player`. Parameters @@ -2045,7 +2045,7 @@ class Game: (str(label) if label is not None else "").encode()) ) - def delete_strategy(self, strategy: typing.Union[Strategy, str]) -> None: + def delete_strategy(self, strategy: Strategy | str) -> None: """Delete `strategy` from the game. Parameters diff --git a/src/pygambit/infoset.pxi b/src/pygambit/infoset.pxi index 1b4a94236..b7e9599ca 100644 --- a/src/pygambit/infoset.pxi +++ b/src/pygambit/infoset.pxi @@ -45,7 +45,7 @@ class InfosetMembers: for member in self.infoset.deref().GetMembers(): yield Node.wrap(member) - def __getitem__(self, index: typing.Union[int, str]) -> Node: + def __getitem__(self, index: int | str) -> Node: if isinstance(index, str): if not index.strip(): raise ValueError("Node label cannot be empty or all whitespace") @@ -86,7 +86,7 @@ class InfosetActions: for action in self.infoset.deref().GetActions(): yield Action.wrap(action) - def __getitem__(self, index: typing.Union[int, str]) -> Action: + def __getitem__(self, index: int | str) -> Action: if isinstance(index, str): if not index.strip(): raise ValueError("Action label cannot be empty or all whitespace") @@ -167,7 +167,7 @@ class Infoset: return InfosetActions.wrap(self.infoset) @property - def own_prior_actions(self) -> typing.List[typing.Optional[Action]]: + def own_prior_actions(self) -> typing.List[Action | None]: """The set of actions taken by the player immediately preceding the member nodes in the information set. diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi index 9f8d73582..d2afa7449 100644 --- a/src/pygambit/nash.pxi +++ b/src/pygambit/nash.pxi @@ -261,7 +261,7 @@ def _logit_strategy_estimate(profile: MixedStrategyProfileDouble, def _logit_strategy_lambda(game: Game, - lam: typing.Union[float, typing.List[float]], + lam: float | typing.List[float], first_step: float = .03, max_accel: float = 1.1) -> typing.List[LogitQREMixedStrategyProfile]: """Compute the first QRE encountered along the principal branch of the strategic @@ -342,7 +342,7 @@ def _logit_behavior_estimate(profile: MixedBehaviorProfileDouble, def _logit_behavior_lambda(game: Game, - lam: typing.Union[float, typing.List[float]], + lam: float | typing.List[float], first_step: float = .03, max_accel: float = 1.1) -> typing.List[LogitQREMixedBehaviorProfile]: """Compute the first QRE encountered along the principal branch of the extensive diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index e5663d4bc..dea60c2d9 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -45,7 +45,7 @@ class NodeChildren: for child in self.parent.deref().GetChildren(): yield Node.wrap(child) - def __getitem__(self, action: typing.Union[int, str, Action]) -> Node: + def __getitem__(self, action: int | str | Action) -> Node: """Returns the successor node which is reached after 'action' is played. .. versionchanged: 16.5.0 @@ -136,7 +136,7 @@ class Node: return Game.wrap(self.node.deref().GetGame()) @property - def infoset(self) -> typing.Optional[Infoset]: + def infoset(self) -> Infoset | None: """The information set to which this node belongs. If this is a terminal node, which belongs to no information set, @@ -147,7 +147,7 @@ class Node: return None @property - def player(self) -> typing.Optional[Player]: + def player(self) -> Player | None: """The player who makes the decision at this node. If this is a terminal node, None is returned. @@ -157,7 +157,7 @@ class Node: return None @property - def parent(self) -> typing.Optional[Node]: + def parent(self) -> Node | None: """The parent of this node. If this is the root node, None is returned. @@ -167,7 +167,7 @@ class Node: return None @property - def prior_action(self) -> typing.Optional[Action]: + def prior_action(self) -> Action | None: """The action which leads to this node. If this is the root node, None is returned. @@ -177,7 +177,7 @@ class Node: return None @property - def own_prior_action(self) -> typing.Optional[Action]: + def own_prior_action(self) -> Action | None: """The last action taken by the node's owner before reaching this node. Returns @@ -196,7 +196,7 @@ class Node: return None @property - def prior_sibling(self) -> typing.Optional[Node]: + def prior_sibling(self) -> Node | None: """The node which is immediately before this one in its parent's children. If this is the root node or the first child of its parent, @@ -207,7 +207,7 @@ class Node: return None @property - def next_sibling(self) -> typing.Optional[Node]: + def next_sibling(self) -> Node | None: """The node which is immediately after this one in its parent's children. If this is the root node or the last child of its parent, @@ -245,7 +245,7 @@ class Node: return self.node.deref().IsStrategyReachable() @property - def outcome(self) -> typing.Optional[Outcome]: + def outcome(self) -> Outcome | None: """Returns the outcome attached to the node. If no outcome is attached to the node, None is returned. diff --git a/src/pygambit/outcome.pxi b/src/pygambit/outcome.pxi index 197e1e88e..9c2953a5c 100644 --- a/src/pygambit/outcome.pxi +++ b/src/pygambit/outcome.pxi @@ -80,8 +80,8 @@ class Outcome: return self.outcome.deref().GetNumber() - 1 def __getitem__( - self, player: typing.Union[Player, str] - ) -> typing.Union[decimal.Decimal, Rational]: + self, player: Player | str + ) -> decimal.Decimal | Rational: """The payoff to `player` at the outcome. Raises @@ -99,7 +99,7 @@ class Outcome: else: return Rational(payoff) - def __setitem__(self, player: typing.Union[Player, str], value: typing.Any) -> None: + def __setitem__(self, player: Player | str, value: typing.Any) -> None: """Set the payoff to `player` at the outcome. Parameters @@ -153,7 +153,7 @@ class TreeGameOutcome: deref(self.psp).deref() == deref(cython.cast(TreeGameOutcome, other).psp).deref() ) - def __getitem__(self, player: typing.Union[Player, str]) -> Rational: + def __getitem__(self, player: Player | str) -> Rational: """The payoff to `player` at the outcome. Parameters diff --git a/src/pygambit/player.pxi b/src/pygambit/player.pxi index b72b136c9..c68eeeb13 100644 --- a/src/pygambit/player.pxi +++ b/src/pygambit/player.pxi @@ -48,7 +48,7 @@ class PlayerInfosets: for infoset in self.player.deref().GetInfosets(): yield Infoset.wrap(infoset) - def __getitem__(self, index: typing.Union[int, str]) -> Infoset: + def __getitem__(self, index: int | str) -> Infoset: if isinstance(index, str): if not index.strip(): raise ValueError("Infoset label cannot be empty or all whitespace") @@ -88,7 +88,7 @@ class PlayerActions: for infoset in self.player.infosets: yield from infoset.actions - def __getitem__(self, index: typing.Union[int, str]) -> Action: + def __getitem__(self, index: int | str) -> Action: if isinstance(index, str): if not index.strip(): raise ValueError("Action label cannot be empty or all whitespace") @@ -133,7 +133,7 @@ class PlayerStrategies: for strategy in self.player.deref().GetStrategies(): yield Strategy.wrap(strategy) - def __getitem__(self, index: typing.Union[int, str]) -> Strategy: + def __getitem__(self, index: int | str) -> Strategy: if isinstance(index, str): if not index.strip(): raise ValueError("Strategy label cannot be empty or all whitespace") diff --git a/src/pygambit/strategy.pxi b/src/pygambit/strategy.pxi index 7e0611572..bcc5aef5e 100644 --- a/src/pygambit/strategy.pxi +++ b/src/pygambit/strategy.pxi @@ -74,7 +74,7 @@ class Strategy: """The number of the strategy.""" return self.strategy.deref().GetNumber() - 1 - def action(self, infoset: typing.Union[Infoset, str]) -> typing.Optional[Action]: + def action(self, infoset: Infoset | str) -> Action | None: """Get the action prescribed by a strategy for a given information set. .. versionadded:: 16.4.0 diff --git a/src/pygambit/stratmixed.pxi b/src/pygambit/stratmixed.pxi index 986ee6388..a69ea96ae 100644 --- a/src/pygambit/stratmixed.pxi +++ b/src/pygambit/stratmixed.pxi @@ -240,8 +240,8 @@ class MixedStrategyProfile: def __getitem__( self, - index: typing.Union[PlayerReference, StrategyReference] - ) -> typing.Union[MixedStrategy, ProfileDType]: + index: PlayerReference | StrategyReference + ) -> MixedStrategy | ProfileDType: """Access a component of the mixed strategy profile specified by `index`. Parameters @@ -293,7 +293,7 @@ class MixedStrategyProfile: def __setitem__( self, - index: typing.Union[PlayerReference, StrategyReference], + index: PlayerReference | StrategyReference, value: typing.Any ) -> None: """Sets a probability or a mixed strategy to `value`. From 903cb80f7a5a98e33e46132376fde46422a35ba5 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 15:45:59 +0000 Subject: [PATCH 7/7] Change typing.List and typing.Tuple to list and tuple equivalents --- src/pygambit/action.pxi | 2 +- src/pygambit/behavmixed.pxi | 12 +++---- src/pygambit/game.pxi | 10 +++--- src/pygambit/infoset.pxi | 4 +-- src/pygambit/nash.pxi | 63 ++++++++++++++++++------------------- src/pygambit/node.pxi | 2 +- 6 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/pygambit/action.pxi b/src/pygambit/action.pxi index 48243e659..cd21b7a5a 100644 --- a/src/pygambit/action.pxi +++ b/src/pygambit/action.pxi @@ -108,7 +108,7 @@ class Action: return Rational(py_string.decode("ascii")) @property - def plays(self) -> typing.List[Node]: + def plays(self) -> list[Node]: """Returns a list of all terminal `Node` objects consistent with it. """ return [ diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 97a1ba6a6..7a34bdd4d 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -81,7 +81,7 @@ class MixedAction: def __len__(self) -> len: return len(self.infoset.actions) - def __iter__(self) -> typing.Iterator[typing.Tuple[Action, ProfileDType], None, None]: + def __iter__(self) -> typing.Iterator[tuple[Action, ProfileDType], None, None]: """Iterate over the probabilities assigned to actions by the mixed action. .. versionadded:: 16.2.0 @@ -225,7 +225,7 @@ class MixedBehavior: def __len__(self) -> int: return len(self.player.actions) - def mixed_actions(self) -> typing.Iterator[typing.Tuple[Infoset, MixedAction], None, None]: + def mixed_actions(self) -> typing.Iterator[tuple[Infoset, MixedAction], None, None]: """Iterate over the mixed actions specified by the mixed behavior. .. versionadded:: 16.2.0 @@ -240,7 +240,7 @@ class MixedBehavior: for infoset in self.player.infosets: yield infoset, self[infoset] - def __iter__(self) -> typing.Iterator[typing.Tuple[Action, ProfileDType], None, None]: + def __iter__(self) -> typing.Iterator[tuple[Action, ProfileDType], None, None]: """Iterate over the probabilities assigned to actions by the mixed behavior. .. versionadded:: 16.2.0 @@ -387,7 +387,7 @@ class MixedBehaviorProfile: """The game on which this mixed behavior profile is defined.""" return self._game - def mixed_behaviors(self) -> typing.Iterator[typing.Tuple[Player, MixedBehavior], None, None]: + def mixed_behaviors(self) -> typing.Iterator[tuple[Player, MixedBehavior], None, None]: """Iterate over the mixed behaviors in the profile. .. versionadded:: 16.2.0 @@ -402,7 +402,7 @@ class MixedBehaviorProfile: for player in self.game.players: yield player, self[player] - def mixed_actions(self) -> typing.Iterator[typing.Tuple[Infoset, MixedAction], None, None]: + def mixed_actions(self) -> typing.Iterator[tuple[Infoset, MixedAction], None, None]: """Iterate over the mixed actions specified by the profile. .. versionadded:: 16.2.0 @@ -417,7 +417,7 @@ class MixedBehaviorProfile: for infoset in self.game.infosets: yield infoset, self[infoset] - def __iter__(self) -> typing.Iterator[typing.Tuple[Action, ProfileDType], None, None]: + def __iter__(self) -> typing.Iterator[tuple[Action, ProfileDType], None, None]: """Iterate over the probabilities assigned to actions by the profile. .. versionadded:: 16.2.0 diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index c20c43aa1..69ca46587 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -468,7 +468,7 @@ class Game: @classmethod def new_tree(cls, - players: typing.List[str] | None = None, + players: list[str] | None = None, title: str = "Untitled extensive game") -> Game: """Create a new ``Game`` consisting of a trivial game tree, with one node, which is both root and terminal. @@ -562,7 +562,7 @@ class Game: g.title = title return g - def to_arrays(self, dtype: typing.Type = Rational) -> typing.List[np.array]: + def to_arrays(self, dtype: typing.Type = Rational) -> list[np.array]: """Generate the payoff tables for players represented as numpy arrays. Parameters @@ -1475,7 +1475,7 @@ class Game: def _resolve_nodes(self, nodes: typing.Any, funcname: str, - argname: str = "nodes") -> typing.List[Node]: + argname: str = "nodes") -> list[Node]: """Resolve an attempt to reference a subset of the nodes of the game of the game. See `_resolve_node` for details on functionality. @@ -1575,7 +1575,7 @@ class Game: def append_move(self, nodes: Node | NodeReferenceSet, player: Player | str, - actions: typing.List[str]) -> None: + actions: list[str]) -> None: """Add a move for `player` at terminal `nodes`. All elements of `nodes` become part of a new information set, with actions labeled according to `actions`. @@ -1753,7 +1753,7 @@ class Game: self.game.deref().DeleteTree(resolved_node.node) def add_action(self, - infoset: typing.Infoset | str, + infoset: Infoset | str, before: Action | str | None = None) -> None: """Add an action at the information set `infoset`. If `before` is not null, the new action is inserted before `before`. diff --git a/src/pygambit/infoset.pxi b/src/pygambit/infoset.pxi index b7e9599ca..2bd862f2e 100644 --- a/src/pygambit/infoset.pxi +++ b/src/pygambit/infoset.pxi @@ -167,7 +167,7 @@ class Infoset: return InfosetActions.wrap(self.infoset) @property - def own_prior_actions(self) -> typing.List[Action | None]: + def own_prior_actions(self) -> list[Action | None]: """The set of actions taken by the player immediately preceding the member nodes in the information set. @@ -216,7 +216,7 @@ class Infoset: return Player.wrap(self.infoset.deref().GetPlayer()) @property - def plays(self) -> typing.List[Node]: + def plays(self) -> list[Node]: """Returns a list of all terminal `Node` objects consistent with it. """ return [ diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi index d2afa7449..1f39db668 100644 --- a/src/pygambit/nash.pxi +++ b/src/pygambit/nash.pxi @@ -25,13 +25,10 @@ from cython.operator cimport dereference as deref from libcpp.list cimport list as stdlist -import typing - - @cython.cfunc def _convert_mspd( inlist: stdlist[c_MixedStrategyProfile[float]] -) -> typing.List[MixedStrategyProfile[double]]: +) -> list[MixedStrategyProfile[double]]: return [MixedStrategyProfileDouble.wrap(profile) for profile in make_list_of_pointer(inlist)] @@ -39,7 +36,7 @@ def _convert_mspd( @cython.cfunc def _convert_mspr( inlist: stdlist[c_MixedStrategyProfile[c_Rational]] -) -> typing.List[MixedStrategyProfile[c_Rational]]: +) -> list[MixedStrategyProfile[c_Rational]]: return [MixedStrategyProfileRational.wrap(profile) for profile in make_list_of_pointer(inlist)] @@ -47,7 +44,7 @@ def _convert_mspr( @cython.cfunc def _convert_mbpd( inlist: stdlist[c_MixedBehaviorProfile[float]] -) -> typing.List[MixedBehaviorProfile[double]]: +) -> list[MixedBehaviorProfile[double]]: return [MixedBehaviorProfileDouble.wrap(profile) for profile in make_list_of_pointer(inlist)] @@ -55,82 +52,82 @@ def _convert_mbpd( @cython.cfunc def _convert_mbpr( inlist: stdlist[c_MixedBehaviorProfile[c_Rational]] -) -> typing.List[MixedBehaviorProfile[c_Rational]]: +) -> list[MixedBehaviorProfile[c_Rational]]: return [MixedBehaviorProfileRational.wrap(profile) for profile in make_list_of_pointer(inlist)] -def _enumpure_strategy_solve(game: Game) -> typing.List[MixedStrategyProfile[c_Rational]]: +def _enumpure_strategy_solve(game: Game) -> list[MixedStrategyProfile[c_Rational]]: return _convert_mspr(EnumPureStrategySolve(game.game)) -def _enumpure_agent_solve(game: Game) -> typing.List[MixedBehaviorProfileRational]: +def _enumpure_agent_solve(game: Game) -> list[MixedBehaviorProfileRational]: return _convert_mbpr(EnumPureAgentSolve(game.game)) -def _enummixed_strategy_solve_double(game: Game) -> typing.List[MixedStrategyProfileDouble]: +def _enummixed_strategy_solve_double(game: Game) -> list[MixedStrategyProfileDouble]: return _convert_mspd(EnumMixedStrategySolve[double](game.game)) -def _enummixed_strategy_solve_rational(game: Game) -> typing.List[MixedStrategyProfileRational]: +def _enummixed_strategy_solve_rational(game: Game) -> list[MixedStrategyProfileRational]: return _convert_mspr(EnumMixedStrategySolve[c_Rational](game.game)) def _lcp_behavior_solve_double( game: Game, stop_after: int, max_depth: int -) -> typing.List[MixedBehaviorProfileDouble]: +) -> list[MixedBehaviorProfileDouble]: return _convert_mbpd(LcpBehaviorSolve[double](game.game, stop_after, max_depth)) def _lcp_behavior_solve_rational( game: Game, stop_after: int, max_depth: int -) -> typing.List[MixedBehaviorProfileRational]: +) -> list[MixedBehaviorProfileRational]: return _convert_mbpr(LcpBehaviorSolve[c_Rational](game.game, stop_after, max_depth)) def _lcp_strategy_solve_double( game: Game, stop_after: int, max_depth: int -) -> typing.List[MixedStrategyProfileDouble]: +) -> list[MixedStrategyProfileDouble]: return _convert_mspd(LcpStrategySolve[double](game.game, stop_after, max_depth)) def _lcp_strategy_solve_rational( game: Game, stop_after: int, max_depth: int -) -> typing.List[MixedStrategyProfileRational]: +) -> list[MixedStrategyProfileRational]: return _convert_mspr(LcpStrategySolve[c_Rational](game.game, stop_after, max_depth)) -def _lp_behavior_solve_double(game: Game) -> typing.List[MixedBehaviorProfileDouble]: +def _lp_behavior_solve_double(game: Game) -> list[MixedBehaviorProfileDouble]: return _convert_mbpd(LpBehaviorSolve[double](game.game)) -def _lp_behavior_solve_rational(game: Game) -> typing.List[MixedBehaviorProfileRational]: +def _lp_behavior_solve_rational(game: Game) -> list[MixedBehaviorProfileRational]: return _convert_mbpr(LpBehaviorSolve[c_Rational](game.game)) -def _lp_strategy_solve_double(game: Game) -> typing.List[MixedStrategyProfileDouble]: +def _lp_strategy_solve_double(game: Game) -> list[MixedStrategyProfileDouble]: return _convert_mspd(LpStrategySolve[double](game.game)) -def _lp_strategy_solve_rational(game: Game) -> typing.List[MixedStrategyProfileRational]: +def _lp_strategy_solve_rational(game: Game) -> list[MixedStrategyProfileRational]: return _convert_mspr(LpStrategySolve[c_Rational](game.game)) def _liap_strategy_solve(start: MixedStrategyProfileDouble, maxregret: float, - maxiter: int) -> typing.List[MixedStrategyProfileDouble]: + maxiter: int) -> list[MixedStrategyProfileDouble]: return _convert_mspd(LiapStrategySolve(deref(start.profile), maxregret, maxiter)) def _liap_behavior_solve(start: MixedBehaviorProfileDouble, maxregret: float, - maxiter: int) -> typing.List[MixedBehaviorProfileDouble]: + maxiter: int) -> list[MixedBehaviorProfileDouble]: return _convert_mbpd(LiapBehaviorSolve(deref(start.profile), maxregret, maxiter)) def _simpdiv_strategy_solve( start: MixedStrategyProfileRational, maxregret: Rational, gridstep: int, leash: int -) -> typing.List[MixedStrategyProfileRational]: +) -> list[MixedStrategyProfileRational]: return _convert_mspr(SimpdivStrategySolve(deref(start.profile), to_rational(str(maxregret).encode("ascii")), gridstep, leash)) @@ -138,7 +135,7 @@ def _simpdiv_strategy_solve( def _ipa_strategy_solve( pert: MixedStrategyProfileDouble -) -> typing.List[MixedStrategyProfileDouble]: +) -> list[MixedStrategyProfileDouble]: try: return _convert_mspd(IPAStrategySolve(deref(pert.profile))) except RuntimeError as e: @@ -153,7 +150,7 @@ def _gnm_strategy_solve( steps: int, local_newton_interval: int, local_newton_maxits: int, -) -> typing.List[MixedStrategyProfileDouble]: +) -> list[MixedStrategyProfileDouble]: try: return _convert_mspd(GNMStrategySolve(deref(pert.profile), end_lambda, steps, local_newton_interval, local_newton_maxits)) @@ -163,7 +160,7 @@ def _gnm_strategy_solve( raise -def _nashsupport_strategy_solve(game: Game) -> typing.List[StrategySupportProfile]: +def _nashsupport_strategy_solve(game: Game) -> list[StrategySupportProfile]: return [ StrategySupportProfile.wrap(support) for support in make_list_of_pointer( @@ -176,7 +173,7 @@ def _enumpoly_strategy_solve( game: Game, stop_after: int, maxregret: float, -) -> typing.List[MixedStrategyProfileDouble]: +) -> list[MixedStrategyProfileDouble]: return _convert_mspd(EnumPolyStrategySolve(game.game, stop_after, maxregret)) @@ -184,19 +181,19 @@ def _enumpoly_behavior_solve( game: Game, stop_after: int, maxregret: float, -) -> typing.List[MixedBehaviorProfileDouble]: +) -> list[MixedBehaviorProfileDouble]: return _convert_mbpd(EnumPolyBehaviorSolve(game.game, stop_after, maxregret)) def _logit_strategy_solve( game: Game, maxregret: float, first_step: float, max_accel: float, -) -> typing.List[MixedStrategyProfileDouble]: +) -> list[MixedStrategyProfileDouble]: return _convert_mspd(LogitStrategySolveWrapper(game.game, maxregret, first_step, max_accel)) def _logit_behavior_solve( game: Game, maxregret: float, first_step: float, max_accel: float, -) -> typing.List[MixedBehaviorProfileDouble]: +) -> list[MixedBehaviorProfileDouble]: return _convert_mbpd(LogitBehaviorSolveWrapper(game.game, maxregret, first_step, max_accel)) @@ -261,9 +258,9 @@ def _logit_strategy_estimate(profile: MixedStrategyProfileDouble, def _logit_strategy_lambda(game: Game, - lam: float | typing.List[float], + lam: float | list[float], first_step: float = .03, - max_accel: float = 1.1) -> typing.List[LogitQREMixedStrategyProfile]: + max_accel: float = 1.1) -> list[LogitQREMixedStrategyProfile]: """Compute the first QRE encountered along the principal branch of the strategic game corresponding to lambda value `lam`. """ @@ -342,9 +339,9 @@ def _logit_behavior_estimate(profile: MixedBehaviorProfileDouble, def _logit_behavior_lambda(game: Game, - lam: float | typing.List[float], + lam: float | list[float], first_step: float = .03, - max_accel: float = 1.1) -> typing.List[LogitQREMixedBehaviorProfile]: + max_accel: float = 1.1) -> list[LogitQREMixedBehaviorProfile]: """Compute the first QRE encountered along the principal branch of the extensive game corresponding to lambda value `lam`. """ diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index dea60c2d9..a9d28466b 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -255,7 +255,7 @@ class Node: return Outcome.wrap(self.node.deref().GetOutcome()) @property - def plays(self) -> typing.List[Node]: + def plays(self) -> list[Node]: """Returns a list of all terminal `Node` objects consistent with it. """ return [Node.wrap(n) for n in self.node.deref().GetGame().deref().GetPlays(self.node)]