"""
Configuration.
"""
import configparser
import os
import pathlib
import subprocess
import tempfile
import time
import traceback
import typing
[docs]
class Configuration:
"""
Configuration.
.. container:: example
Behavior at instantiation:
* Looks for ``$HOME/.abjad/``.
* Creates ``$HOME/.abjad/`` if directory does not exist.
* Looks for ``$HOME/.abjad/abjad.cfg``.
* Creates ``$HOME/.abjad/abjad.cfg`` if file does not exist.
* Parses ``$HOME/.abjad/abjad.cfg``.
* Provides default key-value pairs for pairs which fail validation.
* Writes configuration changes to disk.
* Creates Abjad output directory if directory does not exist.
"""
__slots__ = ("_cached_abjad_configuration_directory", "_settings")
_lilypond_version_string: typing.ClassVar[str] = ""
# TODO: move start-up logic somewhere else?
def __init__(self):
self._cached_abjad_configuration_directory = None
if not os.path.exists(str(self.abjad_configuration_directory())):
try:
os.makedirs(str(self.abjad_configuration_directory()))
except (IOError, OSError):
traceback.print_exc()
old_contents = ""
if self.abjad_configuration_file_path().exists():
try:
old_contents = self.abjad_configuration_file_path().read_text()
except AttributeError:
with self.abjad_configuration_file_path().open(mode="r") as f:
old_contents = f.read()
configuration = self._configuration_from_string(old_contents)
configuration = self._validate_configuration(configuration)
new_contents = self._configuration_to_string(configuration)
if not self._compare_configurations(old_contents, new_contents):
try:
with open(
str(self.abjad_configuration_file_path()), "w"
) as file_pointer:
print(new_contents, file=file_pointer)
except (IOError, OSError):
traceback.print_exc()
self._settings = configuration
self._make_missing_directories()
def __eq__(self, argument):
if isinstance(argument, type(self)):
return self._settings == argument._settings
return False
def __getitem__(self, argument) -> typing.Any:
return self._settings.__getitem__(argument)
def _compare_configurations(self, old, new):
old = "\n".join(old.splitlines()[3:])
new = "\n".join(new.splitlines()[3:])
return old == new
def _configuration_from_string(self, string):
if "[main]" not in string:
string = "[main]\n" + string
config_parser = configparser.ConfigParser()
try:
config_parser.read_string(string)
configuration = dict(config_parser["main"].items())
except configparser.ParsingError:
configuration = {}
return configuration
def _configuration_to_string(self, configuration):
option_definitions = self._get_option_definitions()
known_items, unknown_items = [], []
for key, value in sorted(configuration.items()):
if key in option_definitions:
known_items.append((key, value))
else:
unknown_items.append((key, value))
result = []
for line in self._get_initial_comment():
if line:
result.append(f"# {line}")
else:
result.append("")
for key, value in known_items:
result.append("")
if key in option_definitions:
for line in option_definitions[key]["comment"]:
if line:
result.append(f"# {line}")
else:
result.append("")
if value not in ("", None):
result.append(f"{key!s} = {value!s}")
else:
result.append(f"{key!s} =")
if unknown_items:
result.append("")
result.append("# User-specified keys:")
for key, value in unknown_items:
result.append("")
if value not in ("", None):
result.append(f"{key!s} = {value!s}")
else:
result.append(f"{key!s} =")
string = "\n".join(result)
return string
@staticmethod
def _get_home_directory() -> pathlib.Path:
path = (
os.environ.get("HOME")
or os.environ.get("HOMEPATH")
or os.environ.get("APPDATA")
or tempfile.gettempdir()
)
return pathlib.Path(path).absolute()
def _get_initial_comment(self):
current_time = time.strftime("%d %B %Y %H:%M:%S")
return [
f"Abjad configuration file created on {current_time}.",
"This file is interpreted by Python's ConfigParser ",
"and follows ini syntax.",
]
def _get_option_definitions(self):
options = {
"abjad_output_directory": {
"comment": [
"Set to the directory where all Abjad-generated files",
"(such as PDFs and LilyPond files) should be saved.",
"Defaults to $HOME/.abjad/output/",
],
"default": os.path.join(
str(self.abjad_configuration_directory()), "output"
),
"validator": str,
},
"lilypond_path": {
"comment": [
"Lilypond executable path. Set to override dynamic lookup."
],
"default": "lilypond",
"validator": str,
},
"midi_player": {
"comment": [
"MIDI player to open MIDI files.",
"When unset your OS should know how to open MIDI files.",
],
"default": None,
"validator": str,
},
"pdf_viewer": {
"comment": [
"PDF viewer to open PDF files.",
"When unset your OS should know how to open PDFs.",
],
"default": None,
"validator": str,
},
"text_editor": {
"comment": [
"Text editor to edit text files.",
"When unset your OS should know how to open text files.",
],
"default": None,
"validator": str,
},
}
return options
def _make_missing_directories(self):
if not os.path.exists(self.abjad_output_directory()):
try:
os.makedirs(self.abjad_output_directory())
except (IOError, OSError):
traceback.print_exc()
def _validate_configuration(self, configuration):
option_definitions = self._get_option_definitions()
for key in option_definitions:
if key not in configuration:
configuration[key] = option_definitions[key]["default"]
validator = option_definitions[key]["validator"]
if isinstance(validator, type):
if not isinstance(configuration[key], validator):
configuration[key] = option_definitions[key]["default"]
else:
if not validator(configuration[key]):
configuration[key] = option_definitions[key]["default"]
for key in configuration:
if configuration[key] in ("", "None"):
configuration[key] = None
return configuration
[docs]
def abjad_configuration_file_path(self) -> pathlib.Path:
"""
Gets Abjad configuration file path.
"""
return self.abjad_configuration_directory() / "abjad.cfg"
[docs]
def abjad_configuration_directory(self) -> pathlib.Path:
"""
Gets Abjad configuration directory.
"""
if self._cached_abjad_configuration_directory is None:
directory_name = ".abjad"
home_directory = str(self._get_home_directory())
flags = os.W_OK | os.X_OK
if os.access(home_directory, flags):
path = self._get_home_directory() / directory_name
if not path.exists() or (path.exists() and os.access(str(path), flags)):
self._cached_abjad_configuration_directory = path
return self._cached_abjad_configuration_directory
path = pathlib.Path(tempfile.gettempdir()) / directory_name
self._cached_abjad_configuration_directory = path
return self._cached_abjad_configuration_directory
[docs]
def abjad_install_directory(self) -> pathlib.Path:
"""
Gets Abjad install directory.
"""
return pathlib.Path(__file__).parent.parent
[docs]
def abjad_output_directory(self) -> pathlib.Path:
"""
Gets Abjad output directory.
"""
if "abjad_output_directory" in self._settings:
return pathlib.Path(self._settings["abjad_output_directory"])
return self.abjad_configuration_directory() / "output"
[docs]
def lilypond_log_file_path(self) -> pathlib.Path:
"""
Gets LilyPond log file path.
"""
return self.abjad_output_directory() / "lilypond.log"
[docs]
def lilypond_version_string(self) -> str:
"""
Gets LilyPond version string.
"""
if self._lilypond_version_string != "":
return self._lilypond_version_string
command = ["lilypond", "--version"]
proc = subprocess.run(command, stdout=subprocess.PIPE)
assert proc.stdout is not None
lilypond_version_string = proc.stdout.decode().split()[2]
Configuration._lilypond_version_string = lilypond_version_string
return lilypond_version_string