Source code for abjad.configuration

import collections
import configparser
import importlib
import os
import pathlib
import subprocess
import tempfile
import time
import traceback
import types
import typing

import uqbar.apis


[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. Supports mutable mapping dictionary interface. """ ### CLASS VARIABLES ### __documentation_section__ = "System configuration" __slots__ = ("_cached_configuration_directory", "_settings") _configuration_directory_name = ".abjad" _configuration_file_name = "abjad.cfg" # for caching _lilypond_version_string: typing.ClassVar[str | None] = None ### INITIALIZER ### def __init__(self): self._cached_configuration_directory = None if not os.path.exists(str(self.configuration_directory)): try: os.makedirs(str(self.configuration_directory)) except (IOError, OSError): traceback.print_exc() old_contents = "" if self.configuration_file_path.exists(): try: old_contents = self.configuration_file_path.read_text() except AttributeError: with self.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.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() ### SPECIAL METHODS ###
[docs] def __delitem__(self, i) -> None: """ Deletes item ``i`` from configuration. """ del self._settings[i]
[docs] def __eq__(self, argument): """ Returns true when ``argument`` is configuratioin with same settings. """ if isinstance(argument, type(self)): return self._settings == argument._settings return False
[docs] def __getitem__(self, argument) -> typing.Any: """ Gets item or slice identified by ``argument``. """ return self._settings.__getitem__(argument)
[docs] def __iter__(self) -> typing.Iterator[str]: """ Iterates configuration settings. """ for key in self._settings: yield key
[docs] def __setitem__(self, i, argument) -> None: """ Sets configuration item ``i`` to ``argument``. """ self._settings[i] = argument
### PRIVATE METHODS ### 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 def _get_config_specification(self): specs = self._get_option_specification() return [f"{key} = {value}" for key, value in sorted(specs.items())] def _get_current_time(self): return time.strftime("%d %B %Y %H:%M:%S") def _get_initial_comment(self): current_time = self._get_current_time() return [ f"Abjad configuration file created on {current_time}.", "This file is interpreted by Python's ConfigParser ", "and follows ini syntax.", ] def _get_option_comments(self): options = self._get_option_definitions() comments = [(key, options[key]["comment"]) for key in options] return dict(comments) 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.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 _get_option_specification(self): options = self._get_option_definitions() specs = [(key, options[key]["spec"]) for key in options] return dict(specs) 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 ### PUBLIC PROPERTIES ### @property def abjad_directory(self) -> pathlib.Path: """ Gets Abjad directory. """ return pathlib.Path(__file__).parent.parent @property 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.configuration_directory / "output" @property def boilerplate_directory(self) -> pathlib.Path: """ Gets Abjad boilerplate directory. """ return self.abjad_directory.parent / "boilerplate" @property def configuration_directory(self) -> pathlib.Path: """ Gets configuration directory. .. container:: example >>> configuration = abjad.Configuration() >>> configuration.configuration_directory PosixPath('...') Defaults to $HOME/{directory_name}. Returns $TEMP/{directory_name} if $HOME is read-only or $HOME/{directory_name} is read-only. Also caches the initial result to reduce filesystem interaction. """ if self._cached_configuration_directory is None: directory_name = self._configuration_directory_name home_directory = str(self.home_directory) flags = os.W_OK | os.X_OK if os.access(home_directory, flags): path = self.home_directory / directory_name if not path.exists() or (path.exists() and os.access(str(path), flags)): self._cached_configuration_directory = path return self._cached_configuration_directory path = pathlib.Path(tempfile.gettempdir()) / directory_name self._cached_configuration_directory = path return self._cached_configuration_directory @property def configuration_file_path(self) -> pathlib.Path: """ Gets configuration file path. .. container:: example >>> configuration = abjad.Configuration() >>> configuration.configuration_file_path PosixPath('...') """ return self.configuration_directory / self._configuration_file_name @property def home_directory(self) -> pathlib.Path: """ Gets home directory. .. container:: example >>> configuration = abjad.Configuration() >>> configuration.home_directory PosixPath('...') """ path = ( os.environ.get("HOME") or os.environ.get("HOMEPATH") or os.environ.get("APPDATA") or tempfile.gettempdir() ) return pathlib.Path(path).absolute() @property def lilypond_log_file_path(self) -> pathlib.Path: """ Gets LilyPond log file path. """ return self.abjad_output_directory / "lily.log" ### PUBLIC METHODS ###
[docs] def get(self, *arguments, **keywords): """ Gets a key. """ return self._settings.get(*arguments, **keywords)
[docs] def get_lilypond_version_string(self) -> str: """ Gets LilyPond version string. .. container:: example >>> configuration = abjad.Configuration() >>> configuration.get_lilypond_version_string() # doctest: +SKIP '2.19.84' """ if self._lilypond_version_string is not None: 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
[docs]def list_all_classes(modules="abjad", ignored_classes=None): """ Lists all public classes defined in ``path``. .. container:: example >>> for class_ in abjad.list_all_classes(modules="abjad"): class_ <class 'abjad.bind.Wrapper'> <class 'abjad.configuration.Configuration'> <class 'abjad.contextmanagers.ContextManager'> <class 'abjad.contextmanagers.FilesystemState'> <class 'abjad.contextmanagers.ForbidUpdate'> <class 'abjad.contextmanagers.NullContextManager'> <class 'abjad.contextmanagers.ProgressIndicator'> <class 'abjad.contextmanagers.RedirectedStreams'> <class 'abjad.contextmanagers.TemporaryDirectory'> <class 'abjad.contextmanagers.TemporaryDirectoryChange'> <class 'abjad.contextmanagers.Timer'> <class 'abjad.contributions.ContributionsBySite'> <class 'abjad.cyclictuple.CyclicTuple'> <class 'abjad.duration.Duration'> <class 'abjad.duration.Offset'> <class 'abjad.dynamic.Dynamic'> <class 'abjad.exceptions.AssignabilityError'> <class 'abjad.exceptions.ImpreciseMetronomeMarkError'> <class 'abjad.exceptions.LilyPondParserError'> <class 'abjad.exceptions.MissingContextError'> <class 'abjad.exceptions.MissingMetronomeMarkError'> <class 'abjad.exceptions.ParentageError'> <class 'abjad.exceptions.PersistentIndicatorError'> <class 'abjad.exceptions.SchemeParserFinishedError'> <class 'abjad.exceptions.UnboundedTimeIntervalError'> <class 'abjad.exceptions.WellformednessError'> <class 'abjad.get.Lineage'> <class 'abjad.indicators.Arpeggio'> <class 'abjad.indicators.Articulation'> <class 'abjad.indicators.BarLine'> <class 'abjad.indicators.BeamCount'> <class 'abjad.indicators.BendAfter'> <class 'abjad.indicators.BreathMark'> <class 'abjad.indicators.Clef'> <class 'abjad.indicators.ColorFingering'> <class 'abjad.indicators.Fermata'> <class 'abjad.indicators.Glissando'> <class 'abjad.indicators.InstrumentName'> <class 'abjad.indicators.KeyCluster'> <class 'abjad.indicators.KeySignature'> <class 'abjad.indicators.LaissezVibrer'> <class 'abjad.indicators.LilyPondLiteral'> <class 'abjad.indicators.Markup'> <class 'abjad.indicators.MetronomeMark'> <class 'abjad.indicators.Mode'> <class 'abjad.indicators.Ottava'> <class 'abjad.indicators.RehearsalMark'> <class 'abjad.indicators.Repeat'> <class 'abjad.indicators.RepeatTie'> <class 'abjad.indicators.ShortInstrumentName'> <class 'abjad.indicators.StaffChange'> <class 'abjad.indicators.StartBeam'> <class 'abjad.indicators.StartGroup'> <class 'abjad.indicators.StartHairpin'> <class 'abjad.indicators.StartPhrasingSlur'> <class 'abjad.indicators.StartPianoPedal'> <class 'abjad.indicators.StartSlur'> <class 'abjad.indicators.StartTextSpan'> <class 'abjad.indicators.StartTrillSpan'> <class 'abjad.indicators.StemTremolo'> <class 'abjad.indicators.StopBeam'> <class 'abjad.indicators.StopGroup'> <class 'abjad.indicators.StopHairpin'> <class 'abjad.indicators.StopPhrasingSlur'> <class 'abjad.indicators.StopPianoPedal'> <class 'abjad.indicators.StopSlur'> <class 'abjad.indicators.StopTextSpan'> <class 'abjad.indicators.StopTrillSpan'> <class 'abjad.indicators.Tie'> <class 'abjad.indicators.TimeSignature'> <class 'abjad.indicators.VoiceNumber'> <class 'abjad.instruments.Accordion'> <class 'abjad.instruments.AltoFlute'> <class 'abjad.instruments.AltoSaxophone'> <class 'abjad.instruments.AltoTrombone'> <class 'abjad.instruments.AltoVoice'> <class 'abjad.instruments.BaritoneSaxophone'> <class 'abjad.instruments.BaritoneVoice'> <class 'abjad.instruments.BassClarinet'> <class 'abjad.instruments.BassFlute'> <class 'abjad.instruments.BassSaxophone'> <class 'abjad.instruments.BassTrombone'> <class 'abjad.instruments.BassVoice'> <class 'abjad.instruments.Bassoon'> <class 'abjad.instruments.Cello'> <class 'abjad.instruments.ClarinetInA'> <class 'abjad.instruments.ClarinetInBFlat'> <class 'abjad.instruments.ClarinetInEFlat'> <class 'abjad.instruments.Contrabass'> <class 'abjad.instruments.ContrabassClarinet'> <class 'abjad.instruments.ContrabassFlute'> <class 'abjad.instruments.ContrabassSaxophone'> <class 'abjad.instruments.Contrabassoon'> <class 'abjad.instruments.EnglishHorn'> <class 'abjad.instruments.Flute'> <class 'abjad.instruments.FrenchHorn'> <class 'abjad.instruments.Glockenspiel'> <class 'abjad.instruments.Guitar'> <class 'abjad.instruments.Harp'> <class 'abjad.instruments.Harpsichord'> <class 'abjad.instruments.Instrument'> <class 'abjad.instruments.Marimba'> <class 'abjad.instruments.MezzoSopranoVoice'> <class 'abjad.instruments.Oboe'> <class 'abjad.instruments.Percussion'> <class 'abjad.instruments.Piano'> <class 'abjad.instruments.Piccolo'> <class 'abjad.instruments.SopraninoSaxophone'> <class 'abjad.instruments.SopranoSaxophone'> <class 'abjad.instruments.SopranoVoice'> <class 'abjad.instruments.StringNumber'> <class 'abjad.instruments.TenorSaxophone'> <class 'abjad.instruments.TenorTrombone'> <class 'abjad.instruments.TenorVoice'> <class 'abjad.instruments.Trumpet'> <class 'abjad.instruments.Tuba'> <class 'abjad.instruments.Tuning'> <class 'abjad.instruments.Vibraphone'> <class 'abjad.instruments.Viola'> <class 'abjad.instruments.Violin'> <class 'abjad.instruments.Xylophone'> <class 'abjad.label.ColorMap'> <class 'abjad.lilypondfile.Block'> <class 'abjad.lilypondfile.LilyPondFile'> <class 'abjad.lyproxy.LilyPondContext'> <class 'abjad.lyproxy.LilyPondEngraver'> <class 'abjad.lyproxy.LilyPondGrob'> <class 'abjad.lyproxy.LilyPondGrobInterface'> <class 'abjad.math.Infinity'> <class 'abjad.math.NegativeInfinity'> <class 'abjad.meter.Meter'> <class 'abjad.meter.MetricAccentKernel'> <class 'abjad.metricmodulation.MetricModulation'> <class 'abjad.obgc.OnBeatGraceContainer'> <class 'abjad.overrides.Interface'> <class 'abjad.overrides.LilyPondOverride'> <class 'abjad.overrides.LilyPondSetting'> <class 'abjad.overrides.OverrideInterface'> <class 'abjad.overrides.SettingInterface'> <class 'abjad.parentage.Parentage'> <class 'abjad.parsers.base.Parser'> <class 'abjad.pattern.Pattern'> <class 'abjad.pattern.PatternTuple'> <class 'abjad.pcollections.PitchClassSegment'> <class 'abjad.pcollections.PitchClassSet'> <class 'abjad.pcollections.PitchRange'> <class 'abjad.pcollections.PitchSegment'> <class 'abjad.pcollections.PitchSet'> <class 'abjad.pcollections.TwelveToneRow'> <class 'abjad.pitch.Accidental'> <class 'abjad.pitch.Interval'> <class 'abjad.pitch.IntervalClass'> <class 'abjad.pitch.NamedInterval'> <class 'abjad.pitch.NamedIntervalClass'> <class 'abjad.pitch.NamedInversionEquivalentIntervalClass'> <class 'abjad.pitch.NamedPitch'> <class 'abjad.pitch.NamedPitchClass'> <class 'abjad.pitch.NumberedInterval'> <class 'abjad.pitch.NumberedIntervalClass'> <class 'abjad.pitch.NumberedInversionEquivalentIntervalClass'> <class 'abjad.pitch.NumberedPitch'> <class 'abjad.pitch.NumberedPitchClass'> <class 'abjad.pitch.Octave'> <class 'abjad.pitch.Pitch'> <class 'abjad.pitch.PitchClass'> <class 'abjad.pitch.StaffPosition'> <class 'abjad.rhythmtrees.RhythmTreeContainer'> <class 'abjad.rhythmtrees.RhythmTreeLeaf'> <class 'abjad.rhythmtrees.RhythmTreeMixin'> <class 'abjad.rhythmtrees.RhythmTreeParser'> <class 'abjad.score.AfterGraceContainer'> <class 'abjad.score.BeforeGraceContainer'> <class 'abjad.score.Chord'> <class 'abjad.score.Cluster'> <class 'abjad.score.Component'> <class 'abjad.score.Container'> <class 'abjad.score.Context'> <class 'abjad.score.DrumNoteHead'> <class 'abjad.score.Leaf'> <class 'abjad.score.MultimeasureRest'> <class 'abjad.score.Note'> <class 'abjad.score.NoteHead'> <class 'abjad.score.NoteHeadList'> <class 'abjad.score.Rest'> <class 'abjad.score.Score'> <class 'abjad.score.Skip'> <class 'abjad.score.Staff'> <class 'abjad.score.StaffGroup'> <class 'abjad.score.TremoloContainer'> <class 'abjad.score.Tuplet'> <class 'abjad.score.Voice'> <class 'abjad.select.LogicalTie'> <class 'abjad.setclass.SetClass'> <class 'abjad.tag.Tag'> <class 'abjad.timespan.OffsetCounter'> <class 'abjad.timespan.Timespan'> <class 'abjad.timespan.TimespanList'> <class 'abjad.tweaks.Bundle'> <class 'abjad.tweaks.Tweak'> <class 'abjad.verticalmoment.VerticalMoment'> """ all_classes = set() for module in yield_all_modules(modules): if "parser" in module.__name__: continue name = module.__name__.split(".")[-1] for name in dir(module): item = getattr(module, name) if isinstance(item, type): if "sphinx" in repr(item): continue if item.__name__.startswith("_"): continue if "abjad.io" in str(item): continue if "abjad" in repr(item): all_classes.add(item) if ignored_classes: ignored_classes = set(ignored_classes) all_classes.difference_update(ignored_classes) return list(sorted(all_classes, key=lambda _: (_.__module__, _.__name__)))
[docs]def list_all_functions(modules="abjad"): """ Lists all public functions defined in ``modules``. .. container:: example >>> functions = abjad.list_all_functions(modules="abjad") >>> names = [_.__name__ for _ in functions] >>> # for name in sorted(names): name """ all_functions = set() for module in yield_all_modules(modules): name = module.__name__.split(".")[-1] if name.startswith("_"): continue if "sphinx" in repr(module): continue for name in sorted(dir(module)): item = getattr(module, name) if isinstance(item, types.FunctionType): if item.__name__.startswith("_"): continue if "abjad" not in repr(item.__module__): continue all_functions.add(item) return list(sorted(all_functions, key=lambda _: (_.__module__, _.__name__)))
configuration = Configuration()
[docs]def yield_all_modules(paths=None): """ Yields all modules encountered in ``path``. Returns generator. """ _paths = [] if not paths: _paths = configuration.abjad_directory elif isinstance(paths, str): module = importlib.import_module(paths) _paths.extend(module.__path__) elif isinstance(paths, types.ModuleType): _paths.extend(paths.__path__) elif isinstance(paths, collections.abc.Iterable): for path in paths: if isinstance(path, types.ModuleType): _paths.extend(path.__path__) elif isinstance(path, str): module = importlib.import_module(path) _paths.extend(module.__path__) else: raise ValueError(module) for path in _paths: for source_path in uqbar.apis.collect_source_paths([path]): package_path = uqbar.apis.source_path_to_package_path(source_path) yield importlib.import_module(package_path)