Merge pull request #377 from ihabunek/settings

Implement a settings file
This commit is contained in:
Ivan Habunek 2023-06-30 14:59:14 +02:00 committed by GitHub
commit d71cc7e3b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 204 additions and 41 deletions

View File

@ -5,6 +5,7 @@
- [Installation](installation.md)
- [Usage](usage.md)
- [Advanced](advanced.md)
- [Settings](settings.md)
- [TUI](tui.md)
- [Contributing](contributing.md)
- [Documentation](documentation.md)

52
docs/settings.md Normal file
View File

@ -0,0 +1,52 @@
# Settings
Toot can be configured via a [TOML](https://toml.io/en/) settings file.
> Introduced in toot 0.37.0
> **Warning:** Settings are experimental and things may change without warning.
Toot will look for the settings file at:
* `~/.config/toot/settings.toml` (Linux & co.)
* `%APPDATA%\toot\settings.toml` (Windows)
Toot will respect the `XDG_CONFIG_HOME` environement variable if it's set and
look for the settings file in `$XDG_CONFIG_HOME/toot` instead of
`~/.config/toot`.
## Common options
The `[common]` section includes common options which are applied to all commands.
```toml
[common]
# Whether to use ANSI color in output
color = true
# Enable debug logging, shows HTTP requests
debug = true
# Redirect debug log to the given file
debug_file = "/tmp/toot.log"
# Log request and response bodies in the debug log
verbose = false
# Do not write to output
quiet = false
```
## Overriding command defaults
Defaults for command arguments can be override by specifying a `[command.<name>]` section.
For example, to override `toot post`.
```toml
[command.post]
editor = "vim"
sensitive = true
visibility = "unlisted"
scheduled_in = "30 minutes"
```

View File

@ -38,6 +38,7 @@ setup(
"beautifulsoup4>=4.5.0,<5.0",
"wcwidth>=0.1.7",
"urwid>=2.0.0,<3.0",
"tomlkit>=0.10.0,<1.0"
],
entry_points={
'console_scripts': [

View File

@ -8,7 +8,7 @@ To enable integration tests, export the following environment variables to match
your test server and database:
```
export TOOT_TEST_HOSTNAME="localhost:3000"
export TOOT_TEST_BASE_URL="localhost:3000"
export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
```
"""
@ -26,6 +26,11 @@ from toot.exceptions import ApiError, ConsoleError
from toot.output import print_out
def pytest_configure(config):
import toot.settings
toot.settings.DISABLE_SETTINGS = True
# Mastodon database name, used to confirm user registration without having to click the link
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")

View File

@ -1,3 +1,7 @@
import os
import sys
from os.path import join, expanduser
from collections import namedtuple
__version__ = '0.37.0'
@ -9,3 +13,22 @@ DEFAULT_INSTANCE = 'https://mastodon.social'
CLIENT_NAME = 'toot - a Mastodon CLI client'
CLIENT_WEBSITE = 'https://github.com/ihabunek/toot'
TOOT_CONFIG_DIR_NAME = "toot"
def get_config_dir():
"""Returns the path to toot config directory"""
# On Windows, store the config in roaming appdata
if sys.platform == "win32" and "APPDATA" in os.environ:
return join(os.getenv("APPDATA"), TOOT_CONFIG_DIR_NAME)
# Respect XDG_CONFIG_HOME env variable if set
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if "XDG_CONFIG_HOME" in os.environ:
config_home = expanduser(os.environ["XDG_CONFIG_HOME"])
return join(config_home, TOOT_CONFIG_DIR_NAME)
# Default to ~/.config/toot/
return join(expanduser("~"), ".config", TOOT_CONFIG_DIR_NAME)

View File

@ -1,44 +1,22 @@
import json
import os
import sys
from functools import wraps
from os.path import dirname, join, expanduser
from os.path import dirname, join
from toot import User, App
from toot import User, App, get_config_dir
from toot.exceptions import ConsoleError
from toot.output import print_out
TOOT_CONFIG_DIR_NAME = "toot"
TOOT_CONFIG_FILE_NAME = "config.json"
def get_config_dir():
"""Returns the path to toot config directory"""
# On Windows, store the config in roaming appdata
if sys.platform == "win32" and "APPDATA" in os.environ:
return join(os.getenv("APPDATA"), TOOT_CONFIG_DIR_NAME)
# Respect XDG_CONFIG_HOME env variable if set
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if "XDG_CONFIG_HOME" in os.environ:
config_home = expanduser(os.environ["XDG_CONFIG_HOME"])
return join(config_home, TOOT_CONFIG_DIR_NAME)
# Default to ~/.config/toot/
return join(expanduser("~"), ".config", TOOT_CONFIG_DIR_NAME)
def get_config_file_path():
"""Returns the path to toot config file."""
return join(get_config_dir(), TOOT_CONFIG_FILE_NAME)
CONFIG_FILE = get_config_file_path()
def user_id(user):
return "{}@{}".format(user.username, user.instance)
@ -63,15 +41,18 @@ def make_config(path):
def load_config():
if not os.path.exists(CONFIG_FILE):
make_config(CONFIG_FILE)
path = get_config_file_path()
with open(CONFIG_FILE) as f:
if not os.path.exists(path):
make_config(path)
with open(path) as f:
return json.load(f)
def save_config(config):
with open(CONFIG_FILE, 'w') as f:
path = get_config_file_path()
with open(path, "w") as f:
return json.dump(config, f, indent=True, sort_keys=True)

View File

@ -7,9 +7,10 @@ import sys
from argparse import ArgumentParser, FileType, ArgumentTypeError, Action
from collections import namedtuple
from itertools import chain
from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE, __version__
from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE, __version__, settings
from toot.exceptions import ApiError, ConsoleError
from toot.output import print_out, print_err
from toot.settings import get_setting
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
VISIBILITY_CHOICES_STR = ", ".join(f"'{v}'" for v in VISIBILITY_CHOICES)
@ -882,12 +883,24 @@ def get_argument_parser(name, command):
if command.require_auth:
combined_args += common_auth_args
defaults = get_setting(f"commands.{name}", dict, {})
for args, kwargs in combined_args:
# Set default value from settings if exists
default = get_default_value(defaults, args)
if default is not None:
kwargs["default"] = default
parser.add_argument(*args, **kwargs)
return parser
def get_default_value(defaults, args):
# Hacky way to determine command name from argparse args
name = args[-1].lstrip("-").replace("-", "_")
return defaults.get(name)
def run_command(app, user, name, args):
command = next((c for c in COMMANDS if c.name == name), None)
@ -919,9 +932,8 @@ def run_command(app, user, name, args):
def main():
# Enable debug logging if --debug is in args
if "--debug" in sys.argv:
filename = os.getenv("TOOT_LOG_FILE")
if settings.get_debug():
filename = settings.get_debug_file()
logging.basicConfig(level=logging.DEBUG, filename=filename)
logging.getLogger("urllib3").setLevel(logging.INFO)

View File

@ -3,6 +3,8 @@ import re
import sys
import textwrap
from functools import lru_cache
from toot import settings
from toot.entities import Instance, Notification, Poll, Status
from toot.utils import get_text, parse_html
from toot.wcstring import wc_wrap
@ -99,6 +101,7 @@ def strip_tags(message):
return re.sub(STYLE_TAG_PATTERN, "", message)
@lru_cache(maxsize=None)
def use_ansi_color():
"""Returns True if ANSI color codes should be used."""
@ -115,23 +118,24 @@ def use_ansi_color():
if "--no-color" in sys.argv:
return False
# Check in settings
color = settings.get_setting("common.color", bool)
if color is not None:
return color
# Use color by default
return True
USE_ANSI_COLOR = use_ansi_color()
QUIET = "--quiet" in sys.argv
def print_out(*args, **kwargs):
if not QUIET:
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
if not settings.get_quiet():
args = [colorize(a) if use_ansi_color() else strip_tags(a) for a in args]
print(*args, **kwargs)
def print_err(*args, **kwargs):
args = [f"<red>{a}</red>" for a in args]
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
args = [colorize(a) if use_ansi_color() else strip_tags(a) for a in args]
print(*args, file=sys.stderr, **kwargs)

84
toot/settings.py Normal file
View File

@ -0,0 +1,84 @@
import os
import sys
from functools import lru_cache
from os.path import exists, join
from tomlkit import parse
from toot import get_config_dir
from typing import Optional, Type
DISABLE_SETTINGS = False
TOOT_SETTINGS_FILE_NAME = "settings.toml"
def get_settings_path():
return join(get_config_dir(), TOOT_SETTINGS_FILE_NAME)
def load_settings() -> dict:
# Used for testing without config file
if DISABLE_SETTINGS:
return {}
path = get_settings_path()
if not exists(path):
return {}
with open(path) as f:
return parse(f.read())
@lru_cache(maxsize=None)
def get_settings():
return load_settings()
def get_setting(key: str, type: Type, default=None):
"""
Get a setting value. The key should be a dot-separated string,
e.g. "commands.post.editor" which will correspond to the "editor" setting
inside the `[commands.post]` section.
"""
settings = get_settings()
return _get_setting(settings, key.split("."), type, default)
def _get_setting(dct, keys, type: Type, default=None):
if len(keys) == 0:
if isinstance(dct, type):
return dct
else:
# TODO: warn? cast? both?
return default
key = keys[0]
if isinstance(dct, dict) and key in dct:
return _get_setting(dct[key], keys[1:], type, default)
return default
def get_debug() -> bool:
if "--debug" in sys.argv:
return True
return get_setting("common.debug", bool, False)
def get_debug_file() -> Optional[str]:
from_env = os.getenv("TOOT_LOG_FILE")
if from_env:
return from_env
return get_setting("common.debug_file", str)
@lru_cache(maxsize=None)
def get_quiet():
if "--quiet" in sys.argv:
return True
return get_setting("common.quiet", str, False)