"""
twtxt.config
~~~~~~~~~~~~
This module implements the config file parser/writer.
:copyright: (c) 2016-2022 by buckket.
:license: MIT, see LICENSE for more details.
"""
import configparser
import logging
import os
import click
from twtxt.models import Source
logger = logging.getLogger(__name__)
[docs]class Config:
""":class:`Config` interacts with the configuration file.
:param str config_file: full path to the loaded config file
:param ~configparser.ConfigParser cfg: a :class:`~configparser.ConfigParser` object with config loaded
"""
config_dir = click.get_app_dir("twtxt")
config_name = "config"
def __init__(self, config_file, cfg):
self.config_file = config_file
self.cfg = cfg
[docs] @classmethod
def from_file(cls, file):
"""Try loading given config file.
:param str file: full path to the config file to load
"""
if not os.path.exists(file):
raise ValueError("Config file not found.")
try:
config_parser = configparser.ConfigParser()
config_parser.read(file)
configuration = cls(file, config_parser)
if not configuration.check_config_sanity():
raise ValueError("Error in config file.")
else:
return configuration
except configparser.Error:
raise ValueError("Config file is invalid.")
[docs] @classmethod
def discover(cls):
"""Make a guess about the config file location an try loading it."""
file = os.path.join(Config.config_dir, Config.config_name)
return cls.from_file(file)
[docs] @classmethod
def create_config(cls, cfgfile, nick, twtfile, twturl, disclose_identity, add_news):
"""Create a new config file at the default location.
:param str cfgfile: path to the config file
:param str nick: nickname to use for own tweets
:param str twtfile: path to the local twtxt file
:param str twturl: URL to the remote twtxt file
:param bool disclose_identity: if true the users id will be disclosed
:param bool add_news: if true follow twtxt news feed
"""
cfgfile_dir = os.path.dirname(cfgfile)
if not os.path.exists(cfgfile_dir):
os.makedirs(cfgfile_dir)
cfg = configparser.ConfigParser()
cfg.add_section("twtxt")
cfg.set("twtxt", "nick", nick)
cfg.set("twtxt", "twtfile", twtfile)
cfg.set("twtxt", "twturl", twturl)
cfg.set("twtxt", "disclose_identity", str(disclose_identity))
cfg.set("twtxt", "character_limit", "140")
cfg.set("twtxt", "character_warning", "140")
cfg.add_section("following")
if add_news:
cfg.set("following", "twtxt", "https://buckket.org/twtxt_news.txt")
conf = cls(cfgfile, cfg)
conf.write_config()
return conf
[docs] def write_config(self):
"""Writes `self.cfg` to `self.config_file`."""
with open(self.config_file, "w") as config_file:
self.cfg.write(config_file)
@property
def following(self):
"""A :class:`list` of all :class:`Source` objects."""
following = []
try:
for (nick, url) in self.cfg.items("following"):
source = Source(nick, url)
following.append(source)
except configparser.NoSectionError as e:
logger.debug(e)
return following
@property
def options(self):
"""A :class:`dict` of all config options."""
try:
return dict(self.cfg.items("twtxt"))
except configparser.NoSectionError as e:
logger.debug(e)
return {}
@property
def nick(self):
return self.cfg.get("twtxt", "nick", fallback=os.environ.get("USER", "").lower())
@property
def twtfile(self):
return os.path.expanduser(self.cfg.get("twtxt", "twtfile", fallback="twtxt.txt"))
@property
def twturl(self):
return self.cfg.get("twtxt", "twturl", fallback=None)
@property
def check_following(self):
return self.cfg.getboolean("twtxt", "check_following", fallback=True)
@property
def use_pager(self):
return self.cfg.getboolean("twtxt", "use_pager", fallback=False)
@property
def use_cache(self):
return self.cfg.getboolean("twtxt", "use_cache", fallback=True)
@property
def porcelain(self):
return self.cfg.getboolean("twtxt", "porcelain", fallback=False)
@property
def disclose_identity(self):
return self.cfg.getboolean("twtxt", "disclose_identity", fallback=False)
@property
def character_limit(self):
return self.cfg.getint("twtxt", "character_limit", fallback=None)
@property
def character_warning(self):
return self.cfg.getint("twtxt", "character_warning", fallback=None)
@property
def limit_timeline(self):
return self.cfg.getint("twtxt", "limit_timeline", fallback=20)
@property
def timeline_update_interval(self):
return self.cfg.getint("twtxt", "timeline_update_interval", fallback=10)
@property
def use_abs_time(self):
return self.cfg.getboolean("twtxt", "use_abs_time", fallback=False)
@property
def timeout(self):
return self.cfg.getfloat("twtxt", "timeout", fallback=5.0)
@property
def sorting(self):
return self.cfg.get("twtxt", "sorting", fallback="descending")
@property
def source(self):
return Source(self.nick, self.twturl)
@property
def pre_tweet_hook(self):
return self.cfg.get("twtxt", "pre_tweet_hook", fallback=None)
@property
def post_tweet_hook(self):
return self.cfg.get("twtxt", "post_tweet_hook", fallback=None)
[docs] def add_source(self, source):
"""Adds a new :class:`Source` to the config’s following section."""
if not self.cfg.has_section("following"):
self.cfg.add_section("following")
self.cfg.set("following", source.nick, source.url)
self.write_config()
[docs] def get_source_by_nick(self, nick):
"""Returns the :class:`Source` of the given nick.
:param str nick: nickname for which will be searched in the config
"""
url = self.cfg.get("following", nick, fallback=None)
return Source(nick, url) if url else None
[docs] def remove_source_by_nick(self, nick):
"""Removes a :class:`Source` form the config’s following section.
:param str nick: nickname for which will be searched in the config
"""
if not self.cfg.has_section("following"):
return False
ret_val = self.cfg.remove_option("following", nick)
self.write_config()
return ret_val
[docs] def build_default_map(self):
"""Maps config options to the default values used by click, returns :class:`dict`."""
default_map = {
"following": {
"check": self.check_following,
"timeout": self.timeout,
"porcelain": self.porcelain,
},
"tweet": {
"twtfile": self.twtfile,
},
"timeline": {
"pager": self.use_pager,
"cache": self.use_cache,
"limit": self.limit_timeline,
"timeout": self.timeout,
"sorting": self.sorting,
"porcelain": self.porcelain,
"twtfile": self.twtfile,
"update_interval": self.timeline_update_interval,
},
"view": {
"pager": self.use_pager,
"cache": self.use_cache,
"limit": self.limit_timeline,
"timeout": self.timeout,
"sorting": self.sorting,
"porcelain": self.porcelain,
"update_interval": self.timeline_update_interval,
}
}
return default_map
[docs] def check_config_sanity(self):
"""Checks if the given values in the config file are sane."""
is_sane = True
# This extracts some properties which cannot be checked like "nick",
# but it is definitely better than writing the property names as a
# string literal.
properties = [property_name for property_name, obj
in self.__class__.__dict__.items()
if isinstance(obj, property)]
for property_name in properties:
try:
getattr(self, property_name)
except ValueError as e:
click.echo("✗ Config error on {0} - {1}".format(property_name, e))
is_sane = False
return is_sane