improve type hint coverage, make ruff format compliant
This commit is contained in:
@@ -1,16 +1,10 @@
|
||||
from symconf.runner import Runner
|
||||
from symconf.reader import DictReader
|
||||
from importlib.metadata import version
|
||||
|
||||
from symconf import util, config, reader, matching, template
|
||||
from symconf.config import ConfigManager
|
||||
from symconf.reader import DictReader
|
||||
from symconf.runner import Runner
|
||||
from symconf.matching import Matcher, FilePart
|
||||
from symconf.template import Template, FileTemplate, TOMLTemplate
|
||||
|
||||
from symconf import config
|
||||
from symconf import matching
|
||||
from symconf import reader
|
||||
from symconf import template
|
||||
from symconf import util
|
||||
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
__version__ = version('symconf')
|
||||
__version__ = version("symconf")
|
||||
|
||||
@@ -1,179 +1,214 @@
|
||||
import argparse
|
||||
from importlib.metadata import version
|
||||
from argparse import Namespace, ArgumentParser
|
||||
|
||||
from symconf import util, __version__
|
||||
from symconf.config import ConfigManager
|
||||
|
||||
|
||||
def add_install_subparser(subparsers):
|
||||
def install_apps(args):
|
||||
def add_install_subparser(subparsers: ArgumentParser) -> None:
|
||||
def install_apps(args: Namespace) -> None:
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.install_apps(apps=args.apps)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'install',
|
||||
description='Run install scripts for registered applications.'
|
||||
"install",
|
||||
description="Run install scripts for registered applications.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
required = False,
|
||||
default = "*",
|
||||
type = lambda s: s.split(',') if s != '*' else s,
|
||||
help = 'Application target for theme. App must be present in the registry. ' \
|
||||
+ 'Use "*" to apply to all registered apps'
|
||||
"-a",
|
||||
"--apps",
|
||||
required=False,
|
||||
default="*",
|
||||
type=lambda s: s.split(",") if s != "*" else s,
|
||||
help=(
|
||||
"Application target for theme. App must be present in the "
|
||||
'registry. Use "*" to apply to all registered apps'
|
||||
),
|
||||
)
|
||||
parser.set_defaults(func=install_apps)
|
||||
|
||||
def add_update_subparser(subparsers):
|
||||
def update_apps(args):
|
||||
|
||||
def add_update_subparser(subparsers: ArgumentParser) -> None:
|
||||
def update_apps(args: Namespace) -> None:
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.update_apps(apps=args.apps)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'update',
|
||||
description='Run update scripts for registered applications.'
|
||||
"update", description="Run update scripts for registered applications."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
required = False,
|
||||
default = '*',
|
||||
type = lambda s: s.split(',') if s != '*' else s,
|
||||
help = 'Application target for theme. App must be present in the registry. ' \
|
||||
+ 'Use "*" to apply to all registered apps'
|
||||
"-a",
|
||||
"--apps",
|
||||
required=False,
|
||||
default="*",
|
||||
type=lambda s: s.split(",") if s != "*" else s,
|
||||
help=(
|
||||
"Application target for theme. App must be present in the "
|
||||
'registry. Use "*" to apply to all registered apps'
|
||||
),
|
||||
)
|
||||
parser.set_defaults(func=update_apps)
|
||||
|
||||
def add_config_subparser(subparsers):
|
||||
def configure_apps(args):
|
||||
|
||||
def add_config_subparser(subparsers: ArgumentParser) -> None:
|
||||
def configure_apps(args: Namespace) -> None:
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.configure_apps(
|
||||
apps=args.apps,
|
||||
scheme=args.mode,
|
||||
style=args.style,
|
||||
**args.template_vars
|
||||
**args.template_vars,
|
||||
)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'config',
|
||||
description='Set config files for registered applications.'
|
||||
"config", description="Set config files for registered applications."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--style',
|
||||
required = False,
|
||||
default = 'any',
|
||||
help = 'Style indicator (often a color palette) capturing thematic details in '
|
||||
'a config file'
|
||||
"-s",
|
||||
"--style",
|
||||
required=False,
|
||||
default="any",
|
||||
help=(
|
||||
"Style indicator (often a color palette) capturing "
|
||||
"thematic details in a config file"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-m', '--mode',
|
||||
required = False,
|
||||
default = "any",
|
||||
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."'
|
||||
"-m",
|
||||
"--mode",
|
||||
required=False,
|
||||
default="any",
|
||||
help=(
|
||||
'Preferred lightness mode/scheme, either "light," "dark," '
|
||||
'"any," or "none."'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
required = False,
|
||||
default = "*",
|
||||
type = lambda s: s.split(',') if s != '*' else s,
|
||||
help = 'Application target for theme. App must be present in the registry. ' \
|
||||
+ 'Use "*" to apply to all registered apps'
|
||||
"-a",
|
||||
"--apps",
|
||||
required=False,
|
||||
default="*",
|
||||
type=lambda s: s.split(",") if s != "*" else s,
|
||||
help=(
|
||||
"Application target for theme. App must be present in the "
|
||||
'registry. Use "*" to apply to all registered apps'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-T', '--template-vars',
|
||||
required = False,
|
||||
nargs='+',
|
||||
default = {},
|
||||
"-T",
|
||||
"--template-vars",
|
||||
required=False,
|
||||
nargs="+",
|
||||
default={},
|
||||
action=util.KVPair,
|
||||
help='Groups to use when populating templates, in the form group=value'
|
||||
help=(
|
||||
"Groups to use when populating templates, in the form group=value"
|
||||
),
|
||||
)
|
||||
parser.set_defaults(func=configure_apps)
|
||||
|
||||
def add_generate_subparser(subparsers):
|
||||
def generate_apps(args):
|
||||
|
||||
def add_generate_subparser(subparsers: ArgumentParser) -> None:
|
||||
def generate_apps(args: Namespace) -> None:
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.generate_app_templates(
|
||||
gen_dir=args.output_dir,
|
||||
apps=args.apps,
|
||||
scheme=args.mode,
|
||||
style=args.style,
|
||||
**args.template_vars
|
||||
**args.template_vars,
|
||||
)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'generate',
|
||||
description='Generate all template config files for specified apps'
|
||||
"generate",
|
||||
description="Generate all template config files for specified apps",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output-dir',
|
||||
required = True,
|
||||
type = util.absolute_path,
|
||||
help = 'Path to write generated template files'
|
||||
"-o",
|
||||
"--output-dir",
|
||||
required=True,
|
||||
type=util.absolute_path,
|
||||
help="Path to write generated template files",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--style',
|
||||
required = False,
|
||||
default = 'any',
|
||||
help = 'Style indicator (often a color palette) capturing thematic details in '
|
||||
'a config file'
|
||||
"-s",
|
||||
"--style",
|
||||
required=False,
|
||||
default="any",
|
||||
help=(
|
||||
"Style indicator (often a color palette) capturing "
|
||||
"thematic details in a config file"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-m', '--mode',
|
||||
required = False,
|
||||
default = "any",
|
||||
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."'
|
||||
"-m",
|
||||
"--mode",
|
||||
required=False,
|
||||
default="any",
|
||||
help=(
|
||||
'Preferred lightness mode/scheme, either "light," "dark," '
|
||||
'"any," or "none."'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
required = False,
|
||||
default = "*",
|
||||
type = lambda s: s.split(',') if s != '*' else s,
|
||||
help = 'Application target for theme. App must be present in the registry. ' \
|
||||
+ 'Use "*" to apply to all registered apps'
|
||||
"-a",
|
||||
"--apps",
|
||||
required=False,
|
||||
default="*",
|
||||
type=lambda s: s.split(",") if s != "*" else s,
|
||||
help=(
|
||||
"Application target for theme. App must be present in the "
|
||||
'registry. Use "*" to apply to all registered apps'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-T', '--template-vars',
|
||||
required = False,
|
||||
nargs = '+',
|
||||
default = {},
|
||||
action = util.KVPair,
|
||||
help = 'Groups to use when populating templates, in the form group=value'
|
||||
"-T",
|
||||
"--template-vars",
|
||||
required=False,
|
||||
nargs="+",
|
||||
default={},
|
||||
action=util.KVPair,
|
||||
help=(
|
||||
"Groups to use when populating templates, in the form group=value"
|
||||
),
|
||||
)
|
||||
parser.set_defaults(func=generate_apps)
|
||||
|
||||
|
||||
# central argparse entry point
|
||||
parser = argparse.ArgumentParser(
|
||||
'symconf',
|
||||
description='Manage application configuration with symlinks.'
|
||||
parser = ArgumentParser(
|
||||
"symconf", description="Manage application configuration with symlinks."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--config-dir',
|
||||
default = util.xdg_config_path(),
|
||||
type = util.absolute_path,
|
||||
help = 'Path to config directory'
|
||||
"-c",
|
||||
"--config-dir",
|
||||
default=util.xdg_config_path(),
|
||||
type=util.absolute_path,
|
||||
help="Path to config directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--version',
|
||||
action='version',
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=__version__,
|
||||
help = 'Print symconf version'
|
||||
help="Print symconf version",
|
||||
)
|
||||
|
||||
# add subparsers
|
||||
subparsers = parser.add_subparsers(title='subcommand actions')
|
||||
subparsers = parser.add_subparsers(title="subcommand actions")
|
||||
add_config_subparser(subparsers)
|
||||
add_generate_subparser(subparsers)
|
||||
add_install_subparser(subparsers)
|
||||
add_update_subparser(subparsers)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
args = parser.parse_args()
|
||||
|
||||
if 'func' in args:
|
||||
if "func" in args:
|
||||
args.func(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
'''
|
||||
"""
|
||||
Generic combinatorial name-matching subsystem
|
||||
|
||||
Config files are expected to have names matching the following spec:
|
||||
@@ -7,13 +7,14 @@ Config files are expected to have names matching the following spec:
|
||||
|
||||
<style>-<scheme>.<config_pathname>
|
||||
|
||||
- ``config_pathname``: refers to a concrete filename, typically that which is expected by
|
||||
the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry,
|
||||
however, it merely serves as an identifier, as it can be mapped onto any path.
|
||||
- ``config_pathname``: refers to a concrete filename, typically that which is
|
||||
expected by the target app (e.g., ``kitty.conf``). In the context of
|
||||
``config_map`` in the registry, however, it merely serves as an identifier,
|
||||
as it can be mapped onto any path.
|
||||
- ``scheme``: indicates the lightness mode ("light" or "dark")
|
||||
- ``style``: general identifier capturing the stylizations applied to the config file.
|
||||
This is typically of the form ``<variant>-<palette>``, i.e., including a reference to a
|
||||
particular color palette.
|
||||
- ``style``: general identifier capturing the stylizations applied to the
|
||||
config file. This is typically of the form ``<variant>-<palette>``, i.e.,
|
||||
including a reference to a particular color palette.
|
||||
|
||||
For example
|
||||
|
||||
@@ -21,14 +22,14 @@ For example
|
||||
|
||||
soft-gruvbox-dark.kitty.conf
|
||||
|
||||
gets mapped to
|
||||
gets mapped to
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
style -> "soft-gruvbox"
|
||||
scheme -> "dark"
|
||||
pathname -> "kitty.conf"
|
||||
'''
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -36,33 +37,35 @@ from symconf import util
|
||||
|
||||
|
||||
class FilePart:
|
||||
def __init__(self, path: str | Path):
|
||||
def __init__(self, path: str | Path) -> None:
|
||||
self.path = util.absolute_path(path)
|
||||
self.pathname = self.path.name
|
||||
|
||||
parts = str(self.pathname).split('.')
|
||||
parts = str(self.pathname).split(".")
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f'Filename "{pathname}" incorrectly formatted, ignoring')
|
||||
raise ValueError(
|
||||
f'Filename "{self.pathname}" incorrectly formatted, ignoring'
|
||||
)
|
||||
|
||||
self.theme = parts[0]
|
||||
self.conf = '.'.join(parts[1:])
|
||||
self.conf = ".".join(parts[1:])
|
||||
|
||||
theme_split = self.theme.split('-')
|
||||
theme_split = self.theme.split("-")
|
||||
self.scheme = theme_split[-1]
|
||||
self.style = '-'.join(theme_split[:-1])
|
||||
self.style = "-".join(theme_split[:-1])
|
||||
|
||||
self.index = -1
|
||||
|
||||
def set_index(self, idx: int):
|
||||
def set_index(self, idx: int) -> None:
|
||||
self.index = idx
|
||||
|
||||
|
||||
class Matcher:
|
||||
def get_file_parts(
|
||||
self,
|
||||
self,
|
||||
paths: list[str | Path],
|
||||
) -> list[FilePart]:
|
||||
'''
|
||||
"""
|
||||
Split pathnames into parts for matching.
|
||||
|
||||
Pathnames should be of the format
|
||||
@@ -71,123 +74,128 @@ class Matcher:
|
||||
|
||||
<style>-<scheme>.<config_pathname>
|
||||
|
||||
where ``style`` is typically itself of the form ``<variant>-<palette>``.
|
||||
'''
|
||||
where ``style`` is typically itself of the form
|
||||
``<variant>-<palette>``.
|
||||
"""
|
||||
|
||||
file_parts = []
|
||||
for path in paths:
|
||||
try:
|
||||
config_file = FilePart(path)
|
||||
file_parts.append(config_file)
|
||||
except ValueError as e:
|
||||
print(f'Filename "{pathname}" incorrectly formatted, ignoring')
|
||||
except ValueError:
|
||||
print(f'Filename "{path}" incorrectly formatted, ignoring')
|
||||
|
||||
return file_parts
|
||||
|
||||
def prefix_order(
|
||||
self,
|
||||
scheme,
|
||||
style,
|
||||
strict=False,
|
||||
self,
|
||||
scheme: str,
|
||||
style: str,
|
||||
strict: bool = False,
|
||||
) -> list[tuple[str, str]]:
|
||||
'''
|
||||
Determine the order of concrete config pathname parts to match, given the
|
||||
``scheme`` and ``style`` inputs.
|
||||
"""
|
||||
Determine the order of concrete config pathname parts to match, given
|
||||
the ``scheme`` and ``style`` inputs.
|
||||
|
||||
There is a unique preferred match order when ``style``, ``scheme``, both, or none
|
||||
are ``any``. In general, when ``any`` is provided for a given factor, it is
|
||||
best matched by a config file that expresses indifference under that factor.
|
||||
'''
|
||||
There is a unique preferred match order when ``style``, ``scheme``,
|
||||
both, or none are ``any``. In general, when ``any`` is provided for a
|
||||
given factor, it is best matched by a config file that expresses
|
||||
indifference under that factor.
|
||||
"""
|
||||
|
||||
# explicit cases are the most easily managed here, even if a little redundant
|
||||
# explicit cases are the most easily managed here, even if a little
|
||||
# redundant
|
||||
if strict:
|
||||
theme_order = [
|
||||
(style, scheme),
|
||||
]
|
||||
else:
|
||||
# inverse order of match relaxation; intention being to overwrite with
|
||||
# results from increasingly relevant groups given the conditions
|
||||
if style == 'any' and scheme == 'any':
|
||||
# inverse order of match relaxation; intention being to overwrite
|
||||
# with results from increasingly relevant groups given the
|
||||
# conditions
|
||||
if style == "any" and scheme == "any":
|
||||
# prefer both be "none", with preference for specific scheme
|
||||
theme_order = [
|
||||
(style , scheme),
|
||||
(style , 'none'),
|
||||
('none' , scheme),
|
||||
('none' , 'none'),
|
||||
(style, scheme),
|
||||
(style, "none"),
|
||||
("none", scheme),
|
||||
("none", "none"),
|
||||
]
|
||||
elif style == 'any':
|
||||
# prefer style to be "none", then specific, then relax specific scheme
|
||||
# to "none"
|
||||
elif style == "any":
|
||||
# prefer style to be "none", then specific, then relax specific
|
||||
# scheme to "none"
|
||||
theme_order = [
|
||||
(style , 'none'),
|
||||
('none' , 'none'),
|
||||
(style , scheme),
|
||||
('none' , scheme),
|
||||
(style, "none"),
|
||||
("none", "none"),
|
||||
(style, scheme),
|
||||
("none", scheme),
|
||||
]
|
||||
elif scheme == 'any':
|
||||
# prefer scheme to be "none", then specific, then relax specific style
|
||||
# to "none"
|
||||
elif scheme == "any":
|
||||
# prefer scheme to be "none", then specific, then relax
|
||||
# specific style to "none"
|
||||
theme_order = [
|
||||
('none' , scheme),
|
||||
('none' , 'none'),
|
||||
(style , scheme),
|
||||
(style , 'none'),
|
||||
("none", scheme),
|
||||
("none", "none"),
|
||||
(style, scheme),
|
||||
(style, "none"),
|
||||
]
|
||||
else:
|
||||
# neither component is any; prefer most specific
|
||||
theme_order = [
|
||||
('none' , 'none'),
|
||||
('none' , scheme),
|
||||
(style , 'none'),
|
||||
(style , scheme),
|
||||
("none", "none"),
|
||||
("none", scheme),
|
||||
(style, "none"),
|
||||
(style, scheme),
|
||||
]
|
||||
|
||||
return theme_order
|
||||
|
||||
def match_paths(
|
||||
self,
|
||||
self,
|
||||
paths: list[str | Path],
|
||||
prefix_order: list[tuple[str, str]],
|
||||
) -> list[FilePart]:
|
||||
'''
|
||||
Find and return FilePart matches according to the provided prefix order.
|
||||
"""
|
||||
Find and return FilePart matches according to the provided prefix
|
||||
order.
|
||||
|
||||
The prefix order specifies all valid style-scheme combos that can be considered as
|
||||
"consistent" with some user input (and is computed external to this method). For
|
||||
example, it could be
|
||||
The prefix order specifies all valid style-scheme combos that can be
|
||||
considered as "consistent" with some user input (and is computed
|
||||
external to this method). For example, it could be
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
('none', 'none')
|
||||
('none', 'dark')
|
||||
]
|
||||
[("none", "none")("none", "dark")]
|
||||
|
||||
indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be
|
||||
considered matching pathnames, with the latter being preferred.
|
||||
indicating that either ``none-none.<config>`` or ``none-dark.<config>``
|
||||
would be considered matching pathnames, with the latter being
|
||||
preferred.
|
||||
|
||||
This method exists because we need a way to allow any of the combos in
|
||||
the prefix order to match the candidate files. We don't know a priori
|
||||
how good of a match will be available, so we consider each file for
|
||||
each of the prefixes, and take the latest/best match for each unique
|
||||
config pathname (allowing for a "soft" match).
|
||||
|
||||
This method exists because we need a way to allow any of the combos in the prefix
|
||||
order to match the candidate files. We don't know a priori how good of a match
|
||||
will be available, so we consider each file for each of the prefixes, and take the
|
||||
latest/best match for each unique config pathname (allowing for a "soft" match).
|
||||
|
||||
.. admonition:: Checking for matches
|
||||
|
||||
When thinking about how best to structure this method, it initially felt like
|
||||
indexing factors of the FileParts would make the most sense, preventing the
|
||||
inner loop that needs to inspect each FilePart for each element of the prefix
|
||||
order. But indexing the file parts and checking against prefixes isn't so
|
||||
straightforward, as we'd still need to check matches by factor. For instance,
|
||||
if we index by style-scheme, either are allowed to be "any," so we'd need to
|
||||
check for the 4 valid combos and join the matching lists. If we index by both
|
||||
factors individually, we may have several files associated with a given key,
|
||||
and then need to coordinate the checks across both to ensure they belong to
|
||||
the same file.
|
||||
When thinking about how best to structure this method, it initially
|
||||
felt like indexing factors of the FileParts would make the most
|
||||
sense, preventing the inner loop that needs to inspect each
|
||||
FilePart for each element of the prefix order. But indexing the
|
||||
file parts and checking against prefixes isn't so straightforward,
|
||||
as we'd still need to check matches by factor. For instance, if we
|
||||
index by style-scheme, either are allowed to be "any," so we'd need
|
||||
to check for the 4 valid combos and join the matching lists. If we
|
||||
index by both factors individually, we may have several files
|
||||
associated with a given key, and then need to coordinate the checks
|
||||
across both to ensure they belong to the same file.
|
||||
|
||||
In any case, you should be able to do this in a way that's a bit more
|
||||
efficient, but the loop and the simple conditionals is just much simpler to
|
||||
follow. We're also talking about at most 10s of files, so it really doesn't
|
||||
matter.
|
||||
In any case, you should be able to do this in a way that's a bit
|
||||
more efficient, but the loop and the simple conditionals is just
|
||||
much simpler to follow. We're also talking about at most 10s of
|
||||
files, so it really doesn't matter.
|
||||
|
||||
Parameters:
|
||||
pathnames:
|
||||
@@ -195,51 +203,53 @@ class Matcher:
|
||||
style:
|
||||
prefix_order:
|
||||
strict:
|
||||
'''
|
||||
"""
|
||||
|
||||
file_parts = self.get_file_parts(paths)
|
||||
|
||||
ordered_matches = []
|
||||
for i, (style_prefix, scheme_prefix) in enumerate(prefix_order):
|
||||
for fp in file_parts:
|
||||
style_match = style_prefix == fp.style or style_prefix == 'any'
|
||||
scheme_match = scheme_prefix == fp.scheme or scheme_prefix == 'any'
|
||||
style_match = style_prefix == fp.style or style_prefix == "any"
|
||||
scheme_match = (
|
||||
scheme_prefix == fp.scheme or scheme_prefix == "any"
|
||||
)
|
||||
|
||||
if style_match and scheme_match:
|
||||
fp.set_index(i+1)
|
||||
fp.set_index(i + 1)
|
||||
ordered_matches.append(fp)
|
||||
|
||||
return ordered_matches
|
||||
|
||||
def relaxed_match(
|
||||
self,
|
||||
match_list: list[FilePart]
|
||||
) -> list[FilePart]:
|
||||
'''
|
||||
def relaxed_match(self, match_list: list[FilePart]) -> list[FilePart]:
|
||||
"""
|
||||
Isolate the best match in a match list and find its relaxed variants.
|
||||
|
||||
This method allows us to use the ``match_paths()`` method for matching templates
|
||||
rather than direct user config files. In the latter case, we want to symlink the
|
||||
single best config file match for each stem, across all stems with matching
|
||||
prefixes (e.g., ``none-dark.config.a`` and ``solarized-dark.config.b`` have two
|
||||
separate stems with prefixes that could match ``scheme=dark, style=any`` query).
|
||||
We can find these files by just indexing the ``match_path`` outputs (i.e., all
|
||||
matches) by config pathname and taking the one that appears latest (under the
|
||||
This method allows us to use the ``match_paths()`` method for matching
|
||||
templates rather than direct user config files. In the latter case, we
|
||||
want to symlink the single best config file match for each stem, across
|
||||
all stems with matching prefixes (e.g., ``none-dark.config.a`` and
|
||||
``solarized-dark.config.b`` have two separate stems with prefixes that
|
||||
could match ``scheme=dark, style=any`` query). We can find these files
|
||||
by just indexing the ``match_path`` outputs (i.e., all matches) by
|
||||
config pathname and taking the one that appears latest (under the
|
||||
prefix order) for each unique value.
|
||||
|
||||
In the template matching case, we want only a single best file match, period
|
||||
(there's really no notion of "config stems," it's just the prefixes). Once that
|
||||
match has been found, we can then "relax" either the scheme or style (or both) to
|
||||
``none``, and if the corresponding files exist, we use those as parts of the
|
||||
template keys. For example, if we match ``solarized-dark.toml``, we would also
|
||||
consider the values in ``none-dark.toml`` if available. The TOML values that are
|
||||
defined in the most specific (i.e., better under the prefix order) match are
|
||||
loaded "on top of" those less specific matches, overwriting keys when there's a
|
||||
conflict. ``none-dark.toml``, for instance, might define a general dark scheme
|
||||
background color, but a more specific definition in ``solarized-dark.toml`` would
|
||||
take precedent. These TOML files would be stacked before using the resulting
|
||||
dictionary to populate config templates.
|
||||
'''
|
||||
In the template matching case, we want only a single best file match,
|
||||
period (there's really no notion of "config stems," it's just the
|
||||
prefixes). Once that match has been found, we can then "relax" either
|
||||
the scheme or style (or both) to ``none``, and if the corresponding
|
||||
files exist, we use those as parts of the template keys. For example,
|
||||
if we match ``solarized-dark.toml``, we would also consider the values
|
||||
in ``none-dark.toml`` if available. The TOML values that are defined in
|
||||
the most specific (i.e., better under the prefix order) match are
|
||||
loaded "on top of" those less specific matches, overwriting keys when
|
||||
there's a conflict. ``none-dark.toml``, for instance, might define a
|
||||
general dark scheme background color, but a more specific definition in
|
||||
``solarized-dark.toml`` would take precedent. These TOML files would be
|
||||
stacked before using the resulting dictionary to populate config
|
||||
templates.
|
||||
"""
|
||||
|
||||
if not match_list:
|
||||
return []
|
||||
@@ -248,11 +258,10 @@ class Matcher:
|
||||
match = match_list[-1]
|
||||
|
||||
for fp in match_list:
|
||||
style_match = fp.style == match.style or fp.style == 'none'
|
||||
scheme_match = fp.scheme == match.scheme or fp.scheme == 'none'
|
||||
style_match = fp.style == match.style or fp.style == "none"
|
||||
scheme_match = fp.scheme == match.scheme or fp.scheme == "none"
|
||||
|
||||
if style_match and scheme_match:
|
||||
relaxed_map[fp.pathname] = fp
|
||||
|
||||
return list(relaxed_map.values())
|
||||
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
'''
|
||||
"""
|
||||
Simplified management for nested dictionaries
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
import pprint
|
||||
import tomllib
|
||||
import hashlib
|
||||
import logging
|
||||
import tomllib
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
|
||||
from symconf.util import deep_update
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DictReader:
|
||||
def __init__(self, toml_path=None):
|
||||
def __init__(self, toml_path: str | None = None) -> None:
|
||||
self._config = {}
|
||||
self.toml_path = toml_path
|
||||
|
||||
if toml_path is not None:
|
||||
self._config = self._load_toml(toml_path)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
def __str__(self) -> str:
|
||||
return pprint.pformat(self._config, indent=4)
|
||||
|
||||
@staticmethod
|
||||
def _load_toml(toml_path) -> dict[str, Any]:
|
||||
def _load_toml(toml_path: str) -> dict[str, Any]:
|
||||
return tomllib.loads(Path(toml_path).read_text())
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, config_dict):
|
||||
def from_dict(cls, config_dict: dict) -> "DictReader":
|
||||
new_instance = cls()
|
||||
new_instance._config = copy.deepcopy(config_dict)
|
||||
return new_instance
|
||||
|
||||
def update(self, config, in_place=False):
|
||||
def update(
|
||||
self, config: "DictReader", in_place: bool = False
|
||||
) -> "DictReader":
|
||||
new_config = deep_update(self._config, config._config)
|
||||
|
||||
if in_place:
|
||||
@@ -42,13 +47,14 @@ class DictReader:
|
||||
|
||||
return self.from_dict(new_config)
|
||||
|
||||
def copy(self):
|
||||
def copy(self) -> "DictReader":
|
||||
return self.from_dict(copy.deepcopy(self._config))
|
||||
|
||||
def get_subconfig(self, key): pass
|
||||
def get_subconfig(self, key: str) -> "DictReader":
|
||||
pass
|
||||
|
||||
def get(self, key, default=None):
|
||||
keys = key.split('.')
|
||||
def get(self, key: str, default: str | None = None) -> str:
|
||||
keys = key.split(".")
|
||||
|
||||
subconfig = self._config
|
||||
for subkey in keys[:-1]:
|
||||
@@ -59,8 +65,8 @@ class DictReader:
|
||||
|
||||
return subconfig.get(keys[-1], default)
|
||||
|
||||
def set(self, key, value):
|
||||
keys = key.split('.')
|
||||
def set(self, key: str, value: str) -> bool:
|
||||
keys = key.split(".")
|
||||
|
||||
subconfig = self._config
|
||||
for subkey in keys[:-1]:
|
||||
@@ -69,7 +75,8 @@ class DictReader:
|
||||
|
||||
if type(subconfig) is not dict:
|
||||
logger.debug(
|
||||
'Attempting to set nested key with an existing non-dict parent'
|
||||
"Attempting to set nested key with an "
|
||||
"existing non-dict parent"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -79,20 +86,20 @@ class DictReader:
|
||||
subconfig[subkey] = subdict
|
||||
subconfig = subdict
|
||||
|
||||
subconfig.update({ keys[-1]: value })
|
||||
subconfig.update({keys[-1]: value})
|
||||
|
||||
return True
|
||||
|
||||
def generate_hash(self, exclude_keys=None):
|
||||
|
||||
def generate_hash(self, exclude_keys: list[str] | None = None) -> str:
|
||||
inst_copy = self.copy()
|
||||
|
||||
|
||||
if exclude_keys is not None:
|
||||
for key in exclude_keys:
|
||||
inst_copy.set(key, None)
|
||||
|
||||
|
||||
items = inst_copy._config.items()
|
||||
|
||||
|
||||
# create hash from config options
|
||||
config_str = str(sorted(items))
|
||||
|
||||
return hashlib.md5(config_str.encode()).hexdigest()
|
||||
|
||||
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
'''
|
||||
"""
|
||||
Handle job/script execution
|
||||
'''
|
||||
"""
|
||||
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from colorama import Fore, Back, Style
|
||||
from colorama import Fore, Style
|
||||
|
||||
from symconf.util import printc, color_text
|
||||
from symconf.util import color_text
|
||||
|
||||
|
||||
class Runner:
|
||||
def run_script(
|
||||
self,
|
||||
script: str | Path,
|
||||
):
|
||||
) -> str | None:
|
||||
script_path = Path(script)
|
||||
|
||||
if script_path.stat().st_mode & stat.S_IXUSR == 0:
|
||||
print(
|
||||
color_text("│", Fore.BLUE),
|
||||
color_text(
|
||||
f' > script "{script_path.name}" missing execute permissions, skipping',
|
||||
Fore.RED + Style.DIM
|
||||
)
|
||||
f' > script "{script_path.name}" missing '
|
||||
"execute permissions, skipping",
|
||||
Fore.RED + Style.DIM,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
color_text("│", Fore.BLUE),
|
||||
color_text(
|
||||
f' > running script "{script_path.name}"',
|
||||
Fore.BLUE
|
||||
)
|
||||
color_text(f' > running script "{script_path.name}"', Fore.BLUE),
|
||||
)
|
||||
|
||||
output = subprocess.check_output(str(script_path), shell=True)
|
||||
|
||||
if output:
|
||||
fmt_output = output.decode().strip().replace(
|
||||
'\n',
|
||||
f'\n{Fore.BLUE}{Style.NORMAL}│{Style.DIM} '
|
||||
fmt_output = (
|
||||
output.decode()
|
||||
.strip()
|
||||
.replace("\n", f"\n{Fore.BLUE}{Style.NORMAL}│{Style.DIM} ")
|
||||
)
|
||||
print(
|
||||
color_text("│", Fore.BLUE),
|
||||
color_text(
|
||||
f' captured script output "{fmt_output}"',
|
||||
Fore.BLUE + Style.DIM
|
||||
)
|
||||
Fore.BLUE + Style.DIM,
|
||||
),
|
||||
)
|
||||
|
||||
return output
|
||||
@@ -56,7 +55,7 @@ class Runner:
|
||||
def run_many(
|
||||
self,
|
||||
script_list: list[str | Path],
|
||||
):
|
||||
) -> list[str | None]:
|
||||
outputs = []
|
||||
for script in script_list:
|
||||
output = self.run_script(script)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'''
|
||||
"""
|
||||
Support for basic config templates
|
||||
'''
|
||||
"""
|
||||
|
||||
import re
|
||||
import tomllib
|
||||
@@ -13,37 +13,35 @@ from symconf.reader import DictReader
|
||||
class Template:
|
||||
def __init__(
|
||||
self,
|
||||
template_str : str,
|
||||
key_pattern : str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
template_str: str,
|
||||
key_pattern: str = r"f{{(\S+?)}}",
|
||||
exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
|
||||
) -> None:
|
||||
self.template_str = template_str
|
||||
self.key_pattern = key_pattern
|
||||
self.exe_pattern = exe_pattern
|
||||
self.key_pattern = key_pattern
|
||||
self.exe_pattern = exe_pattern
|
||||
|
||||
def fill(
|
||||
self,
|
||||
template_dict : dict,
|
||||
template_dict: dict,
|
||||
) -> str:
|
||||
dr = DictReader.from_dict(template_dict)
|
||||
|
||||
exe_filled = re.sub(
|
||||
self.exe_pattern,
|
||||
lambda m: self._exe_fill(m, dr),
|
||||
self.template_str
|
||||
self.template_str,
|
||||
)
|
||||
|
||||
key_filled = re.sub(
|
||||
self.key_pattern,
|
||||
lambda m: self._key_fill(m, dr),
|
||||
exe_filled
|
||||
self.key_pattern, lambda m: self._key_fill(m, dr), exe_filled
|
||||
)
|
||||
|
||||
return key_filled
|
||||
|
||||
def _key_fill(
|
||||
self,
|
||||
match,
|
||||
match: re.Match,
|
||||
dict_reader: DictReader,
|
||||
) -> str:
|
||||
key = match.group(1)
|
||||
@@ -52,13 +50,13 @@ class Template:
|
||||
|
||||
def _exe_fill(
|
||||
self,
|
||||
match,
|
||||
match: re.Match,
|
||||
dict_reader: DictReader,
|
||||
) -> str:
|
||||
key_fill = re.sub(
|
||||
self.key_pattern,
|
||||
lambda m: f'"{self._key_fill(m, dict_reader)}"',
|
||||
match.group(1)
|
||||
match.group(1),
|
||||
)
|
||||
|
||||
return str(eval(key_fill))
|
||||
@@ -67,12 +65,12 @@ class Template:
|
||||
class FileTemplate(Template):
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
key_pattern: str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
path: Path,
|
||||
key_pattern: str = r"f{{(\S+?)}}",
|
||||
exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
path.open('r').read(),
|
||||
path.open("r").read(),
|
||||
key_pattern=key_pattern,
|
||||
exe_pattern=exe_pattern,
|
||||
)
|
||||
@@ -81,10 +79,10 @@ class FileTemplate(Template):
|
||||
class TOMLTemplate(FileTemplate):
|
||||
def __init__(
|
||||
self,
|
||||
toml_path: Path,
|
||||
key_pattern: str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
toml_path: Path,
|
||||
key_pattern: str = r"f{{(\S+?)}}",
|
||||
exe_pattern: str = r"x{{((?:(?!x{{).)*)}}",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
toml_path,
|
||||
key_pattern=key_pattern,
|
||||
@@ -93,7 +91,7 @@ class TOMLTemplate(FileTemplate):
|
||||
|
||||
def fill(
|
||||
self,
|
||||
template_dict : dict,
|
||||
template_dict: dict,
|
||||
) -> str:
|
||||
filled_template = super().fill(template_dict)
|
||||
toml_dict = tomllib.loads(filled_template)
|
||||
@@ -101,12 +99,10 @@ class TOMLTemplate(FileTemplate):
|
||||
return toml_dict
|
||||
|
||||
@staticmethod
|
||||
def stack_toml(
|
||||
path_list: list[Path]
|
||||
) -> dict:
|
||||
def stack_toml(path_list: list[Path]) -> dict:
|
||||
stacked_dict = {}
|
||||
for toml_path in path_list:
|
||||
updated_map = tomllib.load(toml_path.open('rb'))
|
||||
updated_map = tomllib.load(toml_path.open("rb"))
|
||||
stacked_dict = util.deep_update(stacked_dict, updated_map)
|
||||
|
||||
return stacked_dict
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from argparse import Action, Namespace, ArgumentParser
|
||||
|
||||
from xdg import BaseDirectory
|
||||
from colorama import Fore, Back, Style
|
||||
from colorama.ansi import AnsiFore, AnsiBack, AnsiStyle
|
||||
from colorama import Back, Fore, Style
|
||||
from colorama.ansi import AnsiCodes
|
||||
|
||||
|
||||
def color_text(text, *colorama_args):
|
||||
'''
|
||||
def color_text(text: str, *colorama_args: AnsiCodes) -> str:
|
||||
"""
|
||||
Colorama text helper function
|
||||
|
||||
Note: we attempt to preserve expected nested behavior by only resetting the groups
|
||||
(Fore, Back, Style) affected the styles passed in. This works when an outer call is
|
||||
changing styles in one group, and an inner call is changing styles in another, but
|
||||
*not* when affected groups overlap.
|
||||
Note: we attempt to preserve expected nested behavior by only resetting the
|
||||
groups (Fore, Back, Style) affected the styles passed in. This works when
|
||||
an outer call is changing styles in one group, and an inner call is
|
||||
changing styles in another, but *not* when affected groups overlap.
|
||||
|
||||
For example, if an outer call is setting the foreground color (e.g., ``Fore.GREEN``),
|
||||
nested calls on the text being passed into the function can modify and reset the
|
||||
background or style with affecting the foreground. The primary use case here is
|
||||
styling a group of text a single color, but applying ``BRIGHT`` or ``DIM`` styles only
|
||||
to some text elements within. If we didn't reset by group, the outer coloration
|
||||
request will be "canceled out" as soon as the first inner call is made (since the
|
||||
unconditional behavior just employs ``Style.RESET_ALL``).
|
||||
'''
|
||||
For example, if an outer call is setting the foreground color (e.g.,
|
||||
``Fore.GREEN``), nested calls on the text being passed into the function
|
||||
can modify and reset the background or style with affecting the foreground.
|
||||
The primary use case here is styling a group of text a single color, but
|
||||
applying ``BRIGHT`` or ``DIM`` styles only to some text elements within. If
|
||||
we didn't reset by group, the outer coloration request will be "canceled
|
||||
out" as soon as the first inner call is made (since the unconditional
|
||||
behavior just employs ``Style.RESET_ALL``).
|
||||
"""
|
||||
|
||||
# reverse map colorama Ansi codes
|
||||
resets = []
|
||||
for carg in colorama_args:
|
||||
match = re.match(r'.*\[(\d+)m', carg)
|
||||
match = re.match(r".*\[(\d+)m", carg)
|
||||
if match:
|
||||
intv = int(match.group(1))
|
||||
if (intv >= 30 and intv <= 39) or (intv >= 90 and intv <= 97):
|
||||
@@ -40,44 +41,62 @@ def color_text(text, *colorama_args):
|
||||
|
||||
return f"{''.join(colorama_args)}{text}{''.join(resets)}"
|
||||
|
||||
def printc(text, *colorama_args):
|
||||
|
||||
def printc(text: str, *colorama_args: AnsiCodes) -> None:
|
||||
print(color_text(text, *colorama_args))
|
||||
|
||||
|
||||
def absolute_path(path: str | Path) -> Path:
|
||||
return Path(path).expanduser().absolute()
|
||||
|
||||
def xdg_config_path():
|
||||
return Path(BaseDirectory.save_config_path('symconf'))
|
||||
|
||||
|
||||
|
||||
def xdg_config_path() -> Path:
|
||||
return Path(BaseDirectory.save_config_path("symconf"))
|
||||
|
||||
|
||||
def to_tilde_path(path: Path) -> Path:
|
||||
'''
|
||||
"""
|
||||
Abbreviate an absolute path by replacing HOME with "~", if applicable.
|
||||
'''
|
||||
"""
|
||||
|
||||
try:
|
||||
return Path(f"~/{path.relative_to(Path.home())}")
|
||||
except ValueError:
|
||||
return path
|
||||
|
||||
|
||||
def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
|
||||
'''Code adapted from pydantic'''
|
||||
"""Code adapted from pydantic"""
|
||||
|
||||
updated_mapping = mapping.copy()
|
||||
for updating_mapping in updating_mappings:
|
||||
for k, v in updating_mapping.items():
|
||||
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
|
||||
if (
|
||||
k in updated_mapping
|
||||
and isinstance(updated_mapping[k], dict)
|
||||
and isinstance(v, dict)
|
||||
):
|
||||
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
||||
else:
|
||||
updated_mapping[k] = v
|
||||
return updated_mapping
|
||||
|
||||
|
||||
class KVPair(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
class KVPair(Action):
|
||||
def __call__(
|
||||
self,
|
||||
parser: ArgumentParser,
|
||||
namespace: Namespace,
|
||||
values: list[str],
|
||||
option_string: str | None = None,
|
||||
) -> None:
|
||||
kv_dict = getattr(namespace, self.dest, {})
|
||||
|
||||
if kv_dict is None:
|
||||
kv_dict = {}
|
||||
|
||||
for value in values:
|
||||
key, val = value.split('=', 1)
|
||||
key, val = value.split("=", 1)
|
||||
kv_dict[key] = val
|
||||
|
||||
setattr(namespace, self.dest, kv_dict)
|
||||
|
||||
Reference in New Issue
Block a user