#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Command line interface for ms3. This module includes the argument parsers for the various commands and has one function
<command>_cmd() per <command> which takes the parsed arguments namespace and dispatches them to the appropriate function
in the ms3.operations module, using a couple of helper functions to that aim.
"""
import argparse
import logging
import os
import re
from collections import defaultdict
from typing import List, Literal, Optional, overload
from ms3 import (
Parse,
compute_path_from_file,
convert_from_metadata_tsv,
get_git_repo,
get_git_version_info,
make_coloring_reports_and_warnings,
)
from ms3._version import __version__
from ms3.logger import get_logger, inspect_loggers
from ms3.operations import (
check,
compare,
extract,
insert_labels_into_score,
store_scores,
transform_to_package,
transform_to_resources,
update,
)
from ms3.utils import (
capture_parse_logs,
convert_folder,
resolve_dir,
write_warnings_to_file,
)
__author__ = "johentsch"
__copyright__ = "École Polytechnique Fédérale de Lausanne"
__license__ = "gpl3"
[docs]
def add_cmd(args, parse_obj: Optional[Parse] = None) -> Parse:
logger = get_logger("ms3.add", level=args.level)
if parse_obj is None:
p = make_parse_obj(args)
else:
p = parse_obj
insert_labels_into_score(
p,
facet=args.use,
ask_for_input=args.ask,
replace=args.replace,
)
corpus2paths = store_scores(
p, root_dir=args.out, folder=".", simulate=args.test, suffix=args.suffix
)
changed = sum(map(len, corpus2paths.values()))
logger.info(
f"Operation resulted in {changed} annotated score{'s' if changed != 1 else ''}."
)
[docs]
def check_cmd(args, parse_obj: Optional[Parse] = None) -> Parse:
if parse_obj is None:
p = make_parse_obj(args)
else:
p = parse_obj
_ = check(
p,
ignore_labels=args.ignore_labels,
ignore_scores=args.ignore_scores,
assertion=args.fail,
ignore_metronome=args.ignore_metronome,
)
return p
[docs]
def compare_cmd(args, parse_obj=None, output: bool = True, logger=None) -> None:
if logger is None:
logger = get_logger("ms3.compare", level=args.level)
if parse_obj is None:
p = make_parse_obj(args)
else:
p = parse_obj
revision = None if args.compare == "" else args.compare
try:
n_changed, n_unchanged = compare(
p,
facet=args.use,
ask=args.ask,
revision_specifier=revision,
flip=args.flip,
force=args.force,
logger=p.logger,
)
except TypeError:
logger.error(
"No files to compare. Try adding the flag -a to parse files even if they are not included in a "
"metdata.tsv file."
)
return
logger.debug(
f"{n_changed} files changed labels during comparison, {n_unchanged} didn't."
)
if output:
corpus2paths = store_scores(
p,
only_changed=not args.force,
root_dir=args.out,
simulate=args.test,
suffix=args.suffix,
)
changed = sum(map(len, corpus2paths.values()))
logger.info(
f"Operation resulted in {changed} comparison file{'s' if changed != 1 else ''}."
)
else:
logger.info(
f"Operation resulted in colored labels within {n_changed} score{'s' if n_changed != 1 else ''}."
)
[docs]
def convert_cmd(args):
# assert target[:len(
# dir)] != dir, "TARGET_DIR cannot be identical with nor a subfolder of DIR.\nDIR: " + dir +
# '\nTARGET_DIR: ' + target
update_logger = get_logger("ms3.convert", level=args.level)
for argument_name, argument, default in (
("-e/--exclude", args.exclude, None),
("-f/--folders", args.folders, None),
("--reviewed", args.reviewed, False),
("-t/--test", args.test, False),
("-v/--verbose", args.verbose, False),
):
if argument != default:
update_logger.info(
f"Argument '{argument_name}' is currently being ignored."
)
out_dir = "." if args.out is None else args.out
ms = "auto" if args.musescore is None else args.musescore
if args.all:
convert_folder(
directory=args.dir,
file_paths=args.files,
target_dir=out_dir,
extensions=args.extensions,
target_extension=args.format,
regex=args.include,
suffix=args.suffix,
recursive=not args.nonrecursive,
ms=ms,
overwrite=args.safe,
parallel=not args.iterative,
logger=update_logger,
)
else:
convert_from_metadata_tsv(
directory=args.dir,
file_paths=args.files,
target_dir=out_dir,
extensions=args.extensions,
target_extension=args.format,
regex=args.include,
suffix=args.suffix,
recursive=not args.nonrecursive,
ms=ms,
overwrite=args.safe,
parallel=not args.iterative,
logger=update_logger,
)
[docs]
def empty(args, parse_obj: Optional[Parse] = None):
if parse_obj is None:
p = make_parse_obj(args, parse_scores=True)
else:
p = parse_obj
p.detach_labels()
p.store_parsed_scores(
only_changed=True,
root_dir=args.out,
suffix=args.suffix,
overwrite=True,
)
# def repair(args):
# print("Sorry, the command has not been implemented yet.")
# print(args.dir)
[docs]
def update_cmd(args, parse_obj: Optional[Parse] = None):
if parse_obj is None:
p = make_parse_obj(args, parse_scores=True)
else:
p = parse_obj
changed_paths = update(
p,
root_dir=args.out,
suffix="" if args.suffix is None else args.suffix,
overwrite=True,
staff=int(args.staff),
harmony_layer=int(args.type),
above=args.above,
safe=args.safe,
)
if len(changed_paths) > 0:
print(f"Paths of scores with updated labels:\n{changed_paths}")
else:
print("Nothing to do.")
[docs]
def check_and_create(d, resolve=True):
"""Turn input into an existing, absolute directory path."""
d_resolved = resolve_dir(os.path.join(os.getcwd(), d))
if os.path.isfile(d_resolved):
raise ValueError(f"Expected directory but got existing filepath {d_resolved!r}")
if not os.path.isdir(d):
if not os.path.isdir(d_resolved):
if input(d_resolved + " does not exist. Create? (y|n)") == "y":
os.mkdir(d_resolved)
else:
raise argparse.ArgumentTypeError(
d + " needs to be an existing directory"
)
return d_resolved if resolve else d
[docs]
def check_and_create_unresolved(d):
return check_and_create(d, resolve=False)
[docs]
def check_dir(d):
if not os.path.isdir(d):
d = resolve_dir(os.path.join(os.getcwd(), d))
if not os.path.isdir(d):
raise argparse.ArgumentTypeError(d + " needs to be an existing directory")
return resolve_dir(d)
[docs]
def precommit_cmd(
args,
parse_obj: Optional[Parse] = None,
) -> bool:
"""Like ms3 review but also adds the resulting files to the Git index."""
logger = get_logger("ms3.precommit", level=args.level)
if args.files:
raise NotImplementedError(
"The --files argument should not be used for ms3 precommit which takes files as "
"positional arguments."
)
args.files = args.positional_args # in the future, maybe use args.include instead
test_passes = review_cmd(args, parse_obj=parse_obj, wrapped_by_precommit=True)
repo = get_git_repo(args.dir)
if repo is not None:
repo.git.add(all=True)
deliver_test_result(test_passes, args.fail, logger)
@overload
def review_cmd(args, parse_obj, wrapped_by_precommit: Literal[False]) -> None: ...
@overload
def review_cmd(args, parse_obj, wrapped_by_precommit: Literal[True]) -> bool: ...
[docs]
def review_cmd(
args,
parse_obj: Optional[Parse] = None,
wrapped_by_precommit: bool = False,
) -> Optional[bool]:
"""This command combines the functionalities check-extract-compare and additionally colors non-chord tones."""
logger = get_logger("ms3.review", level=args.level)
if parse_obj is None:
p = make_parse_obj(args)
else:
p = parse_obj
accumulated_warnings = []
# call ms3 check, collecting all warnings issued while parsing
if args.ignore_scores + args.ignore_labels < 2:
what = "" if args.ignore_scores else "SCORES"
if args.ignore_labels:
what += "LABELS" if what == "" else "AND LABELS"
print(f"CHECKING {what}...")
warning_strings = check(
p,
ignore_labels=args.ignore_labels,
ignore_scores=args.ignore_scores,
assertion=False,
parallel=False,
ignore_metronome=args.ignore_metronome,
)
accumulated_warnings.extend(warning_strings)
test_passes = len(warning_strings) == 0
p.parse_tsv()
else:
test_passes = True
p.parse(parallel=False)
if p.n_parsed_scores == 0:
msg = "NO SCORES PARSED, NOTHING TO DO."
if not args.all:
msg += (
"\nI was disregarding all files whose file names are not listed in the column 'piece' of a "
"'metadata.tsv' file. "
"Add -a if you want me to include all scores."
)
print(msg)
return
# call ms3 extract
params = gather_extract_params(args)
if len(params) > 0:
print("EXTRACTING FACETS...")
extract_cmd(args, p)
# color out-of-label tones
print("COLORING OUT-OF-LABEL TONES...")
with capture_parse_logs(p.logger) as captured_warnings:
test_passes = (
make_coloring_reports_and_warnings(
p, out_dir=args.out, threshold=args.threshold
)
and test_passes
)
accumulated_warnings.extend(captured_warnings.content_list)
# attribute warnings to pieces based on logger names
logger2pieceID = {
corpus.logger_names[piece]: (c, piece)
for c, corpus in p.corpus_objects.items()
for piece in corpus.keys()
}
piece2warnings = defaultdict(list)
for warning_strings in accumulated_warnings:
warning_lines = warning_strings.splitlines()
first_line = warning_lines[0]
match = re.search(r"(ms3\.Parse\..+) --", first_line)
if match is None:
logger.warning(
f"This warning contains no ms3 logger name, skipping: {warning_strings}"
)
continue
warning_lines[0] = first_line[
: match.end(1)
] # cut off the warning's header everything following the logger
# name because paths to source code are system-dependent
pieceID = logger2pieceID[match.group(1)]
piece2warnings[pieceID].append("\n".join(warning_lines))
# write all warnings to piece-specific warnings files and remove existing files where no warnings were captured
for pieceID, score_files in p.get_files(
"scores", unparsed=False, flat=True, include_empty=False
).items():
file = score_files[0]
warnings_path = compute_path_from_file(
file, root_dir=args.out, folder="reviewed"
)
warnings_file = os.path.join(
warnings_path, file.piece + file.suffix + ".warnings"
)
write_warnings_to_file(
warnings_file=warnings_file, warnings=piece2warnings[pieceID], logger=logger
)
# call ms3 compare
if args.compare is not None:
revision = None if args.compare == "" else args.compare
commit_str = "" if revision is None else f" @ {revision}"
expanded = "EXPANDED " if args.use == "expanded" else ""
print(
f"COMPARING CURRENT {expanded}LABELS WITH PREVIOUS ONES FROM THE TSV FILES{commit_str}..."
)
n_changed, n_unchanged = compare(
p,
facet=args.use,
ask=args.ask,
revision_specifier=revision,
flip=args.flip,
force=True, # metadata be updated even if no diffs found because reviewed scores are written either way
logger=logger,
)
logger.debug(
f"{n_changed} files changed labels during comparison, {n_unchanged} didn't."
)
corpus2paths = store_scores(
p, only_changed=not args.force, root_dir=args.out, simulate=args.test
)
changed = sum(map(len, corpus2paths.values()))
logger.info(
f"Operation resulted in {changed} review file{'s' if changed != 1 else ''}."
)
out_dir = "." if args.out is None else args.out
warnings_file = os.path.join(out_dir, "warnings.log")
if os.path.isfile(warnings_file):
logger.info(f"Removing deprecated warnings file: {warnings_file}")
os.remove(warnings_file)
if wrapped_by_precommit:
return test_passes
deliver_test_result(test_passes, args.fail, logger)
[docs]
def deliver_test_result(
test_passes: bool, raise_on_fail: bool, logger: Optional[logging.Logger]
):
def output_text(msg):
if logger is not None:
print(msg)
else:
logger.info(msg)
if test_passes:
output_text("Parsed scores passed all tests.")
else:
msg = "Not all tests have passed. Please check the relevant warnings in the generated .warnings files."
if raise_on_fail:
assert test_passes, msg
else:
output_text(msg)
[docs]
def make_parse_obj(args, parse_scores=False, parse_tsv=False, facets=None):
labels_cfg = {}
if hasattr(args, "positioning"):
labels_cfg["positioning"] = args.positioning
if hasattr(args, "raw"):
labels_cfg["decode"] = args.raw
logger_cfg = {
"level": args.level,
"path": args.log,
}
ms = args.musescore if hasattr(args, "musescore") else None
file_re_str = None if args.include is None else f"'{args.include}'"
folder_re_str = None if args.folders is None else f"'{args.folders}'"
exclude_re_str = None if args.exclude is None else f"'{args.exclude}'"
ms_str = None if ms is None else f"'{ms}'"
print(
f"""CREATING PARSE OBJECT WITH THE FOLLOWING PARAMETERS:
Parse('{args.dir}',
recursive={not args.nonrecursive},
only_metadata_pieces={not args.all},
include_convertible={ms is not None},
exclude_review={not args.reviewed},
file_re={file_re_str},
folder_re={folder_re_str},
exclude_re={exclude_re_str},
file_paths={args.files},
labels_cfg={labels_cfg},
ms={ms_str},
**{logger_cfg})
"""
)
parse_obj = Parse(
args.dir,
recursive=not args.nonrecursive,
only_metadata_pieces=not args.all,
include_convertible=ms is not None,
exclude_review=not args.reviewed,
file_re=args.include,
folder_re=args.folders,
exclude_re=args.exclude,
file_paths=args.files,
labels_cfg=labels_cfg,
ms=ms,
**logger_cfg,
)
if facets is not None:
facets = [f"{f}$" for f in facets if f != "metadata"]
if len(facets) > 0:
parse_obj.view.include("facets", *facets)
if parse_scores:
mode = "ONE AFTER THE OTHER" if args.iterative else "IN PARALLEL"
print(f"PARSING SCORES {mode}...")
parse_obj.parse_scores(parallel=not args.iterative)
if parse_tsv and facets != []:
print("PARSING TSV FILES...")
parse_obj.parse_tsv()
info_str = parse_obj.info(show_discarded=args.verbose, return_str=True).replace(
"\n", "\n\t"
)
print(f"RESULTING PARSE OBJECT:\n\t{info_str}")
if args.verbose:
print(inspect_loggers())
return parse_obj
[docs]
def get_arg_parser():
# reusable argument sets
parse_args = argparse.ArgumentParser(add_help=False)
parse_args.add_argument(
"-d",
"--dir",
metavar="DIR",
default=os.getcwd(),
type=check_dir,
help="Folder(s) that will be scanned for input files. Defaults to current working directory if no individual "
"files are passed via -f.",
)
parse_args.add_argument(
"-o",
"--out",
metavar="OUT_DIR",
type=check_and_create_unresolved,
help="Output directory. For conversion, an absolute path will result in a copy of the original sub-folder "
"structure, whereas a relative path will contain all converted files next to each other.",
)
parse_args.add_argument(
"-n",
"--nonrecursive",
action="store_true",
help="Treat DIR as single corpus even if it contains corpus directories itself.",
)
parse_args.add_argument(
"-a",
"--all",
action="store_true",
help="By default, only files listed in the 'piece' column of a 'metadata.tsv' file are parsed. With "
"this option, all files will be parsed.",
)
parse_args.add_argument(
"-i",
"--include",
metavar="REGEX",
help="Select only files whose names include this string or regular expression.",
)
parse_args.add_argument(
"-e",
"--exclude",
metavar="REGEX",
help="Any files or folders (and their subfolders) including this regex will be disregarded."
"By default, files including '_reviewed' or starting with . or _ or 'concatenated' are excluded.",
)
parse_args.add_argument(
"-f",
"--folders",
metavar="REGEX",
help="Select only folders whose names include this string or regular expression.",
)
parse_args.add_argument(
"-m",
"--musescore",
metavar="PATH",
nargs="?",
const="auto",
help="""Command or path of your MuseScore 3 executable. -m by itself will set 'auto' (attempt to use standard
path for your system).
Other shortcuts are -m win, -m mac, and -m mscore (for Linux).""",
)
parse_args.add_argument(
"--reviewed",
action="store_true",
help="By default, review files and folder are excluded from parsing. With this option, "
"they will be included, too.",
)
parse_args.add_argument(
"--files",
metavar="PATHs",
nargs="+",
help="(Deprecated) The paths are expected to be within DIR. They will be converted into a view "
"that includes only the indicated files. This is equivalent to specifying the file names as "
"a regex via --include (assuming that file names are unique amongst corpora.",
)
parse_args.add_argument(
"--iterative",
action="store_true",
help="Do not use all available CPU cores in parallel to speed up batch jobs.",
)
parse_args.add_argument(
"-l",
"--level",
metavar="{c, e, w, i, d}",
default="i",
help="Choose how many log messages you want to see: c (none), e, w, i, d (maximum)",
)
parse_args.add_argument(
"--log",
nargs="?",
const=".",
help="Can be a file path or directory path. Relative paths are interpreted relative to the current directory.",
)
parse_args.add_argument(
"-t", "--test", action="store_true", help="No data is written to disk."
)
parse_args.add_argument(
"-v",
"--verbose",
action="store_true",
help="Show more output such as files discarded from parsing.",
)
check_args = argparse.ArgumentParser(add_help=False)
check_args.add_argument(
"--ignore_scores",
action="store_true",
help="Don't check scores for encoding errors.",
)
check_args.add_argument(
"--ignore_labels",
action="store_true",
help="Don't check DCML labels for syntactic correctness.",
)
check_args.add_argument(
"--fail",
action="store_true",
help="If you pass this argument the process will deliberately fail with an AssertionError "
"when there are any mistakes.",
)
check_args.add_argument(
"--ignore_metronome",
action="store_true",
help="Pass this flag if you want the check to pass (not fail) even if there is a warning about a missing "
"metronome mark in the first bar of the score.",
)
compare_args = argparse.ArgumentParser(add_help=False)
compare_args.add_argument(
"--flip",
action="store_true",
help="Pass this flag to treat the annotation tables as if updating the scores instead of the other way around, "
"effectively resulting in a swap of the colors in the output files.",
)
compare_args.add_argument(
"--safe", action="store_false", help="Don't overwrite existing files."
)
compare_args.add_argument(
"--force",
action="store_true",
help="Output comparison files even when no differences are found.",
)
extract_args = argparse.ArgumentParser(add_help=False)
extract_args.add_argument(
"-M",
"--measures",
metavar="folder",
nargs="?",
const="../measures",
help="Folder where to store TSV files with measure information needed for tasks such as unfolding repetitions.",
)
extract_args.add_argument(
"-N",
"--notes",
metavar="folder",
nargs="?",
const="../notes",
help="Folder where to store TSV files with information on all notes.",
)
extract_args.add_argument(
"-R",
"--rests",
metavar="folder",
nargs="?",
const="../rests",
help="Folder where to store TSV files with information on all rests.",
)
extract_args.add_argument(
"-L",
"--labels",
metavar="folder",
nargs="?",
const="../labels",
help="Folder where to store TSV files with information on all annotation labels.",
)
extract_args.add_argument(
"-X",
"--expanded",
metavar="folder",
nargs="?",
const="../harmonies",
help="Folder where to store TSV files with expanded DCML labels.",
)
extract_args.add_argument(
"-F",
"--form_labels",
metavar="folder",
nargs="?",
const="../form_labels",
help="Folder where to store TSV files with all form labels.",
)
extract_args.add_argument(
"-E",
"--events",
metavar="folder",
nargs="?",
const="../events",
help="Folder where to store TSV files with all events (chords, rests, articulation, etc.) without further "
"processing.",
)
extract_args.add_argument(
"-C",
"--chords",
metavar="folder",
nargs="?",
const="../chords",
help="Folder where to store TSV files with <chord> tags, "
"i.e. groups of notes in the same voice with identical onset and duration. "
"The tables include lyrics, dynamics, articulation, staff- and system texts, tempo marking, spanners, "
"and thoroughbass figures.",
)
extract_args.add_argument(
"-J",
"--joined_chords",
metavar="folder",
nargs="?",
const="../chords",
help="Like -C except that all Chords are substituted with the actual Notes they contain. This is useful, "
"for example, "
"for relating slurs to the notes they group, or bass figures to their bass notes. ",
)
extract_args.add_argument(
"-D",
"--metadata",
metavar="suffix",
nargs="?",
const="",
help="Set -D to update the 'metadata.tsv' files of the respective corpora with the parsed scores. "
"Add a suffix if you want to update 'metadata{suffix}.tsv' instead.",
)
extract_args.add_argument(
"-p",
"--positioning",
action="store_true",
help="When extracting labels, include manually shifted position coordinates in order to restore them when "
"re-inserting.",
)
extract_args.add_argument(
"--raw",
action="store_false",
help="When extracting labels, leave chord symbols encoded instead of turning them into a single column of "
"strings.",
)
extract_args.add_argument(
"-u",
"--unfold",
action="store_true",
help="Unfold the repeats for all stored DataFrames.",
)
extract_args.add_argument(
"--interval_index",
action="store_true",
help="Prepend a column with [start, end) intervals to the TSV files.",
)
extract_args.add_argument(
"--corpuswise",
action="store_true",
help="Parse one corpus after the other rather than all at once.",
)
select_facet_args = argparse.ArgumentParser(add_help=False)
select_facet_args.add_argument(
"--ask",
action="store_true",
help="If several files are available for the selected facet (default: 'expanded', see --use), I will pick one "
"automatically. Add --ask if you want me to have you select which ones to compare with the scores.",
)
select_facet_args.add_argument(
"--use",
default="expanded",
choices=["expanded", "labels"],
help="Which type of labels you want to compare with the ones in the score. Defaults to 'expanded', "
"i.e., DCML labels. Set --use labels to use other labels available as TSV and set --ask if several "
"sets of labels are available that you want to choose from.",
)
review_args = argparse.ArgumentParser(
add_help=False,
parents=[check_args, select_facet_args, compare_args, extract_args, parse_args],
)
review_args.add_argument(
"-c",
"--compare",
nargs="?",
metavar="GIT_REVISION",
const="",
help="Pass -c if you want the _reviewed file to display removed labels in red and added labels in green, "
"compared to the version currently represented in the present TSV files, if any. If instead you want a "
"comparison with the TSV files from another Git commit, additionally pass its specifier, e.g. 'HEAD~3', "
"<branch-name>, <commit SHA> etc. LATEST_VERSION is accepted as a revision specifier and will result in "
"a comparison with the TSV files at the tag with the highest version number (falling back to HEAD if no "
"tags have been assigned to the repository.",
)
review_args.add_argument(
"--threshold",
default=0.6,
type=float,
help="Harmony segments where the ratio of non-chord tones vs. chord tones lies above this threshold "
"will be printed in a warning and will cause the check to fail if the --fail flag is set. Defaults to "
"0.6 (3:2).",
)
# main argument parser
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
--------------------------
| Welcome to ms3 parsing |
--------------------------
The library offers you the following commands. Add the flag -h to one of them to learn about its parameters.
""",
)
parser.add_argument("--version", action="version", version=__version__)
subparsers = parser.add_subparsers(
help="The action that you want to perform.", dest="action"
)
add_parser = subparsers.add_parser(
"add",
help="Add labels from annotation tables to scores.",
parents=[select_facet_args, parse_args],
)
# TODO: staff parameter needs to accept one to several integers including negative
# add_parser.add_argument('-s', '--staff',
# help="Remove labels from selected staves only. 1=upper staff; -1=lowest staff (
# default)")
# add_parser.add_argument('--type', default=1,
# help="Only remove particular types of harmony labels.")
add_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
default="_annotated",
help="Suffix of the new scores with inserted labels. Defaults to _annotated.",
)
add_parser.add_argument(
"--replace",
action="store_true",
help="Remove existing labels from the scores prior to adding. Like calling ms3 empty first.",
)
add_parser.set_defaults(func=add_cmd)
check_parser = subparsers.add_parser(
"check",
help="""Parse MSCX files and look for errors.
In particular, check DCML harmony labels for syntactic correctness.""",
parents=[check_args, parse_args],
)
check_parser.set_defaults(func=check_cmd)
compare_parser = subparsers.add_parser(
"compare",
help="For MSCX files for which annotation tables exist, create another MSCX file with a coloured label "
"comparison if differences are found.",
parents=[select_facet_args, compare_args, parse_args],
)
compare_parser.add_argument(
"-c",
"--compare",
metavar="GIT_REVISION",
default="",
help="By default, the _reviewed file displays removed labels in red and added labels in green, compared to "
"the version currently represented in the present TSV files, if any. If instead you want a "
"comparison with the TSV files from another Git commit, additionally pass its specifier, e.g. 'HEAD~3', "
"<branch-name>, <commit SHA> etc. LATEST_VERSION is accepted as a revision specifier and will result in "
"a comparison with the TSV files at the tag with the highest version number (falling back to HEAD if no "
"tags have been assigned to the repository.",
)
compare_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
default="_compared",
help="Suffix of the newly created comparison files. Defaults to _compared",
)
compare_parser.set_defaults(func=compare_cmd)
convert_parser = subparsers.add_parser(
"convert",
help="Use your local install of MuseScore to convert MuseScore files.",
parents=[parse_args],
)
convert_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
default="",
help="Suffix of the converted files. Defaults to " ".",
)
convert_parser.add_argument(
"--format",
default="mscx",
help="Output format of converted files. Defaults to mscx. Other options are "
"{png, svg, pdf, mscz, wav, mp3, flac, ogg, musicxml, mxl, mid}",
)
convert_parser.add_argument(
"--extensions",
nargs="+",
default=["mscx", "mscz"],
help="Those file extensions that you want to be converted, separated by spaces. Defaults to mscx mscz",
)
convert_parser.add_argument(
"--safe", action="store_false", help="Don't overwrite existing files."
)
convert_parser.set_defaults(func=convert_cmd)
empty_parser = subparsers.add_parser(
"empty",
help="Remove harmony annotations and store the MuseScore files without them.",
parents=[parse_args],
)
# TODO: staff parameter needs to accept one to several integers including negative
# empty_parser.add_argument('-s', '--staff',
# help="Remove labels from selected staves only. 1=upper staff; -1=lowest staff (
# default)")
# empty_parser.add_argument('--type', default=1,
# help="Only remove particular types of harmony labels.")
empty_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
default="_clean",
help="Suffix of the new scores with removed labels. Defaults to _clean.",
)
empty_parser.set_defaults(func=empty)
extract_parser = subparsers.add_parser(
"extract",
help="Extract selected information from MuseScore files and store it in TSV files.",
parents=[extract_args, parse_args],
)
extract_parser.set_defaults(func=extract_cmd)
metadata_parser = subparsers.add_parser(
"metadata",
help="Update MSCX files with changes made to metadata.tsv (created via ms3 extract -D [-a]).",
parents=[parse_args],
)
metadata_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
help="Suffix of the new scores with updated metadata fields.",
)
metadata_parser.add_argument(
"-p",
"--prelims",
action="store_true",
help="Pass this flag if, in addition to updating metadata fields, you also want score headers to be updated "
"from the columns title_text, subtitle_text, composer_text, lyricist_text, part_name_text.",
)
metadata_parser.add_argument(
"--instrumentation",
action="store_true",
help="Pass this flag to update the score's instrumentation based on changed values from "
"'staff_<i>_instrument' columns.",
)
metadata_parser.add_argument(
"--empty",
action="store_true",
help="Set this flag to also allow empty values to be used for overwriting existing ones.",
)
metadata_parser.add_argument(
"--remove",
action="store_true",
help="Set this flag to remove non-default metadata fields that are not columns in the metadata.tsv file "
"anymore.",
)
metadata_parser.set_defaults(func=metadata)
#
# repair_parser = subparsers.add_parser('repair',
# help="Apply automatic repairs to your uncompressed MuseScore files.",
# parents=[parse_args])
# repair_parser.set_defaults(func=repair)
review_parser = subparsers.add_parser(
"review",
help="Extract facets, check labels, and create _reviewed files.",
parents=[review_args],
)
review_parser.set_defaults(func=review_cmd)
transform_parser = subparsers.add_parser(
"transform",
help="Concatenate and transform TSV data from one or several corpora. "
"Available transformations are unfolding repeats and adding an interval index.",
parents=[parse_args],
)
transform_parser.add_argument(
"-M",
"--measures",
action="store_true",
help="Concatenate measures TSVs for all selected pieces.",
)
transform_parser.add_argument(
"-N",
"--notes",
action="store_true",
help="Concatenate notes TSVs for all selected pieces.",
)
transform_parser.add_argument(
"-R",
"--rests",
action="store_true",
help="Concatenate rests TSVs for all selected pieces (use ms3 extract -R to create those).",
)
transform_parser.add_argument(
"-L",
"--labels",
action="store_true",
help="Concatenate raw harmony label TSVs for all selected pieces (use ms3 extract -L to create those).",
)
transform_parser.add_argument(
"-X",
"--expanded",
action="store_true",
help="Concatenate expanded DCML label TSVs for all selected pieces.",
)
transform_parser.add_argument(
"-F",
"--form_labels",
metavar="folder",
nargs="?",
const="../form_labels",
help="Concatenate form label TSVs for all selected pieces.",
)
transform_parser.add_argument(
"-E",
"--events",
action="store_true",
help="Concatenate events TSVs (notes, rests, articulation, etc.) for all selected pieces (use ms3 extract -E "
"to create those).",
)
transform_parser.add_argument(
"-C",
"--chords",
action="store_true",
help="Concatenate chords TSVs (<chord> tags group notes in the same voice with identical onset and duration) "
"including "
"lyrics, dynamics, articulation, staff- and system texts, tempo marking, spanners, and thoroughbass "
"figures, "
"for all selected pieces (use ms3 extract -C to create those).",
)
transform_parser.add_argument(
"-D",
"--metadata",
action="store_true",
help="Output 'concatenated_metadata.tsv' with one row per selected piece.",
)
transform_parser.add_argument(
"-s",
"--suffix",
nargs="*",
metavar="SUFFIX",
help="Pass -s to use standard suffixes or -s SUFFIX to choose your own. In the latter case they will be "
"assigned to the extracted aspects in the order "
"in which they are listed above (capital letter arguments).",
)
transform_parser.add_argument(
"-u",
"--unfold",
action="store_true",
help="Unfold the repeats for all concatenated DataFrames.",
)
transform_parser.add_argument(
"--interval_index",
action="store_true",
help="Prepend a column with [start, end) intervals to the TSV files.",
)
transform_parser.add_argument(
"--resources",
action="store_true",
help="Store the concatenated DataFrames as TSV files with resource descriptors rather than in a ZIP with a "
"package descriptor.",
)
transform_parser.add_argument(
"--safe", action="store_false", help="Don't overwrite existing files."
)
transform_parser.add_argument(
"--uncompressed",
action="store_true",
help="Store the transformed files as uncompressed TSVs rather than writing them into a ZIP file.",
)
transform_parser.add_argument(
"--dirty",
action="store_true",
help="Allows to override the 'This repository is dirty' blocker.",
)
transform_parser.set_defaults(func=transform_cmd)
update_parser = subparsers.add_parser(
"update",
help="Convert MSCX files to the latest MuseScore version and move all chord annotations "
"to the Roman Numeral Analysis layer. This command overwrites existing files!!!",
parents=[parse_args],
)
# update_parser.add_argument('-a', '--annotations', metavar='PATH', default='../harmonies',
# help='Path relative to the score file(s) where to look for existing annotation
# tables.')
update_parser.add_argument(
"-s",
"--suffix",
metavar="SUFFIX",
help="Add this suffix to the filename of every new file.",
)
update_parser.add_argument(
"--above", action="store_true", help="Display Roman Numerals above the system."
)
update_parser.add_argument(
"--safe",
action="store_true",
help="Only moves labels if their temporal positions stay intact.",
)
update_parser.add_argument(
"--staff",
default=-1,
help="Which staff you want to move the annotations to. 1=upper staff; -1=lowest staff (default)",
)
update_parser.add_argument(
"--type",
default=1,
help="defaults to 1, i.e. moves labels to Roman Numeral layer. Other types have not been tested!",
)
update_parser.set_defaults(func=update_cmd)
precommit_parser = subparsers.add_parser(
"precommit",
help="Like ms3 review but also adds the resulting files to the Git index.",
parents=[review_args],
)
precommit_parser.add_argument(
"positional_args",
metavar="FILE",
help="Shadows the --files argument because pre-commit passes files as positional arguments.",
nargs="+",
)
precommit_parser.set_defaults(func=precommit_cmd)
return parser
[docs]
def run():
parser = get_arg_parser()
args = parser.parse_args()
if "func" not in args:
parser.print_help()
return
if args.files is not None:
args.files = [resolve_dir(path) for path in args.files]
args.func(args)
if __name__ == "__main__":
run()