Skip to content

Commit

Permalink
Remove payu uuid subcommand and extend tests
Browse files Browse the repository at this point in the history
- Add test for listing branch metadata (payu branch)
- Add create_metadata to common test code so most test cases (except those in metadata and branch run with a pre-existing metadata file)
- Catch raised metadata warnings in tests
  • Loading branch information
Jo Basevi committed Nov 27, 2023
1 parent 9a00968 commit d5f49c5
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 218 deletions.
43 changes: 23 additions & 20 deletions payu/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from payu.laboratory import Laboratory
from payu.metadata import Metadata
from payu.git_utils import git_checkout_branch, git_clone, get_git_branch
from payu.git_utils import _get_git_repository
from payu.git_utils import get_git_repository


def add_restart_to_config(restart_path: Path,
Expand Down Expand Up @@ -55,6 +55,13 @@ def add_restart_to_config(restart_path: Path,
config_path.name)


def get_control_path(config_path):
"""Given the config path, return the control path"""
# Note: Control path is set in read_config
config = read_config(config_path)
return Path(config.get('control_path'))


def checkout_branch(lab: Laboratory,
branch_name: str,
is_new_branch: bool = False,
Expand All @@ -63,9 +70,7 @@ def checkout_branch(lab: Laboratory,
restart_path: Optional[Path] = None,
config_path: Optional[Path] = None) -> None:
"""Checkout branch"""
# Note: Control path is set in read_config
config = read_config(config_path)
control_path = Path(config.get('control_path'))
control_path = get_control_path(config_path)

# Checkout branch
git_checkout_branch(control_path, branch_name, is_new_branch, start_point)
Expand All @@ -75,8 +80,8 @@ def checkout_branch(lab: Laboratory,
# Creates new uuid, experiment name, updates and commit metadata file
metadata.setup_new_experiment()
else:
# Setup metadata if there is no uuid, otherwise check existing metadata
# and commit any changes
# Create/update metadata if no uuid, otherwise run checks on existing
# metadata and commit any changes
metadata.setup()
metadata.commit_file()

Expand All @@ -103,7 +108,7 @@ def switch_symlink(lab_dir_path: Path, control_path: Path,
sym_path.unlink()
print(f"Removed {sym_dir} symlink to {previous_path}")

# Create symlink, if directory exists in laboratory
# Create symlink, if experiment directory exists in laboratory
if dir_path.exists():
sym_path.symlink_to(dir_path)
print(f"Added {sym_dir} symlink to {dir_path}")
Expand Down Expand Up @@ -155,18 +160,19 @@ def clone(repository: str,
os.chdir(previous_directory)


def print_metadata_info(branch: git.Head, verbose: bool = False):
"""Print uuid for each branch. If verbose is true, it will print all
the metadata in metadata.yaml"""
def print_branch_metadata(branch: git.Head, verbose: bool = False):
"""Print uuid for each branch. If verbose is true, it will print all lines
of the metadata file"""
contains_metadata = False
# Note: Blobs are files
# Note: Blobs are files in the commit tree
for blob in branch.commit.tree.blobs:
if blob.name == 'metadata.yaml':
contains_metadata = True

# Read file contents
content = blob.data_stream.read().decode('utf-8')
if verbose:
# Print all metadata
for line in content.splitlines():
print(f' {line}')
else:
Expand All @@ -182,19 +188,16 @@ def print_metadata_info(branch: git.Head, verbose: bool = False):
print(" No metadata file found")


def list_branches(config_path, verbose: bool = False):
"""Print out summary of metadata on each branch"""
# Note: Control path is set in read_config
config = read_config(config_path)
control_path = Path(config.get('control_path'))

repo = _get_git_repository(control_path)
def list_branches(config_path: Optional[Path] = None, verbose: bool = False):
"""Print uuid, or metadata if verbose, for each branch in control repo"""
control_path = get_control_path(config_path)
repo = get_git_repository(control_path)

current_branch = repo.active_branch
print(f"* Current Branch: {current_branch.name}")
print_metadata_info(current_branch, verbose)
print_branch_metadata(current_branch, verbose)

for branch in repo.heads:
if branch != current_branch:
print(f"Branch: {branch.name}")
print_metadata_info(branch, verbose)
print_branch_metadata(branch, verbose)
23 changes: 14 additions & 9 deletions payu/git_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ class PayuBranchError(Exception):
"""Custom exception for payu branch operations"""


def _get_git_repository(repo_path: Union[Path, str],
initialise: bool = False,
catch_error: bool = False) -> Optional[git.Repo]:
class PayuGitWarning(Warning):
"""Custom warning class - useful for testing"""


def get_git_repository(repo_path: Union[Path, str],
initialise: bool = False,
catch_error: bool = False) -> Optional[git.Repo]:
"""Return a PythonGit repository object at given path. If initialise is
true, it will attempt to initialise a repository if it does not exist.
Otherwise, if catch_error is true, it will return None"""
Expand All @@ -31,7 +35,8 @@ def _get_git_repository(repo_path: Union[Path, str],
return repo

warnings.warn(
f"Path is not a valid git repository: {repo_path}"
f"Path is not a valid git repository: {repo_path}",
PayuGitWarning
)
if catch_error:
return None
Expand All @@ -41,7 +46,7 @@ def _get_git_repository(repo_path: Union[Path, str],
def get_git_branch(repo_path: Union[Path, str]) -> Optional[str]:
"""Return the current git branch or None if repository path is not a git
repository"""
repo = _get_git_repository(repo_path, catch_error=True)
repo = get_git_repository(repo_path, catch_error=True)
if repo:
return str(repo.active_branch)

Expand All @@ -51,7 +56,7 @@ def get_git_user_info(repo_path: Union[Path, str],
example_value: str) -> Optional[str]:
"""Return git config user info, None otherwise. Used for retrieving
name and email saved in git"""
repo = _get_git_repository(repo_path, catch_error=True)
repo = get_git_repository(repo_path, catch_error=True)
if repo is None:
return

Expand All @@ -72,7 +77,7 @@ def git_commit(repo_path: Union[Path, str],
"""Add a git commit of changes to paths"""
# Get/Create git repository - initialise is true as adding a commit
# directly after
repo = _get_git_repository(repo_path, initialise=True)
repo = get_git_repository(repo_path, initialise=True)

# Un-stage any pre-existing changes
repo.index.reset()
Expand All @@ -82,7 +87,7 @@ def git_commit(repo_path: Union[Path, str],
untracked_files = [Path(repo_path) / path for path in repo.untracked_files]
for path in paths_to_commit:
if repo.git.diff(None, path) or path in untracked_files:
repo.index.add(paths_to_commit)
repo.index.add([path])
changes = True

# Run commit if there's changes
Expand Down Expand Up @@ -112,7 +117,7 @@ def git_checkout_branch(repo_path: Union[Path, str],
start_point: Optional[str] = None) -> None:
"""Checkout branch and create branch if specified"""
# Get git repository
repo = _get_git_repository(repo_path)
repo = get_git_repository(repo_path)

# Existing branches
local_branches = list_local_branches(repo)
Expand Down
81 changes: 41 additions & 40 deletions payu/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,34 @@
from payu.laboratory import Laboratory
from payu.git_utils import get_git_branch, get_git_user_info, git_commit

# Short uuid is used for experiment names (for work and archive directories)
SHORT_UUID_LENGTH = 7
# A truncated uuid is used for branch-uuid aware experiment names
TRUNCATED_UUID_LENGTH = 5
METADATA_FILENAME = 'metadata.yaml'

USAGE_HELP = """
If this is a new experiment, either:
- Create a new git branch, by running:
payu checkout -b NEW_BRANCH_NAME
where NEW_BRANCH_NAME is name of the new branch
- Or generate a new experiment uuid on the current git branch, by running:
payu uuid
Both of the above will generate a new uuid, a branch-uuid aware experiment
name, and update and commit changes to the metadata file.
Note: Experiment names will be of the format:
{CONTROL_DIR}-{BRANCH_NAME}-{SHORTENED_UUID}
If this an older experiment, or if wanting to opt out of branch-uuid aware
experiment names, run:
payu uuid --legacy
This will generate a new uuid, set the experiment name to be the name of
the control directory (default) or the set 'experiment' value in the
configuration file. This command will also update and commit changes to the
If this is a new experiment, create a new git branch by running:
payu checkout -b NEW_BRANCH_NAME
where NEW_BRANCH_NAME is name of the new branch. This will generate a new
uuid, a branch-uuid aware experiment name and commit changes to the
metadata file.
Alternatively to generate a new uuid or experiment name on the current git
branch at the next payu setup or run command, remove the pre-existing uuid or
experiment name from the metadata file.
Note: Experiment names are the name used for work and archive directories
in the laboratory directory.
"""


class ExperimentMetadataError(Exception):
"""Class for metadata processing exceptions"""
"""Class for experiment name exceptions"""
def __init__(self, message="Invalid experiment name in metadata"):
super().__init__(message)
print(USAGE_HELP)

class MetadataWarning(Warning):
pass

class Metadata:
"""
Expand Down Expand Up @@ -83,8 +79,6 @@ def __init__(self,
self.control_path = control_path
self.filepath = self.control_path / METADATA_FILENAME

if branch is None:
branch = get_git_branch(control_path)
self.branch = branch

self.base_experiment_name = self.config.get('experiment',
Expand All @@ -104,13 +98,16 @@ def read_file(self) -> CommentedMap:
return metadata

def setup(self) -> None:
"""To be run at experiment initialisation"""
"""Create/update metadata if no uuid or experiment name, otherwise run
checks on existing metadata"""
if self.uuid is None:
warnings.warn("No experiment uuid found. Generating a new uuid")
warnings.warn("No experiment uuid found in metadata. "
"Generating a new uuid", MetadataWarning)
self.update_metadata()
elif self.experiment_name is None:
# Add an experiment name back into metadata
warnings.warn("No experiment name found in metadata")
warnings.warn("No experiment name found in metadata. "
"Generating a new experiment name.", MetadataWarning)
self.update_metadata(set_only_experiment_name=True)

self.check_experiment_name()
Expand All @@ -123,11 +120,12 @@ def update_metadata(self, set_only_experiment_name: bool = False) -> None:

if archive_path.exists():
warnings.warn(
f"Pre-existing archive found at: {archive_path}"
f"Experiment name will remain: {self.base_experiment_name}"
f"Pre-existing archive found at: {archive_path}. "
f"Experiment name will remain: {self.base_experiment_name}",
MetadataWarning
)
if set_only_experiment_name:
self.base_experiment_name = self.base_experiment_name
self.set_new_experiment_name(legacy=True)
else:
self.set_new_uuid(legacy=True)
else:
Expand All @@ -141,10 +139,9 @@ def update_metadata(self, set_only_experiment_name: bool = False) -> None:

def check_experiment_name(self) -> None:
"""Check experiment name in metadata file"""
truncated_uuid = self.uuid[:SHORT_UUID_LENGTH]
truncated_uuid = self.uuid[:TRUNCATED_UUID_LENGTH]
if self.experiment_name.endswith(truncated_uuid):
# Check whether on the same branch or control directory as
# using the experiment name in metadata.yaml
# Branch-uuid aware experiment name
metadata_experiment = self.experiment_name
self.set_new_experiment_name()
if self.experiment_name != metadata_experiment:
Expand All @@ -153,40 +150,44 @@ def check_experiment_name(self) -> None:
"configured 'experiment' value has changed.\n"
f"Experiment name in {METADATA_FILENAME}: "
f"{metadata_experiment}\nGenerated experiment name: "
f"{self.experiment_name}."
f"{self.experiment_name}.",
MetadataWarning
)
raise ExperimentMetadataError()
else:
# Legacy experiment name: Check metadata's experiment name matches
# base experiment name
# Legacy experiment name
if self.experiment_name != self.base_experiment_name:
msg = f"Experiment name in {METADATA_FILENAME} does not match"
if 'experiment' in self.config:
msg += " the configured 'experiment' value."
else:
msg += " the control directory base name."
warnings.warn(msg + f"{self.experiment_name} does not equal "
"{self.base_experiment_name}")
"{self.base_experiment_name}",
MetadataWarning)
raise ExperimentMetadataError()

def set_new_experiment_name(self, legacy=False) -> None:
"""Set a new experiment name - this the name used work
and archive directories"""
def set_new_experiment_name(self, legacy: bool = False) -> None:
"""Set a new experiment name - this is used for work and archive
directories"""
if legacy:
# Experiment remains base experiment name
self.experiment_name = self.base_experiment_name
return

if self.branch is None:
self.branch = get_git_branch(self.control_path)

# Add branch and a truncated uuid to experiment name
truncated_uuid = self.uuid[:SHORT_UUID_LENGTH]
truncated_uuid = self.uuid[:TRUNCATED_UUID_LENGTH]
if self.branch is None or self.branch in ('main', 'master'):
suffix = f'-{truncated_uuid}'
else:
suffix = f'-{self.branch}-{truncated_uuid}'

self.experiment_name = self.base_experiment_name + suffix

def set_new_uuid(self, legacy=False) -> None:
def set_new_uuid(self, legacy: bool = False) -> None:
"""Create a new uuid and set experiment name"""
# Generate new uuid and experiment name
self.uuid = generate_uuid()
Expand Down
22 changes: 5 additions & 17 deletions payu/subcommands/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@
'action': 'store_true',
'default': False,
'dest': 'keep_uuid',
'help': 'If the cloned experiment uuid exists, leave it \
unchanged'
'help': 'If an experiment uuid exists, leave it unchanged'
}
}

Expand Down Expand Up @@ -223,7 +222,7 @@
}


# Branch start restart
# Branch starting restart
restart_path = {
'flags': ('--restart', '-r'),
'parameters': {
Expand All @@ -244,24 +243,13 @@
}
}

# Legacy experiment
legacy_experiment = {
'flags': ['--legacy'],
'parameters': {
'dest': 'legacy_experiment',
'action': 'store_true',
'default': False,
'help': 'Flag to opt out of branch-uuid aware experiment names'
}
}

# List branches - verbose
# List branches verbose flag
verbose = {
'flags': ['--verbose', '-v'],
'parameters': {
'dest': 'verbose',
'action': 'store_true',
'default': False,
'help': 'Flag to display all contents of metadata file'
'help': 'Display all contents of metadata file'
}
}
}
Loading

0 comments on commit d5f49c5

Please sign in to comment.