From 20a4b9f9d611fedfa0c81086363ad4224f9b60af Mon Sep 17 00:00:00 2001 From: Scezaquer Date: Fri, 8 Dec 2023 16:51:14 -0500 Subject: [PATCH 1/3] docs --- src/popcore/__init__.py | 1 + src/popcore/core.py | 154 ++++++++++------------ src/popcore/population.py | 250 +++++++++++++++++++++++------------- src/popcore/storage/repo.py | 2 +- src/popcore/utils.py | 4 +- 5 files changed, 233 insertions(+), 178 deletions(-) diff --git a/src/popcore/__init__.py b/src/popcore/__init__.py index 3d4ba2f..7739dee 100644 --- a/src/popcore/__init__.py +++ b/src/popcore/__init__.py @@ -1,3 +1,4 @@ from .core import ( # noqa Player, Interaction, Team ) +from .population import Population diff --git a/src/popcore/core.py b/src/popcore/core.py index 5fa309a..5624047 100644 --- a/src/popcore/core.py +++ b/src/popcore/core.py @@ -6,8 +6,29 @@ class Player: """ - Player + A specific version of an agent at a given point in time. + + This is equivalent to a commit in the population. + + :param str id: The id of the player to find it in the population. + ids must be unique within each population. Defaults to None. + :param Optional[Player] parent: The parent of this player. + If None, this is considered a root. Every player may only + have one parent. Defaults to None + :param Optional[Interaction] interaction: __description__ + :param Optional[int] generation: The generation this player belongs to. + Defaults to 0. + :param Optional[int] timestep: The timestep when this player was + created. Defaults to 1. + + .. seealso:: + :class:`popcore.core.Team` + + :class:`popcore.core.Interaction` + + :class:`popcore.population.Population` """ + def __init__( self, id: Optional[str] = None, @@ -17,47 +38,7 @@ def __init__( timestep: Optional[int] = 1, branch: Optional[str] = None, ): - - """A specific version of an agent at a given point in time. - - This is equivalent to a commit in the population. - - Args: TODO - parent (Player | None): The parent of this player. - If None, this is considered a root. Every player may only - have one parent, but if it needs more, it can have - arbitrarily many contributors. Defaults to None - model_parameters (Any): The parameters of the model. With - neural networks, that would be the weights and biases. - Defaults to None. - id_str (str): The id_str of the player to find it in the pop. - id_strs must be unique within each pop. Defaults to the empty - string. - hyperparameters (Dict[str, Any]): A dictionary of the - hyperparameters that define the transition from the parent - to this player. This should contain enough information to - reproduce the evolution step deterministically given the - parent and contributors parameters. - Defaults to an empty dict. - contributors (List[PhylogeneticTree.Node]): All the models - other than the parent that contributed to the evolution. - Typically, that would be opponents and allies, or mates in - the case of genetic crossover. - For example, if the model played a game of chess against - an opponent and learned from it, the parent would be the - model before that game, and the contributor would be the - opponent. Defaults to an empty list. - generation (int): The generation this player belongs to. - Defaults to 1. - timestep (int): The timestep when this player was created. - Defaults to 1. - - Raises: - KeyError: If hyperparameters does not contain one of the - variables that were defined as necessary when creating the - tree. - ValueError: If the id_str conflicts with an other node in the tree. - """ + # TODO: Should this raise an error if the id is already taken? self.id = id self.parent = parent self.descendants: List[Player] = [] @@ -77,38 +58,33 @@ def add_descendant( branch: Optional[str] = None ) -> 'Player': - """Adds a decendant to this node - - If `node` is directly specified then it will be added as a child. - Otherwise, the other parameters will be used to create a new node - and add it as a child. - - Args: - model_parameters (Any): The model parameters of the child to be - added. Defaults to None. - id_str (str): The id_str of the child. If this is the empty string, - a unique id_str will be picked at random. - Defaults to the empty string. - hyperparameters (Dict[str, Any]): A dictionary of the - hyperparameters that define the transition from this node - to the new child. This should contain enough information to - reproduce the evolution step deterministically given the - parent and contributors parameters. - Defaults to an empty dict. - interaction (List[Player]): All the models - other than the parent that contributed to the evolution. - Typically, that would be opponents and allies, or mates in - the case of genetic crossover. - For example, if the model played a game of chess against - an opponent and learned from it, the parent would be the - model before that game, and the contributor would be the - opponent. Defaults to an empty list. - - Returns: - Player: The new descendant - + """Adds a decendant to this player. + + :param Optional[str] id: The id of the child. + Defaults to None. + :param Optional[Interaction] interaction: All the models + other than the parent that contributed to the evolution. + Typically, that would be opponents and allies, or mates in + the case of genetic crossover. + For example, if the model played a game of chess against + an opponent and learned from it, the parent would be the + model before that game, and the contributor would be the + opponent. Defaults to None. + TODO: update interaction description. + :param Optional[int] timestep: The timestep when the descendent was + created. Defaults to 1. + :param Optional[str] branch: The branch this descendent belongs to. + Defaults to None. + + :return: The new descendant + :rtype: Player + + .. seealso:: + :meth:`popcore.core.Player.has_descendants` """ + # TODO: Should this check the branch exists or create it otherwise? + branch = self.branch if branch is None else branch # Create child node @@ -126,12 +102,26 @@ def add_descendant( return descendant def has_descendants(self) -> bool: + """Returns True if the player has descendants + + .. seealso:: + :meth:`popcore.core.Player.add_descendants`""" return len(self.descendants) > 0 class Team(Player): """ - Team + A Team is a Player with an additional `members` attribute which is a + list of (sub)players that make up the team. + + :param str id: The id of the Team. ids must be unique within each + population. + :param list[Player] members: The players that constitute the team. + + .. seealso:: + :class:`popcore.core.Player` + + :class:`popcore.population.Population` """ members: "list[Player]" @@ -141,9 +131,14 @@ def __init__(self, id: str, members: "list[Player]"): class Interaction(Generic[GameOutcome]): - """_summary_ - players: players involved in the game - scores: outcomes for each player involved in the game + """A list of the players that took part in an interaction, and their + individual outcomes. + + :param List[Player] players: Players involved in the game. + :param Lists[GameOutcome] outcomes: outcomes for each player involved in + the game. + :param int timestep: The timestep when the interaction occured. Defaults to + 0. """ def __init__( @@ -152,13 +147,6 @@ def __init__( outcomes: List[GameOutcome], timestep: int = 0 ): - """_summary_ - - Args: - players (List[Player]): TODO _description_ - outcomes (List[OUTCOME]): TODO _description_ - timestep (int): TODO: description - """ assert len(players) == len(outcomes) assert timestep >= 0 self._players = players diff --git a/src/popcore/population.py b/src/popcore/population.py index cbdded2..03baf09 100644 --- a/src/popcore/population.py +++ b/src/popcore/population.py @@ -11,7 +11,16 @@ class PlayerKeyValueSerializer(Serializer[Player, dict]): + """A serializer that turns :class:`~popcore.core.Player` into dictionaries, + and dictionaries of the right format into + :class:`~popcore.core.Player`. + :param List[str] exclude_fields: The fields to exclude during + serialization. Defaults to ['descendants']. + + .. seealso:: + :class:`popcore.core.Player` + """ def __init__( self, exclude_fields: List[str] = ['descendants'] @@ -20,6 +29,14 @@ def __init__( self._exclude_fields = exclude_fields def serialize(self, player: Player) -> dict: + """Creates a dictionary from the `player`, ignoring + `self._exclude_fields` + + :param (Player) player: The player to turn into a dict. + + :return: A dictionary summarizing the player. + :rtype: dict + """ fields = { k: v for k, v in player.__dict__.items() if k not in self._exclude_fields @@ -28,6 +45,15 @@ def serialize(self, player: Player) -> dict: return fields def deserialize(self, key_value_store: dict) -> 'Player': + """Turns a dictionary of the right format into a + :class:`~popcore.core.Player`. + + :param dict key_value_store: The dictionary containing the player's + parameters. + + :return: A player that corresponds to the dictionary passed. + :rtype: Player + """ filtered = { k: v for k, v in key_value_store.items() } @@ -35,11 +61,22 @@ def deserialize(self, key_value_store: dict) -> 'Player': class PlayerAutoIdHook(Hook): + """Hook to automatically assign IDs to players that don't have one. + The ID is generated using cryptographic hashing. + """ def __call__( self, repo: Repository, player: Player, *args, **kwds ): + """Automatically assign an ID to the player if it does not have one. + + :param Repository repo: _description_ + :param Player player: The player to automatically ID. + """ + + # TODO: repo unused, do we want to keep it as an arg? + if player.id is not None: return player.id @@ -50,9 +87,23 @@ def __call__( class Population: - """A data structure to manipulate evolving populations of agents. - The structure is meant to be similar to that of a git repository, where - every commit corresponds to an agent.""" + """A data structure that records the evolution of populations of + agents. It behaves like a git repository, where each branch is a + unique agent and every commit corresponds to a specific iteration of + said agent. + + This is initialized with a '_root' branch. Users should not commit + directly to this branch if they want to track multiple separate agents, + but rather create a branch for every new agent. + + :param Optional[Player] root: If not none, the specified player becomes + the root of the population. Defaults to None. + :param str root_name: The id of the root. Defaults to `'_root'`. + :param str root_branch: The name of the root's branch. + Defaults to `'main'`. + :param Optional[Repository[Player]] stage: _description_. Defaults to + `'.popcache'`. + """ def __init__( self, @@ -61,18 +112,6 @@ def __init__( root_branch: str = "main", stage: 'Optional[Repository[Player]]' = '.popcache', ): - """Instantiates population of players. - - This is a data structure that records the evolution of populations of - agents. It behaves like a git repository, where each branch is a - unique agent and every commit corresponds to a specific iteration of - said agent. - - This is initialized with a '_root' branch. Users should not commit - directly to this branch if they want to track multiple separate agents, - but rather create a branch for every new agent. - """ - root = root if root else Player( parent=None, id=root_name, branch=root_branch ) @@ -102,26 +141,21 @@ def commit( ) -> str: """Creates a new commit in the current branch. - Args: TODO: - parameters (Any): The parameters of the model to commit. - Defaults to None - hyperparameters (Dict[str, Any]): The hyperparameters to commit. - Defaults to an empty dict. - interaction (Interaction): The agents other than the parent that - contributed to the last training step (opponents, allies, - mates...). Defaults to an empty list. - name (str): A unique identifier for the commit (like the commit - hash in git). If this is None, an unique name will be - generated using cryptographic hashing. Defaults to None - pre_commit_hooks (List[PreCommitHooks]): - post_commit_hooks (List[PostCommitHooks]): + :param str id: A unique identifier for the commit (like the commit + hash in git). Defaults to None + :param Interaction interaction: The agents other than the parent that + contributed to the last training step (opponents, allies, + mates...). Defaults to None. + TODO: update interaction description + :param int timestep: The timestep when this commit was + made. Defaults to 1. - Raises: - ValueError: If a player with the specified name already exists + :raises ValueError: If a player with the specified id already exists - Returns: - str: The id_str of the new commit. + :return: The id_str of the new commit. + :rtype: str """ + # TODO: have pre_commit_hooks and post_commit_hooks as kwargs? if self.repo.exists(id): raise ValueError(POPULATION_COMMIT_EXIST.format(id)) # Create the child node @@ -145,17 +179,21 @@ def commit( def branch(self, name: str = None) -> str: """Create a new branch diverging from the current branch. - Args: - name (str): The name of the new branch. Must be unique. - This will be a new alias to the current commit. If None, it - returns the name of the active branch + :param str name: The name of the new branch. Must be unique. + This will be a new alias to the current commit. If None, it + returns the name of the active branch. Defaults to None. - Raises: - ValueError: If a player with the specified name/alias already + :raises ValueError: If a branch with the specified name/alias already exists - Returns: - str: The name of the new commit""" + :return: The name of the new commit + :rtype: str + + .. seealso:: + :meth:`popcore.Population.branches` + + :meth:`popcore.Population.checkout` + """ if name is None: return self._branch @@ -170,11 +208,18 @@ def branch(self, name: str = None) -> str: def checkout(self, name: str) -> str: """Set the current branch to the one specified. - Args: - name (str): The name of the branch or player to switch to. + :param str name: The name of the branch or player to switch to. - Raises: - ValueError: If there is no branch with the specified name""" + :raises ValueError: If there is no branch with the specified name + + :return: The name of the branch checked out + :rtype: str + + .. seealso:: + :meth:`popcore.Population.branch` + + :meth:`popcore.Population.branches` + """ if not self.repo.exists(name): raise ValueError(POPULATION_PLAYER_NOT_EXIST.format(name)) @@ -191,26 +236,39 @@ def checkout(self, name: str) -> str: return self._branch def branches(self) -> Set[str]: - """Returns a set of all branches""" + """Returns a set of all branches. + + :rtype: Str[str] + + .. seealso:: + :meth:`popcore.Population.branch` + + :meth:`popcore.Population.checkout` + """ return set(self.repo._branches) def head(self) -> 'Player': + """Return a reference to the current commit. + + :rtype: Player""" return self._player def detach( self, ) -> 'Population': - """Creates a Population with the current player as root. + """Creates a Population with the current commit as root. The new Population does not have any connection to the current one, hence this should be thread-safe. This may then be reattached once operations have been performed. - Returns: - Population: A new population with the current player as - root. + :returns: A new population with the current commit as root. + :rtype: Population + + .. seealso:: + :meth:`popcore.Population.attach` """ - # TODO: detach persistence + # TODO: detach persistence. Plus this is broken detached_pop = Population( root=self._player, pre_commit_hooks=self._pre_commit_hooks, @@ -246,23 +304,16 @@ def attach( in the operation, and that if one wishes to keep versions of these untouched, they should make a deepcopy beforehand. - Args: - population (Population): The population to merge in the current - object. It's root's id_str should correspond to the id_str of - an already existing node, which is where it will be attached. - id_hook (Callable[[str], str]): A function that takes the current - id_str of every commit in population and returns a new id_str - to be used when attaching. Note that if auto_rehash is set to - true, it happens before id_hook is called, hence the new - hashes is what this function will receive as argument. - Defaults to the identity function. - auto_rehash (bool): If set to true, re-generates an id_str for - every commit before attaching. + :param Population population: The population to merge in the current + object. It's root's id_str should correspond to the id_str of + an already existing node, which is where it will be attached. - Raises: - ValueError: If the population's root's id_str does not match any - current commit. - ValueError: If there is a collision between commit id_str. + :raises ValueError: If the population's root's id_str does not match + any current commit. + :raises ValueError: If there is a collision between commit id_str. + + .. seealso:: + :meth:`popcore.Population.detach` """ # TODO Warning: If a model is saved in a detached pop, and the pop is @@ -325,44 +376,58 @@ def attach( self._branches = self._branches.union(branches_to_add) def lineage(self, branch: str = None) -> Iterator[Player]: - """Returns an iterator with the commits in the given lineage (branch) + """Returns an iterator over all the commits in the given lineage + (branch). - Args: - population (Population): The population to iterate over. + :param Population population: The population to iterate over. + + :param str branch: The name of the branch to iterate over. If None, + iterate over the current branch. Defaults to None - branch (str): The name of the branch to iterate over. If None, - iterate over the current branch. Defaults to None + :return: An iterator over all commits in the given branch + :rtype: Iterator[Player] - Returns: - Iterator[Player]: An iterator over all commits in the given branch""" + .. seealso:: + :meth:`popcore.population.Population.generation` + + :meth:`popcore.population.Population.flatten` + """ lineage = self._get_ancesters(branch)[:-1] for player in self._get_players(lineage): yield player def generation(self, generation: int = -1) -> Iterator[Player]: - """Returns an iterator with the players in the given generation + """Returns an iterator over the commits in the given generation. - Args: - population (Population): The population to iterate over. + :param Population population: The population to iterate over. + + :param int gen: The generation to iterate over. Defaults to -1 + (meaning the last generation). - gen (int): The generation to iterate over. Defaults to -1 (meaning the - last generation). + :retun: An iterator over all commits in the given generation + :rtype: Iterator[Player] - Returns: - Iterator[Player]: An iterator over all commits in the given generation + .. seealso:: + :meth:`popcore.population.Population.lineage` + + :meth:`popcore.population.Population.flatten` """ raise NotImplementedError() def flatten(self) -> Iterator[Player]: - """Returns an iterator with all the players in the population + """Returns an iterator over all the commits in the population. - Args: - population (Population): The population to iterate over. + :param Population population: The population to iterate over. + + :return: An iterator over all commits in the given population + :rtype: Iterator[Player] - Returns: - Iterator[Player]: An iterator over all commits in the given population + .. seealso:: + :meth:`popcore.population.Population.lineage` + + :meth:`popcore.population.Population.generation` """ lineage = self._get_descendents(self._root.id)[1:] @@ -395,13 +460,15 @@ def _rename_conflicting_branches(self, population: 'Population'): def _get_player(self, name: str = None) -> Player: """Returns the commit with the given id_str if it exists. - Args: - name (str): The name of the commit we are trying to get. If - id_str is the empty string, returns the latest commit of the - current branch. Defaults to the empty string. + Args: + name (str): The name of the commit we are trying to get. If + id_str is the empty string, returns the latest commit of + the current branch. Defaults to the empty string. - Raises: - ValueError: If a commit with the specified `name` does not exist""" + Raises: + ValueError: If a commit with the specified `name` does not + exist + """ if name is None: return self._player @@ -479,4 +546,3 @@ def _get_descendents(self, name: str = None) -> List[str]: history.extend(self._get_descendents(player.id)) return history - diff --git a/src/popcore/storage/repo.py b/src/popcore/storage/repo.py index fa1cddc..3cef599 100644 --- a/src/popcore/storage/repo.py +++ b/src/popcore/storage/repo.py @@ -13,7 +13,7 @@ class Hook: """ - Hook + Hook """ @abstractmethod def __call__(self, repo: 'Repository', metadata: Metadata, **kwds): diff --git a/src/popcore/utils.py b/src/popcore/utils.py index 6c71406..b139e99 100644 --- a/src/popcore/utils.py +++ b/src/popcore/utils.py @@ -3,9 +3,9 @@ def draw(population: Population): - """Displays the phylogenetic tree. + """Displays the population as a tree. - Only parental edges are shown, contributors are ignored.""" + Only parental edges are shown.""" import networkx as nx # type: ignore import matplotlib.pyplot as plt # type: ignore From 4fb060e26697857317034387c7dcd333db3e1616 Mon Sep 17 00:00:00 2001 From: Scezaquer Date: Mon, 11 Dec 2023 16:23:47 -0500 Subject: [PATCH 2/3] documented examples --- examples/axelrod/main.py | 22 ++++++++++++++++++++-- examples/dna/fixtures.py | 29 ++++++++++++++++++----------- examples/dna/population_dna.py | 19 ++++++++++--------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/examples/axelrod/main.py b/examples/axelrod/main.py index 5dac9d9..758732a 100644 --- a/examples/axelrod/main.py +++ b/examples/axelrod/main.py @@ -3,8 +3,18 @@ def main(): - """Example of usage of Population with axelrod""" - # TODO: @Scezaquer document this example (i.e., what the purpose?) + """Example: managing an evolving population with axelrod. + + We create a population of agents, and then + 1- Make the agents play a tournament + 2- Select the worst and best performing individuals + 3- Replace the worst agent with a copy of the best + + We repeat these steps for a few generations. This reproduces the original + axelrod paper's concept. + """ + + # Create a population players = [ axl.Cooperator(), axl.Defector(), axl.TitForTat(), axl.Grudger(), axl.Alternator(), axl.Aggravater(), @@ -13,20 +23,26 @@ def main(): ] with Population() as pop: + # Each player has it's own branch (lineage) in the population branches = [ pop.branch(str(p)) for p in players] + # Commit the initial population of agents to their respective branches for player, branch in zip(players, branches): pop.checkout(branch) pop.commit(player) + # Make a few generations of a tournament for i in range(7): + # Play the tournament tournament = axl.Tournament(players) results = tournament.play() + # Pick the best and worst players first = results.ranking[0] last = results.ranking[-1] + # Replace the worst player with a copy of the best pop.checkout(branches[first]) branches[last] = pop.branch( @@ -34,6 +50,8 @@ def main(): ) players[last] = players[first] + # Commit the members of the new generation to their respective + # branches for player, branch in zip(players, branches): pop.checkout(branch) pop.commit(player) diff --git a/examples/dna/fixtures.py b/examples/dna/fixtures.py index c7ce760..05b51e5 100644 --- a/examples/dna/fixtures.py +++ b/examples/dna/fixtures.py @@ -4,28 +4,34 @@ from popcore.population import Population import random -# TODO: @Szacquer document this example. +def random_linear_dna_evolution(): + """Tracking the evolution of a strand of DNA -def mutate(parent_parameters, hyperparameters, contributors=[]): - """Mutate a strand of DNA (replace a character in the str at random)""" - next_dna = list(parent_parameters) - next_dna[hyperparameters["spot"]] = hyperparameters["letter"] - next_dna = ''.join(next_dna) - return next_dna, hyperparameters + We look at a single strand of DNA, represented by a string, as it mutates + over time. We use a Population to store it's evolution. + Here, we only consider a single lineage and suppose that no speciation + happened.""" + + def mutate(parent_parameters, hyperparameters): + """Mutate a strand of DNA (replace a character in the str at random)""" + next_dna = list(parent_parameters) + next_dna[hyperparameters["spot"]] = hyperparameters["letter"] + next_dna = ''.join(next_dna) + return next_dna, hyperparameters -def random_linear_dna_evolution(): - """This tests the correctness of the case where the population consists - of only a single lineage""" pop = Population() - next_dna = "OOOOO" + # Initial DNA strand + next_dna = "AAAAA" dna_history = [next_dna] + # Commit the first DNA strand pop.commit(parameters=next_dna) for x in range(16): + # Mutate the DNA strand letter = random.choice("ACGT") spot = random.randrange(len(next_dna)) @@ -33,6 +39,7 @@ def random_linear_dna_evolution(): next_dna, _ = mutate(next_dna, hyperparameters) dna_history.append(next_dna) + # Commit the new, mutated DNA strand to the population pop.commit(parameters=next_dna, hyperparameters=hyperparameters) return pop, dna_history diff --git a/examples/dna/population_dna.py b/examples/dna/population_dna.py index 5b770b6..34a8afa 100644 --- a/examples/dna/population_dna.py +++ b/examples/dna/population_dna.py @@ -20,12 +20,9 @@ def mutate(parent_parameters, hyperparameters, contributors=[]): def test_visual_construction(self): """Tree tracking the evolution of a strand of DNA along 3 evolutionary paths""" - # Visual test, uncomment the last line to see what the resulting trees - # look like and check that they make sense. with Population() as pop: - # tree.add_root("GGTCAACAAATCATAAAGATATTGG") # Land snail DNA - new_DNA = "OOOOO" + new_DNA = "GGTCAACAAATCATAAAGATATTGG" # Land snail DNA pop.branch("Lineage 1") pop.branch("Lineage 2") pop.branch("Lineage 3") @@ -51,11 +48,15 @@ def test_visual_construction(self): pop.checkout(branch) hyperparameters = {"letter": letter, "spot": spot} - new_DNA, _ = TestPopulation.mutate(pop._player.parameters, - hyperparameters) - - pop.commit(parameters=new_DNA, - hyperparameters=hyperparameters) + new_DNA, _ = TestPopulation.mutate( + pop._player.parameters, + hyperparameters + ) + + pop.commit( + parameters=new_DNA, + hyperparameters=hyperparameters + ) def test_linear(self): """This tests the correctness of the case where the population consists From 00033a4bf36662586c0f272159b449afb4dd6c06 Mon Sep 17 00:00:00 2001 From: Scezaquer Date: Wed, 13 Dec 2023 11:55:03 -0500 Subject: [PATCH 3/3] more docs --- src/popcore/core.py | 15 ++++----------- src/popcore/population.py | 6 +++--- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/popcore/core.py b/src/popcore/core.py index 5624047..6f1a7ea 100644 --- a/src/popcore/core.py +++ b/src/popcore/core.py @@ -20,13 +20,6 @@ class Player: Defaults to 0. :param Optional[int] timestep: The timestep when this player was created. Defaults to 1. - - .. seealso:: - :class:`popcore.core.Team` - - :class:`popcore.core.Interaction` - - :class:`popcore.population.Population` """ def __init__( @@ -80,7 +73,7 @@ def add_descendant( :rtype: Player .. seealso:: - :meth:`popcore.core.Player.has_descendants` + :meth:`popcore.Player.has_descendants` """ # TODO: Should this check the branch exists or create it otherwise? @@ -105,7 +98,7 @@ def has_descendants(self) -> bool: """Returns True if the player has descendants .. seealso:: - :meth:`popcore.core.Player.add_descendants`""" + :meth:`popcore.Player.add_descendant`""" return len(self.descendants) > 0 @@ -119,9 +112,9 @@ class Team(Player): :param list[Player] members: The players that constitute the team. .. seealso:: - :class:`popcore.core.Player` + :class:`popcore.Player` - :class:`popcore.population.Population` + :class:`popcore.Population` """ members: "list[Player]" diff --git a/src/popcore/population.py b/src/popcore/population.py index 03baf09..1db4834 100644 --- a/src/popcore/population.py +++ b/src/popcore/population.py @@ -188,10 +188,10 @@ def branch(self, name: str = None) -> str: :return: The name of the new commit :rtype: str - + .. seealso:: :meth:`popcore.Population.branches` - + :meth:`popcore.Population.checkout` """ @@ -242,7 +242,7 @@ def branches(self) -> Set[str]: .. seealso:: :meth:`popcore.Population.branch` - + :meth:`popcore.Population.checkout` """ return set(self.repo._branches)