diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef526dd97..cc46ffea2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,8 @@ repos: - types-setuptools - types-python-dateutil - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.12.10' + rev: 'v0.15.7' hooks: - id: ruff args: ["--fix"] + - id: ruff-format diff --git a/doc/source/conf.py b/doc/source/conf.py index 5b2c1a965..3e4bf2307 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -24,14 +24,12 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- Generate configspec.rst ---------------------------------------------- -specpath = '../../khal/settings/khal.spec' -config = ConfigObj( - None, configspec=specpath, stringify=False, list_values=False -) +specpath = "../../khal/settings/khal.spec" +config = ConfigObj(None, configspec=specpath, stringify=False, list_values=False) validator = validate.Validator() config.validate(validator) spec = config.configspec @@ -41,89 +39,88 @@ def write_section(specsection, secname, key, comment, output): # why is _parse_check a "private" method? seems to be rather useful... # we don't need fun_kwargs fun_name, fun_args, fun_kwargs, default = validator._parse_check(specsection) - output.write(f'\n.. _{secname}-{key}:') - output.write('\n') - output.write(f'\n.. object:: {key}\n') - output.write('\n') - output.write(' ' + '\n '.join([line.strip('# ') for line in comment])) - output.write('\n') - if fun_name == 'option': - fun_args = [f'*{arg}*' for arg in fun_args] - fun_args = fun_args[:-2] + [fun_args[-2] + ' and ' + fun_args[-1]] + output.write(f"\n.. _{secname}-{key}:") + output.write("\n") + output.write(f"\n.. object:: {key}\n") + output.write("\n") + output.write(" " + "\n ".join([line.strip("# ") for line in comment])) + output.write("\n") + if fun_name == "option": + fun_args = [f"*{arg}*" for arg in fun_args] + fun_args = fun_args[:-2] + [fun_args[-2] + " and " + fun_args[-1]] fun_name += f", allowed values are {', '.join(fun_args)}" fun_args = [] - if fun_name == 'integer' and len(fun_args) == 2: - fun_name += f', allowed values are between {fun_args[0]} and {fun_args[1]}' + if fun_name == "integer" and len(fun_args) == 2: + fun_name += f", allowed values are between {fun_args[0]} and {fun_args[1]}" fun_args = [] - output.write('\n') - if fun_name in ['expand_db_path', 'expand_path']: - fun_name = 'string' - elif fun_name in ['force_list']: - fun_name = 'list' + output.write("\n") + if fun_name in ["expand_db_path", "expand_path"]: + fun_name = "string" + elif fun_name in ["force_list"]: + fun_name = "list" if isinstance(default, list): - default = ['space' if one == ' ' else one for one in default] - default = ', '.join(default) + default = ["space" if one == " " else one for one in default] + default = ", ".join(default) - output.write(f' :type: {fun_name}') - output.write('\n') + output.write(f" :type: {fun_name}") + output.write("\n") if fun_args != []: - output.write(f' :args: {fun_args}') - output.write('\n') - output.write(f' :default: {default}') - output.write('\n') + output.write(f" :args: {fun_args}") + output.write("\n") + output.write(f" :default: {default}") + output.write("\n") -with open('configspec.rst', 'w') as f: +with open("configspec.rst", "w") as f: for secname in sorted(spec): - f.write('\n') - heading = f'The [{secname}] section' - f.write(f'{heading}\n{ len(heading) * "~"}') - f.write('\n') + f.write("\n") + heading = f"The [{secname}] section" + f.write(f"{heading}\n{len(heading) * '~'}") + f.write("\n") comment = spec.comments[secname] - f.write('\n'.join([line[2:] for line in comment])) - f.write('\n') + f.write("\n".join([line[2:] for line in comment])) + f.write("\n") for key, comment in sorted(spec[secname].comments.items()): - if key == '__many__': + if key == "__many__": comment = spec[secname].comments[key] - f.write('\n'.join([line[2:] for line in comment])) - f.write('\n') - for key, comment in sorted(spec[secname]['__many__'].comments.items()): - write_section(spec[secname]['__many__'][key], secname, - key, comment, f) + f.write("\n".join([line[2:] for line in comment])) + f.write("\n") + for key, comment in sorted(spec[secname]["__many__"].comments.items()): + write_section(spec[secname]["__many__"][key], secname, key, comment, f) else: write_section(spec[secname][key], secname, key, comment, f) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinxfeed', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinxfeed", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['ytemplates'] +templates_path = ["ytemplates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'khal' -copyright = 'Copyright (c) 2013-2022 khal contributors' +project = "khal" +copyright = "Copyright (c) 2013-2022 khal contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -136,41 +133,41 @@ def write_section(specsection, secname, key, comment, output): # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['configspec.rst'] +exclude_patterns = ["configspec.rst"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False todo_include_todos = True @@ -179,105 +176,102 @@ def write_section(specsection, secname, key, comment, output): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - 'github_user': 'pimutils', - 'github_repo': 'khal', + "github_user": "pimutils", + "github_repo": "khal", } # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['ystatic'] +html_static_path = ["ystatic"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', - 'searchbox.html', - 'donate.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", + "searchbox.html", + "donate.html", ] } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'khaldoc' +htmlhelp_basename = "khaldoc" # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('man', 'khal', 'khal Documentation', - ['Christan Geier et al.'], 1) -] +man_pages = [("man", "khal", "khal Documentation", ["Christan Geier et al."], 1)] # If true, show URL addresses after external links. man_show_urls = True @@ -289,33 +283,39 @@ def write_section(specsection, secname, key, comment, output): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'khal', 'khal Documentation', - 'khal contributors', 'khal', 'A standards based calendar program', - 'Miscellaneous'), + ( + "index", + "khal", + "khal Documentation", + "khal contributors", + "khal", + "A standards based calendar program", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # sphinxfeed ------------------------------------------------------------------ -feed_base_url = 'https://lostpackets.de/khal' -feed_author = 'khal contributors' -feed_description = 'News feed for khal - a standards based calendar program' -feed_filename = 'index.rss' -feed_field_name = 'date' +feed_base_url = "https://lostpackets.de/khal" +feed_author = "khal contributors" +feed_description = "News feed for khal - a standards based calendar program" +feed_filename = "index.rss" +feed_field_name = "date" # intersphinx ----------------------------------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/khal/__init__.py b/khal/__init__.py index 6608d3774..36b60842b 100644 --- a/khal/__init__.py +++ b/khal/__init__.py @@ -22,17 +22,20 @@ try: from khal.version import version except ImportError: - version = 'invalid' + version = "invalid" import sys - sys.exit('Failed to find (autogenerated) version.py. This might be due to ' - 'using GitHub\'s tarballs or svn access. Either clone ' - 'from GitHub via git or get a tarball from PyPI.') -__productname__ = 'khal' + sys.exit( + "Failed to find (autogenerated) version.py. This might be due to " + "using GitHub's tarballs or svn access. Either clone " + "from GitHub via git or get a tarball from PyPI." + ) + +__productname__ = "khal" __version__ = version -__author__ = 'Christian Geier' -__copyright__ = 'Copyright (c) 2013-2022 khal contributors' -__author_email__ = 'khal@lostpackets.de' -__description__ = 'A standards based terminal calendar' -__license__ = 'Expat/MIT, see COPYING' -__homepage__ = 'https://lostpackets.de/khal/' +__author__ = "Christian Geier" +__copyright__ = "Copyright (c) 2013-2022 khal contributors" +__author_email__ = "khal@lostpackets.de" +__description__ = "A standards based terminal calendar" +__license__ = "Expat/MIT, see COPYING" +__homepage__ = "https://lostpackets.de/khal/" diff --git a/khal/__main__.py b/khal/__main__.py index 5c944cf38..2b5a6a92b 100644 --- a/khal/__main__.py +++ b/khal/__main__.py @@ -21,5 +21,5 @@ from khal.cli import main_khal -if __name__ == '__main__': +if __name__ == "__main__": main_khal() diff --git a/khal/calendar_display.py b/khal/calendar_display.py index 7f120c3e8..738afe3e1 100644 --- a/khal/calendar_display.py +++ b/khal/calendar_display.py @@ -29,14 +29,14 @@ from .terminal import colored from .utils import get_month_abbr_len -setlocale(LC_ALL, '') +setlocale(LC_ALL, "") def get_weekheader(firstweekday: int) -> str: try: - mylocale = '.'.join(getlocale(LC_TIME)) # type: ignore + mylocale = ".".join(getlocale(LC_TIME)) # type: ignore except TypeError: - mylocale = 'C' + mylocale = "C" _calendar = calendar.LocaleTextCalendar(firstweekday, locale=mylocale) # type: ignore return _calendar.formatweekheader(2) @@ -51,17 +51,14 @@ def getweeknumber(date: dt.date) -> int: def get_calendar_color(calendar: str, default_color: str, collection: CalendarCollection) -> str: - """Because multi-line lambdas would be un-Pythonic - """ - if collection._calendars[calendar]['color'] == '': + """Because multi-line lambdas would be un-Pythonic""" + if collection._calendars[calendar]["color"] == "": return default_color - return collection._calendars[calendar]['color'] + return collection._calendars[calendar]["color"] def get_color_list( - calendars: list[str], - default_color: str, - collection: CalendarCollection + calendars: list[str], default_color: str, collection: CalendarCollection ) -> list[str]: """Get the list of possible colors for the day, taking into account priority""" dcolors = [ @@ -89,28 +86,27 @@ def str_highlight_day( bold_for_light_color: bool, collection: CalendarCollection, ) -> str: - """returns a string with day highlighted according to configuration - """ + """returns a string with day highlighted according to configuration""" dstr = str(day.day).rjust(2) - if color == '': + if color == "": dcolors = get_color_list(calendars, default_color, collection) if len(dcolors) > 1: - if multiple == '' or (multiple_on_overflow and len(dcolors) == 2): + if multiple == "" or (multiple_on_overflow and len(dcolors) == 2): if hmethod == "foreground" or hmethod == "fg": - return colored(dstr[:1], fg=dcolors[0], - bold_for_light_color=bold_for_light_color) + \ - colored(dstr[1:], fg=dcolors[1], bold_for_light_color=bold_for_light_color) + return colored( + dstr[:1], fg=dcolors[0], bold_for_light_color=bold_for_light_color + ) + colored(dstr[1:], fg=dcolors[1], bold_for_light_color=bold_for_light_color) else: - return colored(dstr[:1], bg=dcolors[0], - bold_for_light_color=bold_for_light_color) + \ - colored(dstr[1:], bg=dcolors[1], bold_for_light_color=bold_for_light_color) + return colored( + dstr[:1], bg=dcolors[0], bold_for_light_color=bold_for_light_color + ) + colored(dstr[1:], bg=dcolors[1], bold_for_light_color=bold_for_light_color) else: dcolor = multiple else: dcolor = dcolors[0] or default_color else: dcolor = color - if dcolor != '': + if dcolor != "": if hmethod == "foreground" or hmethod == "fg": return colored(dstr, fg=dcolor, bold_for_light_color=bold_for_light_color) else: @@ -121,15 +117,15 @@ def str_highlight_day( def str_week( week: list[dt.date], today: dt.date, - collection: CalendarCollection | None=None, - hmethod: str | None=None, - default_color: str='', - multiple: str='', - multiple_on_overflow: bool=False, - color: str='', - highlight_event_days: bool=False, + collection: CalendarCollection | None = None, + hmethod: str | None = None, + default_color: str = "", + multiple: str = "", + multiple_on_overflow: bool = False, + color: str = "", + highlight_event_days: bool = False, locale=None, - bold_for_light_color: bool=True, + bold_for_light_color: bool = True, ) -> str: """returns a string representing one week, if for day == today color is reversed @@ -139,10 +135,10 @@ def str_week( :return: string, which if printed on terminal appears to have length 20, but may contain ascii escape sequences """ - strweek = '' + strweek = "" if highlight_event_days and collection is None: raise ValueError( - 'if `highlight_event_days` is True, `collection` must be a CalendarCollection' + "if `highlight_event_days` is True, `collection` must be a CalendarCollection" ) for day in week: if day == today: @@ -152,35 +148,42 @@ def str_week( devents = list(collection.get_calendars_on(day)) if len(devents) > 0: day_str = str_highlight_day( - day, devents, hmethod, default_color, multiple, - multiple_on_overflow, color, bold_for_light_color, + day, + devents, + hmethod, + default_color, + multiple, + multiple_on_overflow, + color, + bold_for_light_color, collection, ) else: day_str = str(day.day).rjust(2) else: day_str = str(day.day).rjust(2) - strweek = strweek + day_str + ' ' + strweek = strweek + day_str + " " return strweek -def vertical_month(month: int | None=None, - year: int | None=None, - today: dt.date | None=None, - weeknumber: bool | str=False, - count: int=3, - firstweekday: int=0, - monthdisplay: str='firstday', - collection=None, - hmethod: str='fg', - default_color: str='', - multiple: str='', - multiple_on_overflow: bool=False, - color: str='', - highlight_event_days: bool=False, - locale=None, - bold_for_light_color: bool=True, - ) -> list[str]: +def vertical_month( + month: int | None = None, + year: int | None = None, + today: dt.date | None = None, + weeknumber: bool | str = False, + count: int = 3, + firstweekday: int = 0, + monthdisplay: str = "firstday", + collection=None, + hmethod: str = "fg", + default_color: str = "", + multiple: str = "", + multiple_on_overflow: bool = False, + color: str = "", + highlight_event_days: bool = False, + locale=None, + bold_for_light_color: bool = True, +) -> list[str]: """ returns a list() of str() of weeks for a vertical arranged calendar @@ -204,31 +207,41 @@ def vertical_month(month: int | None=None, today = dt.date.today() khal = [] - w_number = ' ' if weeknumber == 'right' else '' + w_number = " " if weeknumber == "right" else "" calendar.setfirstweekday(firstweekday) weekheaders = get_weekheader(firstweekday) month_abbr_len = get_month_abbr_len() - khal.append(style(' ' * month_abbr_len + weekheaders + ' ' + w_number, bold=True)) + khal.append(style(" " * month_abbr_len + weekheaders + " " + w_number, bold=True)) _calendar = calendar.Calendar(firstweekday) for _ in range(count): for week in _calendar.monthdatescalendar(year, month): - if monthdisplay == 'firstday': + if monthdisplay == "firstday": new_month = len([day for day in week if day.day == 1]) else: new_month = len(week if week[0].day <= 7 else []) - strweek = str_week(week, today, collection, hmethod, default_color, - multiple, multiple_on_overflow, color, highlight_event_days, locale, - bold_for_light_color) + strweek = str_week( + week, + today, + collection, + hmethod, + default_color, + multiple, + multiple_on_overflow, + color, + highlight_event_days, + locale, + bold_for_light_color, + ) if new_month: m_name = style(calendar.month_abbr[week[6].month].ljust(month_abbr_len), bold=True) - elif weeknumber == 'left': + elif weeknumber == "left": m_name = style(str(getweeknumber(week[0])).center(month_abbr_len), bold=True) else: - m_name = ' ' * month_abbr_len - if weeknumber == 'right': - w_number = style(f'{getweeknumber(week[0]):2}', bold=True) + m_name = " " * month_abbr_len + if weeknumber == "right": + w_number = style(f"{getweeknumber(week[0]):2}", bold=True) else: - w_number = '' + w_number = "" sweek = m_name + strweek + w_number if sweek != khal[-1]: diff --git a/khal/cli.py b/khal/cli.py index 51ad78b92..9570a1e98 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -50,16 +50,17 @@ try: from setproctitle import setproctitle except ImportError: + def setproctitle(_): pass -click_log.basic_config('khal') +click_log.basic_config("khal") -days_option = click.option('--days', default=None, type=int, help='How many days to include.') -week_option = click.option('--week', '-w', help='Include all events in one week.', is_flag=True) -events_option = click.option('--events', default=None, type=int, help='How many events to include.') -dates_arg = click.argument('dates', nargs=-1) +days_option = click.option("--days", default=None, type=int, help="How many days to include.") +week_option = click.option("--week", "-w", help="Include all events in one week.", is_flag=True) +events_option = click.option("--events", default=None, type=int, help="How many events to include.") +dates_arg = click.argument("dates", nargs=-1) def time_args(f): @@ -71,15 +72,15 @@ def stringify_conf(conf): # really worth it out = [] for key, value in conf.items(): - out.append(f'[{key}]') + out.append(f"[{key}]") for subkey, subvalue in value.items(): if isinstance(subvalue, dict): - out.append(f' [[{subkey}]]') + out.append(f" [[{subkey}]]") for subsubkey, subsubvalue in subvalue.items(): - out.append(f' {subsubkey}: {subsubvalue}') + out.append(f" {subsubkey}: {subsubvalue}") else: - out.append(f' {subkey}: {subvalue}') - return '\n'.join(out) + out.append(f" {subkey}: {subvalue}") + return "\n".join(out) class _KhalGroup(click.Group): @@ -88,210 +89,206 @@ def list_commands(self, ctx): def get_command(self, ctx, name): if name in COMMANDS: - logger.debug(f'found command {name} as a plugin') + logger.debug(f"found command {name} as a plugin") return COMMANDS[name] return super().get_command(ctx, name) @click.group(cls=_KhalGroup) -@click_log.simple_verbosity_option('khal') +@click_log.simple_verbosity_option("khal") @global_options @click.pass_context def cli(ctx, config): # setting the process title so it looks nicer in ps # shows up as 'khal' under linux and as 'python: khal (python2.7)' # under FreeBSD, which is still nicer than the default - setproctitle('khal') + setproctitle("khal") if ctx.logfilepath: - logger = logging.getLogger('khal') + logger = logging.getLogger("khal") logger.handlers = [logging.FileHandler(ctx.logfilepath)] prepare_context(ctx, config) + @cli.command() @multi_calendar_option -@click.option('--format', '-f', - help=('The format of the events.')) -@click.option('--day-format', '-df', - help=('The format of the day line.')) +@click.option("--format", "-f", help=("The format of the events.")) +@click.option("--day-format", "-df", help=("The format of the day line.")) @click.option( - '--once', '-o', - help=('Print each event only once (even if it is repeated or spans multiple days).'), - is_flag=True) -@click.option('--notstarted', help=('Print only events that have not started.'), - is_flag=True) -@click.argument('DATERANGE', nargs=-1, required=False) + "--once", + "-o", + help=("Print each event only once (even if it is repeated or spans multiple days)."), + is_flag=True, +) +@click.option("--notstarted", help=("Print only events that have not started."), is_flag=True) +@click.argument("DATERANGE", nargs=-1, required=False) @click.pass_context -def calendar(ctx, include_calendar, exclude_calendar, daterange, once, - notstarted, format, day_format): - '''Print calendar with agenda.''' +def calendar( + ctx, include_calendar, exclude_calendar, daterange, once, notstarted, format, day_format +): + """Print calendar with agenda.""" try: rows = controllers.calendar( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), agenda_format=format, day_format=day_format, once=once, notstarted=notstarted, daterange=daterange, - conf=ctx.obj['conf'], - firstweekday=ctx.obj['conf']['locale']['firstweekday'], - locale=ctx.obj['conf']['locale'], - weeknumber=ctx.obj['conf']['locale']['weeknumbers'], - monthdisplay=ctx.obj['conf']['view']['monthdisplay'], - hmethod=ctx.obj['conf']['highlight_days']['method'], - default_color=ctx.obj['conf']['highlight_days']['default_color'], - multiple=ctx.obj['conf']['highlight_days']['multiple'], - multiple_on_overflow=ctx.obj['conf']['highlight_days']['multiple_on_overflow'], - color=ctx.obj['conf']['highlight_days']['color'], - highlight_event_days=ctx.obj['conf']['default']['highlight_event_days'], - bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color'], - env={"calendars": ctx.obj['conf']['calendars']} + conf=ctx.obj["conf"], + firstweekday=ctx.obj["conf"]["locale"]["firstweekday"], + locale=ctx.obj["conf"]["locale"], + weeknumber=ctx.obj["conf"]["locale"]["weeknumbers"], + monthdisplay=ctx.obj["conf"]["view"]["monthdisplay"], + hmethod=ctx.obj["conf"]["highlight_days"]["method"], + default_color=ctx.obj["conf"]["highlight_days"]["default_color"], + multiple=ctx.obj["conf"]["highlight_days"]["multiple"], + multiple_on_overflow=ctx.obj["conf"]["highlight_days"]["multiple_on_overflow"], + color=ctx.obj["conf"]["highlight_days"]["color"], + highlight_event_days=ctx.obj["conf"]["default"]["highlight_event_days"], + bold_for_light_color=ctx.obj["conf"]["view"]["bold_for_light_color"], + env={"calendars": ctx.obj["conf"]["calendars"]}, ) - click.echo('\n'.join(rows)) + click.echo("\n".join(rows)) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command("list") @multi_calendar_option -@click.option('--format', '-f', - help=('The format of the events.')) -@click.option('--day-format', '-df', - help=('The format of the day line.')) -@click.option('--once', '-o', is_flag=True, - help=('Print each event only once ' - '(even if it is repeated or spans multiple days).') - ) -@click.option('--notstarted', help=('Print only events that have not started.'), - is_flag=True) -@click.option('--json', help=("Fields to output in json"), multiple=True) -@click.argument('DATERANGE', nargs=-1, required=False, - metavar='[DATETIME [DATETIME | RANGE]]') +@click.option("--format", "-f", help=("The format of the events.")) +@click.option("--day-format", "-df", help=("The format of the day line.")) +@click.option( + "--once", + "-o", + is_flag=True, + help=("Print each event only once (even if it is repeated or spans multiple days)."), +) +@click.option("--notstarted", help=("Print only events that have not started."), is_flag=True) +@click.option("--json", help=("Fields to output in json"), multiple=True) +@click.argument("DATERANGE", nargs=-1, required=False, metavar="[DATETIME [DATETIME | RANGE]]") @click.pass_context -def klist(ctx, include_calendar, exclude_calendar, - daterange, once, notstarted, json, format, day_format): +def klist( + ctx, include_calendar, exclude_calendar, daterange, once, notstarted, json, format, day_format +): """List all events between a start (default: today) and (optional) end datetime.""" enabled_eventformatters = plugins.FORMATTERS - # TODO: register user given format string as a plugin - logger.debug(f'{enabled_eventformatters}') + # TODO: register user given format string as a plugin + logger.debug(f"{enabled_eventformatters}") try: event_column = controllers.khal_list( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), agenda_format=format, day_format=day_format, daterange=daterange, once=once, notstarted=notstarted, - conf=ctx.obj['conf'], - env={"calendars": ctx.obj['conf']['calendars']}, - json=json + conf=ctx.obj["conf"], + env={"calendars": ctx.obj["conf"]["calendars"]}, + json=json, ) if event_column: - click.echo('\n'.join(event_column)) + click.echo("\n".join(event_column)) else: - logger.debug('No events found') + logger.debug("No events found") except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @calendar_option -@click.option('--interactive', '-i', help=('Add event interactively'), - is_flag=True) -@click.option('--location', '-l', - help=('The location of the new event.')) -@click.option('--categories', '-g', - help=('The categories of the new event, comma separated.')) -@click.option('--repeat', '-r', - help=('Repeat event: daily, weekly, monthly or yearly.')) -@click.option('--until', '-u', - help=('Stop an event repeating on this date.')) -@click.option('--format', '-f', - help=('The format to print the event.')) -@click.option('--json', help=("Fields to output in json"), multiple=True) -@click.option('--alarms', '-m', - help=('Alarm times for the new event as DELTAs comma separated')) -@click.option('--url', help=("URI for the event.")) -@click.argument('info', metavar='[START [END | DELTA] [TIMEZONE] [SUMMARY] [:: DESCRIPTION]]', - nargs=-1) +@click.option("--interactive", "-i", help=("Add event interactively"), is_flag=True) +@click.option("--location", "-l", help=("The location of the new event.")) +@click.option("--categories", "-g", help=("The categories of the new event, comma separated.")) +@click.option("--repeat", "-r", help=("Repeat event: daily, weekly, monthly or yearly.")) +@click.option("--until", "-u", help=("Stop an event repeating on this date.")) +@click.option("--format", "-f", help=("The format to print the event.")) +@click.option("--json", help=("Fields to output in json"), multiple=True) +@click.option("--alarms", "-m", help=("Alarm times for the new event as DELTAs comma separated")) +@click.option("--url", help=("URI for the event.")) +@click.argument( + "info", metavar="[START [END | DELTA] [TIMEZONE] [SUMMARY] [:: DESCRIPTION]]", nargs=-1 +) @click.pass_context -def new(ctx, calendar, info, location, categories, repeat, until, alarms, url, format, - json, interactive): - '''Create a new event from arguments. +def new( + ctx, calendar, info, location, categories, repeat, until, alarms, url, format, json, interactive +): + """Create a new event from arguments. START and END can be either dates, times or datetimes, please have a look at the man page for details. Everything that cannot be interpreted as a (date)time or a timezone is assumed to be the event's summary, if two colons (::) are present, everything behind them is taken as the event's description. - ''' + """ if not info and not interactive: - raise click.BadParameter( - 'no details provided, did you mean to use --interactive/-i?' - ) + raise click.BadParameter("no details provided, did you mean to use --interactive/-i?") - calendar = calendar or ctx.obj['conf']['default']['default_calendar'] + calendar = calendar or ctx.obj["conf"]["default"]["default_calendar"] if calendar is None: if interactive: while calendar is None: - calendar = click.prompt('calendar') - if calendar == '?': - for calendar in ctx.obj['conf']['calendars']: + calendar = click.prompt("calendar") + if calendar == "?": + for calendar in ctx.obj["conf"]["calendars"]: click.echo(calendar) calendar = None - elif calendar not in ctx.obj['conf']['calendars']: - click.echo('unknown calendar enter ? for list') + elif calendar not in ctx.obj["conf"]["calendars"]: + click.echo("unknown calendar enter ? for list") calendar = None else: raise click.BadParameter( - 'No default calendar is configured, ' - 'please provide one explicitly.' + "No default calendar is configured, please provide one explicitly." ) try: new_func = controllers.new_from_string if interactive: new_func = controllers.new_interactive new_func( - build_collection(ctx.obj['conf'], ctx.obj.get('calendar_selection', None)), + build_collection(ctx.obj["conf"], ctx.obj.get("calendar_selection", None)), calendar, - ctx.obj['conf'], - info=' '.join(info), + ctx.obj["conf"], + info=" ".join(info), location=location, categories=categories, repeat=repeat, - env={"calendars": ctx.obj['conf']['calendars']}, + env={"calendars": ctx.obj["conf"]["calendars"]}, until=until, alarms=alarms, url=url, format=format, - json=json + json=json, ) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) -@cli.command('import') -@click.option('--include-calendar', '-a', help=('The calendar to use.'), - callback=_select_one_calendar_callback, multiple=True) -@click.option('--batch', help=('do not ask for any confirmation.'), - is_flag=True) -@click.option('--random_uid', '-r', help=('Select a random uid.'), - is_flag=True) -@click.argument('ics', type=click.File('rb'), nargs=-1) -@click.option('--format', '-f', help=('The format to print the event.')) + +@cli.command("import") +@click.option( + "--include-calendar", + "-a", + help=("The calendar to use."), + callback=_select_one_calendar_callback, + multiple=True, +) +@click.option("--batch", help=("do not ask for any confirmation."), is_flag=True) +@click.option("--random_uid", "-r", help=("Select a random uid."), is_flag=True) +@click.argument("ics", type=click.File("rb"), nargs=-1) +@click.option("--format", "-f", help=("The format to print the event.")) @click.pass_context def import_ics(ctx, ics, include_calendar, batch, random_uid, format): - '''Import events from an .ics file (or stdin). + """Import events from an .ics file (or stdin). If an event with the same UID is already present in the (implicitly) selected calendar import will ask before updating (i.e. overwriting) @@ -302,19 +299,25 @@ def import_ics(ctx, ics, include_calendar, batch, random_uid, format): to choose a calendar. You can either enter the number printed behind each calendar's name or any unique prefix of a calendar's name. - ''' + """ if include_calendar: - ctx.obj['calendar_selection'] = {include_calendar, } - collection = build_collection(ctx.obj['conf'], ctx.obj.get('calendar_selection', None)) - if batch and len(collection.names) > 1 and \ - ctx.obj['conf']['default']['default_calendar'] is None: + ctx.obj["calendar_selection"] = { + include_calendar, + } + collection = build_collection(ctx.obj["conf"], ctx.obj.get("calendar_selection", None)) + if ( + batch + and len(collection.names) > 1 + and ctx.obj["conf"]["default"]["default_calendar"] is None + ): raise click.UsageError( - 'When using batch import, please specify a calendar to import ' - 'into or set the `default_calendar` in the config file.') + "When using batch import, please specify a calendar to import " + "into or set the `default_calendar` in the config file." + ) rvalue = 0 # Default to stdin: if not ics: - ics_strs = ((sys.stdin.read(), 'stdin'),) + ics_strs = ((sys.stdin.read(), "stdin"),) if not batch: def isatty(_file): @@ -323,10 +326,10 @@ def isatty(_file): except Exception: return False - if isatty(sys.stdin) and os.stat('/dev/tty').st_mode & stat.S_IFCHR > 0: - sys.stdin = open('/dev/tty') + if isatty(sys.stdin) and os.stat("/dev/tty").st_mode & stat.S_IFCHR > 0: + sys.stdin = open("/dev/tty") else: - logger.warning('/dev/tty does not exist, importing might not work') + logger.warning("/dev/tty does not exist, importing might not work") else: ics_strs = ((ics_file.read(), ics_file.name) for ics_file in ics) @@ -334,11 +337,11 @@ def isatty(_file): try: controllers.import_ics( collection, - ctx.obj['conf'], + ctx.obj["conf"], ics=ics_str, batch=batch, random_uid=random_uid, - env={"calendars": ctx.obj['conf']['calendars']}, + env={"calendars": ctx.obj["conf"]["calendars"]}, ) except FatalError as error: logger.debug(error, exc_info=True) @@ -349,215 +352,226 @@ def isatty(_file): rvalue = 1 sys.exit(rvalue) + @cli.command() @multi_calendar_option @mouse_option @click.pass_context def interactive(ctx, include_calendar, exclude_calendar, mouse): - '''Interactive UI. Also launchable via `ikhal`.''' + """Interactive UI. Also launchable via `ikhal`.""" if mouse is not None: - ctx.obj['conf']['default']['enable_mouse'] = mouse + ctx.obj["conf"]["default"]["enable_mouse"] = mouse controllers.interactive( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), - ctx.obj['conf'] + ctx.obj["conf"], ) + @click.command() @global_options @multi_calendar_option @mouse_option @click.pass_context def interactive_cli(ctx, config, include_calendar, exclude_calendar, mouse): - '''Interactive UI. Also launchable via `khal interactive`.''' + """Interactive UI. Also launchable via `khal interactive`.""" prepare_context(ctx, config) if mouse is not None: - ctx.obj['conf']['default']['enable_mouse'] = mouse + ctx.obj["conf"]["default"]["enable_mouse"] = mouse controllers.interactive( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), - ctx.obj['conf'] + ctx.obj["conf"], ) + @cli.command() @multi_calendar_option @click.pass_context def printcalendars(ctx, include_calendar, exclude_calendar): - '''List all calendars.''' + """List all calendars.""" try: - click.echo('\n'.join(build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) - ).names)) + click.echo( + "\n".join( + build_collection( + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) + ).names + ) + ) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @click.pass_context def printformats(ctx): - '''Print a date in all formats. + """Print a date in all formats. Print the date 2013-12-21 21:45 in all configured date(time) formats to check if these locale settings are configured to ones - liking.''' + liking.""" time = dt.datetime(2013, 12, 21, 21, 45) try: for strftime_format in [ - 'longdatetimeformat', 'datetimeformat', 'longdateformat', - 'dateformat', 'timeformat']: - dt_str = time.strftime(ctx.obj['conf']['locale'][strftime_format]) - click.echo(f'{strftime_format}: {dt_str}') + "longdatetimeformat", + "datetimeformat", + "longdateformat", + "dateformat", + "timeformat", + ]: + dt_str = time.strftime(ctx.obj["conf"]["locale"][strftime_format]) + click.echo(f"{strftime_format}: {dt_str}") except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() -@click.argument('ics', type=click.File('rb'), required=False) -@click.option('--format', '-f', - help=('The format to print the event.')) +@click.argument("ics", type=click.File("rb"), required=False) +@click.option("--format", "-f", help=("The format to print the event.")) @click.pass_context def printics(ctx, ics, format): - '''Print an ics file (or read from stdin) without importing it. + """Print an ics file (or read from stdin) without importing it. - Just print the ics file, do nothing else.''' + Just print the ics file, do nothing else.""" try: if ics: ics_str = ics.read() name = ics.name else: ics_str = sys.stdin.read() - name = 'stdin input' - controllers.print_ics(ctx.obj['conf'], name, ics_str, format) + name = "stdin input" + controllers.print_ics(ctx.obj["conf"], name, ics_str, format) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @multi_calendar_option -@click.option('--format', '-f', - help=('The format of the events.')) -@click.option('--json', help=("Fields to output in json"), multiple=True) -@click.argument('search_string') +@click.option("--format", "-f", help=("The format of the events.")) +@click.option("--json", help=("Fields to output in json"), multiple=True) +@click.argument("search_string") @click.pass_context def search(ctx, format, json, search_string, include_calendar, exclude_calendar): - '''Search for events matching SEARCH_STRING. + """Search for events matching SEARCH_STRING. For recurring events, only the master event and different overwritten events are shown. - ''' + """ # TODO support for time ranges, location, description etc if format is None: - format = ctx.obj['conf']['view']['event_format'] + format = ctx.obj["conf"]["view"]["event_format"] try: collection = build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ) events = sorted(collection.search(search_string)) event_column = [] term_width, _ = get_terminal_size() now = dt.datetime.now() - env = {"calendars": ctx.obj['conf']['calendars']} + env = {"calendars": ctx.obj["conf"]["calendars"]} if len(json) == 0: formatter = human_formatter(format) else: formatter = json_formatter(json) for event in events: - desc = textwrap.wrap(formatter( - event.attributes(relative_to=now, env=env)), term_width) + desc = textwrap.wrap(formatter(event.attributes(relative_to=now, env=env)), term_width) event_column.extend( - [colored(d, event.color, - bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) - for d in desc] + [ + colored( + d, + event.color, + bold_for_light_color=ctx.obj["conf"]["view"]["bold_for_light_color"], + ) + for d in desc + ] ) if event_column: - click.echo('\n'.join(event_column)) + click.echo("\n".join(event_column)) else: - logger.debug('No events found') + logger.debug("No events found") except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @multi_calendar_option -@click.option('--format', '-f', - help=('The format of the events.')) -@click.option('--show-past', help=('Show events that have already occurred as options'), - is_flag=True) -@click.argument('search_string', nargs=-1) +@click.option("--format", "-f", help=("The format of the events.")) +@click.option( + "--show-past", help=("Show events that have already occurred as options"), is_flag=True +) +@click.argument("search_string", nargs=-1) @click.pass_context def edit(ctx, format, search_string, show_past, include_calendar, exclude_calendar): - '''Interactively edit (or delete) events matching the search string.''' + """Interactively edit (or delete) events matching the search string.""" try: controllers.edit( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), - ' '.join(search_string), + " ".join(search_string), format=format, allow_past=show_past, - locale=ctx.obj['conf']['locale'], - conf=ctx.obj['conf'] + locale=ctx.obj["conf"]["locale"], + conf=ctx.obj["conf"], ) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @multi_calendar_option -@click.option('--format', '-f', - help=('The format of the events.')) -@click.option('--day-format', '-df', - help=('The format of the day line.')) -@click.option('--notstarted', help=('Print only events that have not started'), - is_flag=True) -@click.option('--json', help=("Fields to output in json"), multiple=True) -@click.argument('DATETIME', nargs=-1, required=False, metavar='[[START DATE] TIME | now]') +@click.option("--format", "-f", help=("The format of the events.")) +@click.option("--day-format", "-df", help=("The format of the day line.")) +@click.option("--notstarted", help=("Print only events that have not started"), is_flag=True) +@click.option("--json", help=("Fields to output in json"), multiple=True) +@click.argument("DATETIME", nargs=-1, required=False, metavar="[[START DATE] TIME | now]") @click.pass_context def at(ctx, datetime, notstarted, format, day_format, json, include_calendar, exclude_calendar): - '''Print all events at a specific datetime (defaults to now).''' + """Print all events at a specific datetime (defaults to now).""" if not datetime: datetime = ("now",) if format is None: - format = ctx.obj['conf']['view']['event_format'] + format = ctx.obj["conf"]["view"]["event_format"] try: rows = controllers.khal_list( build_collection( - ctx.obj['conf'], - multi_calendar_select(ctx, include_calendar, exclude_calendar) + ctx.obj["conf"], multi_calendar_select(ctx, include_calendar, exclude_calendar) ), agenda_format=format, day_format=day_format, datepoint=list(datetime), once=True, notstarted=notstarted, - conf=ctx.obj['conf'], - env={"calendars": ctx.obj['conf']['calendars']}, - json=json + conf=ctx.obj["conf"], + env={"calendars": ctx.obj["conf"]["calendars"]}, + json=json, ) if rows: - click.echo('\n'.join(rows)) + click.echo("\n".join(rows)) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) + @cli.command() @click.pass_context def configure(ctx): """Helper for initial configuration of khal.""" from . import configwizard + try: configwizard.configwizard() except FatalError as error: diff --git a/khal/cli_utils.py b/khal/cli_utils.py index 9464fdaf9..5b0095a90 100644 --- a/khal/cli_utils.py +++ b/khal/cli_utils.py @@ -29,13 +29,13 @@ from .exceptions import FatalError from .settings import InvalidSettingsError, NoConfigFile, get_config -logger = logging.getLogger('khal') -click_log.basic_config('khal') +logger = logging.getLogger("khal") +click_log.basic_config("khal") -days_option = click.option('--days', default=None, type=int, help='How many days to include.') -week_option = click.option('--week', '-w', help='Include all events in one week.', is_flag=True) -events_option = click.option('--events', default=None, type=int, help='How many events to include.') -dates_arg = click.argument('dates', nargs=-1) +days_option = click.option("--days", default=None, type=int, help="How many days to include.") +week_option = click.option("--week", "-w", help="Include all events in one week.", is_flag=True) +events_option = click.option("--events", default=None, type=int, help="How many events to include.") +dates_arg = click.argument("dates", nargs=-1) def time_args(f): @@ -44,26 +44,26 @@ def time_args(f): def multi_calendar_select(ctx, include_calendars, exclude_calendars): if include_calendars and exclude_calendars: - raise click.UsageError('Can\'t use both -a and -d.') + raise click.UsageError("Can't use both -a and -d.") selection = set() if include_calendars: for cal_name in include_calendars: - if cal_name not in ctx.obj['conf']['calendars']: + if cal_name not in ctx.obj["conf"]["calendars"]: raise click.BadParameter( - f'Unknown calendar {cal_name}, run `khal printcalendars` ' - 'to get a list of all configured calendars.' + f"Unknown calendar {cal_name}, run `khal printcalendars` " + "to get a list of all configured calendars." ) selection.update(include_calendars) elif exclude_calendars: - selection.update(ctx.obj['conf']['calendars'].keys()) + selection.update(ctx.obj["conf"]["calendars"].keys()) for cal_name in exclude_calendars: if cal_name not in selection: raise click.BadParameter( - f'Unknown calendar {cal_name}, run `khal printcalendars` ' - 'to get a list of all configured calendars.' + f"Unknown calendar {cal_name}, run `khal printcalendars` " + "to get a list of all configured calendars." ) selection.discard(cal_name) @@ -71,22 +71,27 @@ def multi_calendar_select(ctx, include_calendars, exclude_calendars): def multi_calendar_option(f): - a = click.option('--include-calendar', '-a', multiple=True, metavar='CAL', - help=('Include the given calendar. Can be specified ' - 'multiple times.')) - d = click.option('--exclude-calendar', '-d', multiple=True, metavar='CAL', - help=('Exclude the given calendar. Can be specified ' - 'multiple times.')) + a = click.option( + "--include-calendar", + "-a", + multiple=True, + metavar="CAL", + help=("Include the given calendar. Can be specified multiple times."), + ) + d = click.option( + "--exclude-calendar", + "-d", + multiple=True, + metavar="CAL", + help=("Exclude the given calendar. Can be specified multiple times."), + ) return d(a(f)) def mouse_option(f): o = click.option( - '--mouse/--no-mouse', - is_flag=True, - default=None, - help='Disable mouse in interactive UI' + "--mouse/--no-mouse", is_flag=True, default=None, help="Disable mouse in interactive UI" ) return o(f) @@ -95,23 +100,24 @@ def _select_one_calendar_callback(ctx, option, calendar): if isinstance(calendar, tuple): if len(calendar) > 1: raise click.UsageError( - 'Can\'t use "--include-calendar" / "-a" more than once for this command.') + 'Can\'t use "--include-calendar" / "-a" more than once for this command.' + ) elif len(calendar) == 1: calendar = calendar[0] return _calendar_select_callback(ctx, option, calendar) def _calendar_select_callback(ctx, option, calendar): - if calendar and calendar not in ctx.obj['conf']['calendars']: + if calendar and calendar not in ctx.obj["conf"]["calendars"]: raise click.BadParameter( - f'Unknown calendar {calendar}, run `khal printcalendars` to get a ' - 'list of all configured calendars.' + f"Unknown calendar {calendar}, run `khal printcalendars` to get a " + "list of all configured calendars." ) return calendar def calendar_option(f): - return click.option('--calendar', '-a', metavar='CAL', callback=_calendar_select_callback)(f) + return click.option("--calendar", "-a", metavar="CAL", callback=_calendar_select_callback)(f) def global_options(f): @@ -122,26 +128,28 @@ def logfile_callback(ctx, option, path): ctx.logfilepath = path config = click.option( - '--config', '-c', - help='The config file to use.', - default=None, metavar='PATH' + "--config", "-c", help="The config file to use.", default=None, metavar="PATH" ) color = click.option( - '--color/--no-color', - help=('Use colored/uncolored output. Default is to only enable colors ' - 'when not part of a pipe.'), - expose_value=False, default=None, - callback=color_callback + "--color/--no-color", + help=( + "Use colored/uncolored output. Default is to only enable colors " + "when not part of a pipe." + ), + expose_value=False, + default=None, + callback=color_callback, ) logfile = click.option( - '--logfile', '-l', - help='The logfile to use [defaults to stdout]', + "--logfile", + "-l", + help="The logfile to use [defaults to stdout]", type=click.Path(), callback=logfile_callback, default=None, expose_value=False, - metavar='LOGFILE', + metavar="LOGFILE", ) version = click.version_option(version=__version__) @@ -153,62 +161,65 @@ def build_collection(conf, selection): """build and return a khalendar.CalendarCollection from the configuration""" try: props = {} - for name, cal in conf['calendars'].items(): + for name, cal in conf["calendars"].items(): if selection is None or name in selection: props[name] = { - 'name': name, - 'path': cal['path'], - 'readonly': cal['readonly'], - 'color': cal['color'], - 'priority': cal['priority'], - 'ctype': cal['type'], - 'addresses': cal['addresses'] if 'addresses' in cal else '', + "name": name, + "path": cal["path"], + "readonly": cal["readonly"], + "color": cal["color"], + "priority": cal["priority"], + "ctype": cal["type"], + "addresses": cal["addresses"] if "addresses" in cal else "", } collection = khalendar.CalendarCollection( calendars=props, - color=conf['highlight_days']['color'], - locale=conf['locale'], - dbpath=conf['sqlite']['path'], - hmethod=conf['highlight_days']['method'], - default_color=conf['highlight_days']['default_color'], - multiple=conf['highlight_days']['multiple'], - multiple_on_overflow=conf['highlight_days']['multiple_on_overflow'], - highlight_event_days=conf['default']['highlight_event_days'], + color=conf["highlight_days"]["color"], + locale=conf["locale"], + dbpath=conf["sqlite"]["path"], + hmethod=conf["highlight_days"]["method"], + default_color=conf["highlight_days"]["default_color"], + multiple=conf["highlight_days"]["multiple"], + multiple_on_overflow=conf["highlight_days"]["multiple_on_overflow"], + highlight_event_days=conf["default"]["highlight_event_days"], ) except FatalError as error: logger.debug(error, exc_info=True) logger.fatal(error) sys.exit(1) - collection._default_calendar_name = conf['default']['default_calendar'] + collection._default_calendar_name = conf["default"]["default_calendar"] return collection class _NoConfig: def __getitem__(self, key): logger.fatal( - 'Cannot find a config file. If you have no configuration file ' - 'yet, you might want to run `khal configure`.') + "Cannot find a config file. If you have no configuration file " + "yet, you might want to run `khal configure`." + ) sys.exit(1) def prepare_context(ctx, config): assert ctx.obj is None - logger.debug('khal %s', __version__) + logger.debug("khal %s", __version__) try: conf = get_config(config) except NoConfigFile: conf = _NoConfig() except InvalidSettingsError: - logger.info('If your configuration file used to work, please have a ' - 'look at the Changelog to see what changed.') + logger.info( + "If your configuration file used to work, please have a " + "look at the Changelog to see what changed." + ) sys.exit(1) else: - logger.debug('Using config:') + logger.debug("Using config:") logger.debug(stringify_conf(conf)) - ctx.obj = {'conf_path': config, 'conf': conf} + ctx.obj = {"conf_path": config, "conf": conf} def stringify_conf(conf): @@ -216,12 +227,12 @@ def stringify_conf(conf): # really worth it out = [] for key, value in conf.items(): - out.append(f'[{key}]') + out.append(f"[{key}]") for subkey, subvalue in value.items(): if isinstance(subvalue, dict): - out.append(f' [[{subkey}]]') + out.append(f" [[{subkey}]]") for subsubkey, subsubvalue in subvalue.items(): - out.append(f' {subsubkey}: {subsubvalue}') + out.append(f" {subsubkey}: {subsubvalue}") else: - out.append(f' {subkey}: {subvalue}') - return '\n'.join(out) + out.append(f" {subkey}: {subvalue}") + return "\n".join(out) diff --git a/khal/configwizard.py b/khal/configwizard.py index bfa203cf5..5d212edff 100644 --- a/khal/configwizard.py +++ b/khal/configwizard.py @@ -35,15 +35,15 @@ from .exceptions import FatalError from .settings import find_configuration_file, utils -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") def compressuser(path): """Abbreviate home directory to '~', for presenting a path.""" - home = normpath(expanduser('~')) + home = normpath(expanduser("~")) path = normpath(path) if path.startswith(home): - path = '~' + path[len(home):] + path = "~" + path[len(home) :] return path @@ -51,17 +51,17 @@ def validate_int(input, min_value, max_value): try: number = int(input) except ValueError: - raise UsageError('Input must be an integer') + raise UsageError("Input must be an integer") if min_value <= number <= max_value: return number else: - raise UsageError(f'Input must be between {min_value} and {max_value}') + raise UsageError(f"Input must be between {min_value} and {max_value}") DATE_FORMAT_INFO = [ - ('Year', ['%Y', '%y']), - ('Month', ['%m', '%B', '%b']), - ('Day', ['%d', '%a', '%A']) + ("Year", ["%Y", "%y"]), + ("Month", ["%m", "%B", "%b"]), + ("Day", ["%d", "%a", "%A"]), ] @@ -71,60 +71,59 @@ def present_date_format_info(example_date): for title, formats in DATE_FORMAT_INFO: newcol = [title] for f in formats: - newcol.append(f'{f}={example_date.strftime(f)}') + newcol.append(f"{f}={example_date.strftime(f)}") widths.append(max(len(s) for s in newcol) + 2) columns.append(newcol) - print('Common fields for date formatting:') - for row in zip_longest(*columns, fillvalue=''): - print(''.join(s.ljust(w) for (s, w) in zip(row, widths))) + print("Common fields for date formatting:") + for row in zip_longest(*columns, fillvalue=""): + print("".join(s.ljust(w) for (s, w) in zip(row, widths))) - print('More info: ' - 'https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior') + print( + "More info: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior" + ) def choose_datetime_format(): """query user for their date format of choice""" choices = [ - ('year-month-day', '%Y-%m-%d'), - ('day/month/year', '%d/%m/%Y'), - ('month/day/year', '%m/%d/%Y'), + ("year-month-day", "%Y-%m-%d"), + ("day/month/year", "%d/%m/%Y"), + ("month/day/year", "%m/%d/%Y"), ] validate = partial(validate_int, min_value=0, max_value=3) today = dt.date.today() print("What ordering of year, month, date do you want to use?") for num, (desc, fmt) in enumerate(choices): - print(f'[{num}] {desc} (today: {today.strftime(fmt)})') - print('[3] Custom') + print(f"[{num}] {desc} (today: {today.strftime(fmt)})") + print("[3] Custom") choice_no = prompt("Please choose one of the above options", value_proc=validate) if choice_no == 3: present_date_format_info(today) - dateformat = prompt('Make your date format') + dateformat = prompt("Make your date format") else: dateformat = choices[choice_no][1] - print(f"Date format: {dateformat} " - f"(today as an example: {today.strftime(dateformat)})") + print(f"Date format: {dateformat} (today as an example: {today.strftime(dateformat)})") return dateformat def choose_time_format(): """query user for their time format of choice""" - choices = ['%H:%M', '%I:%M %p'] + choices = ["%H:%M", "%I:%M %p"] print("What timeformat do you want to use?") print("[0] 24 hour clock (recommended)\n[1] 12 hour clock") validate = partial(validate_int, min_value=0, max_value=1) prompt_text = "Please choose one of the above options" timeformat = choices[prompt(prompt_text, default=0, value_proc=validate)] now = dt.datetime.now() - print(f"Time format: {timeformat} " - f"(current time as an example: {now.strftime(timeformat)})") + print(f"Time format: {timeformat} (current time as an example: {now.strftime(timeformat)})") return timeformat def get_collection_names_from_vdirs(vdirs): names = [] for name, path, vtype in sorted(vdirs or ()): - if vtype == 'discover': + if vtype == "discover": for vpath in utils.get_all_vdirs(utils.expand_path(path)): names.append(utils.get_unique_name(vpath, names)) else: @@ -157,19 +156,18 @@ def get_vdirs_from_vdirsyncer_config(): try: vdir_config = config.load_config() except UserError as error: - print("Sorry, loading vdirsyncer config failed with the following " - "error message:") + print("Sorry, loading vdirsyncer config failed with the following error message:") print(error) return None vdirs = [] for storage in vdir_config.storages.values(): - if storage['type'] == 'filesystem': + if storage["type"] == "filesystem": # TODO detect type of storage properly - path = storage['path'] - if path[-1] != '/': - path += '/' - path += '*' - vdirs.append((storage['instance_name'], path, 'discover')) + path = storage["path"] + if path[-1] != "/": + path += "/" + path += "*" + vdirs.append((storage["instance_name"], path, "discover")) if vdirs == []: print("No calendars found from vdirsyncer.") return None @@ -189,13 +187,13 @@ def find_vdir(): if synced_vdirs: print(f"Found {len(synced_vdirs)} calendars from vdirsyncer") for name, path, _ in synced_vdirs: - print(f' {name}: {compressuser(path)}') + print(f" {name}: {compressuser(path)}") if confirm("Use these calendars for khal?", default=True): return synced_vdirs vdir_path = prompt("Enter the path to a vdir calendar") vdir_path = normpath(expanduser(expandvars(vdir_path))) - return [('private', vdir_path, 'calendar')] + return [("private", vdir_path, "calendar")] def create_vdir(names=None): @@ -204,21 +202,21 @@ def create_vdir(names=None): :param names: names of existing vdirs """ names = names or [] - name = 'private' + name = "private" while True: - path = join(xdg.BaseDirectory.xdg_data_home, 'khal', 'calendars', name) + path = join(xdg.BaseDirectory.xdg_data_home, "khal", "calendars", name) path = normpath(expanduser(expandvars(path))) if name not in names and not exists(path): break else: - name += '1' + name += "1" try: makedirs(path) except OSError as error: print(f"Could not create directory {path} because of {error}. Exiting") raise print(f"Created new vdir at {path}") - return [(name, path, 'calendar')] + return [(name, path, "calendar")] # Parsing and then dumping config naively could lose comments and formatting. @@ -253,48 +251,45 @@ def vdirsyncer_config_path(): There may or may not already be a file at the returned path. """ - fname = environ.get('VDIRSYNCER_CONFIG', None) + fname = environ.get("VDIRSYNCER_CONFIG", None) if fname is None: - fname = normpath(expanduser('~/.vdirsyncer/config')) + fname = normpath(expanduser("~/.vdirsyncer/config")) if not exists(fname): - xdg_config_dir = environ.get('XDG_CONFIG_HOME', - normpath(expanduser('~/.config/'))) - fname = join(xdg_config_dir, 'vdirsyncer/config') + xdg_config_dir = environ.get("XDG_CONFIG_HOME", normpath(expanduser("~/.config/"))) + fname = join(xdg_config_dir, "vdirsyncer/config") return fname def get_available_pairno(): - """Find N so that 'khal_pair_N' is not already used in vdirsyncer config - """ + """Find N so that 'khal_pair_N' is not already used in vdirsyncer config""" try: from vdirsyncer.cli import config except ImportError: raise FatalError("vdirsyncer config exists, but couldn't import vdirsyncer.") vdir_config = config.load_config() pairno = 1 - while f'khal_pair_{pairno}' in vdir_config.pairs: + while f"khal_pair_{pairno}" in vdir_config.pairs: pairno += 1 return pairno def create_synced_vdir(): - """Create a new vdir, and set up vdirsyncer to sync it. - """ + """Create a new vdir, and set up vdirsyncer to sync it.""" name, path, _ = create_vdir()[0] - caldav_url = prompt('CalDAV URL') - username = prompt('Username') - password = prompt('Password', hide_input=True) + caldav_url = prompt("CalDAV URL") + username = prompt("Username") + password = prompt("Password", hide_input=True) vds_config = vdirsyncer_config_path() if exists(vds_config): # We are adding a pair to vdirsyncer config - mode = 'a' + mode = "a" new_file = False pairno = get_available_pairno() else: # We're setting up vdirsyncer for the first time - mode = 'w' + mode = "w" new_file = True pairno = 1 @@ -302,33 +297,37 @@ def create_synced_vdir(): if new_file: f.write(VDS_CONFIG_START) - f.write(VDS_CONFIG_TEMPLATE.format( - local_path=json.dumps(dirname(path)), - url=json.dumps(caldav_url), - username=json.dumps(username), - password=json.dumps(password), - pairno=pairno, - )) + f.write( + VDS_CONFIG_TEMPLATE.format( + local_path=json.dumps(dirname(path)), + url=json.dumps(caldav_url), + username=json.dumps(username), + password=json.dumps(password), + pairno=pairno, + ) + ) start_syncing() - return [(name, path, 'calendar')] + return [(name, path, "calendar")] def start_syncing(): """Run vdirsyncer to sync the newly created vdir with the remote.""" print("Syncing calendar...") try: - exit_code = call(['vdirsyncer', 'discover']) + exit_code = call(["vdirsyncer", "discover"]) except FileNotFoundError: print("Could not find vdirsyncer - please set it up manually") else: if exit_code == 0: - exit_code = call(['vdirsyncer', 'sync']) + exit_code = call(["vdirsyncer", "sync"]) if exit_code != 0: print("vdirsyncer failed - please set up sync manually") # Add code here to check platform and automatically set up cron or similar - print("Please set up your system to run 'vdirsyncer sync' periodically, " - "using cron or similar mechanisms.") + print( + "Please set up your system to run 'vdirsyncer sync' periodically, " + "using cron or similar mechanisms." + ) def choose_vdir_calendar(): @@ -336,35 +335,37 @@ def choose_vdir_calendar(): choices = [ ("Create a new calendar on this computer", create_vdir), ("Use a calendar already on this computer (vdir format)", find_vdir), - ("Sync a calendar from the internet (CalDAV format, requires vdirsyncer)", - create_synced_vdir), + ( + "Sync a calendar from the internet (CalDAV format, requires vdirsyncer)", + create_synced_vdir, + ), ] validate = partial(validate_int, min_value=0, max_value=2) for i, (desc, _func) in enumerate(choices): - print(f'[{i}] {desc}') - choice_no = prompt("Please choose one of the above options", - value_proc=validate) + print(f"[{i}] {desc}") + choice_no = prompt("Please choose one of the above options", value_proc=validate) return choices[choice_no][1]() def create_config(vdirs, dateformat, timeformat, default_calendar=None): - config = ['[calendars]'] + config = ["[calendars]"] for name, path, type_ in sorted(vdirs or ()): - config.append(f'\n[[{name}]]') - config.append(f'path = {path}') - config.append(f'type = {type_}') - - config.append('\n[locale]') - config.append(f'timeformat = {timeformat}\n' - f'dateformat = {dateformat}\n' - f'longdateformat = {dateformat}\n' - f'datetimeformat = {dateformat} {timeformat}\n' - f'longdatetimeformat = {dateformat} {timeformat}\n' - ) + config.append(f"\n[[{name}]]") + config.append(f"path = {path}") + config.append(f"type = {type_}") + + config.append("\n[locale]") + config.append( + f"timeformat = {timeformat}\n" + f"dateformat = {dateformat}\n" + f"longdateformat = {dateformat}\n" + f"datetimeformat = {dateformat} {timeformat}\n" + f"longdatetimeformat = {dateformat} {timeformat}\n" + ) if default_calendar: - config.append('[default]') - config.append(f'default_calendar = {default_calendar}\n') - config = '\n'.join(config) + config.append("[default]") + config.append(f"default_calendar = {default_calendar}\n") + config = "\n".join(config) return config @@ -375,7 +376,8 @@ def configwizard(): logger.fatal(f"Found an existing config file at {compressuser(config_file)}.") logger.fatal( "If you want to create a new configuration file, " - "please remove the old one first. Exiting.") + "please remove the old one first. Exiting." + ) raise FatalError() dateformat = choose_datetime_format() print() @@ -396,15 +398,19 @@ def configwizard(): default_calendar = None config = create_config( - vdirs, dateformat=dateformat, timeformat=timeformat, + vdirs, + dateformat=dateformat, + timeformat=timeformat, default_calendar=default_calendar, ) - config_path = join(xdg.BaseDirectory.xdg_config_home, 'khal', 'config') + config_path = join(xdg.BaseDirectory.xdg_config_home, "khal", "config") if not confirm( - f"Do you want to write the config to {compressuser(config_path)}? " - "(Choosing `No` will abort)", default=True): - raise FatalError('User aborted...') - config_dir = join(xdg.BaseDirectory.xdg_config_home, 'khal') + f"Do you want to write the config to {compressuser(config_path)}? " + "(Choosing `No` will abort)", + default=True, + ): + raise FatalError("User aborted...") + config_dir = join(xdg.BaseDirectory.xdg_config_home, "khal") if not exists(config_dir) and not isdir(config_dir): try: makedirs(config_dir) @@ -416,6 +422,6 @@ def configwizard(): raise FatalError(error) else: print(f"created directory {compressuser(config_dir)}") - with open(config_path, 'w') as config_file: + with open(config_path, "w") as config_file: config_file.write(config) print(f"Successfully wrote configuration to {compressuser(config_path)}") diff --git a/khal/controllers.py b/khal/controllers.py index e4218ca46..1fc2200a2 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -52,15 +52,15 @@ from .terminal import merge_columns from .utils import human_formatter, json_formatter -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") def format_day(day: dt.date, format_string: str, locale, attributes=None): if attributes is None: attributes = {} - attributes["date"] = day.strftime(locale['dateformat']) - attributes["date-long"] = day.strftime(locale['longdateformat']) + attributes["date"] = day.strftime(locale["dateformat"]) + attributes["date-long"] = day.strftime(locale["longdateformat"]) attributes["name"] = parse_datetime.construct_daynames(day) @@ -78,35 +78,35 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): def calendar( collection: CalendarCollection, agenda_format=None, - notstarted: bool=False, + notstarted: bool = False, once=False, daterange=None, day_format=None, locale=None, conf=None, - firstweekday: int=0, - weeknumber: WeekNumbersType=False, - monthdisplay: MonthDisplayType='firstday', - hmethod: str='fg', - default_color: str='', - multiple='', - multiple_on_overflow: bool=False, - color='', + firstweekday: int = 0, + weeknumber: WeekNumbersType = False, + monthdisplay: MonthDisplayType = "firstday", + hmethod: str = "fg", + default_color: str = "", + multiple="", + multiple_on_overflow: bool = False, + color="", highlight_event_days=0, full=False, - bold_for_light_color: bool=True, + bold_for_light_color: bool = True, env=None, - ): - +): term_width, _ = get_terminal_size() - lwidth = 27 if conf['locale']['weeknumbers'] == 'right' else 25 + lwidth = 27 if conf["locale"]["weeknumbers"] == "right" else 25 rwidth = term_width - lwidth - 4 try: start, end = start_end_from_daterange( - daterange, locale, - default_timedelta_date=conf['default']['timedelta'], - default_timedelta_datetime=conf['default']['timedelta'], + daterange, + locale, + default_timedelta_date=conf["default"]["timedelta"], + default_timedelta_datetime=conf["default"]["timedelta"], ) except ValueError as error: raise FatalError(error) @@ -123,13 +123,14 @@ def calendar( env=env, ) if not event_column: - event_column = [style('No events', bold=True)] + event_column = [style("No events", bold=True)] month_count = (end.year - start.year) * 12 + end.month - start.month + 1 calendar_column = calendar_display.vertical_month( month=start.month, year=start.year, - count=max(conf['view']['min_calendar_display'], month_count), - firstweekday=firstweekday, weeknumber=weeknumber, + count=max(conf["view"]["min_calendar_display"], month_count), + firstweekday=firstweekday, + weeknumber=weeknumber, monthdisplay=monthdisplay, collection=collection, hmethod=hmethod, @@ -139,15 +140,16 @@ def calendar( color=color, highlight_event_days=highlight_event_days, locale=locale, - bold_for_light_color=bold_for_light_color) + bold_for_light_color=bold_for_light_color, + ) return merge_columns(calendar_column, event_column, width=lwidth) def start_end_from_daterange( daterange: list[str], locale: LocaleConfiguration, - default_timedelta_date: dt.timedelta=dt.timedelta(days=1), - default_timedelta_datetime: dt.timedelta=dt.timedelta(hours=1), + default_timedelta_date: dt.timedelta = dt.timedelta(days=1), + default_timedelta_datetime: dt.timedelta = dt.timedelta(hours=1), ): """ convert a string description of a daterange into start and end datetime @@ -162,7 +164,9 @@ def start_end_from_daterange( end = start + default_timedelta_date else: start, end, allday = parse_datetime.guessrangefstr( - daterange, locale, default_timedelta_date=default_timedelta_date, + daterange, + locale, + default_timedelta_date=default_timedelta_date, default_timedelta_datetime=default_timedelta_datetime, ) return start, end @@ -203,8 +207,8 @@ def get_events_between( env = {} assert start assert end - start_local = locale['local_timezone'].localize(start) - end_local = locale['local_timezone'].localize(end) + start_local = locale["local_timezone"].localize(start) + end_local = locale["local_timezone"].localize(end) start = start_local.replace(tzinfo=None) end = end_local.replace(tzinfo=None) @@ -240,7 +244,7 @@ def khal_list( daterange: list[str] | None = None, conf: dict | None = None, agenda_format=None, - day_format: str | None=None, + day_format: str | None = None, once=False, notstarted: bool = False, width: int | None = None, @@ -254,7 +258,7 @@ def khal_list( # because empty strings are also Falsish if agenda_format is None: - agenda_format = conf['view']['agenda_event_format'] + agenda_format = conf["view"]["agenda_event_format"] if json: formatter = json_formatter(json) @@ -265,125 +269,149 @@ def khal_list( if daterange is not None: if day_format is None: - day_format = conf['view']['agenda_day_format'] + day_format = conf["view"]["agenda_day_format"] start, end = start_end_from_daterange( - daterange, conf['locale'], - default_timedelta_date=conf['default']['timedelta'], - default_timedelta_datetime=conf['default']['timedelta'], + daterange, + conf["locale"], + default_timedelta_date=conf["default"]["timedelta"], + default_timedelta_datetime=conf["default"]["timedelta"], ) - logger.debug(f'Getting all events between {start} and {end}') + logger.debug(f"Getting all events between {start} and {end}") elif datepoint is not None: if not datepoint: - datepoint = ['now'] + datepoint = ["now"] try: # hand over a copy of the `datepoint` so error reporting works # (we pop from that list in guessdatetimefstr()) start, allday = parse_datetime.guessdatetimefstr( - list(datepoint), conf['locale'], dt.date.today(), + list(datepoint), + conf["locale"], + dt.date.today(), ) except (ValueError, IndexError): raise FatalError(f"Invalid value of {' '.join(datepoint)} for a datetime") if allday: - logger.debug(f'Got date {start}') - raise FatalError('Please supply a datetime, not a date.') + logger.debug(f"Got date {start}") + raise FatalError("Please supply a datetime, not a date.") end = start + dt.timedelta(seconds=1) if day_format is None: day_format = style( - start.strftime(conf['locale']['longdatetimeformat']), + start.strftime(conf["locale"]["longdatetimeformat"]), bold=True, ) - logger.debug(f'Getting all events between {start} and {end}') + logger.debug(f"Getting all events between {start} and {end}") event_column: list[str] = [] once = set() if once else None if env is None: env = {} - original_start = conf['locale']['local_timezone'].localize(start) + original_start = conf["locale"]["local_timezone"].localize(start) while start < end: if start.date() == end.date(): day_end = end else: day_end = dt.datetime.combine(start.date(), dt.time.max) current_events = get_events_between( - collection, locale=conf['locale'], formatter=formatter, start=start, - end=day_end, notstarted=notstarted, original_start=original_start, + collection, + locale=conf["locale"], + formatter=formatter, + start=start, + end=day_end, + notstarted=notstarted, + original_start=original_start, env=env, seen=once, colors=colors, ) - if day_format and (conf['default']['show_all_days'] or current_events) and not json: - if len(event_column) != 0 and conf['view']['blank_line_before_day']: - event_column.append('') - event_column.append(format_day(start.date(), day_format, conf['locale'])) + if day_format and (conf["default"]["show_all_days"] or current_events) and not json: + if len(event_column) != 0 and conf["view"]["blank_line_before_day"]: + event_column.append("") + event_column.append(format_day(start.date(), day_format, conf["locale"])) event_column.extend(current_events) start = dt.datetime(*start.date().timetuple()[:3]) + dt.timedelta(days=1) return event_column -def new_interactive(collection, calendar_name, conf, info, location=None, - categories=None, repeat=None, until=None, alarms=None, - format=None, json=None, env=None, url=None): +def new_interactive( + collection, + calendar_name, + conf, + info, + location=None, + categories=None, + repeat=None, + until=None, + alarms=None, + format=None, + json=None, + env=None, + url=None, +): info: EventCreationTypes try: info = parse_datetime.eventinfofstr( - info, conf['locale'], - default_event_duration=conf['default']['default_event_duration'], - default_dayevent_duration=conf['default']['default_dayevent_duration'], + info, + conf["locale"], + default_event_duration=conf["default"]["default_event_duration"], + default_dayevent_duration=conf["default"]["default_dayevent_duration"], adjust_reasonably=True, ) except DateTimeParseError: info = {} while True: - summary = info.get('summary') + summary = info.get("summary") if not summary: summary = None - info['summary'] = prompt('summary', default=summary) - if info['summary']: + info["summary"] = prompt("summary", default=summary) + if info["summary"]: break echo("a summary is required") while True: range_string = None - if info.get('dtstart') and info.get('dtend'): - start_string = info["dtstart"].strftime(conf['locale']['datetimeformat']) - end_string = info["dtend"].strftime(conf['locale']['datetimeformat']) - range_string = start_string + ' ' + end_string + if info.get("dtstart") and info.get("dtend"): + start_string = info["dtstart"].strftime(conf["locale"]["datetimeformat"]) + end_string = info["dtend"].strftime(conf["locale"]["datetimeformat"]) + range_string = start_string + " " + end_string daterange = prompt("datetime range", default=range_string) start, end, allday = parse_datetime.guessrangefstr( - daterange, conf['locale'], adjust_reasonably=True) - info['dtstart'] = start - info['dtend'] = end - info['allday'] = allday - if info['dtstart'] and info['dtend']: + daterange, conf["locale"], adjust_reasonably=True + ) + info["dtstart"] = start + info["dtend"] = end + info["allday"] = allday + if info["dtstart"] and info["dtend"]: break echo("invalid datetime range") while True: - tz = info.get('timezone') or conf['locale']['default_timezone'] + tz = info.get("timezone") or conf["locale"]["default_timezone"] timezone = prompt("timezone", default=str(tz)) try: tz = pytz.timezone(timezone) - info['timezone'] = tz + info["timezone"] = tz break except pytz.UnknownTimeZoneError: echo("unknown timezone") - info['description'] = prompt("description (or 'None')", default=info.get('description')) - if info['description'] == 'None': - info['description'] = '' - - info.update({ - 'location': location, - 'categories': categories, - 'repeat': repeat, - 'until': until, - 'alarms': alarms, - 'url': url, - }) + info["description"] = prompt("description (or 'None')", default=info.get("description")) + if info["description"] == "None": + info["description"] = "" + + info.update( + { + "location": location, + "categories": categories, + "repeat": repeat, + "until": until, + "alarms": alarms, + "url": url, + } + ) event = new_from_dict( info, @@ -397,32 +425,47 @@ def new_interactive(collection, calendar_name, conf, info, location=None, echo("event saved") term_width, _ = get_terminal_size() - edit_event(event, collection, conf['locale'], width=term_width) + edit_event(event, collection, conf["locale"], width=term_width) -def new_from_string(collection, calendar_name, conf, info, location=None, - categories=None, repeat=None, until=None, alarms=None, - url=None, format=None, json=None, env=None): +def new_from_string( + collection, + calendar_name, + conf, + info, + location=None, + categories=None, + repeat=None, + until=None, + alarms=None, + url=None, + format=None, + json=None, + env=None, +): """construct a new event from a string and add it""" info = parse_datetime.eventinfofstr( - info, conf['locale'], - conf['default']['default_event_duration'], - conf['default']['default_dayevent_duration'], + info, + conf["locale"], + conf["default"]["default_event_duration"], + conf["default"]["default_dayevent_duration"], adjust_reasonably=True, ) if alarms is None: - if info['allday']: - alarms = timedelta2str(conf['default']['default_dayevent_alarm']) + if info["allday"]: + alarms = timedelta2str(conf["default"]["default_dayevent_alarm"]) else: - alarms = timedelta2str(conf['default']['default_event_alarm']) - info.update({ - 'location': location, - 'categories': categories, - 'repeat': repeat, - 'until': until, - 'alarms': alarms, - 'url': url, - }) + alarms = timedelta2str(conf["default"]["default_event_alarm"]) + info.update( + { + "location": location, + "categories": categories, + "repeat": repeat, + "until": until, + "alarms": alarms, + "url": url, + } + ) new_from_dict( info, collection, @@ -438,7 +481,7 @@ def new_from_dict( event_args: EventCreationTypes, collection: CalendarCollection, conf, - calendar_name: str | None=None, + calendar_name: str | None = None, format=None, env=None, json=None, @@ -447,9 +490,10 @@ def new_from_dict( This is a wrapper around CalendarCollection.create_event_from_dict() """ - if isinstance(event_args['categories'], str): - event_args['categories'] = [event_args['categories'].strip() - for category in event_args['categories'].split(',')] + if isinstance(event_args["categories"], str): + event_args["categories"] = [ + event_args["categories"].strip() for category in event_args["categories"].split(",") + ] try: event = collection.create_event_from_dict(event_args, calendar_name=calendar_name) except ValueError as error: @@ -458,21 +502,19 @@ def new_from_dict( try: collection.insert(event) except ReadOnlyCalendarError: - raise FatalError( - f'ERROR: Cannot modify calendar `{calendar_name}` as it is read-only' - ) + raise FatalError(f"ERROR: Cannot modify calendar `{calendar_name}` as it is read-only") - if conf['default']['print_new'] == 'event': + if conf["default"]["print_new"] == "event": if json is None or len(json) == 0: if format is None: - format = conf['view']['event_format'] + format = conf["view"]["event_format"] formatter = human_formatter(format) else: formatter = json_formatter(json) echo(formatter(event.attributes(dt.datetime.now(), env=env))) - elif conf['default']['print_new'] == 'path': + elif conf["default"]["print_new"] == "path": assert event.href - path = os.path.join(collection._calendars[event.calendar]['path'], event.href) + path = os.path.join(collection._calendars[event.calendar]["path"], event.href) echo(path) return event @@ -483,7 +525,7 @@ def present_options(options, prefix="", sep=" ", width=70): for option in options: char = options[option]["short"] chars[char] = option - option_list.append(option.replace(char, '[' + char + ']', 1)) + option_list.append(option.replace(char, "[" + char + "]", 1)) option_string = sep.join(option_list) option_string = textwrap.fill(option_string, width) char = prompt(option_string) @@ -513,7 +555,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): # if hitting enter, the output (including the escape sequence) gets parsed # and fails the parsing. Therefore we remove ansi escape sequences before # parsing. - ansi = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + ansi = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") now = dt.datetime.now() @@ -522,9 +564,9 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): if choice is None: echo("unknown choice") continue - if choice == 'no': + if choice == "no": return True - if choice in ['quit', 'done']: + if choice in ["quit", "done"]: return False edited = False @@ -537,7 +579,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): current = human_formatter("{start} {end}")(event.attributes(relative_to=now)) value = prompt("datetime range", default=current) try: - start, end, allday = parse_datetime.guessrangefstr(ansi.sub('', value), locale) + start, end, allday = parse_datetime.guessrangefstr(ansi.sub("", value), locale) event.update_start_end(start, end) edited = True except Exception: @@ -547,13 +589,13 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): freq = recur["freq"] if "freq" in recur else "" until = recur["until"] if "until" in recur else "" if not freq: - freq = 'None' + freq = "None" freq = prompt('frequency (or "None")', freq) - if freq == 'None': + if freq == "None": event.update_rrule(None) else: until = prompt('until (or "None")', until) - if until == 'None': + if until == "None": until = None rrule = parse_datetime.rrulefstr(freq, until, locale, event.start.tzinfo) event.update_rrule(rrule) @@ -564,9 +606,9 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): s = parse_datetime.timedelta2str(-1 * a[0]) default_alarms.append(s) - default = ', '.join(default_alarms) + default = ", ".join(default_alarms) if not default: - default = 'None' + default = "None" alarm = prompt('alarm (or "None")', default) if alarm == "None": alarm = "" @@ -587,13 +629,13 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): question += ' (or "None")' allow_none = True if not default: - default = 'None' + default = "None" value = prompt(question, default) if allow_none and value == "None": value = "" - if attr == 'categories': - getattr(event, "update_" + attr)([cat.strip() for cat in value.split(',')]) + if attr == "categories": + getattr(event, "update_" + attr)([cat.strip() for cat in value.split(",")]) else: getattr(event, "update_" + attr)(value) edited = True @@ -606,10 +648,10 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): def edit(collection, search_string, locale, format=None, allow_past=False, conf=None): if conf is not None: if format is None: - format = conf['view']['event_format'] + format = conf["view"]["event_format"] term_width, _ = get_terminal_size() - now = conf['locale']['local_timezone'].localize(dt.datetime.now()) + now = conf["locale"]["local_timezone"].localize(dt.datetime.now()) events = sorted(collection.search(search_string)) for event in events: @@ -618,9 +660,10 @@ def edit(collection, search_string, locale, format=None, allow_past=False, conf= continue elif not event.allday and event.end_local < now: continue - event_text = textwrap.wrap(human_formatter(format)( - event.attributes(relative_to=now)), term_width) - echo(''.join(event_text)) + event_text = textwrap.wrap( + human_formatter(format)(event.attributes(relative_to=now)), term_width + ) + echo("".join(event_text)) if not edit_event(event, collection, locale, allow_quit=True, width=term_width): return @@ -628,17 +671,17 @@ def edit(collection, search_string, locale, format=None, allow_past=False, conf= def interactive(collection, conf): """start the interactive user interface""" from . import ui - pane = ui.ClassicView( - collection, conf, title="select an event", description="do something") + + pane = ui.ClassicView(collection, conf, title="select an event", description="do something") ui.start_pane( - pane, pane.cleanup, - program_info=f'{__productname__} v{__version__}', - quit_keys=conf['keybindings']['quit'], + pane, + pane.cleanup, + program_info=f"{__productname__} v{__version__}", + quit_keys=conf["keybindings"]["quit"], ) -def import_ics(collection, conf, ics, batch=False, random_uid=False, format=None, - env=None): +def import_ics(collection, conf, ics, batch=False, random_uid=False, format=None, env=None): """ :param batch: setting this to True will insert without asking for approval, even when an event with the same uid already exists @@ -649,13 +692,13 @@ def import_ics(collection, conf, ics, batch=False, random_uid=False, format=None :type format: str """ if format is None: - format = conf['view']['event_format'] + format = conf["view"]["event_format"] try: - vevents = split_ics(ics, random_uid, conf['locale']['default_timezone']) + vevents = split_ics(ics, random_uid, conf["locale"]["default_timezone"]) except Exception as error: raise FatalError(error) for vevent in vevents: - import_event(vevent, collection, conf['locale'], batch, format, env) + import_event(vevent, collection, conf["locale"], batch, format, env) def import_event(vevent, collection, locale, batch, format=None, env=None): @@ -668,26 +711,25 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): # print all sub-events if not batch: for item in cal_from_ics(vevent).walk(): - if item.name == 'VEVENT': + if item.name == "VEVENT": event = Event.fromVEvents( - [item], calendar=collection.default_calendar_name, locale=locale) + [item], calendar=collection.default_calendar_name, locale=locale + ) echo(human_formatter(format)(event.attributes(dt.datetime.now(), env=env))) # get the calendar to insert into if not collection.writable_names: - raise ConfigurationError('No writable calendars found, aborting import.') + raise ConfigurationError("No writable calendars found, aborting import.") if len(collection.writable_names) == 1: calendar_name = collection.writable_names[0] elif batch: calendar_name = collection.default_calendar_name else: calendar_names = sorted(collection.writable_names) - choices = ', '.join( - [f'{name}({num})' for num, name in enumerate(calendar_names)]) + choices = ", ".join([f"{name}({num})" for num, name in enumerate(calendar_names)]) while True: value = prompt( - "Which calendar do you want to import to? (unique prefixes are fine)\n" - f"{choices}", + f"Which calendar do you want to import to? (unique prefixes are fine)\n{choices}", default=collection.default_calendar_name, ) try: @@ -698,7 +740,7 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): if len(matches) == 1: calendar_name = matches[0] break - echo('invalid choice') + echo("invalid choice") assert calendar_name in collection.writable_names if batch or confirm(f"Do you want to import this event into `{calendar_name}`?"): @@ -706,7 +748,8 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): collection.insert(Item(vevent), collection=calendar_name) except DuplicateUid: if batch or confirm( - "An event with the same UID already exists. Do you want to update it?"): + "An event with the same UID already exists. Do you want to update it?" + ): collection.force_update(Item(vevent), collection=calendar_name) else: logger.warning(f"Not importing event with UID `{event.uid}`") @@ -714,18 +757,18 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): def print_ics(conf, name, ics, format): if format is None: - format = conf['view']['event_format'] + format = conf["view"]["event_format"] cal = cal_from_ics(ics) - events = [item for item in cal.walk() if item.name == 'VEVENT'] + events = [item for item in cal.walk() if item.name == "VEVENT"] events_grouped = defaultdict(list) for event in events: - events_grouped[event['UID']].append(event) + events_grouped[event["UID"]].append(event) vevents = [] for uid in events_grouped: vevents.append(sorted(events_grouped[uid], key=sort_vevent_key)) - echo(f'{len(vevents)} events found in {name}') + echo(f"{len(vevents)} events found in {name}") for sub_event in vevents: - event = Event.fromVEvents(sub_event, locale=conf['locale']) + event = Event.fromVEvents(sub_event, locale=conf["locale"]) echo(human_formatter(format)(event.attributes(dt.datetime.now()))) diff --git a/khal/custom_types.py b/khal/custom_types.py index ecdbf6ed0..7a5cb1334 100644 --- a/khal/custom_types.py +++ b/khal/custom_types.py @@ -30,12 +30,10 @@ class LocaleConfiguration(TypedDict): class SupportsRaw(Protocol): @property - def uid(self) -> str | None: - ... + def uid(self) -> str | None: ... @property - def raw(self) -> str: - ... + def raw(self) -> str: ... # set this to TypeAlias once we support that python version (PEP613) @@ -77,7 +75,7 @@ class EventCreationTypes(TypedDict): url: str -PathLike = str| os.PathLike +PathLike = str | os.PathLike -WeekNumbersType = Literal['left', 'right', False] -MonthDisplayType = Literal['firstday', 'firstfullweek'] +WeekNumbersType = Literal["left", "right", False] +MonthDisplayType = Literal["firstday", "firstfullweek"] diff --git a/khal/exceptions.py b/khal/exceptions.py index c9f56fa01..ca3c9c220 100644 --- a/khal/exceptions.py +++ b/khal/exceptions.py @@ -21,14 +21,14 @@ class Error(Exception): - """base class for all of khal's Exceptions""" + pass class FatalError(Error): - """execution cannot continue""" + pass @@ -41,14 +41,14 @@ class ConfigurationError(FatalError): class UnsupportedFeatureError(Error): - """something Failed but we know why""" + pass class UnsupportedRecurrence(Error): - """raised if the RRULE is not understood by dateutil.rrule""" + pass diff --git a/khal/icalendar.py b/khal/icalendar.py index 65ccdd7b8..3ccb3d040 100644 --- a/khal/icalendar.py +++ b/khal/icalendar.py @@ -34,14 +34,14 @@ from .parse_datetime import rrulefstr from .utils import generate_random_uid, localize_strip_tz, str2alarm, to_unix_time -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") # Force use of pytz because we rely on functionalities not available in # zoneinfo. icalendar.use_pytz() -def split_ics(ics: str, random_uid: bool=False, default_timezone=None) -> list: +def split_ics(ics: str, random_uid: bool = False, default_timezone=None) -> list: """split an ics string into several according to VEVENT's UIDs and sort the right VTIMEZONEs accordingly @@ -53,25 +53,24 @@ def split_ics(ics: str, random_uid: bool=False, default_timezone=None) -> list: events_grouped = defaultdict(list) for item in cal.walk(): - # Since some events could have a Windows format timezone (e.g. 'New Zealand # Standard Time' for 'Pacific/Auckland' in Olson format), we convert any # Windows format timezones to Olson. - if item.name == 'VTIMEZONE': - if item['TZID'] in icalendar.timezone.windows_to_olson.WINDOWS_TO_OLSON: - key = icalendar.timezone.windows_to_olson.WINDOWS_TO_OLSON[item['TZID']] + if item.name == "VTIMEZONE": + if item["TZID"] in icalendar.timezone.windows_to_olson.WINDOWS_TO_OLSON: + key = icalendar.timezone.windows_to_olson.WINDOWS_TO_OLSON[item["TZID"]] else: - key = item['TZID'] + key = item["TZID"] tzs[key] = item - if item.name == 'VEVENT': - if 'UID' not in item: + if item.name == "VEVENT": + if "UID" not in item: logger.warning( f"Event with summary '{item['SUMMARY']}' doesn't have a unique ID." "A generated ID will be used instead." ) - item['UID'] = sha256(item.to_ical()).hexdigest() - events_grouped[item['UID']].append(item) + item["UID"] = sha256(item.to_ical()).hexdigest() + events_grouped[item["UID"]].append(item) else: continue out = [] @@ -80,7 +79,7 @@ def split_ics(ics: str, random_uid: bool=False, default_timezone=None) -> list: try: ics = ics_from_list(events, tzs, random_uid, default_timezone) except Exception as exception: - logger.warn(f'Error when trying to import the event {uid}') + logger.warn(f"Error when trying to import the event {uid}") saved_exception = exception else: out.append(ics) @@ -89,20 +88,21 @@ def split_ics(ics: str, random_uid: bool=False, default_timezone=None) -> list: return out -def new_vevent(locale, - dtstart: dt.date, - dtend: dt.date, - summary: str, - timezone: pytz.BaseTzInfo | None=None, - allday: bool=False, - description: str | None=None, - location: str | None=None, - categories: list[str] | str | None=None, - repeat: str | None=None, - until=None, - alarms: str | None=None, - url: str | None=None, - ) -> icalendar.Event: +def new_vevent( + locale, + dtstart: dt.date, + dtend: dt.date, + summary: str, + timezone: pytz.BaseTzInfo | None = None, + allday: bool = False, + description: str | None = None, + location: str | None = None, + categories: list[str] | str | None = None, + repeat: str | None = None, + until=None, + alarms: str | None = None, + url: str | None = None, +) -> icalendar.Event: """create a new event :param dtstart: starttime of that event @@ -123,35 +123,32 @@ def new_vevent(locale, dtend = timezone.localize(dtend) event = icalendar.Event() - event.add('dtstart', dtstart) - event.add('dtend', dtend) - event.add('dtstamp', dt.datetime.now()) - event.add('summary', summary) - event.add('uid', generate_random_uid()) + event.add("dtstart", dtstart) + event.add("dtend", dtend) + event.add("dtstamp", dt.datetime.now()) + event.add("summary", summary) + event.add("uid", generate_random_uid()) # event.add('sequence', 0) if description: - event.add('description', description) + event.add("description", description) if location: - event.add('location', location) + event.add("location", location) if categories: - event.add('categories', categories) + event.add("categories", categories) if url: - event.add('url', icalendar.vUri(url)) + event.add("url", icalendar.vUri(url)) if repeat and repeat != "none": - rrule = rrulefstr(repeat, until, locale, getattr(dtstart, 'tzinfo', None)) - event.add('rrule', rrule) + rrule = rrulefstr(repeat, until, locale, getattr(dtstart, "tzinfo", None)) + event.add("rrule", rrule) if alarms: - for alarm in str2alarm(alarms, description or ''): + for alarm in str2alarm(alarms, description or ""): event.add_component(alarm) return event def ics_from_list( - events: list[icalendar.Event], - tzs, - random_uid: bool=False, - default_timezone=None + events: list[icalendar.Event], tzs, random_uid: bool = False, default_timezone=None ) -> str: """convert an iterable of icalendar.Events to an icalendar str @@ -161,10 +158,8 @@ def ics_from_list( :type tzs: dict(icalendar.cal.Vtimzone """ calendar = icalendar.Calendar() - calendar.add('version', '2.0') - calendar.add( - 'prodid', '-//PIMUTILS.ORG//NONSGML khal / icalendar //EN' - ) + calendar.add("version", "2.0") + calendar.add("prodid", "-//PIMUTILS.ORG//NONSGML khal / icalendar //EN") if random_uid: new_uid = generate_random_uid() @@ -173,32 +168,38 @@ def ics_from_list( for sub_event in events: sub_event = sanitize(sub_event, default_timezone=default_timezone) if random_uid: - sub_event['UID'] = new_uid + sub_event["UID"] = new_uid # icalendar round-trip converts `TZID=a b` to `TZID="a b"` investigate, file bug XXX - for prop in ['DTSTART', 'DTEND', 'DUE', 'EXDATE', 'RDATE', 'RECURRENCE-ID', 'DUE']: + for prop in ["DTSTART", "DTEND", "DUE", "EXDATE", "RDATE", "RECURRENCE-ID", "DUE"]: if isinstance(sub_event.get(prop), list): items = sub_event.get(prop) else: items = [sub_event.get(prop)] for item in items: - if not (hasattr(item, 'dt') or hasattr(item, 'dts')): + if not (hasattr(item, "dt") or hasattr(item, "dts")): continue # if prop is a list, all items have the same parameters - datetime_ = item.dts[0].dt if hasattr(item, 'dts') else item.dt - if not hasattr(datetime_, 'tzinfo'): + datetime_ = item.dts[0].dt if hasattr(item, "dts") else item.dt + if not hasattr(datetime_, "tzinfo"): continue # check for datetimes' timezones which are not understood by # icalendar - if datetime_.tzinfo is None and 'TZID' in item.params and \ - item.params['TZID'] not in missing_tz: + if ( + datetime_.tzinfo is None + and "TZID" in item.params + and item.params["TZID"] not in missing_tz + ): logger.warning( f"Cannot find timezone `{item.params['TZID']}` in .ics file, " "using default timezone. This can lead to erroneous time shifts" ) - missing_tz.add(item.params['TZID']) - elif datetime_.tzinfo and datetime_.tzinfo != pytz.UTC and \ - datetime_.tzinfo not in needed_tz: + missing_tz.add(item.params["TZID"]) + elif ( + datetime_.tzinfo + and datetime_.tzinfo != pytz.UTC + and datetime_.tzinfo not in needed_tz + ): needed_tz.add(datetime_.tzinfo) for tzid in needed_tz: @@ -206,16 +207,17 @@ def ics_from_list( calendar.add_component(tzs[str(tzid)]) else: logger.warning( - f'Cannot find timezone `{tzid}` in .ics file, this could be a bug, ' - 'please report this issue at http://github.com/pimutils/khal/.') + f"Cannot find timezone `{tzid}` in .ics file, this could be a bug, " + "please report this issue at http://github.com/pimutils/khal/." + ) for sub_event in events: calendar.add_component(sub_event) - return calendar.to_ical().decode('utf-8') + return calendar.to_ical().decode("utf-8") def expand( vevent: icalendar.Event, - href: str='', + href: str = "", ) -> list[tuple[dt.datetime, dt.datetime]] | None: """ Constructs a list of start and end dates for all recurring instances of the @@ -233,22 +235,23 @@ def expand( :returns: list of start and end (date)times of the expanded event """ # we do this now and than never care about the "real" end time again - if 'DURATION' in vevent: - duration = vevent['DURATION'].dt + if "DURATION" in vevent: + duration = vevent["DURATION"].dt else: - duration = vevent['DTEND'].dt - vevent['DTSTART'].dt + duration = vevent["DTEND"].dt - vevent["DTSTART"].dt # if this vevent has a RECURRENCE_ID property, no expansion will be # performed - expand = not bool(vevent.get('RECURRENCE-ID')) + expand = not bool(vevent.get("RECURRENCE-ID")) - dtstart_prop = vevent['DTSTART'] + dtstart_prop = vevent["DTSTART"] # Check for VALUE=DATE parameter to detect all-day events # icalendar>=7.0 version returns datetime with tzinfo even for DATE values - allday = dtstart_prop.params.get('VALUE') == 'DATE' or \ - not isinstance(dtstart_prop.dt, dt.datetime) + allday = dtstart_prop.params.get("VALUE") == "DATE" or not isinstance( + dtstart_prop.dt, dt.datetime + ) # Don't use timezone for all-day events - events_tz = getattr(dtstart_prop.dt, 'tzinfo', None) if not allday else None + events_tz = getattr(dtstart_prop.dt, "tzinfo", None) if not allday else None def sanitize_datetime(date: dt.date) -> dt.date: if allday and isinstance(date, dt.datetime): @@ -257,7 +260,7 @@ def sanitize_datetime(date: dt.date) -> dt.date: date = events_tz.localize(date) return date - rrule_param = vevent.get('RRULE') + rrule_param = vevent.get("RRULE") if expand and rrule_param is not None: vevent = sanitize_rrule(vevent) @@ -265,7 +268,7 @@ def sanitize_datetime(date: dt.date) -> dt.date: # everything to naive datetime objects and transform back after # expanding # See https://github.com/dateutil/dateutil/issues/102 - dtstart = vevent['DTSTART'].dt + dtstart = vevent["DTSTART"].dt if events_tz: dtstart = dtstart.replace(tzinfo=None) # For all-day events, ensure dtstart is a date, not datetime. @@ -288,35 +291,38 @@ def sanitize_datetime(date: dt.date) -> dt.date: # doesn't know any larger transition times rrule._until = dt.datetime(2037, 12, 31) # type: ignore else: - if events_tz and 'Z' in rrule_param.to_ical().decode(): + if events_tz and "Z" in rrule_param.to_ical().decode(): assert isinstance(rrule._until, dt.datetime) # type: ignore - rrule._until = pytz.UTC.localize( # type: ignore - rrule._until).astimezone(events_tz).replace(tzinfo=None) # type: ignore + rrule._until = ( # type: ignore + pytz.UTC.localize(rrule._until).astimezone(events_tz).replace(tzinfo=None) # type: ignore + ) # rrule._until and dtstart could be dt.date or dt.datetime. They # need to be the same for comparison testuntil = rrule._until # type: ignore - if (type(dtstart) is dt.date and type(testuntil) is dt.datetime): + if type(dtstart) is dt.date and type(testuntil) is dt.datetime: testuntil = testuntil.date() teststart = dtstart - if (type(testuntil) is dt.date and type(teststart) is dt.datetime): + if type(testuntil) is dt.date and type(teststart) is dt.datetime: teststart = teststart.date() if testuntil < teststart: logger.warning( - f'{href}: Unsupported recurrence. UNTIL is before DTSTART.\n' - 'This event will not be available in khal.') + f"{href}: Unsupported recurrence. UNTIL is before DTSTART.\n" + "This event will not be available in khal." + ) return None if rrule.count() == 0: logger.warning( - f'{href}: Recurrence defined but will never occur.\n' - 'This event will not be available in khal.') + f"{href}: Recurrence defined but will never occur.\n" + "This event will not be available in khal." + ) return None rrule = map(sanitize_datetime, rrule) # type: ignore - logger.debug(f'calculating recurrence dates for {href}, this might take some time.') + logger.debug(f"calculating recurrence dates for {href}, this might take some time.") # RRULE and RDATE may specify the same date twice, it is recommended by # the RFC to consider this as only one instance @@ -324,7 +330,7 @@ def sanitize_datetime(date: dt.date) -> dt.date: if not dtstartl: raise UnsupportedRecurrence() else: - dtstartl = {vevent['DTSTART'].dt} + dtstartl = {vevent["DTSTART"].dt} def get_dates(vevent, key): # TODO replace with get_all_properties @@ -340,17 +346,18 @@ def get_dates(vevent, key): # include explicitly specified recursion dates if expand: - dtstartl.update(get_dates(vevent, 'RDATE') or ()) + dtstartl.update(get_dates(vevent, "RDATE") or ()) # remove excluded dates if expand: - for date in get_dates(vevent, 'EXDATE') or (): + for date in get_dates(vevent, "EXDATE") or (): try: dtstartl.remove(date) except KeyError: logger.warning( - f'In event {href}, excluded instance starting at {date} ' - 'not found, event might be invalid.') + f"In event {href}, excluded instance starting at {date} " + "not found, event might be invalid." + ) dtstartend = [(start, start + duration) for start in dtstartl] # not necessary, but I prefer deterministic output @@ -362,8 +369,8 @@ def assert_only_one_uid(cal: icalendar.Calendar): """assert that all VEVENTs in cal have the same UID""" uids = set() for item in cal.walk(): - if item.name == 'VEVENT': - uids.add(item['UID']) + if item.name == "VEVENT": + uids.add(item["UID"]) if len(uids) > 1: return False else: @@ -373,8 +380,8 @@ def assert_only_one_uid(cal: icalendar.Calendar): def sanitize( vevent: icalendar.Event, default_timezone: pytz.BaseTzInfo, - href: str='', - calendar: str='', + href: str = "", + calendar: str = "", ) -> icalendar.Event: """ clean up vevents we do not understand @@ -392,9 +399,9 @@ def sanitize( # convert localized datetimes with timezone information we don't # understand to the default timezone # TODO do this for everything where a TZID can appear (RDATE, EXDATE) - for prop in ['DTSTART', 'DTEND', 'DUE', 'RECURRENCE-ID']: + for prop in ["DTSTART", "DTEND", "DUE", "RECURRENCE-ID"]: if prop in vevent and invalid_timezone(vevent[prop]): - timezone = vevent[prop].params.get('TZID') + timezone = vevent[prop].params.get("TZID") value = default_timezone.localize(vevent.pop(prop).dt) vevent.add(prop, value) logger.warning( @@ -403,43 +410,41 @@ def sanitize( "event being wrongly displayed." ) - vdtstart = vevent.pop('DTSTART', None) - vdtend = vevent.pop('DTEND', None) - dtstart = getattr(vdtstart, 'dt', None) - dtend = getattr(vdtend, 'dt', None) + vdtstart = vevent.pop("DTSTART", None) + vdtend = vevent.pop("DTEND", None) + dtstart = getattr(vdtstart, "dt", None) + dtend = getattr(vdtend, "dt", None) # event with missing DTSTART if dtstart is None: - raise ValueError('Event has no start time (DTSTART).') - dtstart, dtend = sanitize_timerange( - dtstart, dtend, duration=vevent.get('DURATION', None)) + raise ValueError("Event has no start time (DTSTART).") + dtstart, dtend = sanitize_timerange(dtstart, dtend, duration=vevent.get("DURATION", None)) - vevent.add('DTSTART', dtstart) + vevent.add("DTSTART", dtstart) if dtend is not None: - vevent.add('DTEND', dtend) + vevent.add("DTEND", dtend) return vevent def sanitize_timerange(dtstart, dtend, duration=None): - '''return sensible dtstart and end for events that have an invalid or - missing DTEND, assuming the event just lasts one hour.''' + """return sensible dtstart and end for events that have an invalid or + missing DTEND, assuming the event just lasts one hour.""" if isinstance(dtstart, dt.datetime) and isinstance(dtend, dt.datetime): if dtstart.tzinfo and not dtend.tzinfo: logger.warning( - "Event end time has no timezone. " - "Assuming it's the same timezone as the start time" + "Event end time has no timezone. Assuming it's the same timezone as the start time" ) dtend = dtstart.tzinfo.localize(dtend) if not dtstart.tzinfo and dtend.tzinfo: logger.warning( - "Event start time has no timezone. " - "Assuming it's the same timezone as the end time" + "Event start time has no timezone. Assuming it's the same timezone as the end time" ) dtstart = dtend.tzinfo.localize(dtstart) if dtend is not None and type(dtstart) is not type(dtend): raise ValueError( - 'The event\'s end time (DTEND) and start time (DTSTART) are not of the same type.') + "The event's end time (DTEND) and start time (DTSTART) are not of the same type." + ) if dtend is None and duration is None: if isinstance(dtstart, dt.datetime): @@ -448,8 +453,9 @@ def sanitize_timerange(dtstart, dtend, duration=None): dtend = dtstart + dt.timedelta(days=1) elif dtend is not None: if dtend < dtstart: - raise ValueError('The event\'s end time (DTEND) is older than ' - 'the event\'s start time (DTSTART).') + raise ValueError( + "The event's end time (DTEND) is older than the event's start time (DTSTART)." + ) elif dtend == dtstart: logger.warning( "Event start time and end time are the same. " @@ -465,18 +471,18 @@ def sanitize_timerange(dtstart, dtend, duration=None): def sanitize_rrule(vevent): """fix problems with RRULE:UNTIL""" - if 'rrule' in vevent and 'UNTIL' in vevent['rrule']: - until = vevent['rrule']['UNTIL'][0] - dtstart = vevent['dtstart'].dt + if "rrule" in vevent and "UNTIL" in vevent["rrule"]: + until = vevent["rrule"]["UNTIL"][0] + dtstart = vevent["dtstart"].dt # DTSTART is date, UNTIL is datetime if not isinstance(dtstart, dt.datetime) and isinstance(until, dt.datetime): - vevent['rrule']['until'] = until.date() + vevent["rrule"]["until"] = until.date() return vevent def invalid_timezone(prop): """check if an icalendar property has a timezone attached we don't understand""" - return bool(hasattr(prop.dt, 'tzinfo') and prop.dt.tzinfo is None and 'TZID' in prop.params) + return bool(hasattr(prop.dt, "tzinfo") and prop.dt.tzinfo is None and "TZID" in prop.params) def _get_all_properties(vevent, prop): @@ -506,16 +512,16 @@ def delete_instance(vevent: icalendar.Event, instance: dt.datetime) -> None: """ # TODO check where this instance is coming from and only call the # appropriate function - if 'RRULE' in vevent: - exdates = _get_all_properties(vevent, 'EXDATE') + if "RRULE" in vevent: + exdates = _get_all_properties(vevent, "EXDATE") exdates += [instance] - vevent.pop('EXDATE') - vevent.add('EXDATE', exdates) - if 'RDATE' in vevent: - rdates = [one for one in _get_all_properties(vevent, 'RDATE') if one != instance] - vevent.pop('RDATE') + vevent.pop("EXDATE") + vevent.add("EXDATE", exdates) + if "RDATE" in vevent: + rdates = [one for one in _get_all_properties(vevent, "RDATE") if one != instance] + vevent.pop("RDATE") if rdates != []: - vevent.add('RDATE', rdates) + vevent.add("RDATE", rdates) def sort_key(vevent: icalendar.Event) -> tuple[str, float]: @@ -525,12 +531,12 @@ def sort_key(vevent: icalendar.Event) -> tuple[str, float]: :rtype: tuple(str, int) """ assert isinstance(vevent, icalendar.Event) - uid = str(vevent['UID']) - rec_id = vevent.get('RECURRENCE-ID') + uid = str(vevent["UID"]) + rec_id = vevent.get("RECURRENCE-ID") if rec_id is None: return uid, 0 - rrange = rec_id.params.get('RANGE') - if rrange == 'THISANDFUTURE': + rrange = rec_id.params.get("RANGE") + if rrange == "THISANDFUTURE": return uid, to_unix_time(rec_id.dt) else: return uid, 1 @@ -543,11 +549,14 @@ def cal_from_ics(ics: str) -> icalendar.Calendar: try: cal = icalendar.Calendar.from_ical(ics) except ValueError as error: - if (len(error.args) > 0 and isinstance(error.args[0], str) and - error.args[0].startswith('Offset must be less than 24 hours')): + if ( + len(error.args) > 0 + and isinstance(error.args[0], str) + and error.args[0].startswith("Offset must be less than 24 hours") + ): logger.warning( - 'Invalid timezone offset encountered, ' - 'timezone information may be wrong: ' + str(error.args[0]) + "Invalid timezone offset encountered, " + "timezone information may be wrong: " + str(error.args[0]) ) icalendar.vUTCOffset.ignore_exceptions = True cal = icalendar.Calendar.from_ical(ics) diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py index f0387fb7c..823e22653 100644 --- a/khal/khalendar/backend.py +++ b/khal/khalendar/backend.py @@ -45,15 +45,15 @@ from .exceptions import CouldNotCreateDbDir, NonUniqueUID, OutdatedDbVersionError, UpdateFailed -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") DB_VERSION = 5 # The current db layout version -RECURRENCE_ID = 'RECURRENCE-ID' -THISANDFUTURE = 'THISANDFUTURE' -THISANDPRIOR = 'THISANDPRIOR' +RECURRENCE_ID = "RECURRENCE-ID" +THISANDFUTURE = "THISANDFUTURE" +THISANDPRIOR = "THISANDPRIOR" -PROTO = 'PROTO' +PROTO = "PROTO" class EventType(IntEnum): @@ -74,11 +74,12 @@ class SQLiteDb: None, a place according to the XDG specifications will be chosen """ - def __init__(self, - calendars: Iterable[str], - db_path: str | None, - locale: LocaleConfiguration, - ) -> None: + def __init__( + self, + calendars: Iterable[str], + db_path: str | None, + locale: LocaleConfiguration, + ) -> None: assert db_path is not None self.calendars: list[str] = list(calendars) self.db_path = path.expanduser(db_path) @@ -92,7 +93,7 @@ def __init__(self, self._check_table_version() @contextlib.contextmanager - def at_once(self) -> Iterator['SQLiteDb']: + def at_once(self) -> Iterator["SQLiteDb"]: assert not self._at_once self._at_once = True try: @@ -106,55 +107,52 @@ def at_once(self) -> Iterator['SQLiteDb']: def _create_dbdir(self) -> None: """create the dbdir if it doesn't exist""" - if self.db_path == ':memory:': + if self.db_path == ":memory:": return None - dbdir = self.db_path.rsplit('/', 1)[0] + dbdir = self.db_path.rsplit("/", 1)[0] if not path.isdir(dbdir): try: - logger.debug('trying to create the directory for the db') + logger.debug("trying to create the directory for the db") makedirs(dbdir, mode=0o770) - logger.debug('success') + logger.debug("success") except OSError as error: - logger.critical(f'failed to create {dbdir}: {error}') + logger.critical(f"failed to create {dbdir}: {error}") raise CouldNotCreateDbDir() def _check_table_version(self) -> None: """tests for current db Version if the table is still empty, insert db_version """ - self.cursor.execute('SELECT version FROM version') + self.cursor.execute("SELECT version FROM version") result = self.cursor.fetchone() if result is None: - self.cursor.execute('INSERT INTO version (version) VALUES (?)', - (DB_VERSION, )) + self.cursor.execute("INSERT INTO version (version) VALUES (?)", (DB_VERSION,)) self.conn.commit() elif not result[0] == DB_VERSION: raise OutdatedDbVersionError( - str(self.db_path) + - " is probably an invalid or outdated database.\n" - "You should consider removing it and running khal again.") + str(self.db_path) + " is probably an invalid or outdated database.\n" + "You should consider removing it and running khal again." + ) def _create_default_tables(self) -> None: - """creates version and calendar tables and inserts table version number - """ - self.cursor.execute('CREATE TABLE IF NOT EXISTS ' - 'version (version INTEGER)') + """creates version and calendar tables and inserts table version number""" + self.cursor.execute("CREATE TABLE IF NOT EXISTS version (version INTEGER)") logger.debug("created version table") - self.cursor.execute('''CREATE TABLE IF NOT EXISTS calendars ( + self.cursor.execute("""CREATE TABLE IF NOT EXISTS calendars ( calendar TEXT NOT NULL UNIQUE, resource TEXT NOT NULL, ctag TEXT - )''') - self.cursor.execute('''CREATE TABLE IF NOT EXISTS events ( + )""") + self.cursor.execute("""CREATE TABLE IF NOT EXISTS events ( href TEXT NOT NULL, calendar TEXT NOT NULL, sequence INT, etag TEXT, item TEXT, primary key (href, calendar) - );''') - self.cursor.execute('''CREATE TABLE IF NOT EXISTS recs_loc ( + );""") + self.cursor.execute("""CREATE TABLE IF NOT EXISTS recs_loc ( dtstart INT NOT NULL, dtend INT NOT NULL, href TEXT NOT NULL REFERENCES events( href ), @@ -163,8 +161,8 @@ def _create_default_tables(self) -> None: dtype INT NOT NULL, calendar TEXT NOT NULL, primary key (href, rec_inst, calendar) - );''') - self.cursor.execute('''CREATE TABLE IF NOT EXISTS recs_float ( + );""") + self.cursor.execute("""CREATE TABLE IF NOT EXISTS recs_float ( dtstart INT NOT NULL, dtend INT NOT NULL, href TEXT NOT NULL REFERENCES events( href ), @@ -173,7 +171,7 @@ def _create_default_tables(self) -> None: dtype INT NOT NULL, calendar TEXT NOT NULL, primary key (href, rec_inst, calendar) - );''') + );""") self.conn.commit() def _check_calendars_exists(self) -> None: @@ -181,14 +179,14 @@ def _check_calendars_exists(self) -> None: table """ for cal in self.calendars: - self.cursor.execute('''SELECT count(*) FROM calendars WHERE calendar = ?;''', (cal,)) + self.cursor.execute("""SELECT count(*) FROM calendars WHERE calendar = ?;""", (cal,)) result = self.cursor.fetchone() if result[0] != 0: logger.debug(f"tables for calendar {cal} exist") else: - sql_s = 'INSERT INTO calendars (calendar, resource) VALUES (?, ?);' - stuple = (cal, '') + sql_s = "INSERT INTO calendars (calendar, resource) VALUES (?, ?);" + stuple = (cal, "") self.sql_ex(sql_s, stuple) def sql_ex(self, statement: str, stuple: tuple) -> list: @@ -199,12 +197,13 @@ def sql_ex(self, statement: str, stuple: tuple) -> list: self.conn.commit() return result - def update(self, - vevent_str: str, - href: str, - etag: str='', - calendar: str | None=None, - ) -> None: + def update( + self, + vevent_str: str, + href: str, + etag: str = "", + calendar: str | None = None, + ) -> None: """insert a new or update an existing event into the db This is mostly a wrapper around two SQL statements, doing some cleanup @@ -234,8 +233,11 @@ def update(self, "If you want to import it, please use `khal import FILE`." ) raise NonUniqueUID - vevents = (sanitize_vevent(c, self.locale['default_timezone'], href, calendar) for - c in ical.walk() if c.name == 'VEVENT') + vevents = ( + sanitize_vevent(c, self.locale["default_timezone"], href, calendar) + for c in ical.walk() + if c.name == "VEVENT" + ) # Need to delete the whole event in case we are updating a # recurring event with an event which is either not recurring any # more or has EXDATEs, as those would be left in the recursion @@ -247,12 +249,13 @@ def update(self, check_support(vevent, href, calendar) self._update_impl(vevent, href, calendar) - sql_s = ('INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);') + sql_s = "INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);" stuple = (vevent_str, etag, href, calendar) self.sql_ex(sql_s, stuple) - def update_vcf_dates(self, vevent_str: str, href: str, etag: str='', - calendar: str | None=None) -> None: + def update_vcf_dates( + self, vevent_str: str, href: str, etag: str = "", calendar: str | None = None + ) -> None: """insert events from a vcard into the db This is will parse BDAY, ANNIVERSARY, X-ANNIVERSARY and X-ABDATE fields. @@ -271,72 +274,70 @@ def update_vcf_dates(self, vevent_str: str, href: str, etag: str='', assert calendar is not None assert href is not None # Delete all event entries for this contact - self.deletelike(href + '%', calendar=calendar) + self.deletelike(href + "%", calendar=calendar) ical = cal_from_ics(vevent_str) vcard = ical.walk()[0] for key in vcard.keys(): - if key in ['BDAY', 'X-ANNIVERSARY', 'ANNIVERSARY'] or key.endswith('X-ABDATE'): + if key in ["BDAY", "X-ANNIVERSARY", "ANNIVERSARY"] or key.endswith("X-ABDATE"): date = vcard[key] - uuid = vcard.get('UID') + uuid = vcard.get("UID") if isinstance(date, list): logger.warning( - f'Vcard {href} in collection {calendar} has more than one ' - f'{key}, will be skipped and not be available in khal.' + f"Vcard {href} in collection {calendar} has more than one " + f"{key}, will be skipped and not be available in khal." ) continue try: - if date[0:2] == '--' and date[3] != '-': - date = '1900' + date[2:] + if date[0:2] == "--" and date[3] != "-": + date = "1900" + date[2:] orig_date = False else: orig_date = True date = parser.parse(date).date() except ValueError: - logger.warning( - f'cannot parse {key} in {href} in collection {calendar}') + logger.warning(f"cannot parse {key} in {href} in collection {calendar}") continue - if 'FN' in vcard: - name = vcard['FN'] + if "FN" in vcard: + name = vcard["FN"] else: - vn = vcard['N'] - if isinstance (vn, str): # icalendar < 7.0.0 - n = vn.split(';') - name = ' '.join([n[1], n[2], n[0]]) + vn = vcard["N"] + if isinstance(vn, str): # icalendar < 7.0.0 + n = vn.split(";") + name = " ".join([n[1], n[2], n[0]]) else: - name = f'{vn.fields.given} {vn.fields.additional} {vn.fields.family}' + name = f"{vn.fields.given} {vn.fields.additional} {vn.fields.family}" vevent = icalendar.Event() - vevent.add('dtstart', date) - vevent.add('dtend', date + dt.timedelta(days=1)) + vevent.add("dtstart", date) + vevent.add("dtend", date + dt.timedelta(days=1)) if date.month == 2 and date.day == 29: # leap year - vevent.add('rrule', {'freq': 'YEARLY', 'BYYEARDAY': 60}) + vevent.add("rrule", {"freq": "YEARLY", "BYYEARDAY": 60}) else: - vevent.add('rrule', {'freq': 'YEARLY'}) + vevent.add("rrule", {"freq": "YEARLY"}) description = get_vcard_event_description(vcard, key) if orig_date: - if key == 'BDAY': - xtag = 'x-birthday' - elif key.endswith('ANNIVERSARY'): - xtag = 'x-anniversary' + if key == "BDAY": + xtag = "x-birthday" + elif key.endswith("ANNIVERSARY"): + xtag = "x-anniversary" else: - xtag = 'x-abdate' - vevent.add('x-ablabel', description) - vevent.add(xtag, - f'{date.year:04}{date.month:02}{date.day:02}') - vevent.add('x-fname', name) - vevent.add('summary', - f'{name}\'s {description}') - vevent.add('uid', href + key) - vevent_str = vevent.to_ical().decode('utf-8') + xtag = "x-abdate" + vevent.add("x-ablabel", description) + vevent.add(xtag, f"{date.year:04}{date.month:02}{date.day:02}") + vevent.add("x-fname", name) + vevent.add("summary", f"{name}'s {description}") + vevent.add("uid", href + key) + vevent_str = vevent.to_ical().decode("utf-8") self._update_impl(vevent, href + key, calendar) - sql_s = ('INSERT INTO events (item, etag, href, calendar)' - ' VALUES (?, ?, ?, ?);') + sql_s = "INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);" stuple = (vevent_str, etag, href + key, calendar) try: self.sql_ex(sql_s, stuple) except sqlite3.IntegrityError as error: - raise UpdateFailed('Database integrity error creating birthday event ' - f'on {date} for contact {name} (UID: {uuid}): ' - f'{error}') + raise UpdateFailed( + "Database integrity error creating birthday event " + f"on {date} for contact {name} (UID: {uuid}): " + f"{error}" + ) def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> None: """insert `vevent` into the database @@ -351,20 +352,21 @@ def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> if rec_id is None: rrange = None else: - rrange = rec_id.params.get('RANGE') + rrange = rec_id.params.get("RANGE") # testing on datetime.date won't work as datetime is a child of date - if not isinstance(vevent['DTSTART'].dt, dt.datetime): + if not isinstance(vevent["DTSTART"].dt, dt.datetime): dtype = EventType.DATE else: dtype = EventType.DATETIME - if ('TZID' in vevent['DTSTART'].params and dtype == EventType.DATETIME) or \ - getattr(vevent['DTSTART'].dt, 'tzinfo', None): - recs_table = 'recs_loc' + if ("TZID" in vevent["DTSTART"].params and dtype == EventType.DATETIME) or getattr( + vevent["DTSTART"].dt, "tzinfo", None + ): + recs_table = "recs_loc" else: - recs_table = 'recs_float' + recs_table = "recs_float" - thisandfuture = (rrange == THISANDFUTURE) + thisandfuture = rrange == THISANDFUTURE if thisandfuture: start_shift, duration = calc_shift_deltas(vevent) start_shift_seconds = start_shift.days * 3600 * 24 + start_shift.seconds @@ -393,24 +395,30 @@ def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> if thisandfuture: recs_sql_s = ( - f'UPDATE {recs_table} SET dtstart = rec_inst + ?, dtend = rec_inst + ?, ' - 'ref = ? WHERE rec_inst >= ? AND href = ? AND calendar = ?;') + f"UPDATE {recs_table} SET dtstart = rec_inst + ?, dtend = rec_inst + ?, " + "ref = ? WHERE rec_inst >= ? AND href = ? AND calendar = ?;" + ) stuple_f = ( - start_shift_seconds, start_shift_seconds + duration_seconds, - ref, rec_inst, href, calendar, + start_shift_seconds, + start_shift_seconds + duration_seconds, + ref, + rec_inst, + href, + calendar, ) self.sql_ex(recs_sql_s, stuple_f) else: recs_sql_s = ( - f'INSERT OR REPLACE INTO {recs_table} ' - '(dtstart, dtend, href, ref, dtype, rec_inst, calendar)' - 'VALUES (?, ?, ?, ?, ?, ?, ?);') + f"INSERT OR REPLACE INTO {recs_table} " + "(dtstart, dtend, href, ref, dtype, rec_inst, calendar)" + "VALUES (?, ?, ?, ?, ?, ?, ?);" + ) stuple_n = (dbstart, dbend, href, ref, dtype, rec_inst, calendar) self.sql_ex(recs_sql_s, stuple_n) def get_ctag(self, calendar: str) -> str | None: - stuple = (calendar, ) - sql_s = 'SELECT ctag FROM calendars WHERE calendar = ?;' + stuple = (calendar,) + sql_s = "SELECT ctag FROM calendars WHERE calendar = ?;" try: ctag = self.sql_ex(sql_s, stuple)[0][0] return ctag @@ -418,8 +426,11 @@ def get_ctag(self, calendar: str) -> str | None: return None def set_ctag(self, ctag: str, calendar: str) -> None: - stuple = (ctag, calendar, ) - sql_s = 'UPDATE calendars SET ctag = ? WHERE calendar = ?;' + stuple = ( + ctag, + calendar, + ) + sql_s = "UPDATE calendars SET ctag = ? WHERE calendar = ?;" self.sql_ex(sql_s, stuple) self.conn.commit() @@ -428,28 +439,28 @@ def get_etag(self, href: str, calendar: str) -> str | None: return: etag """ - sql_s = 'SELECT etag FROM events WHERE href = ? AND calendar = ?;' + sql_s = "SELECT etag FROM events WHERE href = ? AND calendar = ?;" try: etag = self.sql_ex(sql_s, (href, calendar))[0][0] return etag except IndexError: return None - def delete(self, href: str, etag: Any=None, calendar: str='') -> None: + def delete(self, href: str, etag: Any = None, calendar: str = "") -> None: """ removes the event from the db, :param etag: only there for compatibility with vdirsyncer's Storage, we always delete """ - assert calendar != '' - for table in ['recs_loc', 'recs_float']: - sql_s = f'DELETE FROM {table} WHERE href = ? AND calendar = ?;' + assert calendar != "" + for table in ["recs_loc", "recs_float"]: + sql_s = f"DELETE FROM {table} WHERE href = ? AND calendar = ?;" self.sql_ex(sql_s, (href, calendar)) - sql_s = 'DELETE FROM events WHERE href = ? AND calendar = ?;' + sql_s = "DELETE FROM events WHERE href = ? AND calendar = ?;" self.sql_ex(sql_s, (href, calendar)) - def deletelike(self, href: str, etag: Any=None, calendar: str='') -> None: + def deletelike(self, href: str, etag: Any = None, calendar: str = "") -> None: """ removes events from the db that match an SQL 'like' statement, @@ -458,21 +469,21 @@ def deletelike(self, href: str, etag: Any=None, calendar: str='') -> None: :param etag: only there for compatibility with vdirsyncer's Storage, we always delete """ - assert calendar != '' - for table in ['recs_loc', 'recs_float']: - sql_s = f'DELETE FROM {table} WHERE href LIKE ? AND calendar = ?;' + assert calendar != "" + for table in ["recs_loc", "recs_float"]: + sql_s = f"DELETE FROM {table} WHERE href LIKE ? AND calendar = ?;" self.sql_ex(sql_s, (href, calendar)) - sql_s = 'DELETE FROM events WHERE href LIKE ? AND calendar = ?;' + sql_s = "DELETE FROM events WHERE href LIKE ? AND calendar = ?;" self.sql_ex(sql_s, (href, calendar)) def list(self, calendar: str) -> list[tuple[str, str]]: - """ list all events in `calendar` + """list all events in `calendar` used for testing :returns: list of (href, etag) """ - sql_s = 'SELECT href, etag FROM events WHERE calendar = ?;' - return list(set(self.sql_ex(sql_s, (calendar, )))) + sql_s = "SELECT href, etag FROM events WHERE calendar = ?;" + return list(set(self.sql_ex(sql_s, (calendar,)))) def get_localized_calendars(self, start: dt.datetime, end: dt.datetime) -> Iterable[str]: assert start.tzinfo is not None @@ -480,17 +491,17 @@ def get_localized_calendars(self, start: dt.datetime, end: dt.datetime) -> Itera start_u = utils.to_unix_time(start) end_u = utils.to_unix_time(end) sql_s = ( - 'SELECT events.calendar FROM ' - 'recs_loc JOIN events ON ' - 'recs_loc.href = events.href AND ' - 'recs_loc.calendar = events.calendar WHERE ' - '(dtstart >= ? AND dtstart <= ? OR ' - 'dtend > ? AND dtend <= ? OR ' - 'dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) ' - 'ORDER BY dtstart') - stuple = tuple( - [start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore - result = self.sql_ex(sql_s.format(','.join(["?"] * len(self.calendars))), stuple) + "SELECT events.calendar FROM " + "recs_loc JOIN events ON " + "recs_loc.href = events.href AND " + "recs_loc.calendar = events.calendar WHERE " + "(dtstart >= ? AND dtstart <= ? OR " + "dtend > ? AND dtend <= ? OR " + "dtstart <= ? AND dtend >= ?) AND events.calendar in ({0}) " + "ORDER BY dtstart" + ) + stuple = tuple([start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore + result = self.sql_ex(sql_s.format(",".join(["?"] * len(self.calendars))), stuple) for calendar in result: yield calendar[0] # result is always an iterable, even if getting only one item @@ -500,16 +511,17 @@ def get_localized(self, start: dt.datetime, end: dt.datetime) -> Iterable[EventT start_timestamp = utils.to_unix_time(start) end_timestamp = utils.to_unix_time(end) sql_s = ( - 'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar ' - 'FROM recs_loc JOIN events ON ' - 'recs_loc.href = events.href AND ' - 'recs_loc.calendar = events.calendar WHERE ' - '(dtstart >= ? AND dtstart <= ? OR ' - 'dtend > ? AND dtend <= ? OR ' - 'dtstart <= ? AND dtend >= ?) AND ' + "SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar " + "FROM recs_loc JOIN events ON " + "recs_loc.href = events.href AND " + "recs_loc.calendar = events.calendar WHERE " + "(dtstart >= ? AND dtstart <= ? OR " + "dtend > ? AND dtend <= ? OR " + "dtstart <= ? AND dtend >= ?) AND " # insert as many "?" as we have configured calendars - f'events.calendar in ({",".join("?" * len(self.calendars))}) ' - 'ORDER BY dtstart') + f"events.calendar in ({','.join('?' * len(self.calendars))}) " + "ORDER BY dtstart" + ) stuple = ( start_timestamp, end_timestamp, @@ -530,17 +542,17 @@ def get_floating_calendars(self, start: dt.datetime, end: dt.datetime) -> Iterab start_u = utils.to_unix_time(start) end_u = utils.to_unix_time(end) sql_s = ( - 'SELECT events.calendar FROM ' - 'recs_float JOIN events ON ' - 'recs_float.href = events.href AND ' - 'recs_float.calendar = events.calendar WHERE ' - '(dtstart >= ? AND dtstart < ? OR ' - 'dtend > ? AND dtend <= ? OR ' - 'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) ' - 'ORDER BY dtstart') - stuple = tuple( - [start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore - result = self.sql_ex(sql_s.format(','.join(["?"] * len(self.calendars))), stuple) + "SELECT events.calendar FROM " + "recs_float JOIN events ON " + "recs_float.href = events.href AND " + "recs_float.calendar = events.calendar WHERE " + "(dtstart >= ? AND dtstart < ? OR " + "dtend > ? AND dtend <= ? OR " + "dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) " + "ORDER BY dtstart" + ) + stuple = tuple([start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore + result = self.sql_ex(sql_s.format(",".join(["?"] * len(self.calendars))), stuple) for calendar in result: yield calendar[0] @@ -554,17 +566,17 @@ def get_floating(self, start: dt.datetime, end: dt.datetime) -> Iterable[EventTu start_u = utils.to_unix_time(start) end_u = utils.to_unix_time(end) sql_s = ( - 'SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar ' - 'FROM recs_float JOIN events ON ' - 'recs_float.href = events.href AND ' - 'recs_float.calendar = events.calendar WHERE ' - '(dtstart >= ? AND dtstart < ? OR ' - 'dtend > ? AND dtend <= ? OR ' - 'dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) ' - 'ORDER BY dtstart') - stuple = tuple( - [start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore - result = self.sql_ex(sql_s.format(','.join(["?"] * len(self.calendars))), stuple) + "SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar " + "FROM recs_float JOIN events ON " + "recs_float.href = events.href AND " + "recs_float.calendar = events.calendar WHERE " + "(dtstart >= ? AND dtstart < ? OR " + "dtend > ? AND dtend <= ? OR " + "dtstart <= ? AND dtend > ? ) AND events.calendar in ({0}) " + "ORDER BY dtstart" + ) + stuple = tuple([start_u, end_u, start_u, end_u, start_u, end_u] + list(self.calendars)) # type: ignore + result = self.sql_ex(sql_s.format(",".join(["?"] * len(self.calendars))), stuple) for item, href, start_s, end_s, ref, etag, dtype, calendar in result: start_dt = dt.datetime.fromtimestamp(start_s, pytz.UTC).replace(tzinfo=None) end_dt = dt.datetime.fromtimestamp(end_s, pytz.UTC).replace(tzinfo=None) @@ -576,29 +588,30 @@ def get_floating(self, start: dt.datetime, end: dt.datetime) -> Iterable[EventTu def get(self, href: str, calendar: str) -> str: """returns the ical string matching href and calendar""" assert calendar is not None - sql_s = 'SELECT item, etag FROM events WHERE href = ? AND calendar = ?;' + sql_s = "SELECT item, etag FROM events WHERE href = ? AND calendar = ?;" item, etag = self.sql_ex(sql_s, (href, calendar))[0] return item def get_with_etag(self, href: str, calendar: str) -> tuple[str, str]: """returns the ical string and its etag matching href and calendar""" assert calendar is not None - sql_s = 'SELECT item, etag FROM events WHERE href = ? AND calendar = ?;' + sql_s = "SELECT item, etag FROM events WHERE href = ? AND calendar = ?;" item, etag = self.sql_ex(sql_s, (href, calendar))[0] return item, etag - def search(self, search_string: str) \ - -> Iterable[tuple[str, str, dt.date, dt.date, str, str, str]]: + def search( + self, search_string: str + ) -> Iterable[tuple[str, str, dt.date, dt.date, str, str, str]]: """search for events matching `search_string`""" sql_s = ( - 'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar ' - 'FROM recs_loc JOIN events ON ' - 'recs_loc.href = events.href AND ' - 'recs_loc.calendar = events.calendar ' - 'WHERE item LIKE (?) and events.calendar in ({0});' + "SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar " + "FROM recs_loc JOIN events ON " + "recs_loc.href = events.href AND " + "recs_loc.calendar = events.calendar " + "WHERE item LIKE (?) and events.calendar in ({0});" ) - stuple = tuple([f'%{search_string}%'] + list(self.calendars)) - result = self.sql_ex(sql_s.format(','.join(["?"] * len(self.calendars))), stuple) + stuple = tuple([f"%{search_string}%"] + list(self.calendars)) + result = self.sql_ex(sql_s.format(",".join(["?"] * len(self.calendars))), stuple) for item, href, start, end, ref, etag, dtype, calendar in result: start = dt.datetime.fromtimestamp(start, pytz.UTC) end = dt.datetime.fromtimestamp(end, pytz.UTC) @@ -608,14 +621,14 @@ def search(self, search_string: str) \ yield item, href, start, end, ref, etag, calendar sql_s = ( - 'SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar ' - 'FROM recs_float JOIN events ON ' - 'recs_float.href = events.href AND ' - 'recs_float.calendar = events.calendar ' - 'WHERE item LIKE (?) and events.calendar in ({0});' + "SELECT item, recs_float.href, dtstart, dtend, ref, etag, dtype, events.calendar " + "FROM recs_float JOIN events ON " + "recs_float.href = events.href AND " + "recs_float.calendar = events.calendar " + "WHERE item LIKE (?) and events.calendar in ({0});" ) - stuple = tuple([f'%{search_string}%'] + list(self.calendars)) - result = self.sql_ex(sql_s.format(','.join(["?"] * len(self.calendars))), stuple) + stuple = tuple([f"%{search_string}%"] + list(self.calendars)) + result = self.sql_ex(sql_s.format(",".join(["?"] * len(self.calendars))), stuple) for item, href, start, end, ref, etag, dtype, calendar in result: start = dt.datetime.fromtimestamp(start, pytz.UTC).replace(tzinfo=None) end = dt.datetime.fromtimestamp(end, pytz.UTC).replace(tzinfo=None) @@ -633,32 +646,30 @@ def check_support(vevent: icalendar.cal.Event, href: str, calendar: str) -> None """ rec_id = vevent.get(RECURRENCE_ID) - if rec_id is not None and rec_id.params.get('RANGE') == THISANDPRIOR: + if rec_id is not None and rec_id.params.get("RANGE") == THISANDPRIOR: raise UpdateFailed( - 'The parameter `THISANDPRIOR` is not (and will not be) ' - 'supported by khal (as applications supporting the latest ' - f'standard MUST NOT create those. Therefore event {href} from ' - f'calendar {calendar} will not be shown in khal' + "The parameter `THISANDPRIOR` is not (and will not be) " + "supported by khal (as applications supporting the latest " + f"standard MUST NOT create those. Therefore event {href} from " + f"calendar {calendar} will not be shown in khal" ) - rdate = vevent.get('RDATE') - if rdate is not None and hasattr(rdate, 'params') and rdate.params.get('VALUE') == 'PERIOD': + rdate = vevent.get("RDATE") + if rdate is not None and hasattr(rdate, "params") and rdate.params.get("VALUE") == "PERIOD": raise UpdateFailed( - '`RDATE;VALUE=PERIOD` is currently not supported by khal. ' - f'Therefore event {href} from calendar {calendar} will not be shown in khal.\n' - 'Please post exemplary events (please remove any private data) ' - 'to https://github.com/pimutils/khal/issues/152 .' + "`RDATE;VALUE=PERIOD` is currently not supported by khal. " + f"Therefore event {href} from calendar {calendar} will not be shown in khal.\n" + "Please post exemplary events (please remove any private data) " + "to https://github.com/pimutils/khal/issues/152 ." ) def check_for_errors(component: icalendar.cal.Component, calendar: str, href: str) -> None: """checking if component.errors exists, is not empty and if so warn the user""" - if hasattr(component, 'errors') and component.errors: - logger.error( - f'Errors occurred when parsing {calendar}/{href} for ' - 'the following reasons:') + if hasattr(component, "errors") and component.errors: + logger.error(f"Errors occurred when parsing {calendar}/{href} for the following reasons:") for error in component.errors: logger.error(error) - logger.error('This might lead to this event being shown wrongly or not at all.') + logger.error("This might lead to this event being shown wrongly or not at all.") def calc_shift_deltas(vevent: icalendar.Event) -> tuple[dt.timedelta, dt.timedelta]: @@ -668,28 +679,28 @@ def calc_shift_deltas(vevent: icalendar.Event) -> tuple[dt.timedelta, dt.timedel :param event: an event with a RECURRENCE-ID property """ assert isinstance(vevent, icalendar.Event) # REMOVE ME - start_shift = vevent['DTSTART'].dt - vevent['RECURRENCE-ID'].dt + start_shift = vevent["DTSTART"].dt - vevent["RECURRENCE-ID"].dt try: - duration = vevent['DTEND'].dt - vevent['DTSTART'].dt + duration = vevent["DTEND"].dt - vevent["DTSTART"].dt except KeyError: - duration = vevent['DURATION'].dt + duration = vevent["DURATION"].dt return start_shift, duration def get_vcard_event_description(vcard: icalendar.cal.Component, key: str) -> str: - if key == 'BDAY': - return 'birthday' - elif key.endswith('ANNIVERSARY'): - return 'anniversary' - elif key.endswith('X-ABDATE'): - desc_key = key[:-8] + 'X-ABLABEL' + if key == "BDAY": + return "birthday" + elif key.endswith("ANNIVERSARY"): + return "anniversary" + elif key.endswith("X-ABDATE"): + desc_key = key[:-8] + "X-ABLABEL" if desc_key in vcard.keys(): return vcard[desc_key] else: - desc_key = key[:-8] + 'X-ABLabel' + desc_key = key[:-8] + "X-ABLabel" if desc_key in vcard.keys(): return vcard[desc_key] else: - return 'custom event from vcard' + return "custom event from vcard" else: - return 'unknown event from vcard' + return "unknown event from vcard" diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index b8a914ab7..d4bca6598 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -41,7 +41,7 @@ from khal.plugins import FORMATTERS from khal.utils import generate_random_uid, is_aware, to_naive_utc, to_unix_time -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") class Event: @@ -56,51 +56,53 @@ class Event: only one day will have the same start and end date (even though the icalendar standard would have the end date be one day later) """ + allday: bool = False - def __init__(self, - vevents: dict[str, icalendar.Event], - locale: LocaleConfiguration, - ref: str | None = None, - readonly: bool = False, - href: str | None = None, - etag: str | None = None, - calendar: str | None = None, - color: str | None = None, - start: dt.datetime | None = None, - end: dt.datetime | None = None, - addresses: list[str] | None =None, - ): + def __init__( + self, + vevents: dict[str, icalendar.Event], + locale: LocaleConfiguration, + ref: str | None = None, + readonly: bool = False, + href: str | None = None, + etag: str | None = None, + calendar: str | None = None, + color: str | None = None, + start: dt.datetime | None = None, + end: dt.datetime | None = None, + addresses: list[str] | None = None, + ): """ :param start: start datetime of this event instance :param end: end datetime of this event instance """ - if self.__class__.__name__ == 'Event': - raise ValueError('do not initialize this class directly') + if self.__class__.__name__ == "Event": + raise ValueError("do not initialize this class directly") if ref is None: - raise ValueError('ref should not be None') + raise ValueError("ref should not be None") self._vevents = vevents self.ref = ref self._locale = locale self.readonly = readonly self.href = href self.etag = etag - self.calendar = calendar if calendar else '' + self.calendar = calendar if calendar else "" self.color = color self._start: dt.datetime self._end: dt.datetime self.addresses = addresses if addresses else [] if start is None: - self._start = self._vevents[self.ref]['DTSTART'].dt + self._start = self._vevents[self.ref]["DTSTART"].dt else: self._start = start if end is None: try: - self._end = self._vevents[self.ref]['DTEND'].dt + self._end = self._vevents[self.ref]["DTEND"].dt except KeyError: try: - self._end = self._start + self._vevents[self.ref]['DURATION'].dt + self._end = self._start + self._vevents[self.ref]["DURATION"].dt except KeyError: self._end = self._start + dt.timedelta(days=1) else: @@ -111,13 +113,13 @@ def _get_type_from_vDDD(cls, start: icalendar.prop.vDDDTypes) -> type: """infere the type of the class from the START type of the event""" if not isinstance(start.dt, dt.datetime): return AllDayEvent - if 'TZID' in start.params or start.dt.tzinfo is not None: + if "TZID" in start.params or start.dt.tzinfo is not None: return LocalizedEvent return FloatingEvent @classmethod - def _get_type_from_date(cls, start: dt.datetime) -> type['Event']: - if hasattr(start, 'tzinfo') and start.tzinfo is not None: + def _get_type_from_date(cls, start: dt.datetime) -> type["Event"]: + if hasattr(start, "tzinfo") and start.tzinfo is not None: cls = LocalizedEvent elif isinstance(start, dt.datetime): cls = FloatingEvent @@ -126,47 +128,49 @@ def _get_type_from_date(cls, start: dt.datetime) -> type['Event']: return cls @classmethod - def fromVEvents(cls, - events_list: list[icalendar.Event], - ref: str | None=None, - start: dt.datetime | None=None, - **kwargs) -> 'Event': + def fromVEvents( + cls, + events_list: list[icalendar.Event], + ref: str | None = None, + start: dt.datetime | None = None, + **kwargs, + ) -> "Event": assert isinstance(events_list, list) vevents = {} for event in events_list: - if 'RECURRENCE-ID' in event: - if invalid_timezone(event['RECURRENCE-ID']): - default_timezone = kwargs['locale']['default_timezone'] - recur_id = default_timezone.localize(event['RECURRENCE-ID'].dt) + if "RECURRENCE-ID" in event: + if invalid_timezone(event["RECURRENCE-ID"]): + default_timezone = kwargs["locale"]["default_timezone"] + recur_id = default_timezone.localize(event["RECURRENCE-ID"].dt) ident = str(to_unix_time(recur_id)) else: - ident = str(to_unix_time(event['RECURRENCE-ID'].dt)) + ident = str(to_unix_time(event["RECURRENCE-ID"].dt)) vevents[ident] = event else: - vevents['PROTO'] = event + vevents["PROTO"] = event if ref is None: - ref = 'PROTO' if ref in vevents.keys() else list(vevents.keys())[0] + ref = "PROTO" if ref in vevents.keys() else list(vevents.keys())[0] try: - if type(vevents[ref]['DTSTART'].dt) is not type(vevents[ref]['DTEND'].dt): - raise ValueError('DTSTART and DTEND should be of the same type (datetime or date)') + if type(vevents[ref]["DTSTART"].dt) is not type(vevents[ref]["DTEND"].dt): + raise ValueError("DTSTART and DTEND should be of the same type (datetime or date)") except KeyError: pass if start: instcls = cls._get_type_from_date(start) else: - instcls = cls._get_type_from_vDDD(vevents[ref]['DTSTART']) + instcls = cls._get_type_from_vDDD(vevents[ref]["DTSTART"]) return instcls(vevents, ref=ref, start=start, **kwargs) @classmethod - def fromString(cls, ics: str, ref=None, **kwargs) -> 'Event': + def fromString(cls, ics: str, ref=None, **kwargs) -> "Event": calendar_collection = cal_from_ics(ics) - events = [item for item in calendar_collection.walk() if item.name == 'VEVENT'] + events = [item for item in calendar_collection.walk() if item.name == "VEVENT"] return cls.fromVEvents(events, ref, **kwargs) - def __lt__(self, other: 'Event') -> bool: + def __lt__(self, other: "Event") -> bool: start = self.start_local other_start = other.start_local if isinstance(start, dt.date) and not isinstance(start, dt.datetime): @@ -196,12 +200,12 @@ def __lt__(self, other: 'Event') -> bool: try: return end < other_end except TypeError: - raise ValueError(f'Cannot compare events {end} and {other_end}') + raise ValueError(f"Cannot compare events {end} and {other_end}") try: return start < other_start except TypeError: - raise ValueError(f'Cannot compare events {start} and {other_start}') + raise ValueError(f"Cannot compare events {start} and {other_start}") def update_start_end(self, start: dt.datetime, end: dt.datetime) -> None: """update start and end time of this event @@ -212,28 +216,30 @@ def update_start_end(self, start: dt.datetime, end: dt.datetime) -> None: beware, this methods performs some open heart surgery """ if type(start) is not type(end): - raise ValueError('DTSTART and DTEND should be of the same type (datetime or date)') + raise ValueError("DTSTART and DTEND should be of the same type (datetime or date)") self.__class__ = self._get_type_from_date(start) - self._vevents[self.ref].pop('DTSTART') - self._vevents[self.ref].add('DTSTART', start) + self._vevents[self.ref].pop("DTSTART") + self._vevents[self.ref].add("DTSTART", start) self._start = start if not isinstance(end, dt.datetime): end = end + dt.timedelta(days=1) self._end = end - if 'DTEND' in self._vevents[self.ref]: - self._vevents[self.ref].pop('DTEND') - self._vevents[self.ref].add('DTEND', end) + if "DTEND" in self._vevents[self.ref]: + self._vevents[self.ref].pop("DTEND") + self._vevents[self.ref].add("DTEND", end) else: - self._vevents[self.ref].pop('DURATION') - self._vevents[self.ref].add('DURATION', end - start) + self._vevents[self.ref].pop("DURATION") + self._vevents[self.ref].add("DURATION", end - start) @property def recurring(self) -> bool: try: - rval = 'RRULE' in self._vevents[self.ref] or \ - 'RECURRENCE-ID' in self._vevents[self.ref] or \ - 'RDATE' in self._vevents[self.ref] + rval = ( + "RRULE" in self._vevents[self.ref] + or "RECURRENCE-ID" in self._vevents[self.ref] + or "RDATE" in self._vevents[self.ref] + ) except KeyError: logger.fatal( f"The event at {self.href} might be broken. You might want to " @@ -245,28 +251,27 @@ def recurring(self) -> bool: @property def recurpattern(self) -> str: - if 'RRULE' in self._vevents[self.ref]: - return self._vevents[self.ref]['RRULE'].to_ical().decode('utf-8') + if "RRULE" in self._vevents[self.ref]: + return self._vevents[self.ref]["RRULE"].to_ical().decode("utf-8") else: - return '' + return "" @property def recurobject(self) -> icalendar.vRecur: - if 'RRULE' in self._vevents[self.ref]: - return self._vevents[self.ref]['RRULE'] + if "RRULE" in self._vevents[self.ref]: + return self._vevents[self.ref]["RRULE"] else: return icalendar.vRecur() def update_rrule(self, rrule: str) -> None: - self._vevents['PROTO'].pop('RRULE') + self._vevents["PROTO"].pop("RRULE") if rrule is not None: - self._vevents['PROTO'].add('RRULE', rrule) + self._vevents["PROTO"].add("RRULE", rrule) @property def recurrence_id(self) -> dt.datetime | str: - """return the "original" start date of this event (i.e. their recurrence-id) - """ - if self.ref == 'PROTO': + """return the "original" start date of this event (i.e. their recurrence-id)""" + if self.ref == "PROTO": return self.start else: return dt.datetime.fromtimestamp(int(self.ref), pytz.UTC) @@ -276,39 +281,39 @@ def increment_sequence(self) -> None: # TODO we might want to do this automatically in raw() everytime # the event has changed, this will f*ck up the tests though try: - self._vevents[self.ref]['SEQUENCE'] += 1 + self._vevents[self.ref]["SEQUENCE"] += 1 except KeyError: - self._vevents[self.ref]['SEQUENCE'] = 0 + self._vevents[self.ref]["SEQUENCE"] = 0 @property def symbol_strings(self) -> dict[str, str]: - if self._locale['unicode_symbols']: + if self._locale["unicode_symbols"]: return { - 'recurring': '\N{Clockwise gapped circle arrow}', - 'alarming': '\N{Alarm clock}', - 'range': '\N{Left right arrow}', - 'range_end': '\N{Rightwards arrow to bar}', - 'range_start': '\N{Rightwards arrow from bar}', - 'right_arrow': '\N{Rightwards arrow}', - 'cancelled': '\N{Cross mark}', - 'confirmed': '\N{Heavy check mark}', - 'tentative': '?', - 'declined': '\N{Cross mark}', - 'accepted': '\N{Heavy check mark}', + "recurring": "\N{CLOCKWISE GAPPED CIRCLE ARROW}", + "alarming": "\N{ALARM CLOCK}", + "range": "\N{LEFT RIGHT ARROW}", + "range_end": "\N{RIGHTWARDS ARROW TO BAR}", + "range_start": "\N{RIGHTWARDS ARROW FROM BAR}", + "right_arrow": "\N{RIGHTWARDS ARROW}", + "cancelled": "\N{CROSS MARK}", + "confirmed": "\N{HEAVY CHECK MARK}", + "tentative": "?", + "declined": "\N{CROSS MARK}", + "accepted": "\N{HEAVY CHECK MARK}", } else: return { - 'recurring': '(R)', - 'alarming': '(A)', - 'range': '<->', - 'range_end': '->|', - 'range_start': '|->', - 'right_arrow': '->', - 'cancelled': 'X', - 'confirmed': 'V', - 'tentative': '?', - 'declined': 'X', - 'accepted': 'V', + "recurring": "(R)", + "alarming": "(A)", + "range": "<->", + "range_end": "->|", + "range_start": "|->", + "right_arrow": "->", + "cancelled": "X", + "confirmed": "V", + "tentative": "?", + "declined": "X", + "accepted": "V", } @property @@ -335,61 +340,61 @@ def end(self) -> dt.datetime: @property def duration(self) -> dt.timedelta: try: - return self._vevents[self.ref]['DURATION'].dt + return self._vevents[self.ref]["DURATION"].dt except KeyError: return self.end - self.start @property def uid(self) -> str: - return self._vevents[self.ref]['UID'] + return self._vevents[self.ref]["UID"] @property def organizer(self) -> str: - if 'ORGANIZER' not in self._vevents[self.ref]: - return '' - organizer = self._vevents[self.ref]['ORGANIZER'] - cn = organizer.params.get('CN', '') - email = organizer.split(':')[-1] + if "ORGANIZER" not in self._vevents[self.ref]: + return "" + organizer = self._vevents[self.ref]["ORGANIZER"] + cn = organizer.params.get("CN", "") + email = organizer.split(":")[-1] if cn: - return f'{cn} ({email})' + return f"{cn} ({email})" else: return email @property def url(self) -> str: - if 'URL' not in self._vevents[self.ref]: - return '' - return self._vevents[self.ref]['URL'] + if "URL" not in self._vevents[self.ref]: + return "" + return self._vevents[self.ref]["URL"] def update_url(self, url: str) -> None: if url: - self._vevents[self.ref]['URL'] = url + self._vevents[self.ref]["URL"] = url else: - self._vevents[self.ref].pop('URL') + self._vevents[self.ref].pop("URL") @staticmethod def _create_calendar() -> icalendar.Calendar: """create the calendar""" calendar = icalendar.Calendar() - calendar.add('version', '2.0') - calendar.add( - 'prodid', '-//PIMUTILS.ORG//NONSGML khal / icalendar //EN' - ) + calendar.add("version", "2.0") + calendar.add("prodid", "-//PIMUTILS.ORG//NONSGML khal / icalendar //EN") return calendar @property def raw(self) -> str: - """Creates a VCALENDAR containing VTIMEZONEs - """ + """Creates a VCALENDAR containing VTIMEZONEs""" calendar = self._create_calendar() tzs = [] for vevent in self._vevents.values(): - if hasattr(vevent['DTSTART'].dt, 'tzinfo') and vevent['DTSTART'].dt.tzinfo is not None: - tzs.append(vevent['DTSTART'].dt.tzinfo) - if 'DTEND' in vevent and hasattr(vevent['DTEND'].dt, 'tzinfo') and \ - vevent['DTEND'].dt.tzinfo is not None and \ - vevent['DTEND'].dt.tzinfo not in tzs: - tzs.append(vevent['DTEND'].dt.tzinfo) + if hasattr(vevent["DTSTART"].dt, "tzinfo") and vevent["DTSTART"].dt.tzinfo is not None: + tzs.append(vevent["DTSTART"].dt.tzinfo) + if ( + "DTEND" in vevent + and hasattr(vevent["DTEND"].dt, "tzinfo") + and vevent["DTEND"].dt.tzinfo is not None + and vevent["DTEND"].dt.tzinfo not in tzs + ): + tzs.append(vevent["DTEND"].dt.tzinfo) for tzinfo in tzs: if tzinfo == pytz.UTC: @@ -399,59 +404,59 @@ def raw(self) -> str: for vevent in self._vevents.values(): calendar.add_component(vevent) - return calendar.to_ical().decode('utf-8') + return calendar.to_ical().decode("utf-8") def export_ics(self, path: str) -> None: - """export event as ICS - """ + """export event as ICS""" export_path = os.path.expanduser(path) - with open(export_path, 'w') as fh: + with open(export_path, "w") as fh: fh.write(self.raw) @property def summary(self) -> str: description = None - date = self._vevents[self.ref].get('x-birthday', None) + date = self._vevents[self.ref].get("x-birthday", None) if date: - description = 'birthday' + description = "birthday" else: - date = self._vevents[self.ref].get('x-anniversary', None) + date = self._vevents[self.ref].get("x-anniversary", None) if date: - description = 'anniversary' + description = "anniversary" else: - date = self._vevents[self.ref].get('x-abdate', None) + date = self._vevents[self.ref].get("x-abdate", None) if date: - description = self._vevents[self.ref].get('x-ablabel', 'custom event') + description = self._vevents[self.ref].get("x-ablabel", "custom event") if date: number = self.start_local.year - int(date[:4]) - name = self._vevents[self.ref].get('x-fname', None) + name = self._vevents[self.ref].get("x-fname", None) if int(date[4:6]) == 2 and int(date[6:8]) == 29: - leap = ' (29th of Feb.)' + leap = " (29th of Feb.)" else: - leap = '' + leap = "" if (number - 1) % 10 == 0 and number != 11: - suffix = 'st' + suffix = "st" elif (number - 2) % 10 == 0 and number != 12: - suffix = 'nd' + suffix = "nd" elif (number - 3) % 10 == 0 and number != 13: - suffix = 'rd' + suffix = "rd" else: - suffix = 'th' - return f'{name}\'s {number}{suffix} {description}{leap}' + suffix = "th" + return f"{name}'s {number}{suffix} {description}{leap}" else: - return self._vevents[self.ref].get('SUMMARY', '') + return self._vevents[self.ref].get("SUMMARY", "") def update_summary(self, summary: str) -> None: - self._vevents[self.ref]['SUMMARY'] = summary + self._vevents[self.ref]["SUMMARY"] = summary @staticmethod def _can_handle_alarm(alarm) -> bool: """ Decides whether we can handle a certain alarm. """ - return alarm.get('ACTION') == 'DISPLAY' and \ - isinstance(alarm.get('TRIGGER').dt, dt.timedelta) + return alarm.get("ACTION") == "DISPLAY" and isinstance( + alarm.get("TRIGGER").dt, dt.timedelta + ) @property def alarms(self) -> list[tuple[dt.timedelta, str]]: @@ -459,9 +464,11 @@ def alarms(self) -> list[tuple[dt.timedelta, str]]: Returns a list of all alarms in th original event that we can handle. Unknown types of alarms are ignored. """ - return [(a.get('TRIGGER').dt, a.get('DESCRIPTION')) - for a in self._vevents[self.ref].subcomponents - if a.name == 'VALARM' and self._can_handle_alarm(a)] + return [ + (a.get("TRIGGER").dt, a.get("DESCRIPTION")) + for a in self._vevents[self.ref].subcomponents + if a.name == "VALARM" and self._can_handle_alarm(a) + ] def update_alarms(self, alarms: list[tuple[dt.timedelta, str]]) -> None: """ @@ -469,34 +476,36 @@ def update_alarms(self, alarms: list[tuple[dt.timedelta, str]]) -> None: """ components = self._vevents[self.ref].subcomponents # remove all alarms that we can handle from the subcomponents - components = [c for c in components - if not (c.name == 'VALARM' and self._can_handle_alarm(c))] + components = [ + c for c in components if not (c.name == "VALARM" and self._can_handle_alarm(c)) + ] # add all alarms we could handle from the input for alarm in alarms: new = icalendar.Alarm() - new.add('ACTION', 'DISPLAY') - new.add('TRIGGER', alarm[0]) - new.add('DESCRIPTION', alarm[1]) + new.add("ACTION", "DISPLAY") + new.add("TRIGGER", alarm[0]) + new.add("DESCRIPTION", alarm[1]) components.append(new) self._vevents[self.ref].subcomponents = components @property def location(self) -> str: - return self._vevents[self.ref].get('LOCATION', '') + return self._vevents[self.ref].get("LOCATION", "") def update_location(self, location: str) -> None: if location: - self._vevents[self.ref]['LOCATION'] = location + self._vevents[self.ref]["LOCATION"] = location else: - self._vevents[self.ref].pop('LOCATION') + self._vevents[self.ref].pop("LOCATION") @property def attendees(self) -> str: - addresses = self._vevents[self.ref].get('ATTENDEE', []) + addresses = self._vevents[self.ref].get("ATTENDEE", []) if not isinstance(addresses, list): - addresses = [addresses, ] - return ", ".join([address.split(':')[-1] - for address in addresses]) + addresses = [ + addresses, + ] + return ", ".join([address.split(":")[-1] for address in addresses]) def update_attendees(self, attendees: list[str]): assert isinstance(attendees, list) @@ -505,7 +514,7 @@ def update_attendees(self, attendees: list[str]): # first check for overlaps in existing attendees. # Existing vCalAddress objects will be copied, non-existing # vCalAddress objects will be created and appended. - old_attendees = self._vevents[self.ref].get('ATTENDEE', []) + old_attendees = self._vevents[self.ref].get("ATTENDEE", []) unchanged_attendees = [] vCalAddresses = [] for attendee in attendees: @@ -515,88 +524,88 @@ def update_attendees(self, attendees: list[str]): vCalAddresses.append(old_attendee) unchanged_attendees.append(attendee) for attendee in [a for a in attendees if a not in unchanged_attendees]: - item = icalendar.prop.vCalAddress(f'MAILTO:{attendee}') - item.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') - item.params['PARTSTAT'] = icalendar.prop.vText('NEEDS-ACTION') - item.params['CUTYPE'] = icalendar.prop.vText('INDIVIDUAL') - item.params['RSVP'] = icalendar.prop.vText('TRUE') + item = icalendar.prop.vCalAddress(f"MAILTO:{attendee}") + item.params["ROLE"] = icalendar.prop.vText("REQ-PARTICIPANT") + item.params["PARTSTAT"] = icalendar.prop.vText("NEEDS-ACTION") + item.params["CUTYPE"] = icalendar.prop.vText("INDIVIDUAL") + item.params["RSVP"] = icalendar.prop.vText("TRUE") # TODO use khard here to receive full information from email address vCalAddresses.append(item) - self._vevents[self.ref]['ATTENDEE'] = vCalAddresses + self._vevents[self.ref]["ATTENDEE"] = vCalAddresses else: - self._vevents[self.ref].pop('ATTENDEE') + self._vevents[self.ref].pop("ATTENDEE") @property def categories(self) -> str: try: - return self._vevents[self.ref].get('CATEGORIES', '').to_ical().decode('utf-8') + return self._vevents[self.ref].get("CATEGORIES", "").to_ical().decode("utf-8") except AttributeError: - return '' + return "" def update_categories(self, categories: list[str]) -> None: assert isinstance(categories, list) categories = [c.strip() for c in categories if c != ""] - self._vevents[self.ref].pop('CATEGORIES', False) + self._vevents[self.ref].pop("CATEGORIES", False) if categories: - self._vevents[self.ref].add('CATEGORIES', categories) + self._vevents[self.ref].add("CATEGORIES", categories) @property def description(self) -> str: - return self._vevents[self.ref].get('DESCRIPTION', '') + return self._vevents[self.ref].get("DESCRIPTION", "") def update_description(self, description: str): if description: - self._vevents[self.ref]['DESCRIPTION'] = description + self._vevents[self.ref]["DESCRIPTION"] = description else: - self._vevents[self.ref].pop('DESCRIPTION') + self._vevents[self.ref].pop("DESCRIPTION") @property def _recur_str(self) -> str: if self.recurring: - recurstr = ' ' + self.symbol_strings['recurring'] + recurstr = " " + self.symbol_strings["recurring"] else: - recurstr = '' + recurstr = "" return recurstr @property def _alarm_str(self) -> str: if self.alarms: - alarmstr = ' ' + self.symbol_strings['alarming'] + alarmstr = " " + self.symbol_strings["alarming"] else: - alarmstr = '' + alarmstr = "" return alarmstr @property def _status_str(self) -> str: - if self.status == 'CANCELLED': - statusstr = self.symbol_strings['cancelled'] - elif self.status == 'TENTATIVE': - statusstr = self.symbol_strings['tentative'] - elif self.status == 'CONFIRMED': - statusstr = self.symbol_strings['confirmed'] + if self.status == "CANCELLED": + statusstr = self.symbol_strings["cancelled"] + elif self.status == "TENTATIVE": + statusstr = self.symbol_strings["tentative"] + elif self.status == "CONFIRMED": + statusstr = self.symbol_strings["confirmed"] else: - statusstr = '' + statusstr = "" return statusstr @property def _partstat_str(self) -> str: partstat = self.partstat - if partstat == 'ACCEPTED': - partstatstr = self.symbol_strings['accepted'] - elif partstat == 'TENTATIVE': - partstatstr = self.symbol_strings['tentative'] - elif partstat == 'DECLINED': - partstatstr = self.symbol_strings['declined'] + if partstat == "ACCEPTED": + partstatstr = self.symbol_strings["accepted"] + elif partstat == "TENTATIVE": + partstatstr = self.symbol_strings["tentative"] + elif partstat == "DECLINED": + partstatstr = self.symbol_strings["declined"] else: - partstatstr = '' + partstatstr = "" return partstatstr def attributes( - self, - relative_to: tuple[dt.date, dt.date] | dt.date, - env=None, - colors: bool=True, + self, + relative_to: tuple[dt.date, dt.date] | dt.date, + env=None, + colors: bool = True, ): """ :param colors: determines if colors codes should be printed or not @@ -618,32 +627,34 @@ def attributes( start_local_datetime = self.start_local end_local_datetime = self.end_local else: - start_local_datetime = self._locale['local_timezone'].localize( - dt.datetime.combine(self.start, dt.time.min)) - end_local_datetime = self._locale['local_timezone'].localize( - dt.datetime.combine(self.end, dt.time.min)) + start_local_datetime = self._locale["local_timezone"].localize( + dt.datetime.combine(self.start, dt.time.min) + ) + end_local_datetime = self._locale["local_timezone"].localize( + dt.datetime.combine(self.end, dt.time.min) + ) - day_start = self._locale['local_timezone'].localize( + day_start = self._locale["local_timezone"].localize( dt.datetime.combine(relative_to_start, dt.time.min), ) - day_end = self._locale['local_timezone'].localize( + day_end = self._locale["local_timezone"].localize( dt.datetime.combine(relative_to_end, dt.time.max), ) next_day_start = day_start + dt.timedelta(days=1) allday = isinstance(self, AllDayEvent) - attributes["start"] = self.start_local.strftime(self._locale['datetimeformat']) - attributes["start-long"] = self.start_local.strftime(self._locale['longdatetimeformat']) - attributes["start-date"] = self.start_local.strftime(self._locale['dateformat']) - attributes["start-date-long"] = self.start_local.strftime(self._locale['longdateformat']) - attributes["start-time"] = self.start_local.strftime(self._locale['timeformat']) + attributes["start"] = self.start_local.strftime(self._locale["datetimeformat"]) + attributes["start-long"] = self.start_local.strftime(self._locale["longdatetimeformat"]) + attributes["start-date"] = self.start_local.strftime(self._locale["dateformat"]) + attributes["start-date-long"] = self.start_local.strftime(self._locale["longdateformat"]) + attributes["start-time"] = self.start_local.strftime(self._locale["timeformat"]) - attributes["end"] = self.end_local.strftime(self._locale['datetimeformat']) - attributes["end-long"] = self.end_local.strftime(self._locale['longdatetimeformat']) - attributes["end-date"] = self.end_local.strftime(self._locale['dateformat']) - attributes["end-date-long"] = self.end_local.strftime(self._locale['longdateformat']) - attributes["end-time"] = self.end_local.strftime(self._locale['timeformat']) + attributes["end"] = self.end_local.strftime(self._locale["datetimeformat"]) + attributes["end-long"] = self.end_local.strftime(self._locale["longdatetimeformat"]) + attributes["end-date"] = self.end_local.strftime(self._locale["dateformat"]) + attributes["end-date-long"] = self.end_local.strftime(self._locale["longdateformat"]) + attributes["end-time"] = self.end_local.strftime(self._locale["timeformat"]) attributes["duration"] = timedelta2str(self.duration) @@ -665,15 +676,15 @@ def attributes( if self.start_local.timetuple() < relative_to_start.timetuple(): attributes["start-style"] = self.symbol_strings["right_arrow"] elif self.start_local.timetuple() == relative_to_start.timetuple(): - attributes["start-style"] = self.symbol_strings['range_start'] + attributes["start-style"] = self.symbol_strings["range_start"] else: attributes["start-style"] = attributes["start-time"] tostr = "-" if end_local_datetime in [day_end, next_day_start]: - if self._locale["timeformat"] == '%H:%M': - attributes["end-style"] = '24:00' - tostr = '-' + if self._locale["timeformat"] == "%H:%M": + attributes["end-style"] = "24:00" + tostr = "-" else: attributes["end-style"] = self.symbol_strings["range_end"] tostr = "" @@ -684,40 +695,41 @@ def attributes( attributes["end-style"] = attributes["end-time"] if self.start < self.end: - attributes["to-style"] = '-' + attributes["to-style"] = "-" else: - attributes["to-style"] = '' + attributes["to-style"] = "" if start_local_datetime < day_start and end_local_datetime > day_end: attributes["start-end-time-style"] = self.symbol_strings["range"] else: - attributes["start-end-time-style"] = attributes["start-style"] + \ - tostr + attributes["end-style"] + attributes["start-end-time-style"] = ( + attributes["start-style"] + tostr + attributes["end-style"] + ) if allday: if self.start == self.end: - attributes['start-end-time-style'] = '' + attributes["start-end-time-style"] = "" elif self.start == relative_to_start and self.end > relative_to_end: - attributes['start-end-time-style'] = self.symbol_strings['range_start'] + attributes["start-end-time-style"] = self.symbol_strings["range_start"] elif self.start < relative_to_start and self.end > relative_to_end: - attributes['start-end-time-style'] = self.symbol_strings['range'] + attributes["start-end-time-style"] = self.symbol_strings["range"] elif self.start < relative_to_start and self.end == relative_to_end: - attributes['start-end-time-style'] = self.symbol_strings['range_end'] + attributes["start-end-time-style"] = self.symbol_strings["range_end"] else: - attributes['start-end-time-style'] = '' + attributes["start-end-time-style"] = "" if allday: - attributes['end-necessary'] = '' - attributes['end-necessary-long'] = '' + attributes["end-necessary"] = "" + attributes["end-necessary-long"] = "" if self.start_local != self.end_local: - attributes['end-necessary'] = attributes['end-date'] - attributes['end-necessary-long'] = attributes['end-date-long'] + attributes["end-necessary"] = attributes["end-date"] + attributes["end-necessary-long"] = attributes["end-date-long"] else: - attributes['end-necessary'] = attributes['end-time'] - attributes['end-necessary-long'] = attributes['end-time'] + attributes["end-necessary"] = attributes["end-time"] + attributes["end-necessary-long"] = attributes["end-time"] if self.start_local.date() != self.end_local.date(): - attributes['end-necessary'] = attributes['end'] - attributes['end-necessary-long'] = attributes['end-long'] + attributes["end-necessary"] = attributes["end"] + attributes["end-necessary-long"] = attributes["end-long"] attributes["repeat-symbol"] = self._recur_str attributes["repeat-pattern"] = self.recurpattern @@ -731,7 +743,9 @@ def attributes( if len(formatters) == 1: fmt: Callable[[str], str] = list(formatters)[0] else: - def fmt(s: str) -> str: return s.strip() + + def fmt(s: str) -> str: + return s.strip() attributes["description"] = fmt(self.description) attributes["description-separator"] = "" @@ -741,46 +755,46 @@ def fmt(s: str) -> str: return s.strip() attributes["attendees"] = self.attendees attributes["all-day"] = str(allday) attributes["categories"] = self.categories - attributes['uid'] = self.uid - attributes['url'] = self.url - attributes['url-separator'] = "" - if attributes['url']: - attributes['url-separator'] = " :: " + attributes["uid"] = self.uid + attributes["url"] = self.url + attributes["url-separator"] = "" + if attributes["url"]: + attributes["url-separator"] = " :: " if "calendars" in env and self.calendar in env["calendars"]: cal = env["calendars"][self.calendar] - attributes["calendar-color"] = cal.get('color', '') + attributes["calendar-color"] = cal.get("color", "") attributes["calendar"] = cal.get("displayname", self.calendar) else: - attributes["calendar-color"] = attributes["calendar"] = '' + attributes["calendar-color"] = attributes["calendar"] = "" attributes["calendar"] = self.calendar if colors: - attributes['reset'] = style('', reset=True) - attributes['bold'] = style('', bold=True, reset=False) + attributes["reset"] = style("", reset=True) + attributes["bold"] = style("", bold=True, reset=False) for c in ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]: attributes[c] = style("", reset=False, fg=c) attributes[c + "-bold"] = style("", reset=False, fg=c, bold=True) else: - attributes['reset'] = attributes['bold'] = '' + attributes["reset"] = attributes["bold"] = "" for c in ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]: - attributes[c] = attributes[c + '-bold'] = '' + attributes[c] = attributes[c + "-bold"] = "" - attributes['nl'] = '\n' - attributes['tab'] = '\t' - attributes['bell'] = '\a' + attributes["nl"] = "\n" + attributes["tab"] = "\t" + attributes["bell"] = "\a" - attributes['status'] = self.status + ' ' if self.status else '' - attributes['cancelled'] = 'CANCELLED ' if self.status == 'CANCELLED' else '' + attributes["status"] = self.status + " " if self.status else "" + attributes["cancelled"] = "CANCELLED " if self.status == "CANCELLED" else "" return attributes - def duplicate(self) -> 'Event': + def duplicate(self) -> "Event": """duplicate this event's PROTO event""" new_uid = generate_random_uid() - vevent = self._vevents['PROTO'].copy() - vevent['SEQUENCE'] = 0 - vevent['UID'] = icalendar.vText(new_uid) - vevent['SUMMARY'] = icalendar.vText(vevent['SUMMARY'] + ' Copy') + vevent = self._vevents["PROTO"].copy() + vevent["SEQUENCE"] = 0 + vevent["UID"] = icalendar.vText(new_uid) + vevent["SUMMARY"] = icalendar.vText(vevent["SUMMARY"] + " Copy") event = self.fromVEvents([vevent], locale=self._locale) event.calendar = self.calendar event._locale = self._locale @@ -793,16 +807,16 @@ def delete_instance(self, instance: dt.datetime) -> None: defined in the event """ assert self.recurring - delete_instance(self._vevents['PROTO'], instance) + delete_instance(self._vevents["PROTO"], instance) # in case the instance we want to delete is specified as a RECURRENCE-ID # event, we should delete that as well to_pop = [] for key in self._vevents: - if key == 'PROTO': + if key == "PROTO": continue try: - if self._vevents[key].get('RECURRENCE-ID').dt == instance: + if self._vevents[key].get("RECURRENCE-ID").dt == instance: to_pop.append(key) except TypeError: # localized/floating datetime mismatch continue @@ -811,14 +825,14 @@ def delete_instance(self, instance: dt.datetime) -> None: @property def status(self) -> str: - return self._vevents[self.ref].get('STATUS', '') + return self._vevents[self.ref].get("STATUS", "") @property def partstat(self) -> str | None: - for attendee in self._vevents[self.ref].get('ATTENDEE', []): + for attendee in self._vevents[self.ref].get("ATTENDEE", []): for address in self.addresses: - if attendee == 'mailto:' + address: - return attendee.params.get('PARTSTAT', '') + if attendee == "mailto:" + address: + return attendee.params.get("PARTSTAT", "") return None @@ -834,7 +848,7 @@ class LocalizedEvent(DatetimeEvent): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) try: - starttz = getattr(self._vevents[self.ref]['DTSTART'].dt, 'tzinfo', None) + starttz = getattr(self._vevents[self.ref]["DTSTART"].dt, "tzinfo", None) except KeyError: msg = ( f"Cannot understand event {kwargs.get('href')} from " @@ -847,13 +861,13 @@ def __init__(self, *args, **kwargs) -> None: ) if starttz is None: - starttz = self._locale['default_timezone'] + starttz = self._locale["default_timezone"] try: - endtz = getattr(self._vevents[self.ref]['DTEND'].dt, 'tzinfo', None) + endtz = getattr(self._vevents[self.ref]["DTEND"].dt, "tzinfo", None) except KeyError: endtz = starttz if endtz is None: - endtz = self._locale['default_timezone'] + endtz = self._locale["default_timezone"] if is_aware(self._start): self._start = self._start.astimezone(starttz) @@ -870,28 +884,28 @@ def start_local(self) -> dt.datetime: """ see parent """ - return self.start.astimezone(self._locale['local_timezone']) + return self.start.astimezone(self._locale["local_timezone"]) @property def end_local(self) -> dt.datetime: """ see parent """ - return self.end.astimezone(self._locale['local_timezone']) + return self.end.astimezone(self._locale["local_timezone"]) class FloatingEvent(DatetimeEvent): - """ - """ + """ """ + allday: bool = False @property def start_local(self) -> dt.datetime: - return self._locale['local_timezone'].localize(self.start) + return self._locale["local_timezone"].localize(self.start) @property def end_local(self) -> dt.datetime: - return self._locale['local_timezone'].localize(self.end) + return self._locale["local_timezone"].localize(self.end) class AllDayEvent(Event): @@ -902,26 +916,26 @@ def end(self) -> dt.datetime: end = super().end if end == self.start: # https://github.com/pimutils/khal/issues/129 - logger.warning(f'{self.href} ("{self.summary}"): The event\'s end ' - 'date property contains the same value as the start ' - 'date, which is invalid as per RFC 5545. Khal will ' - 'assume this is meant to be a single-day event on ' - f'{self.start}') + logger.warning( + f'{self.href} ("{self.summary}"): The event\'s end ' + "date property contains the same value as the start " + "date, which is invalid as per RFC 5545. Khal will " + "assume this is meant to be a single-day event on " + f"{self.start}" + ) end += dt.timedelta(days=1) return end - dt.timedelta(days=1) @property def duration(self) -> dt.timedelta: try: - return self._vevents[self.ref]['DURATION'].dt + return self._vevents[self.ref]["DURATION"].dt except KeyError: return self.end - self.start + dt.timedelta(days=1) def create_timezone( - tz: pytz.BaseTzInfo, - first_date: dt.datetime | None=None, - last_date: dt.datetime | None=None + tz: pytz.BaseTzInfo, first_date: dt.datetime | None = None, last_date: dt.datetime | None = None ) -> icalendar.Timezone: """ create an icalendar vtimezone from a pytz.tzinfo object @@ -956,14 +970,14 @@ def create_timezone( first_date = dt.datetime.today() if not first_date else to_naive_utc(first_date) last_date = first_date + dt.timedelta(days=1) if not last_date else to_naive_utc(last_date) timezone = icalendar.Timezone() - timezone.add('TZID', tz) + timezone.add("TZID", tz) dst = { - one[2]: 'DST' in two.__repr__() + one[2]: "DST" in two.__repr__() for one, two in iter(tz._tzinfos.items()) # type: ignore } bst = { - one[2]: 'BST' in two.__repr__() + one[2]: "BST" in two.__repr__() for one, two in iter(tz._tzinfos.items()) # type: ignore } @@ -984,11 +998,10 @@ def create_timezone( name = tz._transition_info[num][2] # type: ignore if name in timezones: ttime = tz.fromutc(tz._utc_transition_times[num]).replace(tzinfo=None) # type: ignore - if 'RDATE' in timezones[name]: - timezones[name]['RDATE'].dts.append( - icalendar.prop.vDDDTypes(ttime)) + if "RDATE" in timezones[name]: + timezones[name]["RDATE"].dts.append(icalendar.prop.vDDDTypes(ttime)) else: - timezones[name].add('RDATE', ttime) + timezones[name].add("RDATE", ttime) continue if dst[name] or bst[name]: @@ -996,12 +1009,10 @@ def create_timezone( else: subcomp = icalendar.TimezoneStandard() - subcomp.add('TZNAME', tz._transition_info[num][2]) # type: ignore - subcomp.add( - 'DTSTART', - tz.fromutc(tz._utc_transition_times[num]).replace(tzinfo=None)) # type: ignore - subcomp.add('TZOFFSETTO', tz._transition_info[num][0]) # type: ignore - subcomp.add('TZOFFSETFROM', tz._transition_info[num - 1][0]) # type: ignore + subcomp.add("TZNAME", tz._transition_info[num][2]) # type: ignore + subcomp.add("DTSTART", tz.fromutc(tz._utc_transition_times[num]).replace(tzinfo=None)) # type: ignore + subcomp.add("TZOFFSETTO", tz._transition_info[num][0]) # type: ignore + subcomp.add("TZOFFSETFROM", tz._transition_info[num - 1][0]) # type: ignore timezones[name] = subcomp for subcomp in timezones.values(): @@ -1017,12 +1028,12 @@ def _create_timezone_static(tz: StaticTzInfo) -> icalendar.Timezone: :returns: timezone information """ timezone = icalendar.Timezone() - timezone.add('TZID', tz) + timezone.add("TZID", tz) subcomp = icalendar.TimezoneStandard() - subcomp.add('TZNAME', tz) - subcomp.add('DTSTART', dt.datetime(1601, 1, 1)) - subcomp.add('RDATE', dt.datetime(1601, 1, 1)) - subcomp.add('TZOFFSETTO', tz._utcoffset) # type: ignore - subcomp.add('TZOFFSETFROM', tz._utcoffset) # type: ignore + subcomp.add("TZNAME", tz) + subcomp.add("DTSTART", dt.datetime(1601, 1, 1)) + subcomp.add("RDATE", dt.datetime(1601, 1, 1)) + subcomp.add("TZOFFSETTO", tz._utcoffset) # type: ignore + subcomp.add("TZOFFSETFROM", tz._utcoffset) # type: ignore timezone.add_component(subcomp) return timezone diff --git a/khal/khalendar/exceptions.py b/khal/khalendar/exceptions.py index cf7a90e2e..844472c75 100644 --- a/khal/khalendar/exceptions.py +++ b/khal/khalendar/exceptions.py @@ -24,49 +24,42 @@ class UnsupportedRruleExceptionError(UnsupportedFeatureError): - """we do not support exceptions that do not delete events yet""" - def __init__(self, message='') -> None: - x = 'This kind of recurrence exception is currently unsupported' + def __init__(self, message="") -> None: + x = "This kind of recurrence exception is currently unsupported" if message: - x += f': {message.strip()}' + x += f": {message.strip()}" UnsupportedFeatureError.__init__(self, x) class ReadOnlyCalendarError(Error): - """this calendar is readonly and should not be modifiable from within khal""" class EtagMissmatch(Error): - """An event is trying to be modified from khal which has also been externally modified""" class OutdatedDbVersionError(FatalError): - """the db file has an older version and needs to be deleted""" class CouldNotCreateDbDir(FatalError): - """the db directory could not be created. Abort.""" class UpdateFailed(Error): - """could not update the event in the database""" class DuplicateUid(Error): - """an event with this UID already exists""" + existing_href: str | None = None class NonUniqueUID(Error): - """the .ics file contains more than one UID""" diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index dd971b892..22aed97b6 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -25,6 +25,7 @@ calendars. Each calendar is defined by the contents of a vdir, but uses an SQLite db for caching (see backend if you're interested). """ + import datetime as dt import itertools import logging @@ -53,7 +54,7 @@ get_etag_from_file, ) -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") class CalendarCollection: @@ -61,18 +62,19 @@ class CalendarCollection: all calendars are cached in an sqlitedb for performance reasons""" - def __init__(self, - calendars: dict[str, CalendarConfiguration], - hmethod: str='fg', - default_color: str='', - multiple: str='', - multiple_on_overflow: bool=False, - color: str='', - priority: int=10, - highlight_event_days: bool=False, - locale: LocaleConfiguration | None=None, - dbpath: str | None=None, - ) -> None: + def __init__( + self, + calendars: dict[str, CalendarConfiguration], + hmethod: str = "fg", + default_color: str = "", + multiple: str = "", + multiple_on_overflow: bool = False, + color: str = "", + priority: int = 10, + highlight_event_days: bool = False, + locale: LocaleConfiguration | None = None, + dbpath: str | None = None, + ) -> None: assert locale assert dbpath is not None assert calendars is not None @@ -83,19 +85,19 @@ def __init__(self, file_ext: str for name, calendar in self._calendars.items(): - ctype = calendar.get('ctype', 'calendar') - if ctype == 'calendar': - file_ext = '.ics' - elif ctype == 'birthdays': - file_ext = '.vcf' + ctype = calendar.get("ctype", "calendar") + if ctype == "calendar": + file_ext = ".ics" + elif ctype == "birthdays": + file_ext = ".vcf" else: - raise ValueError('ctype must be either `calendar` or `birthdays`') + raise ValueError("ctype must be either `calendar` or `birthdays`") try: - self._storages[name] = Vdir(calendar['path'], file_ext) + self._storages[name] = Vdir(calendar["path"], file_ext) except CollectionNotFoundError: - os.makedirs(calendar['path']) + os.makedirs(calendar["path"]) logger.info(f"created non-existing vdir {calendar['path']}") - self._storages[name] = Vdir(calendar['path'], file_ext) + self._storages[name] = Vdir(calendar["path"], file_ext) self.hmethod = hmethod self.default_color = default_color @@ -111,7 +113,7 @@ def __init__(self, @property def writable_names(self) -> list[str]: - return [c for c in self._calendars if not self._calendars[c].get('readonly', False)] + return [c for c in self._calendars if not self._calendars[c].get("readonly", False)] @property def calendars(self) -> Iterable[CalendarConfiguration]: @@ -130,18 +132,17 @@ def default_calendar_name(self, default: str) -> None: if default is None: self._default_calendar_name = default elif default not in self.names: - raise ValueError(f'Unknown calendar: {default}') + raise ValueError(f"Unknown calendar: {default}") - readonly = self._calendars[default].get('readonly', False) + readonly = self._calendars[default].get("readonly", False) if not readonly: self._default_calendar_name = default else: - raise ValueError( - f'Calendar "{default}" is read-only and cannot be used as default') + raise ValueError(f'Calendar "{default}" is read-only and cannot be used as default') def _local_ctag(self, calendar: str) -> str: - return get_etag_from_file(self._calendars[calendar]['path']) + return get_etag_from_file(self._calendars[calendar]["path"]) def get_floating(self, start: dt.datetime, end: dt.datetime) -> Iterable[Event]: for args in self._backend.get_floating(start, end): @@ -156,14 +157,14 @@ def get_events_on(self, day: dt.date) -> Iterable[Event]: start = dt.datetime.combine(day, dt.time.min) end = dt.datetime.combine(day, dt.time.max) floating_events = self.get_floating(start, end) - localize = self._locale['local_timezone'].localize + localize = self._locale["local_timezone"].localize localized_events = self.get_localized(localize(start), localize(end)) return itertools.chain(localized_events, floating_events) def get_calendars_on(self, day: dt.date) -> list[str]: start = dt.datetime.combine(day, dt.time.min) end = dt.datetime.combine(day, dt.time.max) - localize = self._locale['local_timezone'].localize + localize = self._locale["local_timezone"].localize calendars = itertools.chain( self._backend.get_floating_calendars(start, end), self._backend.get_localized_calendars(localize(start), localize(end)), @@ -176,19 +177,19 @@ def update(self, event: Event) -> None: assert event.calendar is not None assert event.href is not None assert event.raw is not None - if self._calendars[event.calendar]['readonly']: + if self._calendars[event.calendar]["readonly"]: raise ReadOnlyCalendarError() with self._backend.at_once(): event.etag = self._storages[event.calendar].update(event.href, event, event.etag) self._backend.update(event.raw, event.href, event.etag, calendar=event.calendar) self._backend.set_ctag(self._local_ctag(event.calendar), calendar=event.calendar) - def force_update(self, event: Event, collection: str | None=None) -> None: + def force_update(self, event: Event, collection: str | None = None) -> None: """update `event` even if an event with the same uid/href already exists""" href: str calendar = collection if collection is not None else event.calendar assert calendar is not None - if self._calendars[calendar]['readonly']: + if self._calendars[calendar]["readonly"]: raise ReadOnlyCalendarError() with self._backend.at_once(): @@ -201,7 +202,7 @@ def force_update(self, event: Event, collection: str | None=None) -> None: self._backend.update(event.raw, href, etag, calendar=calendar) self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar) - def insert(self, event: Event, collection: str | None=None) -> None: + def insert(self, event: Event, collection: str | None = None) -> None: """Insert a new event to the vdir and the database The event will get a new href and etag properties. If ``collection`` is @@ -215,23 +216,23 @@ def insert(self, event: Event, collection: str | None=None) -> None: # complain. calendar = collection if collection is not None else event.calendar assert calendar is not None - if hasattr(event, 'etag'): + if hasattr(event, "etag"): assert not event.etag - if self._calendars[calendar]['readonly']: + if self._calendars[calendar]["readonly"]: raise ReadOnlyCalendarError() with self._backend.at_once(): try: event.href, event.etag = self._storages[calendar].upload(event) except AlreadyExistingError as Error: - href = getattr(Error, 'existing_href', None) + href = getattr(Error, "existing_href", None) raise DuplicateUid(href) self._backend.update(event.raw, event.href, event.etag, calendar=calendar) self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar) def delete(self, href: str, etag: str | None, calendar: str) -> None: """Delete an event specified by `href` from `calendar`""" - if self._calendars[calendar]['readonly']: + if self._calendars[calendar]["readonly"]: raise ReadOnlyCalendarError() try: self._storages[calendar].delete(href, etag) @@ -239,17 +240,18 @@ def delete(self, href: str, etag: str | None, calendar: str) -> None: raise EtagMissmatch() self._backend.delete(href, calendar=calendar) - def delete_instance(self, - href: str, - etag: str | None, - calendar: str, - rec_id: dt.datetime, - ) -> Event: + def delete_instance( + self, + href: str, + etag: str | None, + calendar: str, + rec_id: dt.datetime, + ) -> Event: """Delete a recurrence instance from an event specified by `href` from `calendar` returns the updated event """ - if self._calendars[calendar]['readonly']: + if self._calendars[calendar]["readonly"]: raise ReadOnlyCalendarError() event = self.get_event(href, calendar) if etag and etag != event.etag: @@ -264,15 +266,16 @@ def get_event(self, href: str, calendar: str) -> Event: event_str, etag = self._backend.get_with_etag(href, calendar) return self._construct_event(event_str, etag=etag, href=href, calendar=calendar) - def _construct_event(self, - item: str, - href: str, - start: dt.datetime | dt.date | None = None, - end: dt.datetime | dt.date | None = None, - ref: str='PROTO', - etag: str | None=None, - calendar: str | None=None, - ) -> Event: + def _construct_event( + self, + item: str, + href: str, + start: dt.datetime | dt.date | None = None, + end: dt.datetime | dt.date | None = None, + ref: str = "PROTO", + etag: str | None = None, + calendar: str | None = None, + ) -> Event: assert calendar is not None event = Event.fromString( item, @@ -283,9 +286,9 @@ def _construct_event(self, start=start, end=end, ref=ref, - color=self._calendars[calendar]['color'], - readonly=self._calendars[calendar]['readonly'], - addresses=self._calendars[calendar]['addresses'], + color=self._calendars[calendar]["color"], + readonly=self._calendars[calendar]["readonly"], + addresses=self._calendars[calendar]["addresses"], ) return event @@ -298,23 +301,24 @@ def change_collection(self, event: Event, new_collection: str) -> None: assert calendar is not None self.delete(href, etag, calendar=calendar) - def create_event_from_ics(self, - ical: str, - calendar_name: str, - etag: str | None=None, - href: str | None=None, - ) -> Event: + def create_event_from_ics( + self, + ical: str, + calendar_name: str, + etag: str | None = None, + href: str | None = None, + ) -> Event: """creates and returns (but does not insert) a new event from ical string""" calendar = calendar_name or self.writable_names[0] return Event.fromString(ical, locale=self._locale, calendar=calendar, etag=etag, href=href) - def create_event_from_dict(self, - event_dict: EventCreationTypes, - calendar_name: str | None = None, - ) -> Event: - """Creates an Event from the method's arguments - """ + def create_event_from_dict( + self, + event_dict: EventCreationTypes, + calendar_name: str | None = None, + ) -> Event: + """Creates an Event from the method's arguments""" vevent = new_vevent(locale=self._locale, **event_dict) calendar_name = calendar_name or self.default_calendar_name or self.writable_names[0] assert calendar_name is not None @@ -350,12 +354,13 @@ def needs_update(self) -> bool: # # and the API would be made even uglier than it already is... for calendar in self._calendars: - if self._needs_update(calendar) or \ - self._last_ctags[calendar] != self._local_ctag(calendar): + if self._needs_update(calendar) or self._last_ctags[calendar] != self._local_ctag( + calendar + ): return True return False - def _needs_update(self, calendar: str, remember: bool=False) -> bool: + def _needs_update(self, calendar: str, remember: bool = False) -> bool: """checks if the db for the given calendar needs an update""" local_ctag = self._local_ctag(calendar) if remember: @@ -367,14 +372,14 @@ def _db_update(self, calendar: str) -> None: local_ctag = self._local_ctag(calendar) db_hrefs = {href for href, etag in self._backend.list(calendar)} storage_hrefs: set[str] = set() - bdays = self._calendars[calendar].get('ctype') == 'birthdays' + bdays = self._calendars[calendar].get("ctype") == "birthdays" with self._backend.at_once(): for href, etag in self._storages[calendar].list(): storage_hrefs.add(href) db_etag = self._backend.get_etag(href, calendar=calendar) if etag != db_etag: - logger.debug(f'Updating {href} because {etag} != {db_etag}') + logger.debug(f"Updating {href} because {etag} != {db_etag}") self._update_vevent(href, calendar=calendar) for href in db_hrefs - storage_hrefs: if bdays: @@ -393,7 +398,7 @@ def _update_vevent(self, href: str, calendar: str) -> bool: does not check for readonly""" event, etag = self._storages[calendar].get(href) try: - if self._calendars[calendar].get('ctype') == 'birthdays': + if self._calendars[calendar].get("ctype") == "birthdays": update = self._backend.update_vcf_dates else: update = self._backend.update @@ -401,10 +406,10 @@ def _update_vevent(self, href: str, calendar: str) -> bool: return True except Exception as e: if not isinstance(e, UpdateFailed | UnsupportedFeatureError | NonUniqueUID): - logger.exception('Unknown exception happened.') + logger.exception("Unknown exception happened.") logger.warning( - f'Skipping {calendar}/{href}: {e!s}\n' - 'This event will not be available in khal.') + f"Skipping {calendar}/{href}: {e!s}\nThis event will not be available in khal." + ) return False def search(self, search_string: str) -> Iterable[Event]: @@ -415,23 +420,23 @@ def get_day_styles(self, day: dt.date, focus: bool) -> str | tuple[str, str] | N calendars = self.get_calendars_on(day) if len(calendars) == 0: return None - if self.color != '': - return 'highlight_days_color' + if self.color != "": + return "highlight_days_color" if len(calendars) == 1: - return 'calendar ' + calendars[0] - if self.multiple != '' and not (self.multiple_on_overflow and len(calendars) == 2): - return 'highlight_days_multiple' - return ('calendar ' + calendars[0], 'calendar ' + calendars[1]) + return "calendar " + calendars[0] + if self.multiple != "" and not (self.multiple_on_overflow and len(calendars) == 2): + return "highlight_days_multiple" + return ("calendar " + calendars[0], "calendar " + calendars[1]) def get_styles(self, date: dt.date, focus: bool) -> str | tuple[str, str] | None: if focus: if date == date.today(): - return 'today focus' + return "today focus" else: - return 'reveal focus' + return "reveal focus" else: if date == date.today(): - return 'today' + return "today" else: if self.highlight_event_days: return self.get_day_styles(date, focus) diff --git a/khal/khalendar/vdir.py b/khal/khalendar/vdir.py index be191e97f..9041b1f6c 100644 --- a/khal/khalendar/vdir.py +++ b/khal/khalendar/vdir.py @@ -1,7 +1,7 @@ -''' +""" Based off https://github.com/pimutils/python-vdir, which is itself based off vdirsyncer. -''' +""" import contextlib import errno @@ -18,17 +18,15 @@ class HasMetaProtocol(Protocol): color_type: Callable - def get_meta(self, key: str) -> str: - ... + def get_meta(self, key: str) -> str: ... - def set_meta(self, key: str, value: str) -> None: - ... + def set_meta(self, key: str, value: str) -> None: ... class cached_property: - '''A read-only @property that is only evaluated once. Only usable on class + """A read-only @property that is only evaluated once. Only usable on class instances' methods. - ''' + """ def __init__(self, fget, doc=None) -> None: self.__name__ = fget.__name__ @@ -43,16 +41,14 @@ def __get__(self, obj, cls): return result -SAFE_UID_CHARS = ('abcdefghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '0123456789_.-+@') +SAFE_UID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-+@" -def _href_safe(uid: str, safe: str=SAFE_UID_CHARS) -> bool: +def _href_safe(uid: str, safe: str = SAFE_UID_CHARS) -> bool: return not bool(set(uid) - set(safe)) -def _generate_href(uid: str | None=None, safe: str=SAFE_UID_CHARS) -> str: +def _generate_href(uid: str | None = None, safe: str = SAFE_UID_CHARS) -> str: if not uid: return str(uuid.uuid4().hex) elif not _href_safe(uid, safe): @@ -62,14 +58,14 @@ def _generate_href(uid: str | None=None, safe: str=SAFE_UID_CHARS) -> str: def get_etag_from_file(f) -> str: - '''Get mtime-based etag from a filepath, file-like object or raw file + """Get mtime-based etag from a filepath, file-like object or raw file descriptor. This function will flush/sync the file as much as necessary to obtain a correct mtime. - ''' + """ close_f = False - if hasattr(f, 'read'): + if hasattr(f, "read"): f.flush() f = f.fileno() elif isinstance(f, str): @@ -88,17 +84,17 @@ def get_etag_from_file(f) -> str: if close_f: os.close(f) - mtime = getattr(stat, 'st_mtime_ns', None) + mtime = getattr(stat, "st_mtime_ns", None) if mtime is None: mtime = stat.st_mtime - return f'{mtime:.9f}' + return f"{mtime:.9f}" class VdirError(IOError): def __init__(self, *args, **kwargs) -> None: for key, value in kwargs.items(): - if getattr(self, key, object()) not in [None, '']: # pragma: no cover - raise TypeError(f'Invalid argument: {key}') + if getattr(self, key, object()) not in [None, ""]: # pragma: no cover + raise TypeError(f"Invalid argument: {key}") setattr(self, key, value) super().__init__(*args) @@ -117,7 +113,7 @@ class WrongEtagError(VdirError): class AlreadyExistingError(VdirError): - existing_href: str = '' + existing_href: str = "" class Item: @@ -127,15 +123,15 @@ def __init__(self, raw: str) -> None: @cached_property def uid(self) -> str | None: - uid = '' + uid = "" lines = iter(self.raw.splitlines()) for line in lines: - if line.startswith('UID:'): + if line.startswith("UID:"): uid += line[4:].strip() break for line in lines: - if not line.startswith(' '): + if not line.startswith(" "): break uid += line[1:] @@ -145,7 +141,7 @@ def uid(self) -> str | None: @contextlib.contextmanager def atomic_write(dest, overwrite=False): fd, src = tempfile.mkstemp(prefix=os.path.basename(dest), dir=os.path.dirname(dest)) - file = os.fdopen(fd, mode='wb') + file = os.fdopen(fd, mode="wb") try: yield file @@ -167,7 +163,7 @@ class VdirBase: item_class = Item default_mode = 0o750 - def __init__(self, path: str, fileext: str, encoding: str='utf-8') -> None: + def __init__(self, path: str, fileext: str, encoding: str = "utf-8") -> None: if not os.path.isdir(path): raise CollectionNotFoundError(path) self.path = path @@ -175,7 +171,7 @@ def __init__(self, path: str, fileext: str, encoding: str='utf-8') -> None: self.fileext = fileext @classmethod - def discover(cls, path: str, **kwargs) -> Iterable['VdirBase']: + def discover(cls, path: str, **kwargs) -> Iterable["VdirBase"]: try: collections = os.listdir(path) except OSError as e: @@ -191,15 +187,15 @@ def discover(cls, path: str, **kwargs) -> Iterable['VdirBase']: @classmethod def create(cls, collection_name: PathLike, **kwargs: PathLike) -> dict[str, PathLike]: kwargs = dict(kwargs) - path = kwargs['path'] + path = kwargs["path"] pathn = os.path.join(path, collection_name) if not os.path.exists(pathn): os.makedirs(pathn, mode=cls.default_mode) elif not os.path.isdir(pathn): - raise OSError(f'{pathn!r} is not a directory.') + raise OSError(f"{pathn!r} is not a directory.") - kwargs['path'] = pathn + kwargs["path"] = pathn return kwargs def _get_filepath(self, href: str) -> str: @@ -217,11 +213,8 @@ def list(self) -> Iterable[tuple[str, str]]: def get(self, href: str) -> tuple[Item, str]: fpath = self._get_filepath(href) try: - with open(fpath, 'rb') as f: - return ( - Item(f.read().decode(self.encoding)), - get_etag_from_file(fpath) - ) + with open(fpath, "rb") as f: + return (Item(f.read().decode(self.encoding)), get_etag_from_file(fpath)) except OSError as e: if e.errno == errno.ENOENT: raise NotFoundError(href) @@ -230,7 +223,7 @@ def get(self, href: str) -> tuple[Item, str]: def upload(self, item: SupportsRaw) -> tuple[str, str]: if not isinstance(item.raw, str): - raise TypeError('item.raw must be a unicode string.') + raise TypeError("item.raw must be a unicode string.") try: href = self._get_href(item.uid) @@ -238,7 +231,7 @@ def upload(self, item: SupportsRaw) -> tuple[str, str]: except OSError as e: if e.errno in ( errno.ENAMETOOLONG, # Unix - errno.ENOENT # Windows + errno.ENOENT, # Windows ): # random href instead of UID-based href = self._get_href(None) @@ -269,7 +262,7 @@ def update(self, href: str, item: SupportsRaw, etag: str) -> str: raise WrongEtagError(etag, actual_etag) if not isinstance(item.raw, str): - raise TypeError('item.raw must be a unicode string.') + raise TypeError("item.raw must be a unicode string.") with atomic_write(fpath, overwrite=True) as f: f.write(item.raw.encode(self.encoding)) @@ -289,7 +282,7 @@ def delete(self, href: str, etag: str | None) -> None: def get_meta(self, key: str) -> str | None: fpath = os.path.join(self.path, key) try: - with open(fpath, 'rb') as f: + with open(fpath, "rb") as f: return f.read().decode(self.encoding).strip() or None except OSError as e: if e.errno == errno.ENOENT: @@ -298,7 +291,7 @@ def get_meta(self, key: str) -> str | None: raise def set_meta(self, key: str, value: str) -> None: - value = value or '' + value = value or "" assert isinstance(value, str) fpath = os.path.join(self.path, key) with atomic_write(fpath, overwrite=True) as f: @@ -308,12 +301,11 @@ def set_meta(self, key: str, value: str) -> None: class Color: def __init__(self, x: str) -> None: if not x: - raise ValueError('Color is false-ish.') - if not x.startswith('#'): - raise ValueError('Color must start with a #.') + raise ValueError("Color is false-ish.") + if not x.startswith("#"): + raise ValueError("Color must start with a #.") if len(x) != 7: - raise ValueError('Color must not have shortcuts. ' - '#ffffff instead of #fff') + raise ValueError("Color must not have shortcuts. #ffffff instead of #fff") self.raw: str = x.upper() @cached_property @@ -327,7 +319,7 @@ def rgb(self) -> tuple[int, int, int]: if len(r) == len(g) == len(b) == 2: return int(r, 16), int(g, 16), int(b, 16) else: - raise ValueError(f'Unable to parse color value: {self.raw}') + raise ValueError(f"Unable to parse color value: {self.raw}") class ColorMixin: @@ -335,20 +327,20 @@ class ColorMixin: def get_color(self: HasMetaProtocol) -> str | None: try: - return self.color_type(self.get_meta('color')) + return self.color_type(self.get_meta("color")) except ValueError: return None def set_color(self: HasMetaProtocol, value: str) -> None: - self.set_meta('color', self.color_type(value).raw) + self.set_meta("color", self.color_type(value).raw) class DisplayNameMixin: def get_displayname(self: HasMetaProtocol) -> str: - return self.get_meta('displayname') + return self.get_meta("displayname") def set_displayname(self: HasMetaProtocol, value: str) -> None: - self.set_meta('displayname', value) + self.set_meta("displayname", value) class Vdir(VdirBase, ColorMixin, DisplayNameMixin): diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py index fc73556dc..38adbfff7 100644 --- a/khal/parse_datetime.py +++ b/khal/parse_datetime.py @@ -36,7 +36,7 @@ from .custom_types import LocaleConfiguration, RRuleMapType -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") def timefstr(dtime_list: list[str], timeformat: str) -> dt.datetime: @@ -58,9 +58,9 @@ def timefstr(dtime_list: list[str], timeformat: str) -> dt.datetime: def datetimefstr( dtime_list: list[str], dateformat: str, - default_day: dt.date | None=None, - infer_year: bool=True, - in_future: bool=True, + default_day: dt.date | None = None, + infer_year: bool = True, + in_future: bool = True, ) -> dt.datetime: """converts a datetime (as one or several string elements of a list) to a datetimeobject, if infer_year is True, use the `default_day`'s year as @@ -77,19 +77,23 @@ def datetimefstr( now = dt.datetime.now() if default_day is None: default_day = now.date() - parts = dateformat.count(' ') + 1 - dtstring = ' '.join(dtime_list[0:parts]) + parts = dateformat.count(" ") + 1 + dtstring = " ".join(dtime_list[0:parts]) # only time.strptime can parse the 29th of Feb. if no year is given dtstart_struct = strptime(dtstring, dateformat) - if infer_year and dtstart_struct.tm_mon == 2 and dtstart_struct.tm_mday == 29 and \ - not isleap(default_day.year): + if ( + infer_year + and dtstart_struct.tm_mon == 2 + and dtstart_struct.tm_mday == 29 + and not isleap(default_day.year) + ): raise ValueError for _ in range(parts): item = dtime_list.pop(0) - if ' ' in item: - logger.warn('detected a space in datetime specification, this can lead to errors.') - logger.warn('Make sure not to quote your datetime specification.') + if " " in item: + logger.warn("detected a space in datetime specification, this can lead to errors.") + logger.warn("Make sure not to quote your datetime specification.") if infer_year: dtstart = dt.datetime(*(default_day.timetuple()[:1] + dtstart_struct[1:5])) @@ -109,21 +113,21 @@ def weekdaypstr(dayname: str) -> int: :return: number of the day in a week """ - if dayname in ['monday', 'mon']: + if dayname in ["monday", "mon"]: return 0 - if dayname in ['tuesday', 'tue']: + if dayname in ["tuesday", "tue"]: return 1 - if dayname in ['wednesday', 'wed']: + if dayname in ["wednesday", "wed"]: return 2 - if dayname in ['thursday', 'thu']: + if dayname in ["thursday", "thu"]: return 3 - if dayname in ['friday', 'fri']: + if dayname in ["friday", "fri"]: return 4 - if dayname in ['saturday', 'sat']: + if dayname in ["saturday", "sat"]: return 5 - if dayname in ['sunday', 'sun']: + if dayname in ["sunday", "sun"]: return 6 - raise ValueError(f'invalid weekday name `{dayname}`') + raise ValueError(f"invalid weekday name `{dayname}`") def construct_daynames(date_: dt.date) -> str: @@ -132,11 +136,11 @@ def construct_daynames(date_: dt.date) -> str: either `Today`, `Tomorrow` or name of weekday. """ if date_ == dt.date.today(): - return 'Today' + return "Today" elif date_ == dt.date.today() + dt.timedelta(days=1): - return 'Tomorrow' + return "Tomorrow" else: - return date_.strftime('%A') + return date_.strftime("%A") def calc_day(dayname: str) -> dt.datetime: @@ -147,11 +151,11 @@ def calc_day(dayname: str) -> dt.datetime: """ today = dt.datetime.combine(dt.date.today(), dt.time.min) dayname = dayname.lower() - if dayname == 'today': + if dayname == "today": return today - if dayname == 'tomorrow': + if dayname == "tomorrow": return today + dt.timedelta(days=1) - if dayname == 'yesterday': + if dayname == "yesterday": return today - dt.timedelta(days=1) wday = weekdaypstr(dayname) @@ -194,7 +198,7 @@ def datetimefstr_weekday(dtime_list: list[str], timeformat: str, infer_year: boo def guessdatetimefstr( dtime_list: list[str], locale: LocaleConfiguration, - default_day: dt.date | None=None, + default_day: dt.date | None = None, in_future=True, ) -> tuple[dt.datetime, bool]: """ @@ -205,7 +209,7 @@ def guessdatetimefstr( # TODO rename in guessdatetimefstrLIST or something saner altogether def timefstr_day(dtime_list: list[str], timeformat: str, infer_year: bool) -> dt.datetime: - if locale['timeformat'] == '%H:%M' and dtime_list[0] == '24:00': + if locale["timeformat"] == "%H:%M" and dtime_list[0] == "24:00": a_date = dt.datetime.combine(day, dt.time(0)) dtime_list.pop(0) else: @@ -214,7 +218,7 @@ def timefstr_day(dtime_list: list[str], timeformat: str, infer_year: bool) -> dt return a_date def datetimefwords(dtime_list: list[str], _: str, infer_year: bool) -> dt.datetime: - if len(dtime_list) > 0 and dtime_list[0].lower() == 'now': + if len(dtime_list) > 0 and dtime_list[0].lower() == "now": dtime_list.pop(0) return dt.datetime.now() raise ValueError @@ -228,17 +232,17 @@ def datefstr_year(dtime_list: list[str], dtformat: str, infer_year: bool) -> dt. all_day: bool infer_year: bool for fun, dtformat, all_day, infer_year in [ - (datefstr_year, locale['datetimeformat'], False, True), - (datefstr_year, locale['longdatetimeformat'], False, False), - (timefstr_day, locale['timeformat'], False, False), - (datetimefstr_weekday, locale['timeformat'], False, False), - (datefstr_year, locale['dateformat'], True, True), - (datefstr_year, locale['longdateformat'], True, False), - (datefstr_weekday, '', True, False), - (datetimefwords, '', False, False), + (datefstr_year, locale["datetimeformat"], False, True), + (datefstr_year, locale["longdatetimeformat"], False, False), + (timefstr_day, locale["timeformat"], False, False), + (datetimefstr_weekday, locale["timeformat"], False, False), + (datefstr_year, locale["dateformat"], True, True), + (datefstr_year, locale["longdateformat"], True, False), + (datefstr_weekday, "", True, False), + (datetimefwords, "", False, False), ]: # if a `short` format contains a year, treat it as a `long` format - if infer_year and '97' in dt.datetime(1997, 10, 11).strftime(dtformat): + if infer_year and "97" in dt.datetime(1997, 10, 11).strftime(dtformat): infer_year = False try: dtstart = fun(dtime_list, dtformat, infer_year=infer_year) @@ -247,7 +251,7 @@ def datefstr_year(dtime_list: list[str], dtformat: str, infer_year: bool) -> dt. else: return dtstart, all_day raise DateTimeParseError( - f"Could not parse \"{dtime_list}\".\nPlease check your configuration " + f'Could not parse "{dtime_list}".\nPlease check your configuration ' "or run `khal printformats` to see if this does match your configured " "[long](date|time|datetime)format.\nIf you suspect a bug, please " "file an issue at https://github.com/pimutils/khal/issues/ " @@ -281,7 +285,7 @@ def timedelta2str(delta: dt.timedelta) -> str: if delta != abs(delta): s = ["-" + part for part in s] - return ' '.join(s) + return " ".join(s) def guesstimedeltafstr(delta_string: str) -> dt.timedelta: @@ -290,31 +294,30 @@ def guesstimedeltafstr(delta_string: str) -> dt.timedelta: :param delta_string: string encoding time-delta, e.g. '1h 15m' """ - tups = re.split(r'(-?\+?\d+)', delta_string) - if not re.match(r'^\s*$', tups[0]): + tups = re.split(r"(-?\+?\d+)", delta_string) + if not re.match(r"^\s*$", tups[0]): raise ValueError(f'Invalid beginning of timedelta string "{delta_string}": "{tups[0]}"') tups = tups[1:] res = dt.timedelta() for num, unit in zip(tups[0::2], tups[1::2]): try: - if num[0] == '+': + if num[0] == "+": num = num[1:] numint = int(num) except ValueError: raise DateTimeParseError( - f'Invalid number in timedelta string "{delta_string}": "{num}"') + f'Invalid number in timedelta string "{delta_string}": "{num}"' + ) ulower = unit.lower().strip() - if ulower == 'd' or ulower == 'day' or ulower == 'days': + if ulower == "d" or ulower == "day" or ulower == "days": res += dt.timedelta(days=numint) - elif ulower == 'h' or ulower == 'hour' or ulower == 'hours': + elif ulower == "h" or ulower == "hour" or ulower == "hours": res += dt.timedelta(hours=numint) - elif (ulower == 'm' or ulower == 'minute' or ulower == 'minutes' or - ulower == 'min'): + elif ulower == "m" or ulower == "minute" or ulower == "minutes" or ulower == "min": res += dt.timedelta(minutes=numint) - elif (ulower == 's' or ulower == 'second' or ulower == 'seconds' or - ulower == 'sec'): + elif ulower == "s" or ulower == "second" or ulower == "seconds" or ulower == "sec": res += dt.timedelta(seconds=numint) else: raise ValueError(f'Invalid unit in timedelta string "{delta_string}": "{unit}"') @@ -322,12 +325,13 @@ def guesstimedeltafstr(delta_string: str) -> dt.timedelta: return res -def guessrangefstr(daterange: str | list[str], - locale: LocaleConfiguration, - default_timedelta_date: dt.timedelta=dt.timedelta(days=1), - default_timedelta_datetime: dt.timedelta=dt.timedelta(hours=1), - adjust_reasonably: bool=False, - ) -> tuple[dt.datetime, dt.datetime, bool]: +def guessrangefstr( + daterange: str | list[str], + locale: LocaleConfiguration, + default_timedelta_date: dt.timedelta = dt.timedelta(days=1), + default_timedelta_datetime: dt.timedelta = dt.timedelta(hours=1), + adjust_reasonably: bool = False, +) -> tuple[dt.datetime, dt.datetime, bool]: """parses a range string :param daterange: date1 [date2 | timedelta] @@ -338,18 +342,18 @@ def guessrangefstr(daterange: str | list[str], """ range_list = daterange if isinstance(daterange, str): - range_list = daterange.split(' ') + range_list = daterange.split(" ") assert isinstance(range_list, list) - if range_list == ['week']: + if range_list == ["week"]: today_weekday = dt.datetime.today().weekday() - startdt = dt.datetime.today() - dt.timedelta(days=(today_weekday - locale['firstweekday'])) + startdt = dt.datetime.today() - dt.timedelta(days=(today_weekday - locale["firstweekday"])) enddt = startdt + dt.timedelta(days=8) return startdt, enddt, True for i in reversed(range(1, len(range_list) + 1)): - startstr = ' '.join(range_list[:i]) - endstr = ' '.join(range_list[i:]) + startstr = " ".join(range_list[:i]) + endstr = " ".join(range_list[i:]) allday = False try: # figuring out start @@ -364,10 +368,10 @@ def guessrangefstr(daterange: str | list[str], end = start + default_timedelta_date else: end = start + default_timedelta_datetime - elif endstr.lower() == 'eod': + elif endstr.lower() == "eod": end = dt.datetime.combine(start.date(), dt.time.max) - elif endstr.lower() == 'week': - start -= dt.timedelta(days=(start.weekday() - locale['firstweekday'])) + elif endstr.lower() == "week": + start -= dt.timedelta(days=(start.weekday() - locale["firstweekday"])) end = start + dt.timedelta(days=8) else: try: @@ -379,16 +383,15 @@ def guessrangefstr(daterange: str | list[str], ) raise FatalError() elif delta.total_seconds() == 0: - logger.fatal( - "Events that last no time are not allowed" - ) + logger.fatal("Events that last no time are not allowed") raise FatalError() end = start + delta except (ValueError, DateTimeParseError): split = endstr.split(" ") end, end_allday = guessdatetimefstr( - split, locale, default_day=start.date(), in_future=False) + split, locale, default_day=start.date(), in_future=False + ) if len(split) != 0: continue if allday: @@ -413,50 +416,52 @@ def guessrangefstr(daterange: str | list[str], pass raise DateTimeParseError( - f"Could not parse \"{daterange}\".\nPlease check your configuration or " + f'Could not parse "{daterange}".\nPlease check your configuration or ' "run `khal printformats` to see if this does match your configured " "[long](date|time|datetime)format.\nIf you suspect a bug, please " "file an issue at https://github.com/pimutils/khal/issues/ " ) -def rrulefstr(repeat: str, - until: str, - locale: LocaleConfiguration, - timezone: dt.tzinfo | None, - ) -> RRuleMapType: +def rrulefstr( + repeat: str, + until: str, + locale: LocaleConfiguration, + timezone: dt.tzinfo | None, +) -> RRuleMapType: if repeat in ["daily", "weekly", "monthly", "yearly"]: - rrule_settings: RRuleMapType = {'freq': repeat} + rrule_settings: RRuleMapType = {"freq": repeat} if until: - until_dt, _ = guessdatetimefstr(until.split(' '), locale) + until_dt, _ = guessdatetimefstr(until.split(" "), locale) if timezone: - rrule_settings['until'] = until_dt.\ - replace(tzinfo=timezone).\ - astimezone(pytz.UTC) + rrule_settings["until"] = until_dt.replace(tzinfo=timezone).astimezone(pytz.UTC) else: - rrule_settings['until'] = until_dt + rrule_settings["until"] = until_dt return rrule_settings else: - logger.fatal("Invalid value for the repeat option. \ - Possible values are: daily, weekly, monthly or yearly") + logger.fatal( + "Invalid value for the repeat option. \ + Possible values are: daily, weekly, monthly or yearly" + ) raise FatalError() -def eventinfofstr(info_string: str, - locale: LocaleConfiguration, - default_event_duration: dt.timedelta, - default_dayevent_duration: dt.timedelta, - adjust_reasonably: bool=False, - ) -> dict[str, Any]: +def eventinfofstr( + info_string: str, + locale: LocaleConfiguration, + default_event_duration: dt.timedelta, + default_dayevent_duration: dt.timedelta, + adjust_reasonably: bool = False, +) -> dict[str, Any]: """parses a string of the form START [END | DELTA] [TIMEZONE] [SUMMARY] [:: DESCRIPTION] into a dictionary with keys: dtstart, dtend, timezone, allday, summary, description """ description = None if " :: " in info_string: - info_string, description = info_string.split(' :: ') + info_string, description = info_string.split(" :: ") - parts = info_string.split(' ') + parts = info_string.split(" ") summary = None start: dt.datetime | dt.date | None = None end: dt.datetime | dt.date | None = None @@ -465,7 +470,8 @@ def eventinfofstr(info_string: str, for i in reversed(range(1, len(parts) + 1)): try: start, end, allday = guessrangefstr( - ' '.join(parts[0:i]), locale, + " ".join(parts[0:i]), + locale, default_timedelta_datetime=default_event_duration, default_timedelta_date=default_dayevent_duration, adjust_reasonably=adjust_reasonably, @@ -479,12 +485,12 @@ def eventinfofstr(info_string: str, i += 1 except (pytz.UnknownTimeZoneError, UnicodeDecodeError, IndexError): tz = None - summary = ' '.join(parts[i:]) + summary = " ".join(parts[i:]) break if start is None or end is None: raise DateTimeParseError( - f"Could not parse \"{info_string}\".\nPlease check your " + f'Could not parse "{info_string}".\nPlease check your ' "configuration or run `khal printformats` to see if this does " "match your configured [long](date|time|datetime)format.\nIf you " "suspect a bug, please file an issue at " @@ -492,7 +498,7 @@ def eventinfofstr(info_string: str, ) if tz is None: - tz = locale['default_timezone'] + tz = locale["default_timezone"] if allday: assert isinstance(start, dt.datetime) diff --git a/khal/plugins.py b/khal/plugins.py index 440ae3aa8..798faaaee 100644 --- a/khal/plugins.py +++ b/khal/plugins.py @@ -14,15 +14,21 @@ def _load_formatters() -> dict[str, Callable[[str], str]]: FORMATTERS: Mapping[str, Callable[[str], str]] = _load_formatters() + def _load_color_themes() -> dict[str, list[tuple[str, ...]]]: color_theme_entrypoints = importlib_metadata.entry_points(group="khal.color_theme") return {ep.name: ep.load() for ep in color_theme_entrypoints} -THEMES: dict[str, list[tuple[str, ...]],] = _load_color_themes() + +THEMES: dict[ + str, + list[tuple[str, ...]], +] = _load_color_themes() def _load_commands() -> dict[str, Callable]: command_entrypoints = importlib_metadata.entry_points(group="khal.commands") return {ep.name: ep.load() for ep in command_entrypoints} + COMMANDS: dict[str, Callable] = _load_commands() diff --git a/khal/settings/exceptions.py b/khal/settings/exceptions.py index 0588d9436..de8252880 100644 --- a/khal/settings/exceptions.py +++ b/khal/settings/exceptions.py @@ -24,6 +24,7 @@ class InvalidSettingsError(Error): """Invalid Settings detected""" + pass diff --git a/khal/settings/settings.py b/khal/settings/settings.py index 7063da887..23bd0a91c 100644 --- a/khal/settings/settings.py +++ b/khal/settings/settings.py @@ -50,8 +50,8 @@ weeknumber_option, ) -logger = logging.getLogger('khal') -SPECPATH = os.path.join(os.path.dirname(__file__), 'khal.spec') +logger = logging.getLogger("khal") +SPECPATH = os.path.join(os.path.dirname(__file__), "khal.spec") def find_configuration_file() -> str | None: @@ -65,7 +65,7 @@ def find_configuration_file() -> str | None: """ for dir in xdg.BaseDirectory.xdg_config_dirs: - path = os.path.join(dir, __productname__, 'config') + path = os.path.join(dir, __productname__, "config") if os.path.exists(path): return path @@ -73,9 +73,10 @@ def find_configuration_file() -> str | None: def get_config( - config_path: str | None=None, - _get_color_from_vdir: Callable=get_color_from_vdir, - _get_vdir_type: Callable=get_vdir_type) -> ConfigObj: + config_path: str | None = None, + _get_color_from_vdir: Callable = get_color_from_vdir, + _get_vdir_type: Callable = get_vdir_type, +) -> ConfigObj: """reads the config file, validates it and return a config dict :param config_path: path to a custom config file, if none is given the @@ -89,30 +90,33 @@ def get_config( if config_path is None or not os.path.exists(config_path): raise NoConfigFile() - logger.debug(f'using the config file at {config_path}') + logger.debug(f"using the config file at {config_path}") try: - user_config = ConfigObj(config_path, - configspec=SPECPATH, - interpolation=False, - file_error=True, - ) + user_config = ConfigObj( + config_path, + configspec=SPECPATH, + interpolation=False, + file_error=True, + ) except ConfigObjError as error: - logger.fatal('parsing the config file with the following error: ' - f'{error}') - logger.fatal('if you recently updated khal, the config file format ' - 'might have changed, in that case please consult the ' - 'CHANGELOG or other documentation') + logger.fatal(f"parsing the config file with the following error: {error}") + logger.fatal( + "if you recently updated khal, the config file format " + "might have changed, in that case please consult the " + "CHANGELOG or other documentation" + ) raise CannotParseConfigFileError() - fdict = {'timezone': is_timezone, - 'timedelta': is_timedelta, - 'expand_path': expand_path, - 'expand_db_path': expand_db_path, - 'weeknumbers': weeknumber_option, - 'monthdisplay': monthdisplay_option, - 'color': is_color, - } + fdict = { + "timezone": is_timezone, + "timedelta": is_timedelta, + "expand_path": expand_path, + "expand_db_path": expand_db_path, + "weeknumbers": weeknumber_option, + "monthdisplay": monthdisplay_option, + "color": is_color, + } validator = Validator(fdict) results = user_config.validate(validator, preserve_errors=True) @@ -120,16 +124,14 @@ def get_config( for section, subsection, config_error in flatten_errors(user_config, results): abort = True if isinstance(config_error, Exception): - logger.fatal( - f'config error:\n' - f'in [{section[0]}] {subsection}: {config_error}') + logger.fatal(f"config error:\nin [{section[0]}] {subsection}: {config_error}") else: for key in config_error: if isinstance(config_error[key], Exception): logger.fatal( - 'config error:\n' - f'in {sectionize(section + [subsection])} {key}: ' - f'{config_error[key]!s}' + "config error:\n" + f"in {sectionize(section + [subsection])} {key}: " + f"{config_error[key]!s}" ) if abort or not results: @@ -141,20 +143,19 @@ def get_config( for section, value in extras: if section == (): logger.warning(f'unknown section "{value}" in config file') - elif section == ('palette',): + elif section == ("palette",): # we don't validate the palette section, because there is no way to # automatically extract valid attributes from the ui module continue else: section = sectionize(section) - logger.warning( - f'unknown key or subsection "{value}" in section "{section}"') + logger.warning(f'unknown key or subsection "{value}" in section "{section}"') return user_config -def sectionize(sections: list[str], depth: int=1) -> str: +def sectionize(sections: list[str], depth: int = 1) -> str: """converts list of string into [list][[of]][[[strings]]]""" - this_part = depth * '[' + sections[0] + depth * ']' + this_part = depth * "[" + sections[0] + depth * "]" if len(sections) > 1: return this_part + sectionize(sections[1:], depth=depth + 1) else: diff --git a/khal/settings/utils.py b/khal/settings/utils.py index 910b17fe6..4376023e7 100644 --- a/khal/settings/utils.py +++ b/khal/settings/utils.py @@ -45,7 +45,7 @@ from .exceptions import InvalidSettingsError -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") def is_timezone(tzstring: str | None) -> dt.tzinfo: @@ -71,35 +71,36 @@ def is_timedelta(string: str) -> dt.timedelta: raise VdtValueError(f"Invalid timedelta: {string}") -def weeknumber_option(option: str) -> Literal['left', 'right'] | Literal[False]: +def weeknumber_option(option: str) -> Literal["left", "right"] | Literal[False]: """checks if *option* is a valid value :param option: the option the user set in the config file :returns: 'off', 'left', 'right' or False """ option = option.lower() - if option == 'left': - return 'left' - elif option == 'right': - return 'right' - elif option in ['off', 'false', '0', 'no', 'none']: + if option == "left": + return "left" + elif option == "right": + return "right" + elif option in ["off", "false", "0", "no", "none"]: return False else: raise VdtValueError( f"Invalid value '{option}' for option 'weeknumber', must be one of " - "'off', 'left' or 'right'") + "'off', 'left' or 'right'" + ) -def monthdisplay_option(option: str) -> Literal['firstday', 'firstfullweek']: +def monthdisplay_option(option: str) -> Literal["firstday", "firstfullweek"]: """checks if *option* is a valid value :param option: the option the user set in the config file """ option = option.lower() - if option == 'firstday': - return 'firstday' - elif option == 'firstfullweek': - return 'firstfullweek' + if option == "firstday": + return "firstday" + elif option == "firstfullweek": + return "firstfullweek" else: raise VdtValueError( f"Invalid value '{option}' for option 'monthdisplay', must be one " @@ -115,7 +116,7 @@ def expand_path(path: str) -> str: def expand_db_path(path: str) -> str: """expands `~` as well as variable names, defaults to $XDG_CACHE_HOME""" if path is None: - path = join(xdg.BaseDirectory.xdg_cache_home, 'khal', 'khal.db') + path = join(xdg.BaseDirectory.xdg_cache_home, "khal", "khal.db") return expanduser(expandvars(path)) @@ -130,11 +131,16 @@ def is_color(color: str) -> str: # 3) a color name from the 16 color palette # 4) a color index from the 256 color palette # 5) an HTML-style color code - if (color in ['', 'auto'] or - color in COLORS.keys() or - (color.isdigit() and int(color) >= 0 and int(color) <= 255) or - (color.startswith('#') and (len(color) in [4, 7, 9]) and - all(c in '01234567890abcdefABCDEF' for c in color[1:]))): + if ( + color in ["", "auto"] + or color in COLORS.keys() + or (color.isdigit() and int(color) >= 0 and int(color) <= 255) + or ( + color.startswith("#") + and (len(color) in [4, 7, 9]) + and all(c in "01234567890abcdefABCDEF" for c in color[1:]) + ) + ): return color raise VdtValueError(color) @@ -143,27 +149,27 @@ def test_default_calendar(config) -> None: """test if config['default']['default_calendar'] is set to a sensible value """ - if config['default']['default_calendar'] is None: + if config["default"]["default_calendar"] is None: pass - elif config['default']['default_calendar'] not in config['calendars']: + elif config["default"]["default_calendar"] not in config["calendars"]: logger.fatal( f"in section [default] {config['default']['default_calendar']} is " "not valid for 'default_calendar', must be one of " f"{config['calendars'].keys()}" ) raise InvalidSettingsError() - elif config['calendars'][config['default']['default_calendar']]['readonly']: - logger.fatal('default_calendar may not be read_only!') + elif config["calendars"][config["default"]["default_calendar"]]["readonly"]: + logger.fatal("default_calendar may not be read_only!") raise InvalidSettingsError() def get_color_from_vdir(path: str) -> str | None: try: - color = Vdir(path, '.ics').get_meta('color') + color = Vdir(path, ".ics").get_meta("color") except CollectionNotFoundError: color = None - if color is None or color == '': - logger.debug(f'Found no or empty file `color` in {path}') + if color is None or color == "": + logger.debug(f"Found no or empty file `color` in {path}") return None color = color.strip() try: @@ -177,26 +183,25 @@ def get_color_from_vdir(path: str) -> str | None: def get_unique_name(path: str, names: Iterable[str]) -> str: # TODO take care of edge cases, make unique name finding less brain-dead try: - name = Vdir(path, '.ics').get_meta('displayname') + name = Vdir(path, ".ics").get_meta("displayname") except CollectionNotFoundError: - logger.fatal(f'The calendar at `{path}` is not a directory.') + logger.fatal(f"The calendar at `{path}` is not a directory.") raise - if name is None or name == '': - logger.debug(f'Found no or empty file `displayname` in {path}') + if name is None or name == "": + logger.debug(f"Found no or empty file `displayname` in {path}") name = os.path.split(path)[-1] if name in names: while name in names: - name = name + '1' + name = name + "1" return name def get_all_vdirs(expand_path: str) -> Iterable[str]: - """returns a list of paths, expanded using glob - """ + """returns a list of paths, expanded using glob""" # FIXME currently returns a list of all directories in path # we add an additional / at the end to make sure we are only getting # directories - items = glob.glob(f'{expand_path}/', recursive=True) + items = glob.glob(f"{expand_path}/", recursive=True) paths = [pathlib.Path(item) for item in sorted(items, key=len, reverse=True)] leaves = set() parents = set() @@ -213,72 +218,79 @@ def get_all_vdirs(expand_path: str) -> Iterable[str]: def get_vdir_type(_: str) -> str: # TODO implement - return 'calendar' + return "calendar" + def validate_palette_entry(attr, definition: str) -> bool: if len(definition) not in (2, 3, 5): - logging.error('Invalid color definition for %s: %s, must be of length, 2, 3, or 5', - attr, definition) + logging.error( + "Invalid color definition for %s: %s, must be of length, 2, 3, or 5", attr, definition + ) return False - if (definition[0] not in COLORS and definition[0] != '') or \ - (definition[1] not in COLORS and definition[1] != ''): - logging.error('Invalid color definition for %s: %s, must be one of %s', - attr, definition, COLORS.keys()) + if (definition[0] not in COLORS and definition[0] != "") or ( + definition[1] not in COLORS and definition[1] != "" + ): + logging.error( + "Invalid color definition for %s: %s, must be one of %s", + attr, + definition, + COLORS.keys(), + ) return False return True + def config_checks( config, - _get_color_from_vdir: Callable=get_color_from_vdir, - _get_vdir_type: Callable=get_vdir_type, + _get_color_from_vdir: Callable = get_color_from_vdir, + _get_vdir_type: Callable = get_vdir_type, ) -> None: """do some tests on the config we cannot do with configobj's validator""" # TODO rename or split up, we are also expanding vdirs of type discover - if len(config['calendars'].keys()) < 1: - logger.fatal('Found no calendar section in the config file') + if len(config["calendars"].keys()) < 1: + logger.fatal("Found no calendar section in the config file") raise InvalidSettingsError() - config['sqlite']['path'] = expand_db_path(config['sqlite']['path']) - if not config['locale']['default_timezone']: - config['locale']['default_timezone'] = is_timezone( - config['locale']['default_timezone']) - if not config['locale']['local_timezone']: - config['locale']['local_timezone'] = is_timezone( - config['locale']['local_timezone']) + config["sqlite"]["path"] = expand_db_path(config["sqlite"]["path"]) + if not config["locale"]["default_timezone"]: + config["locale"]["default_timezone"] = is_timezone(config["locale"]["default_timezone"]) + if not config["locale"]["local_timezone"]: + config["locale"]["local_timezone"] = is_timezone(config["locale"]["local_timezone"]) # expand calendars with type = discover # we need a copy of config['calendars'], because we modify config in the body of the loop - for cname, cconfig in sorted(config['calendars'].items()): - if not isinstance(config['calendars'][cname], dict): - logger.fatal('Invalid config file, probably missing calendar sections') + for cname, cconfig in sorted(config["calendars"].items()): + if not isinstance(config["calendars"][cname], dict): + logger.fatal("Invalid config file, probably missing calendar sections") raise InvalidSettingsError - if config['calendars'][cname]['type'] == 'discover': + if config["calendars"][cname]["type"] == "discover": logger.debug(f"discovering calendars in {cconfig['path']}") - vdirs_discovered = get_all_vdirs(cconfig['path']) + vdirs_discovered = get_all_vdirs(cconfig["path"]) logger.debug(f"found the following vdirs: {vdirs_discovered}") for vdir in vdirs_discovered: vdir_config = { - 'path': vdir, - 'color': _get_color_from_vdir(vdir) or cconfig.get('color', None), - 'type': _get_vdir_type(vdir), - 'readonly': cconfig.get('readonly', False), - 'priority': 10, + "path": vdir, + "color": _get_color_from_vdir(vdir) or cconfig.get("color", None), + "type": _get_vdir_type(vdir), + "readonly": cconfig.get("readonly", False), + "priority": 10, } - unique_vdir_name = get_unique_name(vdir, config['calendars'].keys()) - config['calendars'][unique_vdir_name] = vdir_config - config['calendars'].pop(cname) + unique_vdir_name = get_unique_name(vdir, config["calendars"].keys()) + config["calendars"][unique_vdir_name] = vdir_config + config["calendars"].pop(cname) test_default_calendar(config) - for calendar in config['calendars']: - if config['calendars'][calendar]['type'] == 'birthdays': - config['calendars'][calendar]['readonly'] = True - if config['calendars'][calendar]['color'] == 'auto': - config['calendars'][calendar]['color'] = \ - _get_color_from_vdir(config['calendars'][calendar]['path']) + for calendar in config["calendars"]: + if config["calendars"][calendar]["type"] == "birthdays": + config["calendars"][calendar]["readonly"] = True + if config["calendars"][calendar]["color"] == "auto": + config["calendars"][calendar]["color"] = _get_color_from_vdir( + config["calendars"][calendar]["path"] + ) # check palette settings valid_palette = True - for attr in config.get('palette', []): - valid_palette = valid_palette and validate_palette_entry(attr, config['palette'][attr]) + for attr in config.get("palette", []): + valid_palette = valid_palette and validate_palette_entry(attr, config["palette"][attr]) if not valid_palette: - logger.fatal('Invalid palette entry') + logger.fatal("Invalid palette entry") raise InvalidSettingsError() diff --git a/khal/terminal.py b/khal/terminal.py index 285994910..bc0c7d5d2 100644 --- a/khal/terminal.py +++ b/khal/terminal.py @@ -30,34 +30,34 @@ class NamedColor(NamedTuple): light: bool -RTEXT = '\x1b[7m' # reverse -NTEXT = '\x1b[0m' # normal -BTEXT = '\x1b[1m' # bold -RESET = '\33[0m' +RTEXT = "\x1b[7m" # reverse +NTEXT = "\x1b[0m" # normal +BTEXT = "\x1b[1m" # bold +RESET = "\33[0m" COLORS: dict[str, NamedColor] = { - 'black': NamedColor(color_index=0, light=False), - 'dark red': NamedColor(color_index=1, light=False), - 'dark green': NamedColor(color_index=2, light=False), - 'brown': NamedColor(color_index=3, light=False), - 'dark blue': NamedColor(color_index=4, light=False), - 'dark magenta': NamedColor(color_index=5, light=False), - 'dark cyan': NamedColor(color_index=6, light=False), - 'white': NamedColor(color_index=7, light=False), - 'light gray': NamedColor(color_index=7, light=True), - 'dark gray': NamedColor(color_index=0, light=True), # actually light black - 'light red': NamedColor(color_index=1, light=True), - 'light green': NamedColor(color_index=2, light=True), - 'yellow': NamedColor(color_index=3, light=True), - 'light blue': NamedColor(color_index=4, light=True), - 'light magenta': NamedColor(color_index=5, light=True), - 'light cyan': NamedColor(color_index=6, light=True) + "black": NamedColor(color_index=0, light=False), + "dark red": NamedColor(color_index=1, light=False), + "dark green": NamedColor(color_index=2, light=False), + "brown": NamedColor(color_index=3, light=False), + "dark blue": NamedColor(color_index=4, light=False), + "dark magenta": NamedColor(color_index=5, light=False), + "dark cyan": NamedColor(color_index=6, light=False), + "white": NamedColor(color_index=7, light=False), + "light gray": NamedColor(color_index=7, light=True), + "dark gray": NamedColor(color_index=0, light=True), # actually light black + "light red": NamedColor(color_index=1, light=True), + "light green": NamedColor(color_index=2, light=True), + "yellow": NamedColor(color_index=3, light=True), + "light blue": NamedColor(color_index=4, light=True), + "light magenta": NamedColor(color_index=5, light=True), + "light cyan": NamedColor(color_index=6, light=True), } def get_color( - fg: str | None=None, - bg: str | None=None, - bold_for_light_color: bool=False, + fg: str | None = None, + bg: str | None = None, + bold_for_light_color: bool = False, ) -> str: """convert foreground and/or background color in ANSI color codes @@ -70,10 +70,10 @@ def get_color( :returns: ANSI color code """ - result = '' + result = "" for colorstring, is_bg in ((fg, False), (bg, True)): if colorstring: - color = '\33[' + color = "\33[" if colorstring in COLORS: # 16 color palette if not is_bg: @@ -81,7 +81,7 @@ def get_color( c = 30 + COLORS[colorstring].color_index if COLORS[colorstring].light: if bold_for_light_color: - color += '1;' + color += "1;" else: c += 60 else: @@ -94,9 +94,9 @@ def get_color( elif colorstring.isdigit(): # 256 color palette if not is_bg: - color += '38;5;' + colorstring + color += "38;5;" + colorstring else: - color += '48;5;' + colorstring + color += "48;5;" + colorstring else: # HTML-style 24-bit color if len(colorstring) == 4: @@ -110,19 +110,19 @@ def get_color( g = int(colorstring[3:5], 16) b = int(colorstring[5:7], 16) if not is_bg: - color += f'38;2;{r!s};{g!s};{b!s}' + color += f"38;2;{r!s};{g!s};{b!s}" else: - color += f'48;2;{r!s};{g!s};{b!s}' - color += 'm' + color += f"48;2;{r!s};{g!s};{b!s}" + color += "m" result += color return result def colored( string: str, - fg: str | None=None, - bg: str | None=None, - bold_for_light_color: bool=True, + fg: str | None = None, + bg: str | None = None, + bold_for_light_color: bool = True, ) -> str: """colorize `string` with ANSI color codes @@ -137,7 +137,7 @@ def colored( return result -def merge_columns(lcolumn: list[str], rcolumn: list[str], width: int=25) -> list[str]: +def merge_columns(lcolumn: list[str], rcolumn: list[str], width: int = 25) -> list[str]: """merge two lists elementwise together Wrap right columns to terminal width. @@ -148,6 +148,6 @@ def merge_columns(lcolumn: list[str], rcolumn: list[str], width: int=25) -> list """ missing = len(rcolumn) - len(lcolumn) if missing > 0: - lcolumn = lcolumn + missing * [width * ' '] + lcolumn = lcolumn + missing * [width * " "] - return [' '.join(one) for one in zip_longest(lcolumn, rcolumn, fillvalue='')] + return [" ".join(one) for one in zip_longest(lcolumn, rcolumn, fillvalue="")] diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py index b9f16177d..6b094a7f3 100644 --- a/khal/ui/__init__.py +++ b/khal/ui/__init__.py @@ -41,7 +41,7 @@ from .widgets import CAttrMap, NColumns, NPile, button, linebox from .widgets import ExtendedEdit as Edit -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") # Overview of how this all meant to fit together: # @@ -77,6 +77,7 @@ # │ └─────────────────┘ └──────────────────────────────────┘ │ # └───────────────────────────────────────────────────────────┘ + class DeletionType(IntEnum): ALL = 0 INSTANCES = 1 @@ -113,7 +114,7 @@ def __init__(self, day: dt.date, dateformat: str, conf) -> None: self._day = day self._dateformat = dateformat self._conf = conf - super().__init__('', wrap='clip') + super().__init__("", wrap="clip") self.update_date_line() def update_date_line(self) -> None: @@ -130,29 +131,29 @@ def relative_day(self, day: dt.date, dtformat: str) -> str: :param dtformat: the format day is to be printed in, passed to strftime """ - weekday = day.strftime('%A') + weekday = day.strftime("%A") daystr = day.strftime(dtformat) if day == dt.date.today(): - return f'Today ({weekday}, {daystr})' + return f"Today ({weekday}, {daystr})" elif day == dt.date.today() + dt.timedelta(days=1): - return f'Tomorrow ({weekday}, {daystr})' + return f"Tomorrow ({weekday}, {daystr})" elif day == dt.date.today() - dt.timedelta(days=1): - return f'Yesterday ({weekday}, {daystr})' + return f"Yesterday ({weekday}, {daystr})" approx_delta = utils.relative_timedelta_str(day) - return f'{weekday}, {daystr} ({approx_delta})' + return f"{weekday}, {daystr} ({approx_delta})" def keypress(self, size: tuple[int], key: str | None) -> str | None: - binds = self._conf['keybindings'] - if key in binds['left']: - key = 'left' - elif key in binds['up']: - key = 'up' - elif key in binds['right']: - key = 'right' - elif key in binds['down']: - key = 'down' + binds = self._conf["keybindings"] + if key in binds["left"]: + key = "left" + elif key in binds["up"]: + key = "up" + elif key in binds["right"]: + key = "right" + elif key in binds["down"]: + key = "down" return key @@ -165,14 +166,15 @@ def __init__(self, event, conf, delete_status, this_date=None, relative=True) -> """ if relative: if isinstance(this_date, dt.datetime) or not isinstance(this_date, dt.date): - raise ValueError(f'`this_date` is of type `{type(this_date)}`, ' - 'should be `datetime.date`') + raise ValueError( + f"`this_date` is of type `{type(this_date)}`, should be `datetime.date`" + ) self.event = event self.delete_status = delete_status self.this_date = this_date self._conf = conf self.relative = relative - super().__init__('', wrap='clip') + super().__init__("", wrap="clip") self.set_title() def get_cursor_coords(self, size) -> tuple[int, int]: @@ -191,23 +193,22 @@ def selectable(cls) -> bool: @property def uid(self) -> str: - return self.event.calendar + '\n' + \ - str(self.event.href) + '\n' + str(self.event.etag) + return self.event.calendar + "\n" + str(self.event.href) + "\n" + str(self.event.etag) @property def recuid(self) -> tuple[str, str]: return (self.uid, self.event.recurrence_id) - def set_title(self, mark: str=' ') -> None: + def set_title(self, mark: str = " ") -> None: mark = { - DeletionType.ALL: 'D', - DeletionType.INSTANCES: 'd', - None: '', + DeletionType.ALL: "D", + DeletionType.INSTANCES: "d", + None: "", }[self.delete_status(self.recuid)] if self.relative: - format_ = self._conf['view']['agenda_event_format'] + format_ = self._conf["view"]["agenda_event_format"] else: - format_ = self._conf['view']['event_format'] + format_ = self._conf["view"]["event_format"] formatter_ = utils.human_formatter(format_, colors=False) if self.this_date: date_ = self.this_date @@ -216,23 +217,23 @@ def set_title(self, mark: str=' ') -> None: else: date_ = self.event.start.date() text = formatter_(self.event.attributes(date_, colors=False)) - if self._conf['locale']['unicode_symbols']: - newline = ' \N{LEFTWARDS ARROW WITH HOOK} ' + if self._conf["locale"]["unicode_symbols"]: + newline = " \N{LEFTWARDS ARROW WITH HOOK} " else: - newline = ' -- ' + newline = " -- " - self.set_text(mark + ' ' + text.replace('\n', newline)) + self.set_text(mark + " " + text.replace("\n", newline)) def keypress(self, size: tuple[int], key: str | None) -> str | None: - binds = self._conf['keybindings'] - if key in binds['left']: - key = 'left' - elif key in binds['up']: - key = 'up' - elif key in binds['right']: - key = 'right' - elif key in binds['down']: - key = 'down' + binds = self._conf["keybindings"] + if key in binds["left"]: + key = "left" + elif key in binds["up"]: + key = "up" + elif key in binds["right"]: + key = "right" + elif key in binds["down"]: + key = "down" return key @@ -240,10 +241,16 @@ class EventListBox(urwid.ListBox): """Container for list of U_Events""" def __init__( - self, *args, parent, conf, - delete_status, toggle_delete_instance, toggle_delete_all, - set_focus_date_callback=None, - **kwargs) -> None: + self, + *args, + parent, + conf, + delete_status, + toggle_delete_instance, + toggle_delete_all, + set_focus_date_callback=None, + **kwargs, + ) -> None: self._init: bool = True self.parent: ClassicView = parent self.delete_status = delete_status @@ -278,17 +285,18 @@ def refresh_titles(self, min_date, max_date, everything): class DListBox(EventListBox): """Container for a DayWalker""" + # XXX unfortunate naming, there is also DateListBox def __init__(self, *args, **kwargs) -> None: - dynamic_days = kwargs.pop('dynamic_days', True) + dynamic_days = kwargs.pop("dynamic_days", True) super().__init__(*args, **kwargs) self._init = dynamic_days def render(self, size, focus=False): if self._init: self._init = False - while not isinstance(self.body, StaticDayWalker) and 'bottom' in self.ends_visible(size): + while not isinstance(self.body, StaticDayWalker) and "bottom" in self.ends_visible(size): self.body._autoextend() return super().render(size, focus) @@ -296,7 +304,7 @@ def clean(self): """reset event most recently in focus""" if self._old_focus is not None: try: - self.body[self._old_focus].body[0].set_attr_map({None: 'date'}) + self.body[self._old_focus].body[0].set_attr_map({None: "date"}) except IndexError: # after reseting the EventList, the old focus might not exist pass @@ -311,17 +319,17 @@ def ensure_date(self, day: dt.date) -> None: self.clean() def keypress(self, size: tuple[int], key: str | None) -> str | None: - if key in self._conf['keybindings']['up']: - key = 'up' - if key in self._conf['keybindings']['down']: - key = 'down' + if key in self._conf["keybindings"]["up"]: + key = "up" + if key in self._conf["keybindings"]["down"]: + key = "down" - if key in self._conf['keybindings']['today']: + if key in self._conf["keybindings"]["today"]: self.parent.calendar.base_widget.set_focus_date(dt.date.today()) rval = super().keypress(size, key) self.clean() - if key in ['up', 'down']: + if key in ["up", "down"]: try: self._old_focus = self.focus_position except IndexError: @@ -368,14 +376,12 @@ def __init__(self, this_date, eventcolumn, conf, collection, delete_status) -> N super().__init__([]) self.ensure_date(this_date) - def reset(self): """delete all events contained in this DayWalker""" self.clear() self._last_day = None self._first_day = None - def ensure_date(self, day: dt.date) -> None: """make sure a DateListBox for `day` exists, update it and bring it into focus""" # TODO this function gets called twice on every date change, not necessary but @@ -442,7 +448,7 @@ def refresh_titles(self, start: dt.date, end: dt.date, everything: bool): for index in range(offset, offset + length + 1): self[index].refresh_titles() - def update_range(self, start: dt.date, end: dt.date, everything: bool=False): + def update_range(self, start: dt.date, end: dt.date, everything: bool = False): """refresh contents of all days between start and end (inclusive)""" start = start.date() if isinstance(start, dt.datetime) else start end = end.date() if isinstance(end, dt.datetime) else end @@ -488,23 +494,30 @@ def _autoprepend(self): self.insert(0, pile) def _get_events(self, day: dt.date) -> urwid.Widget: - """get all events on day, return a DateListBox of `U_Event()`s """ + """get all events on day, return a DateListBox of `U_Event()`s""" event_list = [] date_header = DateHeader( day=day, - dateformat=self._conf['locale']['longdateformat'], + dateformat=self._conf["locale"]["longdateformat"], conf=self._conf, ) - event_list.append(urwid.AttrMap(date_header, 'date')) + event_list.append(urwid.AttrMap(date_header, "date")) self.events = sorted(self._collection.get_events_on(day)) - event_list.extend([ - urwid.AttrMap( - U_Event(event, conf=self._conf, this_date=day, delete_status=self.delete_status), - 'calendar ' + event.calendar, 'reveal focus') - for event in self.events]) + event_list.extend( + [ + urwid.AttrMap( + U_Event( + event, conf=self._conf, this_date=day, delete_status=self.delete_status + ), + "calendar " + event.calendar, + "reveal focus", + ) + for event in self.events + ] + ) return urwid.BoxAdapter( DateListBox(urwid.SimpleFocusListWalker(event_list), date=day), - (len(event_list) + 1) if self.events else 1 + (len(event_list) + 1) if self.events else 1, ) def selectable(self) -> bool: @@ -527,13 +540,14 @@ def first_date(self) -> dt.date: def last_date(self) -> dt.date: return self[-1].original_widget.date + class StaticDayWalker(DayWalker): """Only show events for a fixed number of days.""" def ensure_date(self, day: dt.date) -> None: """make sure a DateListBox for `day` exists, update it and bring it into focus""" # TODO cache events for each day and update as needed - num_days = max(1, self._conf['default']['timedelta'].days) + num_days = max(1, self._conf["default"]["timedelta"].days) for delta in range(num_days): pile = self._get_events(day + dt.timedelta(days=delta)) @@ -557,14 +571,14 @@ def refresh_titles(self, start: dt.date, end: dt.date, everything: bool) -> None for one in self: one.refresh_titles() - def update_range(self, start: dt.date, end: dt.date, everything: bool=False): + def update_range(self, start: dt.date, end: dt.date, everything: bool = False): """refresh contents of all days between start and end (inclusive)""" start = start.date() if isinstance(start, dt.datetime) else start end = end.date() if isinstance(end, dt.datetime) else end update = everything for one in self: - if (start <= one.date <= end): + if start <= one.date <= end: update = True if update: self.ensure_date(self[0].date) @@ -586,21 +600,21 @@ def __init__(self, content, date) -> None: super().__init__(content) def __repr__(self) -> str: - return f'' + return f"" __str__ = __repr__ def render(self, size, focus): if focus: - self.body[0].set_attr_map({None: 'date header focused'}) + self.body[0].set_attr_map({None: "date header focused"}) elif DateListBox.selected_date == self.date: - self.body[0].set_attr_map({None: 'date header selected'}) + self.body[0].set_attr_map({None: "date header selected"}) else: self.reset_style() return super().render(size, focus) def reset_style(self): - self.body[0].set_attr_map({None: 'date header'}) + self.body[0].set_attr_map({None: "date header"}) def set_selected_date(self, day: dt.date) -> None: """Mark `day` as selected @@ -640,11 +654,11 @@ class EventColumn(urwid.WidgetWrap): def __init__(self, elistbox, pane) -> None: self.pane = pane self._conf = pane._conf - self.divider = urwid.Divider('─') + self.divider = urwid.Divider("─") self.editor = False self._last_focused_date: dt.date | None = None self._eventshown: tuple[str, str] | None = None - self.event_width = int(self.pane._conf['view']['event_view_weighting']) + self.event_width = int(self.pane._conf["view"]["event_view_weighting"]) self.delete_status = pane.delete_status self.toggle_delete_all = pane.toggle_delete_all self.toggle_delete_instance = pane.toggle_delete_instance @@ -659,10 +673,13 @@ def focus_event(self) -> U_Event | None: def view(self, event): """show event in the lower part of this column""" - self.container.contents.append((self.divider, ('pack', None))) + self.container.contents.append((self.divider, ("pack", None))) self.container.contents.append( - (EventDisplay(self.pane._conf, event, collection=self.pane.collection), - ('weight', self.event_width))) + ( + EventDisplay(self.pane._conf, event, collection=self.pane.collection), + ("weight", self.event_width), + ) + ) def clear_event_view(self): while len(self.container.contents) > 1: @@ -708,7 +725,7 @@ def update_date_line(self) -> None: """refresh titles in DateListBoxes""" self.dlistbox.update_date_line() - def edit(self, event, always_save: bool=False, external_edit: bool=False) -> None: + def edit(self, event, always_save: bool = False, external_edit: bool = False) -> None: """create an EventEditor and display it :param event: event to edit @@ -716,8 +733,7 @@ def edit(self, event, always_save: bool=False, external_edit: bool=False) -> Non :param always_save: even save the event if it hasn't changed """ if event.readonly: - self.pane.window.alert( - ('alert', f'Calendar `{event.calendar}` is read-only.')) + self.pane.window.alert(("alert", f"Calendar `{event.calendar}` is read-only.")) return if isinstance(event.start_local, dt.datetime): @@ -729,7 +745,7 @@ def edit(self, event, always_save: bool=False, external_edit: bool=False) -> Non else: original_end = event.end_local - def update_colors(new_start: dt.date, new_end: dt.date, everything: bool=False): + def update_colors(new_start: dt.date, new_end: dt.date, everything: bool = False): """reset colors in the calendar widget and dates in DayWalker between min(new_start, original_start) @@ -770,36 +786,40 @@ def update_colors(new_start: dt.date, new_end: dt.date, everything: bool=False): ) self.pane.collection.update(new_event) update_colors( - new_event.start_local, - new_event.end_local, - (event.recurring or new_event.recurring) + new_event.start_local, new_event.end_local, (event.recurring or new_event.recurring) ) else: self.editor = True editor = EventEditor(self.pane, event, update_colors, always_save=always_save) - ContainerWidget = linebox[self.pane._conf['view']['frame']] - new_pane = urwid.Columns([ - ('weight', 2, CAttrMap(ContainerWidget(editor), 'editor', 'editor focus')), - ('weight', 1, CAttrMap(ContainerWidget(self.dlistbox), 'eventcolumn')), - ], dividechars=0, focus_column=0) + ContainerWidget = linebox[self.pane._conf["view"]["frame"]] + new_pane = urwid.Columns( + [ + ("weight", 2, CAttrMap(ContainerWidget(editor), "editor", "editor focus")), + ("weight", 1, CAttrMap(ContainerWidget(self.dlistbox), "eventcolumn")), + ], + dividechars=0, + focus_column=0, + ) new_pane.title = editor.title def teardown(data): self.editor = False + self.pane.window.open(new_pane, callback=teardown) def export_event(self): """export the event in focus as an ICS file""" + def export_this(_, user_data): try: self.focus_event.event.export_ics(user_data.get_edit_text()) except Exception as error: self.pane.window.backtrack() - self.pane.window.alert(('alert', f'Failed to save event: {error}')) + self.pane.window.alert(("alert", f"Failed to save event: {error}")) else: self.pane.window.backtrack() - self.pane.window.alert('Event successfully exported') + self.pane.window.alert("Event successfully exported") overlay = urwid.Overlay( ExportDialog( @@ -808,7 +828,11 @@ def export_this(_, user_data): self.focus_event.event, ), self.pane, - 'center', ('relative', 50), ('relative', 50), None) + "center", + ("relative", 50), + ("relative", 50), + None, + ) self.pane.window.open(overlay) def toggle_delete(self): @@ -819,17 +843,19 @@ def delete_this(_): self.toggle_delete_instance(event.recuid) self.pane.window.backtrack() self.refresh_titles( - event.event.start_local, event.event.end_local, event.event.recurring) + event.event.start_local, event.event.end_local, event.event.recurring + ) def delete_all(_): self.toggle_delete_all(event.recuid) self.pane.window.backtrack() self.refresh_titles( - event.event.start_local, event.event.end_local, event.event.recurring) + event.event.start_local, event.event.end_local, event.event.recurring + ) if event.event.readonly: self.pane.window.alert( - ('alert', f'Calendar {event.event.calendar} is read-only.'), + ("alert", f"Calendar {event.event.calendar} is read-only."), ) return status = self.delete_status(event.recuid) @@ -842,19 +868,20 @@ def delete_all(_): # FIXME if in search results, original pane is used for overlay, not search results # also see issue of reseting titles below, probably related self.pane.dialog( - text='This is a recurring event.\nWhich instances do you want to delete?', + text="This is a recurring event.\nWhich instances do you want to delete?", buttons=[ - ('Only this', delete_this), - ('All (past and future)', delete_all), - ('Abort', self.pane.window.backtrack), - ] + ("Only this", delete_this), + ("All (past and future)", delete_all), + ("Abort", self.pane.window.backtrack), + ], ) refresh = False else: self.toggle_delete_all(event.recuid) if refresh: self.refresh_titles( - event.event.start_local, event.event.end_local, event.event.recurring) + event.event.start_local, event.event.end_local, event.event.recurring + ) event.set_title() # if we are in search results, refresh_titles doesn't work properly def duplicate(self) -> None: @@ -869,8 +896,9 @@ def duplicate(self) -> None: try: self.pane.collection.insert(event) except ReadOnlyCalendarError: - event.calendar = self.pane.collection.default_calendar_name or \ - self.pane.collection.writable_names[0] + event.calendar = ( + self.pane.collection.default_calendar_name or self.pane.collection.writable_names[0] + ) self.edit(event, always_save=True) start_date, end_date = event.start_local, event.end_local if isinstance(start_date, dt.datetime): @@ -883,7 +911,7 @@ def duplicate(self) -> None: except IndexError: pass - def new(self, date: dt.date, end: dt.date | None=None) -> None: + def new(self, date: dt.date, end: dt.date | None = None) -> None: """create a new event on `date` at the next full hour and edit it :param date: default date for new event @@ -892,7 +920,7 @@ def new(self, date: dt.date, end: dt.date | None=None) -> None: dtstart: dt.date dtend: dt.date if not self.pane.collection.writable_names: - self.pane.window.alert(('alert', 'No writable calendar.')) + self.pane.window.alert(("alert", "No writable calendar.")) return if date is None: date = dt.datetime.now() @@ -904,14 +932,16 @@ def new(self, date: dt.date, end: dt.date | None=None) -> None: dtstart = date dtend = end + dt.timedelta(days=1) allday = True - event = self.pane.collection.create_event_from_dict({ - 'dtstart': dtstart, - 'dtend': dtend, - 'summary': '', - 'timezone': self._conf['locale']['default_timezone'], - 'allday': allday, - 'alarms': timedelta2str(self._conf['default']['default_event_alarm']), - }) + event = self.pane.collection.create_event_from_dict( + { + "dtstart": dtstart, + "dtend": dtend, + "summary": "", + "timezone": self._conf["locale"]["default_timezone"], + "allday": allday, + "alarms": timedelta2str(self._conf["default"]["default_event_alarm"]), + } + ) self.edit(event) def selectable(self): @@ -922,33 +952,34 @@ def keypress(self, size: tuple[int], key: str | None) -> str | None: self._eventshown = None self.clear_event_view() - if key in self._conf['keybindings']['new']: + if key in self._conf["keybindings"]["new"]: self.new(self.focus_date, None) key = None if self.focus_event: - if key in self._conf['keybindings']['delete']: + if key in self._conf["keybindings"]["delete"]: self.toggle_delete() - key = 'down' - elif key in self._conf['keybindings']['duplicate']: + key = "down" + elif key in self._conf["keybindings"]["duplicate"]: self.duplicate() key = None - elif key in self._conf['keybindings']['export']: + elif key in self._conf["keybindings"]["export"]: self.export_event() key = None rval = super().keypress(size, key) if self.focus_event: - if key in self._conf['keybindings']['view'] and \ - prev_shown == self.focus_event.recuid: + if key in self._conf["keybindings"]["view"] and prev_shown == self.focus_event.recuid: # the event in focus is already viewed -> edit if self.delete_status(self.focus_event.recuid): - self.pane.window.alert(('alert', 'This event is marked as deleted')) + self.pane.window.alert(("alert", "This event is marked as deleted")) self.edit(self.focus_event.event) - elif key in self._conf['keybindings']['external_edit']: + elif key in self._conf["keybindings"]["external_edit"]: self.edit(self.focus_event.event, external_edit=True) - elif key in self._conf['keybindings']['view'] or \ - self._conf['view']['event_view_always_visible']: + elif ( + key in self._conf["keybindings"]["view"] + or self._conf["view"]["event_view_always_visible"] + ): self._eventshown = self.focus_event.recuid self.view(self.focus_event.event) return rval @@ -960,94 +991,92 @@ def render(self, a, focus): class EventDisplay(urwid.WidgetWrap): - """A widget showing one Event()'s details """ + """A widget showing one Event()'s details""" def __init__(self, conf, event, collection=None) -> None: self._conf = conf self.collection = collection self.event = event - divider = urwid.Divider(' ') + divider = urwid.Divider(" ") lines = [] - lines.append(urwid.Text('Title: ' + event.summary)) + lines.append(urwid.Text("Title: " + event.summary)) # show organizer - if event.organizer != '': - lines.append(urwid.Text('Organizer: ' + event.organizer)) + if event.organizer != "": + lines.append(urwid.Text("Organizer: " + event.organizer)) - if event.location != '': - lines.append(urwid.Text('Location: ' + event.location)) + if event.location != "": + lines.append(urwid.Text("Location: " + event.location)) - if event.categories != '': - lines.append(urwid.Text('Categories: ' + event.categories)) + if event.categories != "": + lines.append(urwid.Text("Categories: " + event.categories)) - if event.url != '': - lines.append(urwid.Text('URL: ' + event.url)) + if event.url != "": + lines.append(urwid.Text("URL: " + event.url)) - if event.attendees != '': - lines.append(urwid.Text('Attendees:')) - for attendee in event.attendees.split(', '): - lines.append(urwid.Text(f' - {attendee}')) + if event.attendees != "": + lines.append(urwid.Text("Attendees:")) + for attendee in event.attendees.split(", "): + lines.append(urwid.Text(f" - {attendee}")) # start and end time/date if event.allday: - startstr = event.start_local.strftime(self._conf['locale']['dateformat']) - endstr = event.end_local.strftime(self._conf['locale']['dateformat']) + startstr = event.start_local.strftime(self._conf["locale"]["dateformat"]) + endstr = event.end_local.strftime(self._conf["locale"]["dateformat"]) else: startstr = event.start_local.strftime( - f"{self._conf['locale']['dateformat']} " - f"{self._conf['locale']['timeformat']}" + f"{self._conf['locale']['dateformat']} {self._conf['locale']['timeformat']}" ) if event.start_local.date == event.end_local.date: - endstr = event.end_local.strftime(self._conf['locale']['timeformat']) + endstr = event.end_local.strftime(self._conf["locale"]["timeformat"]) else: endstr = event.end_local.strftime( - f"{self._conf['locale']['dateformat']} " - f"{self._conf['locale']['timeformat']}" + f"{self._conf['locale']['dateformat']} {self._conf['locale']['timeformat']}" ) if startstr == endstr: - lines.append(urwid.Text('Date: ' + startstr)) + lines.append(urwid.Text("Date: " + startstr)) else: - lines.append(urwid.Text('Date: ' + startstr + ' - ' + endstr)) + lines.append(urwid.Text("Date: " + startstr + " - " + endstr)) - lines.append(urwid.Text('Calendar: ' + event.calendar)) + lines.append(urwid.Text("Calendar: " + event.calendar)) lines.append(divider) - if event.description != '': + if event.description != "": lines.append(urwid.Text(event.description)) pile = urwid.Pile(lines) - urwid.WidgetWrap.__init__(self, urwid.Filler(pile, valign='top')) + urwid.WidgetWrap.__init__(self, urwid.Filler(pile, valign="top")) class SearchDialog(urwid.WidgetWrap): """A Search Dialog Widget""" def __init__(self, search_func, abort_func) -> None: - class Search(Edit): - def keypress(self, size: tuple[int], key: str | None) -> str | None: - if key == 'enter': + if key == "enter": search_func(self.text) return None else: return super().keypress(size, key) - search_field = Search('') + search_field = Search("") def this_func(_): search_func(search_field.text) lines = [] - lines.append(urwid.Text('Please enter a search term (Escape cancels):')) - lines.append(urwid.AttrMap(search_field, 'edit', 'edit focused')) - lines.append(urwid.Text('')) - buttons = NColumns([ - button('Search', on_press=this_func, padding_left=0), - button('Abort', on_press=abort_func, padding_right=0), - ]) + lines.append(urwid.Text("Please enter a search term (Escape cancels):")) + lines.append(urwid.AttrMap(search_field, "edit", "edit focused")) + lines.append(urwid.Text("")) + buttons = NColumns( + [ + button("Search", on_press=this_func, padding_left=0), + button("Abort", on_press=abort_func, padding_right=0), + ] + ) lines.append(buttons) content = NPile(lines, outermost=True) urwid.WidgetWrap.__init__(self, urwid.LineBox(content)) @@ -1060,7 +1089,7 @@ class ClassicView(Pane): on the right """ - def __init__(self, collection, conf=None, title: str='', description: str='') -> None: + def __init__(self, collection, conf=None, title: str = "", description: str = "") -> None: self.init = True # Will be set when opening the view inside a Window self.window = None @@ -1068,44 +1097,54 @@ def __init__(self, collection, conf=None, title: str='', description: str='') -> self.collection = collection self._deleted: dict[int, list[str]] = {DeletionType.ALL: [], DeletionType.INSTANCES: []} - ContainerWidget = linebox[self._conf['view']['frame']] - if self._conf['view']['dynamic_days']: + ContainerWidget = linebox[self._conf["view"]["frame"]] + if self._conf["view"]["dynamic_days"]: Walker = DayWalker else: Walker = StaticDayWalker daywalker = Walker( - dt.date.today(), eventcolumn=self, conf=self._conf, - delete_status=self.delete_status, collection=self.collection, + dt.date.today(), + eventcolumn=self, + conf=self._conf, + delete_status=self.delete_status, + collection=self.collection, ) elistbox = DListBox( - daywalker, parent=self, conf=self._conf, + daywalker, + parent=self, + conf=self._conf, delete_status=self.delete_status, toggle_delete_all=self.toggle_delete_all, toggle_delete_instance=self.toggle_delete_instance, - dynamic_days=self._conf['view']['dynamic_days'], + dynamic_days=self._conf["view"]["dynamic_days"], ) self.eventscolumn = ContainerWidget( - CAttrMap(EventColumn(pane=self, elistbox=elistbox), - 'eventcolumn', - 'eventcolumn focus', - ), + CAttrMap( + EventColumn(pane=self, elistbox=elistbox), + "eventcolumn", + "eventcolumn focus", + ), + ) + calendar = CAttrMap( + CalendarWidget( + on_date_change=self.eventscolumn.original_widget.set_focus_date, + keybindings=self._conf["keybindings"], + on_press=dict.fromkeys(self._conf["keybindings"]["new"], self.new_event), + firstweekday=self._conf["locale"]["firstweekday"], + weeknumbers=self._conf["locale"]["weeknumbers"], + monthdisplay=self._conf["view"]["monthdisplay"], + get_styles=collection.get_styles, + ), + "calendar", + "calendar focus", ) - calendar = CAttrMap(CalendarWidget( - on_date_change=self.eventscolumn.original_widget.set_focus_date, - keybindings=self._conf['keybindings'], - on_press=dict.fromkeys(self._conf['keybindings']['new'], self.new_event), - firstweekday=self._conf['locale']['firstweekday'], - weeknumbers=self._conf['locale']['weeknumbers'], - monthdisplay=self._conf['view']['monthdisplay'], - get_styles=collection.get_styles - ), 'calendar', 'calendar focus') - if self._conf['view']['dynamic_days']: + if self._conf["view"]["dynamic_days"]: elistbox.set_focus_date_callback = calendar.set_focus_date else: elistbox.set_focus_date_callback = lambda _: None self.calendar = ContainerWidget(calendar) - self.lwidth = 31 if self._conf['locale']['weeknumbers'] == 'right' else 28 - if self._conf['view']['frame'] in ["width", "color"]: + self.lwidth = 31 if self._conf["locale"]["weeknumbers"] == "right" else 28 + if self._conf["view"]["frame"] in ["width", "color"]: self.lwidth += 2 columns = NColumns( [(self.lwidth, self.calendar), self.eventscolumn], @@ -1145,28 +1184,30 @@ def cleanup(self, data): # deleted. updated_etags = {} for part in self._deleted[DeletionType.ALL]: - account, href, etag = part.split('\n', 2) + account, href, etag = part.split("\n", 2) self.collection.delete(href, etag, account) for part, rec_id in self._deleted[DeletionType.INSTANCES]: - account, href, etag = part.split('\n', 2) + account, href, etag = part.split("\n", 2) etag = updated_etags.get(href) or etag event = self.collection.delete_instance(href, etag, account, rec_id) updated_etags[event.href] = event.etag def keypress(self, size: tuple[int], key: str | None) -> str | None: - binds = self._conf['keybindings'] - if key in binds['search']: + binds = self._conf["keybindings"] + if key in binds["search"]: self.search() return super().keypress(size, key) def search(self): """create a search dialog and display it""" overlay = urwid.Overlay( - SearchDialog(self._search, self.window.backtrack), self, - align='center', - width=('relative', 70), - valign=('relative', 50), - height=None) + SearchDialog(self._search, self.window.backtrack), + self, + align="center", + width=("relative", 70), + valign=("relative", 50), + height=None, + ) self.window.open(overlay) def _search(self, search_term: str) -> None: @@ -1175,19 +1216,28 @@ def _search(self, search_term: str) -> None: self.window.backtrack() events = sorted(self.collection.search(search_term)) event_list = [] - event_list.extend([ - urwid.AttrMap( - U_Event(event, relative=False, conf=self._conf, delete_status=self.delete_status), - 'calendar ' + event.calendar, 'reveal focus') - for event in events]) + event_list.extend( + [ + urwid.AttrMap( + U_Event( + event, relative=False, conf=self._conf, delete_status=self.delete_status + ), + "calendar " + event.calendar, + "reveal focus", + ) + for event in events + ] + ) events = EventListBox( - urwid.SimpleFocusListWalker(event_list), parent=self.eventscolumn, conf=self._conf, + urwid.SimpleFocusListWalker(event_list), + parent=self.eventscolumn, + conf=self._conf, delete_status=self.delete_status, toggle_delete_all=self.toggle_delete_all, - toggle_delete_instance=self.toggle_delete_instance + toggle_delete_instance=self.toggle_delete_instance, ) events = EventColumn(pane=self, elistbox=events) - ContainerWidget = linebox[self._conf['view']['frame']] + ContainerWidget = linebox[self._conf["view"]["frame"]] columns = NColumns( [(self.lwidth, self.calendar), ContainerWidget(events)], dividechars=0, @@ -1196,7 +1246,7 @@ def _search(self, search_term: str) -> None: ) pane = Pane( columns, - title=f"Search results for \"{search_term}\" (Esc for backtrack)", + title=f'Search results for "{search_term}" (Esc for backtrack)', ) pane._conf = self._conf columns.set_focus_column(1) @@ -1216,8 +1266,12 @@ def new_event(self, date, end): def _urwid_palette_entry( - name: str, color: str, hmethod: str, color_mode: Literal['256colors', 'rgb'], - foreground: str = '', background: str = '', + name: str, + color: str, + hmethod: str, + color_mode: Literal["256colors", "rgb"], + foreground: str = "", + background: str = "", ) -> tuple[str, str, str, str, str, str]: """Create an urwid compatible palette entry. @@ -1232,14 +1286,15 @@ def _urwid_palette_entry( :returns: an urwid palette entry """ from khal.terminal import COLORS - if color == '' or color in COLORS or color is None: + + if color == "" or color in COLORS or color is None: # Named colors already use urwid names, no need to change anything. pass elif color.isdigit(): # Colors from the 256 color palette need to be prefixed with h in # urwid. - color = 'h' + color - elif color_mode == '256color': + color = "h" + color + elif color_mode == "256color": # Convert to some color on the 256 color palette that might resemble # the 24-bit color. # First, generate the palette (indices 16-255 only). This assumes, that @@ -1247,16 +1302,19 @@ def _urwid_palette_entry( # the case. colors = {} # Colorcube - colorlevels = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) + colorlevels = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF) for r in range(0, 6): for g in range(0, 6): for b in range(0, 6): - colors[r * 36 + g * 6 + b + 16] = \ - (colorlevels[r], colorlevels[g], colorlevels[b]) + colors[r * 36 + g * 6 + b + 16] = ( + colorlevels[r], + colorlevels[g], + colorlevels[b], + ) # Grayscale graylevels = [0x08 + 10 * i for i in range(0, 24)] for c in range(0, 24): - colors[232 + c] = (graylevels[c], ) * 3 + colors[232 + c] = (graylevels[c],) * 3 # Parse the HTML-style color into the variables r, g, b. if len(color) == 4: # e.g. #ABC, equivalent to #AABBCC @@ -1279,21 +1337,21 @@ def _urwid_palette_entry( if best is None or dist < bestdist: best = index bestdist = dist - color = 'h' + str(best) + color = "h" + str(best) # We unconditionally add the color to the high color slot. It seems to work # in lower color terminals as well. - if hmethod in ['fg', 'foreground']: - return (name, '', '', '', color, background) + if hmethod in ["fg", "foreground"]: + return (name, "", "", "", color, background) else: - return (name, '', '', '', foreground, color) + return (name, "", "", "", foreground, color) def _add_calendar_colors( palette: list[tuple[str, ...]], - collection: 'CalendarCollection', - color_mode: Literal['256colors', 'rgb'], + collection: "CalendarCollection", + color_mode: Literal["256colors", "rgb"], base: str | None = None, - attr_template: str = 'calendar {}', + attr_template: str = "calendar {}", ) -> list[tuple[str, ...]]: """Add the colors for the defined calendars to the palette. @@ -1307,10 +1365,10 @@ def _add_calendar_colors( :param attr_template: the template to use for the attribute name :returns: the modified palette """ - bg_color, fg_color = '', '' + bg_color, fg_color = "", "" for attr in palette: if base and attr[0] == base: - if color_mode == 'rgb' and len(attr) >= 5: + if color_mode == "rgb" and len(attr) >= 5: bg_color = attr[5] fg_color = attr[4] else: @@ -1318,21 +1376,21 @@ def _add_calendar_colors( fg_color = attr[1] for cal in collection.calendars: - if cal['color'] == '': + if cal["color"] == "": # No color set for this calendar, use default_color instead. color = collection.default_color else: - color = cal['color'] + color = cal["color"] # In case the color contains an alpha value, remove it for urwid. # eg '#RRGGBBAA' -> '#RRGGBB' and '#RGBA' -> '#RGB'. - if color and len(color) == 9 and color[0] == '#': + if color and len(color) == 9 and color[0] == "#": color = color[0:7] - elif color and len(color) == 5 and color[0] == '#': + elif color and len(color) == 5 and color[0] == "#": color = color[0:4] entry = _urwid_palette_entry( - attr_template.format(cal['name']), + attr_template.format(cal["name"]), color, collection.hmethod, color_mode=color_mode, @@ -1342,7 +1400,7 @@ def _add_calendar_colors( palette.append(entry) entry = _urwid_palette_entry( - 'highlight_days_color', + "highlight_days_color", collection.color, collection.hmethod, color_mode=color_mode, @@ -1350,63 +1408,65 @@ def _add_calendar_colors( background=bg_color, ) palette.append(entry) - entry = _urwid_palette_entry('highlight_days_multiple', + entry = _urwid_palette_entry( + "highlight_days_multiple", collection.multiple, collection.hmethod, color_mode=color_mode, foreground=fg_color, - background=bg_color) + background=bg_color, + ) palette.append(entry) return palette def start_pane( - pane, - callback, - program_info='', - quit_keys=None, - color_mode: Literal['rgb', '256colors']='rgb', + pane, + callback, + program_info="", + quit_keys=None, + color_mode: Literal["rgb", "256colors"] = "rgb", ): """Open the user interface with the given initial pane.""" # We don't validate the themes in settings.spec but instead here # first try to load built-in themes, then try to load themes from # plugins - theme = colors.themes.get(pane._conf['view']['theme']) + theme = colors.themes.get(pane._conf["view"]["theme"]) if theme is None: - theme = plugins.THEMES.get(pane._conf['view']['theme']) + theme = plugins.THEMES.get(pane._conf["view"]["theme"]) if theme is None: - logger.fatal(f'Invalid theme {pane._conf["view"]["theme"]} configured') - logger.fatal(f'Available themes are: {", ".join(colors.themes.keys())}') + logger.fatal(f"Invalid theme {pane._conf['view']['theme']} configured") + logger.fatal(f"Available themes are: {', '.join(colors.themes.keys())}") raise FatalError - quit_keys = quit_keys or ['q'] + quit_keys = quit_keys or ["q"] frame = Window( - footer=program_info + f' | {quit_keys[0]}: quit, ?: help', + footer=program_info + f" | {quit_keys[0]}: quit, ?: help", quit_keys=quit_keys, ) class LogPaneFormatter(logging.Formatter): def get_prefix(self, level): if level >= 50: - return 'CRITICAL' + return "CRITICAL" if level >= 40: - return 'ERROR' + return "ERROR" if level >= 30: - return 'WARNING' + return "WARNING" if level >= 20: - return 'INFO' + return "INFO" else: - return 'DEBUG' + return "DEBUG" def format(self, record) -> str: - return f'{self.get_prefix(record.levelno)}: {record.msg}' + return f"{self.get_prefix(record.levelno)}: {record.msg}" class HeaderFormatter(LogPaneFormatter): def format(self, record): return ( - super().format(record)[:30] + '... ' + super().format(record)[:30] + "... " f"[Press `{pane._conf['keybindings']['log'][0]}` to view log]" ) @@ -1431,12 +1491,18 @@ def emit(self, record): frame.open(pane, callback) palette = _add_calendar_colors( - theme, pane.collection, color_mode=color_mode, - base='calendar', attr_template='calendar {}', + theme, + pane.collection, + color_mode=color_mode, + base="calendar", + attr_template="calendar {}", ) palette = _add_calendar_colors( - palette, pane.collection, color_mode=color_mode, - base='popupbg', attr_template='calendar {} popup', + palette, + pane.collection, + color_mode=color_mode, + base="popupbg", + attr_template="calendar {} popup", ) def merge_palettes(pallete_a, pallete_b) -> list[tuple[str, ...]]: @@ -1448,25 +1514,25 @@ def merge_palettes(pallete_a, pallete_b) -> list[tuple[str, ...]]: merged[entry[0]] = entry return list(merged.values()) - overwrite = [(key, *values) for key, values in pane._conf['palette'].items()] + overwrite = [(key, *values) for key, values in pane._conf["palette"].items()] palette = merge_palettes(palette, overwrite) loop = urwid.MainLoop( widget=frame, palette=palette, unhandled_input=frame.on_key_press, pop_ups=True, - handle_mouse=pane._conf['default']['enable_mouse'], + handle_mouse=pane._conf["default"]["enable_mouse"], ) frame.loop = loop def redraw_today(loop, pane, meta=None): - meta = meta or {'last_today': None} + meta = meta or {"last_today": None} # XXX TODO this currently assumes, today moves forward by exactly one # day, but it could either move forward more (suspend-to-disk/ram) or # even move backwards today = dt.date.today() - if meta['last_today'] != today: - meta['last_today'] = today + if meta["last_today"] != today: + meta["last_today"] = today pane.calendar.original_widget.reset_styles_range(today - dt.timedelta(days=1), today) pane.eventscolumn.original_widget.update_date_line() loop.set_alarm_in(60, redraw_today, pane) @@ -1475,17 +1541,18 @@ def redraw_today(loop, pane, meta=None): def check_for_updates(loop, pane): if pane.collection.needs_update(): - pane.window.alert('detected external vdir modification, updating...') + pane.window.alert("detected external vdir modification, updating...") pane.collection.update_db() pane.eventscolumn.base_widget.update(None, None, everything=True) - pane.window.alert('detected external vdir modification, updated.') + pane.window.alert("detected external vdir modification, updated.") loop.set_alarm_in(60, check_for_updates, pane) loop.set_alarm_in(60, check_for_updates, pane) - colors_ = 2**24 if color_mode == 'rgb' else 256 + colors_ = 2**24 if color_mode == "rgb" else 256 loop.screen.set_terminal_properties( - colors=colors_, bright_is_bold=pane._conf['view']['bold_for_light_color'], + colors=colors_, + bright_is_bold=pane._conf["view"]["bold_for_light_color"], ) def ctrl_c(signum, f): @@ -1496,6 +1563,7 @@ def ctrl_c(signum, f): loop.run() except Exception: import traceback + tb = traceback.format_exc() try: # Try to leave terminal in usable state loop.stop() diff --git a/khal/ui/base.py b/khal/ui/base.py index cc31a9ba0..a79646bbb 100644 --- a/khal/ui/base.py +++ b/khal/ui/base.py @@ -34,18 +34,17 @@ from .widgets import NColumns -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") class Pane(urwid.WidgetWrap): - """An abstract Pane to be used in a Window object.""" def __init__(self, widget, title=None, description=None) -> None: self.widget = widget urwid.WidgetWrap.__init__(self, widget) - self._title = title or '' - self._description = description or '' + self._title = title or "" + self._description = description or "" self.window = None @property @@ -75,15 +74,16 @@ def dialog(self, text: str, buttons: list[tuple[str, Callable]]) -> None: ) lines.append(buttons) content = urwid.LineBox(urwid.Pile(lines)) - overlay = urwid.Overlay(content, self, 'center', ('relative', 70), ('relative', 70), None) + overlay = urwid.Overlay(content, self, "center", ("relative", 70), ("relative", 70), None) assert self.window is not None self.window.open(overlay) - def scrollable_dialog(self, - text: str | list[urwid.Text], - buttons: list[tuple[str, Callable]] | None = None, - title: str = "Press `ESC` to close this window", - ) -> None: + def scrollable_dialog( + self, + text: str | list[urwid.Text], + buttons: list[tuple[str, Callable]] | None = None, + title: str = "Press `ESC` to close this window", + ) -> None: """Open a scrollable dialog box. :param text: Text to appear as the body of the Dialog box @@ -98,33 +98,39 @@ def scrollable_dialog(self, [urwid.Button(label, on_press=func) for label, func in buttons], outermost=True, ) - content = urwid.LineBox(urwid.Pile([body, ('pack', buttons)])) + content = urwid.LineBox(urwid.Pile([body, ("pack", buttons)])) else: content = urwid.LineBox(urwid.Pile([body])) # put the title on the upper line over = urwid.Overlay( - urwid.Text(" " + title + " "), content, 'center', len(title) + 2, 'top', None, + urwid.Text(" " + title + " "), + content, + "center", + len(title) + 2, + "top", + None, ) overlay = urwid.Overlay( - over, self, 'center', ('relative', 70), 'middle', ('relative', 70), None) + over, self, "center", ("relative", 70), "middle", ("relative", 70), None + ) assert self.window is not None self.window.open(overlay) def keypress(self, size, key): """Handle application-wide key strokes.""" - if key in ['f1', '?']: + if key in ["f1", "?"]: self.show_keybindings() - elif key in ['L']: + elif key in ["L"]: self.show_log() else: return super().keypress(size, key) def show_keybindings(self): lines = [] - lines.append(urwid.AttrMap(urwid.Text(' Command Keys'), 'alt header')) - for command, keys in self._conf['keybindings'].items(): - lines.append(urwid.Text(f' {command:20} {", ".join(keys)}')) + lines.append(urwid.AttrMap(urwid.Text(" Command Keys"), "alt header")) + for command, keys in self._conf["keybindings"].items(): + lines.append(urwid.Text(f" {command:20} {', '.join(keys)}")) self.scrollable_dialog( lines, title="Press `ESC` to close this window, arrows to scroll", @@ -132,7 +138,7 @@ def show_keybindings(self): def show_log(self): self.scrollable_dialog( - '\n'.join(self.window._log), + "\n".join(self.window._log), title="Press `ESC` to close this window, arrows to scroll", ) @@ -150,14 +156,17 @@ class Window(urwid.Frame): to carry data between them. """ - def __init__(self, footer='', quit_keys=None) -> None: - quit_keys = quit_keys or ['q'] + def __init__(self, footer="", quit_keys=None) -> None: + quit_keys = quit_keys or ["q"] self._track: list[urwid.Overlay] = [] - header = urwid.AttrWrap(urwid.Text(''), 'header') - footer = urwid.AttrWrap(urwid.Text(footer), 'footer') + header = urwid.AttrWrap(urwid.Text(""), "header") + footer = urwid.AttrWrap(urwid.Text(footer), "footer") urwid.Frame.__init__( - self, urwid.Text(''), header=header, footer=footer, + self, + urwid.Text(""), + header=header, + footer=footer, ) self.update_header() self._original_w = None @@ -165,6 +174,7 @@ def __init__(self, footer='', quit_keys=None) -> None: def alert(message): self.update_header(message, warn=True) + self._alert_daemon = AlertDaemon(alert) self._alert_daemon.start() self.alert = self._alert_daemon.alert @@ -201,15 +211,14 @@ def backtrack(self, data=None): raise urwid.ExitMainLoop() def is_top_level(self): - """Is the current pane the top-level one? - """ + """Is the current pane the top-level one?""" return len(self._track) == 1 def on_key_press(self, key): """Handle application-wide key strokes.""" if key in self.quit_keys: self.backtrack() - elif key == 'esc' and not self.is_top_level(): + elif key == "esc" and not self.is_top_level(): self.backtrack() return key @@ -226,7 +235,7 @@ def _get_current_pane(self): def clear_header(self): """clears header if we are not currently showing a warning""" if not self._header_is_warning: - pane_title = getattr(self._get_current_pane(), 'title', '') + pane_title = getattr(self._get_current_pane(), "title", "") self.header.w.set_text(pane_title) def update_header(self, alert=None, warn=False): @@ -238,15 +247,15 @@ def update_header(self, alert=None, warn=False): :type alert: str or (palette_entry, str) """ self._header_is_warning = warn - pane_title = getattr(self._get_current_pane(), 'title', None) + pane_title = getattr(self._get_current_pane(), "title", None) text = [] for part in (pane_title, alert): if part: text.append(part) - text.append(('black', ' | ')) + text.append(("black", " | ")) - self.header.w.set_text(text[:-1] or '') + self.header.w.set_text(text[:-1] or "") class AlertDaemon(threading.Thread): diff --git a/khal/ui/calendarwidget.py b/khal/ui/calendarwidget.py index f242e6537..eae69ed27 100644 --- a/khal/ui/calendarwidget.py +++ b/khal/ui/calendarwidget.py @@ -39,11 +39,13 @@ class MarkType(TypedDict): date: dt.date pos: tuple[int, int] + + OnPressType = dict[str, Callable[[dt.date, dt.date | None], str | None]] GetStylesSignature = Callable[[dt.date, bool], str | tuple[str, str] | None] -setlocale(LC_ALL, '') +setlocale(LC_ALL, "") def getweeknumber(day: dt.date) -> int: @@ -55,7 +57,6 @@ def getweeknumber(day: dt.date) -> int: class DatePart(urwid.Text): - """used in the Date widget (single digit)""" def __init__(self, digit: str) -> None: @@ -71,7 +72,7 @@ def keypress(self, size: tuple[int], key: str) -> str: def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]: return 1, 0 - def render(self, size: tuple[int], focus: bool=False) -> urwid.Canvas: + def render(self, size: tuple[int], focus: bool = False) -> urwid.Canvas: canv = super().render(size, focus) if focus: canv = urwid.CompositeCanvas(canv) @@ -80,13 +81,14 @@ def render(self, size: tuple[int], focus: bool=False) -> urwid.Canvas: class Date(urwid.WidgetWrap): - """used in the main calendar for dates (a number)""" def __init__(self, date: dt.date, get_styles: GetStylesSignature) -> None: dstr = str(date.day).rjust(2) - self.halves = [urwid.AttrMap(DatePart(dstr[:1]), None, None), - urwid.AttrMap(DatePart(dstr[1:]), None, None)] + self.halves = [ + urwid.AttrMap(DatePart(dstr[:1]), None, None), + urwid.AttrMap(DatePart(dstr[1:]), None, None), + ] self.date = date self._get_styles = get_styles super().__init__(urwid.Columns(self.halves)) @@ -106,12 +108,12 @@ def set_styles(self, styles: None | str | tuple[str, str]) -> None: self.halves[0].set_focus_map({None: styles}) self.halves[1].set_focus_map({None: styles}) - def reset_styles(self, focus: bool=False) -> None: + def reset_styles(self, focus: bool = False) -> None: self.set_styles(self._get_styles(self.date, focus)) @property def marked(self) -> bool: - return 'mark' in [self.halves[0].attr_map[None], self.halves[1].attr_map[None]] + return "mark" in [self.halves[0].attr_map[None], self.halves[1].attr_map[None]] @classmethod def selectable(cls) -> bool: @@ -122,7 +124,6 @@ def keypress(self, _: Any, key: str) -> str: class DateCColumns(urwid.Columns): - """container for one week worth of dates which are horizontally aligned @@ -131,16 +132,19 @@ class DateCColumns(urwid.Columns): focus can only move away by pressing 'TAB', calls 'on_date_change' on every focus change (see below for details) """ + # TODO only call on_date_change when we change our date ourselves, # not if it gets changed by an (external) call to set_focus_date() - def __init__(self, - widget_list, - on_date_change: Callable[[dt.date], None], - on_press: OnPressType, - keybindings: dict[str, list[str]], - get_styles: GetStylesSignature, - **kwargs) -> None: + def __init__( + self, + widget_list, + on_date_change: Callable[[dt.date], None], + on_press: OnPressType, + keybindings: dict[str, list[str]], + get_styles: GetStylesSignature, + **kwargs, + ) -> None: self.on_date_change = on_date_change self.on_press = on_press self.keybindings = keybindings @@ -149,12 +153,11 @@ def __init__(self, super().__init__(widget_list, **kwargs) def __repr__(self) -> str: - return f'' + return f"" def _clear_cursor(self) -> None: old_pos: int = self.focus_position - self.contents[old_pos][0].set_styles( - self.get_styles(self.contents[old_pos][0].date, False)) + self.contents[old_pos][0].set_styles(self.get_styles(self.contents[old_pos][0].date, False)) @property def focus_position(self) -> int: @@ -171,7 +174,8 @@ def focus_position(self, position: int) -> None: else: self._clear_cursor() self.contents[position][0].set_styles( - self.get_styles(self.contents[position][0].date, True)) + self.get_styles(self.contents[position][0].date, True) + ) self.on_date_change(self.contents[position][0].date) urwid.Columns.focus_position.fset(self, position) @@ -180,28 +184,28 @@ def set_focus_date(self, a_date: dt.date) -> None: if day[0].date == a_date: self.focus_position = num return None - raise ValueError(f'{a_date} not found in this week') + raise ValueError(f"{a_date} not found in this week") def get_date_column(self, a_date: dt.date) -> int: """return the column `a_date` is in, raises ValueError if `a_date` - cannot be found + cannot be found """ for num, day in enumerate(self.contents[1:8], 1): if day[0].date == a_date: return num - raise ValueError(f'{a_date} not found in this week') + raise ValueError(f"{a_date} not found in this week") def keypress(self, size: tuple[int], key: str) -> str: """only leave calendar area on pressing 'tab' or 'enter'""" - if key in self.keybindings['left']: - key = 'left' - elif key in self.keybindings['up']: - key = 'up' - elif key in self.keybindings['right']: - key = 'right' - elif key in self.keybindings['down']: - key = 'down' + if key in self.keybindings["left"]: + key = "left" + elif key in self.keybindings["up"]: + key = "up" + elif key in self.keybindings["right"]: + key = "right" + elif key in self.keybindings["down"]: + key = "down" exit_row = False # set this, if we are leaving the current row old_pos = self.focus_position @@ -209,18 +213,18 @@ def keypress(self, size: tuple[int], key: str) -> str: key = super().keypress(size, key) # make sure we don't leave the calendar - if old_pos == 7 and key == 'right': + if old_pos == 7 and key == "right": self.focus_position = 1 exit_row = True - key = 'down' - elif old_pos == 1 and key == 'left': + key = "down" + elif old_pos == 1 and key == "left": self.focus_position = 7 exit_row = True - key = 'up' - elif key in self.keybindings['view']: # TODO make this more generic + key = "up" + elif key in self.keybindings["view"]: # TODO make this more generic self.focus_position = old_pos - key = 'right' - elif key in ['up', 'down']: + key = "right" + elif key in ["up", "down"]: exit_row = True if exit_row: @@ -228,13 +232,14 @@ def keypress(self, size: tuple[int], key: str) -> str: return key + class CListBox(urwid.ListBox): """our custom version of ListBox containing a CalendarWalker instance it should contain a `CalendarWalker` instance which it autoextends on - rendering, if needed """ + rendering, if needed""" - def __init__(self, walker: 'CalendarWalker') -> None: + def __init__(self, walker: "CalendarWalker") -> None: self._init: bool = True self.keybindings = walker.keybindings self.on_press = walker.on_press @@ -251,11 +256,11 @@ def focus_position(self) -> int: def focus_position(self, position: int) -> None: super().set_focus(position) - def render(self, size: tuple[int], focus: bool=False) -> urwid.Canvas: - while 'bottom' in self.ends_visible(size): + def render(self, size: tuple[int], focus: bool = False) -> urwid.Canvas: + while "bottom" in self.ends_visible(size): self.body._autoextend() if self._init: - self.set_focus_valign('middle') + self.set_focus_valign("middle") self._init = False return super().render(size, focus) @@ -263,9 +268,8 @@ def render(self, size: tuple[int], focus: bool=False) -> urwid.Canvas: def mouse_event(self, *args): size, event, button, col, row, focus = args - if event == 'mouse press' and button == 1: - self.focus.focus.set_styles( - self.focus.get_styles(self.body.focus_date, False)) + if event == "mouse press" and button == 1: + self.focus.focus.set_styles(self.focus.get_styles(self.body.focus_date, False)) return super().mouse_event(*args) def _date(self, row: int, column: int) -> dt.date: @@ -280,9 +284,9 @@ def _unmark_one(self, row: int, column: int) -> None: def _mark_one(self, row: int, column: int) -> None: """set attribute *mark* on the date at row `row` and column `column`""" - self.body[row].contents[column][0].set_styles('mark') + self.body[row].contents[column][0].set_styles("mark") - def _mark(self, a_date: dt.date | None=None) -> None: + def _mark(self, a_date: dt.date | None = None) -> None: """make sure everything between the marked entry and `a_date` is visually marked, and nothing else""" @@ -299,17 +303,17 @@ def toggle(row: int, column: int) -> None: else: self._unmark_one(row, column) - start = min(self._marked['pos'][0], self.focus_position) - 2 - stop = max(self._marked['pos'][0], self.focus_position) + 2 + start = min(self._marked["pos"][0], self.focus_position) - 2 + stop = max(self._marked["pos"][0], self.focus_position) + 2 for row in range(start, stop): for col in range(1, 8): - if a_date > self._marked['date']: - if self._marked['date'] <= self._date(row, col) <= a_date: + if a_date > self._marked["date"]: + if self._marked["date"] <= self._date(row, col) <= a_date: self._mark_one(row, col) else: self._unmark_one(row, col) else: - if self._marked['date'] >= self._date(row, col) >= a_date: + if self._marked["date"] >= self._date(row, col) >= a_date: self._mark_one(row, col) else: self._unmark_one(row, col) @@ -320,8 +324,8 @@ def toggle(row: int, column: int) -> None: def _unmark_all(self) -> None: """remove attribute *mark* from all dates""" if self._marked and self._pos_old: - start = min(self._marked['pos'][0], self.focus_position, self._pos_old[0]) - end = max(self._marked['pos'][0], self.focus_position, self._pos_old[0]) + 1 + start = min(self._marked["pos"][0], self.focus_position, self._pos_old[0]) + end = max(self._marked["pos"][0], self.focus_position, self._pos_old[0]) + 1 for row in range(start, end): for col in range(1, 8): self._unmark_one(row, col) @@ -335,33 +339,37 @@ def set_focus_date(self, a_day: dt.date) -> None: self.body.set_focus_date(a_day) def keypress(self, size: bool, key: str) -> str | None: - if key in self.keybindings['mark'] + ['esc'] and self._marked: + if key in self.keybindings["mark"] + ["esc"] and self._marked: self._unmark_all() self._marked = None return None - if key in self.keybindings['mark']: - self._marked = {'date': self.body.focus_date, - 'pos': (self.focus_position, self.focus.focus_col)} - if self._marked and key in self.keybindings['other']: - row, col = self._marked['pos'] - self._marked = {'date': self.body.focus_date, - 'pos': (self.focus_position, self.focus.focus_col)} + if key in self.keybindings["mark"]: + self._marked = { + "date": self.body.focus_date, + "pos": (self.focus_position, self.focus.focus_col), + } + if self._marked and key in self.keybindings["other"]: + row, col = self._marked["pos"] + self._marked = { + "date": self.body.focus_date, + "pos": (self.focus_position, self.focus.focus_col), + } self.focus.focus_col = col self.focus_position = row if key in self.on_press: if self._marked: - start = min(self.body.focus_date, self._marked['date']) - end = max(self.body.focus_date, self._marked['date']) + start = min(self.body.focus_date, self._marked["date"]) + end = max(self.body.focus_date, self._marked["date"]) else: start = self.body.focus_date end = None return self.on_press[key](start, end) - if key in self.keybindings['today'] + ['page down', 'page up']: + if key in self.keybindings["today"] + ["page down", "page up"]: # reset colors of currently focused Date widget self.focus.focus.set_styles(self.focus.get_styles(self.body.focus_date, False)) - if key in self.keybindings['today']: + if key in self.keybindings["today"]: self.set_focus_date(dt.date.today()) - self.set_focus_valign(('relative', 10)) + self.set_focus_valign(("relative", 10)) key = super().keypress(size, key) if self._marked: @@ -370,16 +378,17 @@ def keypress(self, size: bool, key: str) -> str | None: class CalendarWalker(urwid.SimpleFocusListWalker): - def __init__(self, - on_date_change: Callable[[dt.date], None], - on_press: dict[str, Callable[[dt.date, dt.date | None], str | None]], - keybindings: dict[str, list[str]], - get_styles: GetStylesSignature, - firstweekday: int = 0, - weeknumbers: Literal['left', 'right', False]=False, - monthdisplay: Literal['firstday', 'firstfullweek']='firstday', - initial: dt.date | None=None, - ) -> None: + def __init__( + self, + on_date_change: Callable[[dt.date], None], + on_press: dict[str, Callable[[dt.date, dt.date | None], str | None]], + keybindings: dict[str, list[str]], + get_styles: GetStylesSignature, + firstweekday: int = 0, + weeknumbers: Literal["left", "right", False] = False, + monthdisplay: Literal["firstday", "firstfullweek"] = "firstday", + initial: dt.date | None = None, + ) -> None: self.firstweekday = firstweekday self.weeknumbers = weeknumbers self.monthdisplay = monthdisplay @@ -389,7 +398,7 @@ def __init__(self, self.get_styles = get_styles self.reset(initial) - def reset(self, initial: dt.date | None=None) -> None: + def reset(self, initial: dt.date | None = None) -> None: if initial is None: initial = dt.date.today() weeks = self._construct_month(initial.year, initial.month) @@ -424,11 +433,11 @@ def focus_date(self) -> dt.date: def set_focus_date(self, a_day: dt.date) -> None: """set the focus to `a_day`""" - if self.days_to_next_already_loaded(a_day) > 200: # arbitrary number + if self.days_to_next_already_loaded(a_day) > 200: # arbitrary number self.reset(a_day) row, column = self.get_date_pos(a_day) self.set_focus(row) - self[self.focus].focus_position = (column) + self[self.focus].focus_position = column @property def earliest_date(self) -> dt.date: @@ -448,7 +457,7 @@ def reset_styles_range(self, min_date: dt.date, max_date: dt.date) -> None: for row in range(minr, maxr + 1): for column in range(1, 8): - focus = ((row, column) == focus_pos) + focus = (row, column) == focus_pos self[row][column].reset_styles(focus) def get_date_pos(self, a_day: dt.date) -> tuple[int, int]: @@ -475,7 +484,7 @@ def get_date_pos(self, a_day: dt.date) -> tuple[int, int]: except (ValueError, IndexError): pass # we didn't find the date we were looking for... - raise ValueError('something is wrong') + raise ValueError("something is wrong") def _autoextend(self) -> None: """appends the next month""" @@ -520,17 +529,17 @@ def _construct_week(self, week: list[dt.date]) -> DateCColumns: :param week: list of datetime.date objects :returns: the week as an CColumns object """ - if self.monthdisplay == 'firstday' and 1 in (day.day for day in week): + if self.monthdisplay == "firstday" and 1 in (day.day for day in week): month_name = calendar.month_abbr[week[-1].month].ljust(4) - attr = 'monthname' - elif self.monthdisplay == 'firstfullweek' and week[0].day <= 7: + attr = "monthname" + elif self.monthdisplay == "firstfullweek" and week[0].day <= 7: month_name = calendar.month_abbr[week[-1].month].ljust(4) - attr = 'monthname' - elif self.weeknumbers == 'left': - month_name = f' {getweeknumber(week[0]):2} ' - attr = 'weeknumber_left' + attr = "monthname" + elif self.weeknumbers == "left": + month_name = f" {getweeknumber(week[0]):2} " + attr = "weeknumber_left" else: - month_name = ' ' + month_name = " " attr = None this_week: list[tuple[int, Date | urwid.AttrMap]] @@ -539,24 +548,28 @@ def _construct_week(self, week: list[dt.date]) -> DateCColumns: new_date = Date(day, self.get_styles) this_week.append((2, new_date)) new_date.set_styles(self.get_styles(new_date.date, False)) - if self.weeknumbers == 'right': - this_week.append((2, urwid.AttrMap( - urwid.Text(f'{getweeknumber(week[0]):2}'), 'weeknumber_right'))) - - week = DateCColumns(this_week, - on_date_change=self.on_date_change, - on_press=self.on_press, - keybindings=self.keybindings, - dividechars=1, - get_styles=self.get_styles) + if self.weeknumbers == "right": + this_week.append( + (2, urwid.AttrMap(urwid.Text(f"{getweeknumber(week[0]):2}"), "weeknumber_right")) + ) + + week = DateCColumns( + this_week, + on_date_change=self.on_date_change, + on_press=self.on_press, + keybindings=self.keybindings, + dividechars=1, + get_styles=self.get_styles, + ) return week - def _construct_month(self, - year: int=dt.date.today().year, - month: int=dt.date.today().month, - clean_first_row: bool=False, - clean_last_row: bool=False, - ) -> list[DateCColumns]: + def _construct_month( + self, + year: int = dt.date.today().year, + month: int = dt.date.today().month, + clean_first_row: bool = False, + clean_last_row: bool = False, + ) -> list[DateCColumns]: """construct one month of DateCColumns :param year: the year this month is set in @@ -584,16 +597,17 @@ def _construct_month(self, class CalendarWidget(urwid.WidgetWrap): - def __init__(self, - on_date_change: Callable[[dt.date], None], - keybindings: dict[str, list[str]], - on_press: OnPressType | None=None, - firstweekday: int=0, - weeknumbers: Literal['left', 'right', False]=False, - monthdisplay: Literal['firstday', 'firstfullweek']='firstday', - get_styles: GetStylesSignature | None=None, - initial: dt.date | None=None, - ) -> None: + def __init__( + self, + on_date_change: Callable[[dt.date], None], + keybindings: dict[str, list[str]], + on_press: OnPressType | None = None, + firstweekday: int = 0, + weeknumbers: Literal["left", "right", False] = False, + monthdisplay: Literal["firstday", "firstfullweek"] = "firstday", + get_styles: GetStylesSignature | None = None, + initial: dt.date | None = None, + ) -> None: """A calendar widget that can be used in urwid applications :param on_date_change: a function that is called every time the selected @@ -635,46 +649,51 @@ def __init__(self, on_press = {} default_keybindings: dict[str, list[str]] = { - 'left': ['left'], 'down': ['down'], 'right': ['right'], 'up': ['up'], - 'today': ['t'], - 'view': [], - 'mark': ['v'], - 'other': ['%'], + "left": ["left"], + "down": ["down"], + "right": ["right"], + "up": ["up"], + "today": ["t"], + "view": [], + "mark": ["v"], + "other": ["%"], } default_keybindings.update(keybindings) calendar.setfirstweekday(firstweekday) try: - mylocale: str = '.'.join(getlocale(LC_TIME)) # type: ignore + mylocale: str = ".".join(getlocale(LC_TIME)) # type: ignore except TypeError: # language code and encoding may be None - mylocale = 'C' + mylocale = "C" _calendar = calendar.LocaleTextCalendar(firstweekday, mylocale) # type: ignore weekheader = _calendar.formatweekheader(2) - dnames = weekheader.split(' ') + dnames = weekheader.split(" ") def _get_styles(date: dt.date, focus: bool) -> str | None: if focus: if date == dt.date.today(): - return 'today focus' + return "today focus" else: - return 'reveal focus' + return "reveal focus" else: if date == dt.date.today(): - return 'today' + return "today" else: return None + if get_styles is None: get_styles = _get_styles - if weeknumbers == 'right': - dnames.append('#w') + if weeknumbers == "right": + dnames.append("#w") month_names_length = get_month_abbr_len() cnames = urwid.Columns( - [(month_names_length, urwid.Text(' ' * month_names_length))] + - [(2, urwid.AttrMap(urwid.Text(name), 'dayname')) for name in dnames], - dividechars=1) + [(month_names_length, urwid.Text(" " * month_names_length))] + + [(2, urwid.AttrMap(urwid.Text(name), "dayname")) for name in dnames], + dividechars=1, + ) self.walker = CalendarWalker( on_date_change=on_date_change, on_press=on_press, diff --git a/khal/ui/colors.py b/khal/ui/colors.py index 403d3806f..05ce89f83 100644 --- a/khal/ui/colors.py +++ b/khal/ui/colors.py @@ -21,88 +21,82 @@ dark = [ - ('header', 'white', 'black'), - ('footer', 'white', 'black'), - ('line header', 'black', 'white', 'bold'), - ('alt header', 'white', '', 'bold'), - ('bright', 'dark blue', 'white', 'bold,standout'), - ('list', 'black', 'white'), - ('list focused', 'white', 'light blue', 'bold'), - ('edit', 'black', 'white'), - ('edit focus', 'white', 'light blue', 'bold'), - ('button', 'black', 'dark cyan'), - ('button focused', 'white', 'light blue', 'bold'), - - ('reveal focus', 'black', 'light gray'), - ('today focus', 'white', 'dark magenta'), - ('today', 'dark gray', 'dark green',), - - ('date header', 'light gray', 'black'), - ('date header focused', 'black', 'white'), - ('date header selected', 'dark gray', 'light gray'), - - ('dayname', 'light gray', ''), - ('monthname', 'light gray', ''), - ('weeknumber_right', 'light gray', ''), - ('alert', 'white', 'dark red'), - ('mark', 'white', 'dark green'), - ('frame', 'white', 'black'), - ('frame focus', 'light red', 'black'), - ('frame focus color', 'dark blue', 'black'), - ('frame focus top', 'dark magenta', 'black'), - - ('eventcolumn', '', '', ''), - ('eventcolumn focus', '', '', ''), - ('calendar', '', '', ''), - ('calendar focus', '', '', ''), - - ('editbx', 'light gray', 'dark blue'), - ('editcp', 'black', 'light gray', 'standout'), - ('popupbg', 'white', 'black', 'bold'), - ('popupper', 'white', 'dark cyan'), - ('caption', 'white', '', 'bold'), + ("header", "white", "black"), + ("footer", "white", "black"), + ("line header", "black", "white", "bold"), + ("alt header", "white", "", "bold"), + ("bright", "dark blue", "white", "bold,standout"), + ("list", "black", "white"), + ("list focused", "white", "light blue", "bold"), + ("edit", "black", "white"), + ("edit focus", "white", "light blue", "bold"), + ("button", "black", "dark cyan"), + ("button focused", "white", "light blue", "bold"), + ("reveal focus", "black", "light gray"), + ("today focus", "white", "dark magenta"), + ( + "today", + "dark gray", + "dark green", + ), + ("date header", "light gray", "black"), + ("date header focused", "black", "white"), + ("date header selected", "dark gray", "light gray"), + ("dayname", "light gray", ""), + ("monthname", "light gray", ""), + ("weeknumber_right", "light gray", ""), + ("alert", "white", "dark red"), + ("mark", "white", "dark green"), + ("frame", "white", "black"), + ("frame focus", "light red", "black"), + ("frame focus color", "dark blue", "black"), + ("frame focus top", "dark magenta", "black"), + ("eventcolumn", "", "", ""), + ("eventcolumn focus", "", "", ""), + ("calendar", "", "", ""), + ("calendar focus", "", "", ""), + ("editbx", "light gray", "dark blue"), + ("editcp", "black", "light gray", "standout"), + ("popupbg", "white", "black", "bold"), + ("popupper", "white", "dark cyan"), + ("caption", "white", "", "bold"), ] light = [ - ('header', 'black', 'white'), - ('footer', 'black', 'white'), - ('line header', 'black', 'white', 'bold'), - ('alt header', 'black', '', 'bold'), - ('bright', 'dark blue', 'white', 'bold,standout'), - ('list', 'black', 'white'), - ('list focused', 'white', 'light blue', 'bold'), - ('edit', 'black', 'white'), - ('edit focus', 'white', 'light blue', 'bold'), - ('button', 'black', 'dark cyan'), - ('button focused', 'white', 'light blue', 'bold'), - - ('reveal focus', 'black', 'dark cyan', 'standout'), - ('today focus', 'white', 'dark cyan', 'standout'), - ('today', 'black', 'light gray'), - - ('date header', '', 'white'), - ('date header focused', 'white', 'dark gray', 'bold,standout'), - ('date header selected', 'dark gray', 'light cyan'), - - ('dayname', 'dark gray', 'white'), - ('monthname', 'dark gray', 'white'), - ('weeknumber_right', 'dark gray', 'white'), - ('alert', 'white', 'dark red'), - ('mark', 'white', 'dark green'), - ('frame', 'dark gray', 'white'), - ('frame focus', 'light red', 'white'), - ('frame focus color', 'dark blue', 'white'), - ('frame focus top', 'dark magenta', 'white'), - - ('eventcolumn', '', '', ''), - ('eventcolumn focus', '', '', ''), - ('calendar', '', '', ''), - ('calendar focus', '', '', ''), - - ('editbx', 'light gray', 'dark blue'), - ('editcp', 'black', 'light gray', 'standout'), - ('popupbg', 'white', 'black', 'bold'), - ('popupper', 'black', 'light gray'), - ('caption', 'black', '', ''), + ("header", "black", "white"), + ("footer", "black", "white"), + ("line header", "black", "white", "bold"), + ("alt header", "black", "", "bold"), + ("bright", "dark blue", "white", "bold,standout"), + ("list", "black", "white"), + ("list focused", "white", "light blue", "bold"), + ("edit", "black", "white"), + ("edit focus", "white", "light blue", "bold"), + ("button", "black", "dark cyan"), + ("button focused", "white", "light blue", "bold"), + ("reveal focus", "black", "dark cyan", "standout"), + ("today focus", "white", "dark cyan", "standout"), + ("today", "black", "light gray"), + ("date header", "", "white"), + ("date header focused", "white", "dark gray", "bold,standout"), + ("date header selected", "dark gray", "light cyan"), + ("dayname", "dark gray", "white"), + ("monthname", "dark gray", "white"), + ("weeknumber_right", "dark gray", "white"), + ("alert", "white", "dark red"), + ("mark", "white", "dark green"), + ("frame", "dark gray", "white"), + ("frame focus", "light red", "white"), + ("frame focus color", "dark blue", "white"), + ("frame focus top", "dark magenta", "white"), + ("eventcolumn", "", "", ""), + ("eventcolumn focus", "", "", ""), + ("calendar", "", "", ""), + ("calendar focus", "", "", ""), + ("editbx", "light gray", "dark blue"), + ("editcp", "black", "light gray", "standout"), + ("popupbg", "white", "black", "bold"), + ("popupper", "black", "light gray"), + ("caption", "black", "", ""), ] -themes: dict[str, list[tuple[str, ...]]] = {'light': light, 'dark': dark} +themes: dict[str, list[tuple[str, ...]]] = {"light": light, "dark": dark} diff --git a/khal/ui/editor.py b/khal/ui/editor.py index b6ae9ecde..fbe47a94a 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -48,8 +48,8 @@ if TYPE_CHECKING: import khal.khalendar.event -class StartEnd: +class StartEnd: def __init__(self, startdate, starttime, enddate, endtime) -> None: """collecting some common properties""" self.startdate = startdate @@ -59,8 +59,15 @@ def __init__(self, startdate, starttime, enddate, endtime) -> None: class CalendarPopUp(urwid.PopUpLauncher): - def __init__(self, widget, on_date_change, weeknumbers: Literal['left', 'right', False]=False, - firstweekday=0, monthdisplay='firstday', keybindings=None) -> None: + def __init__( + self, + widget, + on_date_change, + weeknumbers: Literal["left", "right", False] = False, + firstweekday=0, + monthdisplay="firstday", + keybindings=None, + ) -> None: self._on_date_change = on_date_change self._weeknumbers = weeknumbers self._monthdisplay = monthdisplay @@ -69,7 +76,7 @@ def __init__(self, widget, on_date_change, weeknumbers: Literal['left', 'right', super().__init__(widget) def keypress(self, size, key): - if key == 'enter': + if key == "enter": self.open_pop_up() else: return super().keypress(size, key) @@ -79,26 +86,31 @@ def on_change(new_date): self.base_widget.set_value(new_date) self._on_date_change(new_date) - on_press = {'enter': lambda _, __: self.close_pop_up(), - 'esc': lambda _, __: self.close_pop_up()} + on_press = { + "enter": lambda _, __: self.close_pop_up(), + "esc": lambda _, __: self.close_pop_up(), + } try: initial_date = self.base_widget._get_current_value() except DateConversionError: return None else: pop_up = CalendarWidget( - on_change, self._keybindings, on_press, + on_change, + self._keybindings, + on_press, firstweekday=self._firstweekday, weeknumbers=self._weeknumbers, monthdisplay=self._monthdisplay, - initial=initial_date) - pop_up = CAttrMap(pop_up, 'calendar', ' calendar focus') - pop_up = CAttrMap(urwid.LineBox(pop_up), 'calendar', 'calendar focus') + initial=initial_date, + ) + pop_up = CAttrMap(pop_up, "calendar", " calendar focus") + pop_up = CAttrMap(urwid.LineBox(pop_up), "calendar", "calendar focus") return pop_up def get_pop_up_parameters(self): - width = 31 if self._weeknumbers == 'right' else 28 - return {'left': 0, 'top': 1, 'overlay_width': width, 'overlay_height': 8} + width = 31 if self._weeknumbers == "right" else 28 + return {"left": 0, "top": 1, "overlay_width": width, "overlay_height": 8} class DateEdit(urwid.WidgetWrap): @@ -111,11 +123,11 @@ class DateEdit(urwid.WidgetWrap): def __init__( self, startdt: dt.date, - dateformat: str='%Y-%m-%d', - on_date_change: Callable=lambda _: None, - weeknumbers: Literal['left', 'right', False]=False, - firstweekday: int=0, - monthdisplay: Literal['firstday', 'firstfullweek']='firstday', + dateformat: str = "%Y-%m-%d", + on_date_change: Callable = lambda _: None, + weeknumbers: Literal["left", "right", False] = False, + firstweekday: int = 0, + monthdisplay: Literal["firstday", "firstfullweek"] = "firstday", keybindings: dict[str, list[str]] | None = None, ) -> None: datewidth = len(startdt.strftime(dateformat)) @@ -127,12 +139,15 @@ def __init__( EditWidget=DateWidget, validate=self._validate, edit_text=startdt.strftime(dateformat), - on_date_change=on_date_change) - wrapped = CalendarPopUp(self._edit, on_date_change, weeknumbers, - firstweekday, monthdisplay, keybindings) + on_date_change=on_date_change, + ) + wrapped = CalendarPopUp( + self._edit, on_date_change, weeknumbers, firstweekday, monthdisplay, keybindings + ) padded = CAttrMap( - urwid.Padding(wrapped, align='left', width=datewidth, left=0, right=1), - 'calendar', 'calendar focus', + urwid.Padding(wrapped, align="left", width=datewidth, left=0, right=1), + "calendar", + "calendar focus", ) super().__init__(padded) @@ -165,14 +180,15 @@ def date(self, date): class StartEndEditor(urwid.WidgetWrap): """Widget for editing start and end times (of an event).""" - def __init__(self, - start: dt.datetime, - end: dt.datetime, - conf, - on_start_date_change=lambda x: None, - on_end_date_change=lambda x: None, - on_type_change: Callable[[bool], None]=lambda _: None, - ) -> None: + def __init__( + self, + start: dt.datetime, + end: dt.datetime, + conf, + on_start_date_change=lambda x: None, + on_end_date_change=lambda x: None, + on_type_change: Callable[[bool], None] = lambda _: None, + ) -> None: """ :param on_start_date_change: a callable that gets called everytime a new start date is entered, with that new date as an argument @@ -191,12 +207,11 @@ def __init__(self, self.on_start_date_change = on_start_date_change self.on_end_date_change = on_end_date_change self.on_type_change = on_type_change - self._datewidth = len(start.strftime(self.conf['locale']['longdateformat'])) - self._timewidth = len(start.strftime(self.conf['locale']['timeformat'])) + self._datewidth = len(start.strftime(self.conf["locale"]["longdateformat"])) + self._timewidth = len(start.strftime(self.conf["locale"]["timeformat"])) # this will contain the widgets for [start|end] [date|time] self.widgets = StartEnd(None, None, None, None) - self.checkallday = urwid.CheckBox( - 'Allday', state=self.allday, on_state_change=self.toggle) + self.checkallday = urwid.CheckBox("Allday", state=self.allday, on_state_change=self.toggle) self.toggle(None, self.allday) def keypress(self, size, key): @@ -218,15 +233,15 @@ def _start_time(self): @property def localize_start(self): - if getattr(self.startdt, 'tzinfo', None) is None: - return self.conf['locale']['default_timezone'].localize + if getattr(self.startdt, "tzinfo", None) is None: + return self.conf["locale"]["default_timezone"].localize else: return self.startdt.tzinfo.localize @property def localize_end(self): - if getattr(self.enddt, 'tzinfo', None) is None: - return self.conf['locale']['default_timezone'].localize + if getattr(self.enddt, "tzinfo", None) is None: + return self.conf["locale"]["default_timezone"].localize else: return self.enddt.tzinfo.localize @@ -246,9 +261,10 @@ def _end_time(self): def _validate_start_time(self, text): try: - startval = dt.datetime.strptime(text, self.conf['locale']['timeformat']) + startval = dt.datetime.strptime(text, self.conf["locale"]["timeformat"]) self._startdt = self.localize_start( - dt.datetime.combine(self._startdt.date(), startval.time())) + dt.datetime.combine(self._startdt.date(), startval.time()) + ) except ValueError: return False else: @@ -260,7 +276,7 @@ def _start_date_change(self, date): def _validate_end_time(self, text): try: - endval = dt.datetime.strptime(text, self.conf['locale']['timeformat']) + endval = dt.datetime.strptime(text, self.conf["locale"]["timeformat"]) self._enddt = self.localize_end(dt.datetime.combine(self._enddt.date(), endval.time())) except ValueError: return False @@ -291,55 +307,74 @@ def toggle(self, checkbox, state: bool): self._enddt = self._enddt.date() self.allday = state self.widgets.startdate = DateEdit( - self._startdt, self.conf['locale']['longdateformat'], - self._start_date_change, self.conf['locale']['weeknumbers'], - self.conf['locale']['firstweekday'], - self.conf['view']['monthdisplay'], - self.conf['keybindings'], + self._startdt, + self.conf["locale"]["longdateformat"], + self._start_date_change, + self.conf["locale"]["weeknumbers"], + self.conf["locale"]["firstweekday"], + self.conf["view"]["monthdisplay"], + self.conf["keybindings"], ) self.widgets.enddate = DateEdit( - self._enddt, self.conf['locale']['longdateformat'], - self._end_date_change, self.conf['locale']['weeknumbers'], - self.conf['locale']['firstweekday'], - self.conf['view']['monthdisplay'], - self.conf['keybindings'], + self._enddt, + self.conf["locale"]["longdateformat"], + self._end_date_change, + self.conf["locale"]["weeknumbers"], + self.conf["locale"]["firstweekday"], + self.conf["view"]["monthdisplay"], + self.conf["keybindings"], ) if state is True: # allday event self.on_type_change(True) timewidth = 1 - self.widgets.starttime = urwid.Text('') - self.widgets.endtime = urwid.Text('') + self.widgets.starttime = urwid.Text("") + self.widgets.endtime = urwid.Text("") elif state is False: # datetime event self.on_type_change(False) timewidth = self._timewidth + 1 raw_start_time_widget = ValidatedEdit( - dateformat=self.conf['locale']['timeformat'], + dateformat=self.conf["locale"]["timeformat"], EditWidget=TimeWidget, validate=self._validate_start_time, - edit_text=self.startdt.strftime(self.conf['locale']['timeformat']), + edit_text=self.startdt.strftime(self.conf["locale"]["timeformat"]), ) self.widgets.starttime = urwid.Padding( - raw_start_time_widget, align='left', width=self._timewidth + 1, left=1) + raw_start_time_widget, align="left", width=self._timewidth + 1, left=1 + ) raw_end_time_widget = ValidatedEdit( - dateformat=self.conf['locale']['timeformat'], + dateformat=self.conf["locale"]["timeformat"], EditWidget=TimeWidget, validate=self._validate_end_time, - edit_text=self.enddt.strftime(self.conf['locale']['timeformat']), + edit_text=self.enddt.strftime(self.conf["locale"]["timeformat"]), ) self.widgets.endtime = urwid.Padding( - raw_end_time_widget, align='left', width=self._timewidth + 1, left=1) - - columns = NPile([ - self.checkallday, - NColumns([(5, urwid.Text('From:')), (self._datewidth, self.widgets.startdate), ( - timewidth, self.widgets.starttime)], dividechars=1), - NColumns( - [(5, urwid.Text('To:')), (self._datewidth, self.widgets.enddate), - (timewidth, self.widgets.endtime)], - dividechars=1) - ], focus_item=1) + raw_end_time_widget, align="left", width=self._timewidth + 1, left=1 + ) + + columns = NPile( + [ + self.checkallday, + NColumns( + [ + (5, urwid.Text("From:")), + (self._datewidth, self.widgets.startdate), + (timewidth, self.widgets.starttime), + ], + dividechars=1, + ), + NColumns( + [ + (5, urwid.Text("To:")), + (self._datewidth, self.widgets.enddate), + (timewidth, self.widgets.endtime), + ], + dividechars=1, + ), + ], + focus_item=1, + ) urwid.WidgetWrap.__init__(self, columns) @property @@ -357,9 +392,9 @@ class EventEditor(urwid.WidgetWrap): def __init__( self, pane, - event: 'khal.khalendar.event.Event', + event: "khal.khalendar.event.Event", save_callback=None, - always_save: bool=False, + always_save: bool = False, ) -> None: """ :param save_callback: call when saving event with new start and end @@ -382,81 +417,110 @@ def __init__( self.categories = event.categories self.url = event.url self.startendeditor = StartEndEditor( - event.start_local, event.end_local, self._conf, - self.start_datechange, self.end_datechange, + event.start_local, + event.end_local, + self._conf, + self.start_datechange, + self.end_datechange, self.type_change, ) # TODO make sure recurrence rules cannot be edited if we only # edit one instance (or this and future) (once we support that) self.recurrenceeditor = RecurrenceEditor( - self.event.recurobject, self._conf, event.start_local, + self.event.recurobject, + self._conf, + event.start_local, ) - self.summary = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Title: '), edit_text=event.summary), 'edit', 'edit focus', + self.summary = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Title: "), edit_text=event.summary), + "edit", + "edit focus", ) - divider = urwid.Divider(' ') + divider = urwid.Divider(" ") def decorate_choice(c) -> tuple[str, str]: - return ('calendar ' + c['name'] + ' popup', c['name']) + return ("calendar " + c["name"] + " popup", c["name"]) - self.calendar_chooser= CAttrMap(Choice( - [self.collection._calendars[c] for c in self.collection.writable_names], - self.collection._calendars[self.event.calendar], - decorate_choice - ), 'caption') + self.calendar_chooser = CAttrMap( + Choice( + [self.collection._calendars[c] for c in self.collection.writable_names], + self.collection._calendars[self.event.calendar], + decorate_choice, + ), + "caption", + ) self.description = urwid.AttrMap( ExtendedEdit( - caption=('caption', 'Description: '), - edit_text=self.description, - multiline=True + caption=("caption", "Description: "), edit_text=self.description, multiline=True ), - 'edit', 'edit focus', + "edit", + "edit focus", ) - self.location = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Location: '), edit_text=self.location), 'edit', 'edit focus', + self.location = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Location: "), edit_text=self.location), + "edit", + "edit focus", ) - self.categories = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', + self.categories = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Categories: "), edit_text=self.categories), + "edit", + "edit focus", ) self.attendees = urwid.AttrMap( ExtendedEdit( - caption=('caption', 'Attendees: '), - edit_text=self.attendees, - multiline=True + caption=("caption", "Attendees: "), edit_text=self.attendees, multiline=True ), - 'edit', 'edit focus', + "edit", + "edit focus", ) - self.url = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', + self.url = urwid.AttrMap( + ExtendedEdit(caption=("caption", "URL: "), edit_text=self.url), + "edit", + "edit focus", ) self.alarmseditor: AlarmsEditor = AlarmsEditor(self.event) - self.pile = NListBox(urwid.SimpleFocusListWalker([ - self.summary, - urwid.Columns([(13, urwid.AttrMap(urwid.Text('Calendar:'), 'caption')), - (12, self.calendar_chooser)], - ), - divider, - self.location, - self.categories, - self.description, - self.url, - divider, - self.attendees, - divider, - self.startendeditor, - self.recurrenceeditor, - divider, - self.alarmseditor, - divider, - urwid.Columns( - [(12, button('Save', on_press=self.save, padding_left=0, padding_right=0))] + self.pile = NListBox( + urwid.SimpleFocusListWalker( + [ + self.summary, + urwid.Columns( + [ + (13, urwid.AttrMap(urwid.Text("Calendar:"), "caption")), + (12, self.calendar_chooser), + ], + ), + divider, + self.location, + self.categories, + self.description, + self.url, + divider, + self.attendees, + divider, + self.startendeditor, + self.recurrenceeditor, + divider, + self.alarmseditor, + divider, + urwid.Columns( + [(12, button("Save", on_press=self.save, padding_left=0, padding_right=0))] + ), + urwid.Columns( + [ + ( + 12, + button( + "Export", on_press=self.export, padding_left=0, padding_right=0 + ), + ) + ], + ), + ] ), - urwid.Columns( - [(12, button('Export', on_press=self.export, padding_left=0, padding_right=0))], - ) - ]), outermost=True) + outermost=True, + ) self._always_save = always_save urwid.WidgetWrap.__init__(self, self.pile) @@ -473,13 +537,13 @@ def type_change(self, allday: bool) -> None: :params allday: True if the event is now an allday event, False if it isn't """ # test if self.alarmseditor exists - if not hasattr(self, 'alarmseditor'): + if not hasattr(self, "alarmseditor"): return # to make the alarms before the event, we need to set it them to # negative values - default_event_alarm = -1 * self._conf['default']['default_event_alarm'] - default_dayevent_alarm =-1 * self._conf['default']['default_dayevent_alarm'] + default_event_alarm = -1 * self._conf["default"]["default_event_alarm"] + default_dayevent_alarm = -1 * self._conf["default"]["default_dayevent_alarm"] alarms = self.alarmseditor.get_alarms() if len(alarms) == 1: timedelta = alarms[0][0] @@ -495,7 +559,7 @@ def type_change(self, allday: bool) -> None: @property def title(self): # Window title - return f'Edit: {get_wrapped_text(self.summary)}' + return f"Edit: {get_wrapped_text(self.summary)}" @classmethod def selectable(cls): @@ -527,13 +591,12 @@ def update_vevent(self): self.event.update_summary(get_wrapped_text(self.summary)) self.event.update_description(get_wrapped_text(self.description)) self.event.update_location(get_wrapped_text(self.location)) - self.event.update_attendees(get_wrapped_text(self.attendees).split(',')) - self.event.update_categories(get_wrapped_text(self.categories).split(',')) + self.event.update_attendees(get_wrapped_text(self.attendees).split(",")) + self.event.update_categories(get_wrapped_text(self.categories).split(",")) self.event.update_url(get_wrapped_text(self.url)) if self.startendeditor.changed: - self.event.update_start_end( - self.startendeditor.startdt, self.startendeditor.enddt) + self.event.update_start_end(self.startendeditor.startdt, self.startendeditor.enddt) if self.recurrenceeditor.changed: rrule = self.recurrenceeditor.active self.event.update_rrule(rrule) @@ -546,20 +609,17 @@ def export(self, button): export the event as ICS :param button: not needed, passed via the button press """ + def export_this(_, user_data): try: self.event.export_ics(user_data.get_edit_text()) except Exception as e: self.pane.window.backtrack() - self.pane.window.alert( - ('light red', - f'Failed to save event: {e}')) + self.pane.window.alert(("light red", f"Failed to save event: {e}")) return self.pane.window.backtrack() - self.pane.window.alert( - ('light green', - 'Event successfuly exported')) + self.pane.window.alert(("light green", "Event successfuly exported")) overlay = urwid.Overlay( ExportDialog( @@ -568,7 +628,11 @@ def export_this(_, user_data): self.event, ), self.pane, - 'center', ('relative', 50), ('relative', 50), None) + "center", + ("relative", 50), + ("relative", 50), + None, + ) self.pane.window.open(overlay) def save(self, button): @@ -578,8 +642,7 @@ def save(self, button): :param button: not needed, passed via the button press """ if not self.startendeditor.validate(): - self.pane.window.alert( - ('light red', "Can't save: end date is before start date!")) + self.pane.window.alert(("light red", "Can't save: end date is before start date!")) return if self._always_save or self.changed is True: @@ -587,44 +650,40 @@ def save(self, button): self.event.allday = self.startendeditor.allday self.event.increment_sequence() if self.event.etag is None: # has not been saved before - self.event.calendar = self.calendar_chooser.original_widget.active['name'] + self.event.calendar = self.calendar_chooser.original_widget.active["name"] self.collection.insert(self.event) elif self.calendar_chooser.changed: - self.collection.change_collection( - self.event, - self.calendar_chooser.active['name'] - ) + self.collection.change_collection(self.event, self.calendar_chooser.active["name"]) else: self.collection.update(self.event) self._save_callback( - self.event.start_local, self.event.end_local, + self.event.start_local, + self.event.end_local, self.event.recurring or self.recurrenceeditor.changed, ) self._abort_confirmed = False self.pane.window.backtrack() def keypress(self, size: tuple[int], key: str) -> str | None: - if key in ['esc'] and self.changed and not self._abort_confirmed: - self.pane.window.alert( - ('light red', 'Unsaved changes! Hit ESC again to discard.')) + if key in ["esc"] and self.changed and not self._abort_confirmed: + self.pane.window.alert(("light red", "Unsaved changes! Hit ESC again to discard.")) self._abort_confirmed = True return None else: self._abort_confirmed = False return_value = super().keypress(size, key) - if key in self.pane._conf['keybindings']['save']: + if key in self.pane._conf["keybindings"]["save"]: self.save(None) return None return return_value -WEEKDAYS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] # TODO use locale and respect weekdaystart +WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] # TODO use locale and respect weekdaystart class WeekDaySelector(urwid.WidgetWrap): def __init__(self, startdt, selected_days) -> None: - self._weekday_boxes = {day: urwid.CheckBox(day, state=False) for day in WEEKDAYS} weekday = startdt.weekday() self._weekday_boxes[WEEKDAYS[weekday]].state = True @@ -640,7 +699,6 @@ def days(self): class RecurrenceEditor(urwid.WidgetWrap): - def __init__(self, rrule, conf, startdt) -> None: self._conf = conf self._startdt = startdt @@ -648,7 +706,9 @@ def __init__(self, rrule, conf, startdt) -> None: self.repeat = bool(rrule) self._allow_edit = not self.repeat or self.check_understood_rrule(rrule) self.repeat_box = urwid.CheckBox( - 'Repeat: ', state=self.repeat, on_state_change=self.check_repeat, + "Repeat: ", + state=self.repeat, + on_state_change=self.check_repeat, ) if "UNTIL" in self._rrule: @@ -658,24 +718,42 @@ def __init__(self, rrule, conf, startdt) -> None: else: self._until = "Forever" - recurrence = self._rrule['freq'][0].lower() if self._rrule else "weekly" - self.recurrence_choice = CPadding(CAttrMap(Choice( - ["daily", "weekly", "monthly", "yearly"], - recurrence, - callback=self.rebuild, - ), 'popupper'), align='center', left=2, right=2) + recurrence = self._rrule["freq"][0].lower() if self._rrule else "weekly" + self.recurrence_choice = CPadding( + CAttrMap( + Choice( + ["daily", "weekly", "monthly", "yearly"], + recurrence, + callback=self.rebuild, + ), + "popupper", + ), + align="center", + left=2, + right=2, + ) self.interval_edit = PositiveIntEdit( - caption=('caption', 'every:'), - edit_text=str(self._rrule.get('INTERVAL', [1])[0]), + caption=("caption", "every:"), + edit_text=str(self._rrule.get("INTERVAL", [1])[0]), + ) + self.until_choice = CPadding( + CAttrMap( + Choice( + ["Forever", "Until", "Repetitions"], + self._until, + callback=self.rebuild, + ), + "popupper", + ), + align="center", + left=2, + right=2, ) - self.until_choice = CPadding(CAttrMap(Choice( - ["Forever", "Until", "Repetitions"], self._until, callback=self.rebuild, - ), 'popupper'), align='center', left=2, right=2) - count = str(self._rrule.get('COUNT', [1])[0]) + count = str(self._rrule.get("COUNT", [1])[0]) self.repetitions_edit = PositiveIntEdit(edit_text=count) - until = self._rrule.get('UNTIL', [None])[0] + until = self._rrule.get("UNTIL", [None])[0] if until is None and isinstance(self._startdt, dt.datetime): until = self._startdt.date() elif until is None: @@ -684,31 +762,36 @@ def __init__(self, rrule, conf, startdt) -> None: if isinstance(until, dt.datetime): until = until.date() self.until_edit = DateEdit( - until, self._conf['locale']['longdateformat'], - lambda _: None, self._conf['locale']['weeknumbers'], - self._conf['locale']['firstweekday'], - self._conf['view']['monthdisplay'], + until, + self._conf["locale"]["longdateformat"], + lambda _: None, + self._conf["locale"]["weeknumbers"], + self._conf["locale"]["firstweekday"], + self._conf["view"]["monthdisplay"], ) self._rebuild_weekday_checks() self._rebuild_monthly_choice() - self._pile = pile = NPile([urwid.Text('')]) + self._pile = pile = NPile([urwid.Text("")]) urwid.WidgetWrap.__init__(self, pile) self.rebuild() def _rebuild_monthly_choice(self): weekday, xth = get_weekday_occurrence(self._startdt) - ords = {1: 'st', 2: 'nd', 3: 'rd', 21: 'st', 22: 'nd', 23: 'rd', 31: 'st'} + ords = {1: "st", 2: "nd", 3: "rd", 21: "st", 22: "nd", 23: "rd", 31: "st"} self._xth_weekday = f"on every {xth}{ords.get(xth, 'th')} {WEEKDAYS[weekday]}" - self._xth_monthday = (f"on every {self._startdt.day}" - f"{ords.get(self._startdt.day, 'th')} of the month") + self._xth_monthday = ( + f"on every {self._startdt.day}{ords.get(self._startdt.day, 'th')} of the month" + ) self.monthly_choice = Choice( - [self._xth_monthday, self._xth_weekday], self._xth_monthday, callback=self.rebuild, + [self._xth_monthday, self._xth_weekday], + self._xth_monthday, + callback=self.rebuild, ) def _rebuild_weekday_checks(self): - if self.recurrence_choice.active == 'weekly': - initial_days = self._rrule.get('BYDAY', []) + if self.recurrence_choice.active == "weekly": + initial_days = self._rrule.get("BYDAY", []) else: initial_days = [] self.weekday_checks = WeekDaySelector(self._startdt, initial_days) @@ -723,25 +806,30 @@ def update_startdt(self, startdt): def check_understood_rrule(rrule): """test if we can reproduce `rrule`.""" keys = set(rrule.keys()) - freq = rrule.get('FREQ', [None])[0] + freq = rrule.get("FREQ", [None])[0] unsupported_rrule_parts = { - 'BYSECOND', 'BYMINUTE', 'BYHOUR', 'BYYEARDAY', 'BYWEEKNO', 'BYMONTH', + "BYSECOND", + "BYMINUTE", + "BYHOUR", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", } if keys.intersection(unsupported_rrule_parts): return False - if len(rrule.get('BYMONTHDAY', [1])) > 1: + if len(rrule.get("BYMONTHDAY", [1])) > 1: return False # we don't support negative BYMONTHDAY numbers # don't need to check whole list, we only support one monthday anyway - if rrule.get('BYMONTHDAY', [1])[0] < 1: + if rrule.get("BYMONTHDAY", [1])[0] < 1: return False - if rrule.get('BYDAY', ['1'])[0][0] == '-': + if rrule.get("BYDAY", ["1"])[0][0] == "-": return False - if rrule.get('BYSETPOS', [1])[0] != 1: + if rrule.get("BYSETPOS", [1])[0] != 1: return False - if freq not in ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']: + if freq not in ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]: return False - if 'BYDAY' in keys and freq == 'YEARLY': + if "BYDAY" in keys and freq == "YEARLY": return False return True @@ -755,7 +843,7 @@ def _refill_contents(self, lines): self._pile.contents.pop() except IndexError: break - [self._pile.contents.append((line, ('pack', None))) for line in lines] + [self._pile.contents.append((line, ("pack", None))) for line in lines] def rebuild(self): old_focus_y = self._pile.focus_position @@ -771,6 +859,7 @@ def _rebuild_no_edit(self): def _allow_edit(_): self._allow_edit = True self.rebuild() + lines = [ urwid.Text("We cannot reproduce this event's repetition rules."), urwid.Text("Editing the repetition rules will destroy the current rules."), @@ -784,11 +873,13 @@ def _rebuild_edit_no_repeat(self): self._refill_contents(lines) def _rebuild_edit(self): - firstline = NColumns([ - (13, self.repeat_box), - (18, self.recurrence_choice), - (13, self.interval_edit), - ]) + firstline = NColumns( + [ + (13, self.repeat_box), + (18, self.recurrence_choice), + (13, self.interval_edit), + ] + ) lines = [firstline] if self.recurrence_choice.active == "weekly": @@ -813,25 +904,25 @@ def changed(self): def rrule(self): rrule = {} - rrule['freq'] = [self.recurrence_choice.active] + rrule["freq"] = [self.recurrence_choice.active] interval = int(self.interval_edit.get_edit_text()) if interval != 1: - rrule['interval'] = [interval] - if rrule['freq'] == ['weekly'] and len(self.weekday_checks.days) > 1: - rrule['byday'] = self.weekday_checks.days - if rrule['freq'] == ['monthly'] and self.monthly_choice.active == self._xth_weekday: + rrule["interval"] = [interval] + if rrule["freq"] == ["weekly"] and len(self.weekday_checks.days) > 1: + rrule["byday"] = self.weekday_checks.days + if rrule["freq"] == ["monthly"] and self.monthly_choice.active == self._xth_weekday: weekday, occurrence = get_weekday_occurrence(self._startdt) - rrule['byday'] = [f'{occurrence}{WEEKDAYS[weekday]}'] - if self.until_choice.active == 'Until': + rrule["byday"] = [f"{occurrence}{WEEKDAYS[weekday]}"] + if self.until_choice.active == "Until": if isinstance(self._startdt, dt.datetime): - rrule['until'] = dt.datetime.combine( + rrule["until"] = dt.datetime.combine( self.until_edit.date, self._startdt.time(), ) else: - rrule['until'] = self.until_edit.date - elif self.until_choice.active == 'Repetitions': - rrule['count'] = int(self.repetitions_edit.get_edit_text()) + rrule["until"] = self.until_edit.date + elif self.until_choice.active == "Repetitions": + rrule["count"] = int(self.repetitions_edit.get_edit_text()) return rrule @property @@ -850,14 +941,19 @@ def active(self, val): class ExportDialog(urwid.WidgetWrap): def __init__(self, this_func, abort_func, event) -> None: lines = [] - lines.append(urwid.Text('Export event as ICS file')) - lines.append(urwid.Text('')) + lines.append(urwid.Text("Export event as ICS file")) + lines.append(urwid.Text("")) export_location = ExtendedEdit( - caption='Location: ', edit_text=f"~/{event.summary.strip()}.ics") + caption="Location: ", edit_text=f"~/{event.summary.strip()}.ics" + ) lines.append(export_location) - lines.append(urwid.Divider(' ')) - lines.append(CAttrMap( - urwid.Button('Save', on_press=this_func, user_data=export_location), - 'button', 'button focus')) + lines.append(urwid.Divider(" ")) + lines.append( + CAttrMap( + urwid.Button("Save", on_press=this_func, user_data=export_location), + "button", + "button focus", + ) + ) content = NPile(lines) urwid.WidgetWrap.__init__(self, urwid.LineBox(content)) diff --git a/khal/ui/widgets.py b/khal/ui/widgets.py index 5dd794309..c1af51260 100644 --- a/khal/ui/widgets.py +++ b/khal/ui/widgets.py @@ -24,6 +24,7 @@ Widgets that are specific to calendaring/khal should go into __init__.py or, if they are large, into their own files """ + import datetime as dt import re @@ -39,24 +40,24 @@ def delete_last_word(text, number=1): words = re.findall(r"[\w]+|[^\w\s]", text, re.UNICODE) for one in range(1, number + 1): text = text.rstrip() - if text == '': + if text == "": return text - text = text[:len(text) - len(words[-one])] + text = text[: len(text) - len(words[-one])] return text def delete_till_beginning_of_line(text): """delete till beginning of line""" if text.rfind("\n") == -1: - return '' - return text[0:text.rfind("\n") + 1] + return "" + return text[0 : text.rfind("\n") + 1] def delete_till_end_of_line(text): """delete till beginning of line""" if text.find("\n") == -1: - return '' - return text[text.find("\n"):] + return "" + return text[text.find("\n") :] def goto_beginning_of_line(text): @@ -75,15 +76,15 @@ class ExtendedEdit(urwid.Edit): """A text editing widget supporting some more editing commands""" def keypress(self, size: tuple[int], key: str | None) -> str | None: - if key == 'ctrl w': + if key == "ctrl w": self._delete_word() - elif key == 'ctrl u': + elif key == "ctrl u": self._delete_till_beginning_of_line() - elif key == 'ctrl k': + elif key == "ctrl k": self._delete_till_end_of_line() - elif key == 'ctrl a': + elif key == "ctrl a": self._goto_beginning_of_line() - elif key == 'ctrl e': + elif key == "ctrl e": self._goto_end_of_line() else: return super().keypress(size, key) @@ -93,52 +94,51 @@ def keypress(self, size: tuple[int], key: str | None) -> str | None: def _delete_word(self): """delete word before cursor""" text = self.get_edit_text() - f_text = delete_last_word(text[:self.edit_pos]) - self.set_edit_text(f_text + text[self.edit_pos:]) + f_text = delete_last_word(text[: self.edit_pos]) + self.set_edit_text(f_text + text[self.edit_pos :]) self.set_edit_pos(len(f_text)) def _delete_till_beginning_of_line(self): """delete till start of line before cursor""" text = self.get_edit_text() - f_text = delete_till_beginning_of_line(text[:self.edit_pos]) - self.set_edit_text(f_text + text[self.edit_pos:]) + f_text = delete_till_beginning_of_line(text[: self.edit_pos]) + self.set_edit_text(f_text + text[self.edit_pos :]) self.set_edit_pos(len(f_text)) def _delete_till_end_of_line(self): """delete till end of line before cursor""" text = self.get_edit_text() - f_text = delete_till_end_of_line(text[self.edit_pos:]) - self.set_edit_text(text[:self.edit_pos] + f_text) + f_text = delete_till_end_of_line(text[self.edit_pos :]) + self.set_edit_text(text[: self.edit_pos] + f_text) def _goto_beginning_of_line(self): text = self.get_edit_text() - self.set_edit_pos(goto_beginning_of_line(text[:self.edit_pos])) + self.set_edit_pos(goto_beginning_of_line(text[: self.edit_pos])) def _goto_end_of_line(self): text = self.get_edit_text() - self.set_edit_pos(goto_end_of_line(text[self.edit_pos:]) + self.edit_pos) + self.set_edit_pos(goto_end_of_line(text[self.edit_pos :]) + self.edit_pos) class DateTimeWidget(ExtendedEdit): - def __init__(self, dateformat: str, on_date_change=lambda x: None, **kwargs) -> None: self.dateformat = dateformat self.on_date_change = on_date_change - super().__init__(wrap='any', **kwargs) + super().__init__(wrap="any", **kwargs) def keypress(self, size, key): - if key == 'ctrl x': + if key == "ctrl x": self.decrease() return None - elif key == 'ctrl a': + elif key == "ctrl a": self.increase() return None if ( - key in ['up', 'down', 'tab', 'shift tab', - 'page up', 'page down', 'meta enter'] or - (key in ['right'] and self.edit_pos >= len(self.edit_text)) or - (key in ['left'] and self.edit_pos == 0)): + key in ["up", "down", "tab", "shift tab", "page up", "page down", "meta enter"] + or (key in ["right"] and self.edit_pos >= len(self.edit_text)) + or (key in ["left"] and self.edit_pos == 0) + ): # when leaving the current Widget we check if currently # entered value is valid and if so pass the new value try: @@ -199,8 +199,12 @@ def _get_current_value(self): class Choice(urwid.PopUpLauncher): def __init__( - self, choices: list[str], active: str, - decorate_func=None, overlay_width: int=32, callback=lambda: None, + self, + choices: list[str], + active: str, + decorate_func=None, + overlay_width: int = 32, + callback=lambda: None, ) -> None: self.choices = choices self._callback = callback @@ -210,14 +214,16 @@ def __init__( def create_pop_up(self): pop_up = ChoiceList(self, callback=self._callback) - urwid.connect_signal(pop_up, 'close', lambda button: self.close_pop_up()) + urwid.connect_signal(pop_up, "close", lambda button: self.close_pop_up()) return pop_up def get_pop_up_parameters(self): - return {'left': 0, - 'top': 1, - 'overlay_width': self._overlay_width, - 'overlay_height': len(self.choices)} + return { + "left": 0, + "top": 1, + "overlay_width": self._overlay_width, + "overlay_height": len(self.choices), + } @property def changed(self): @@ -232,12 +238,13 @@ def active(self, val): self._active = val self.button = urwid.Button(self._decorate(self._active)) urwid.PopUpLauncher.__init__(self, self.button) - urwid.connect_signal(self.button, 'click', lambda button: self.open_pop_up()) + urwid.connect_signal(self.button, "click", lambda button: self.open_pop_up()) class ChoiceList(urwid.WidgetWrap): """A pile of Button() widgets, intended to be used with Choice()""" - signals = ['close'] + + signals = ["close"] def __init__(self, parent, callback=lambda: None) -> None: self.parent = parent @@ -247,8 +254,8 @@ def __init__(self, parent, callback=lambda: None) -> None: buttons.append( button( parent._decorate(c), - attr_map='popupbg', - focus_map='popupbg focus', + attr_map="popupbg", + focus_map="popupbg focus", on_press=self.set_choice, user_data=c, ) @@ -258,7 +265,7 @@ def __init__(self, parent, callback=lambda: None) -> None: num = [num for num, elem in enumerate(parent.choices) if elem == parent.active][0] pile.focus_position = num fill = urwid.Filler(pile) - urwid.WidgetWrap.__init__(self, urwid.AttrMap(fill, 'popupbg')) + urwid.WidgetWrap.__init__(self, urwid.AttrMap(fill, "popupbg")) def set_choice(self, button, account): self.parent.active = account @@ -273,9 +280,9 @@ class SupportsNext: """ def __init__(self, *args, **kwargs) -> None: - self.outermost = kwargs.get('outermost', False) - if 'outermost' in kwargs: - kwargs.pop('outermost') + self.outermost = kwargs.get("outermost", False) + if "outermost" in kwargs: + kwargs.pop("outermost") super().__init__(*args, **kwargs) @@ -305,7 +312,7 @@ def _first_selectable(self): def _last_selectable(self): """return sequence number of self._contents last selectable item""" - for j in range(len(self._contents) - 1, - 1, - 1): + for j in range(len(self._contents) - 1, -1, -1): if self._contents[j][0].selectable(): return j return False @@ -313,7 +320,7 @@ def _last_selectable(self): def keypress(self, size, key): key = super().keypress(size, key) - if key == 'tab': + if key == "tab": if self.outermost and self.focus_position == self._last_selectable(): self._select_first_selectable() else: @@ -325,7 +332,7 @@ def keypress(self, size, key): break else: # no break return key - elif key == 'shift tab': + elif key == "shift tab": if self.outermost and self.focus_position == self._first_selectable(): self._select_last_selectable() else: @@ -373,19 +380,19 @@ def _first_selectable(self): def _last_selectable(self): """return sequence number of self.contents last selectable item""" - for j in range(len(self.body) - 1, - 1, - 1): + for j in range(len(self.body) - 1, -1, -1): if self.body[j].selectable(): return j return False def keypress(self, size, key): key = super().keypress(size, key) - if key == 'tab': + if key == "tab": if self.outermost and self.focus_position == self._last_selectable(): self._select_first_selectable() else: self._keypress_down(size) - elif key == 'shift tab': + elif key == "shift tab": if self.outermost and self.focus_position == self._first_selectable(): self._select_last_selectable() else: @@ -398,7 +405,7 @@ class ValidatedEdit(urwid.WidgetWrap): def __init__(self, *args, EditWidget=ExtendedEdit, validate=False, **kwargs) -> None: assert validate self._validate_func = validate - self._original_widget = urwid.AttrMap(EditWidget(*args, **kwargs), 'edit', 'edit focused') + self._original_widget = urwid.AttrMap(EditWidget(*args, **kwargs), "edit", "edit focused") super().__init__(self._original_widget) @property @@ -412,12 +419,12 @@ def base_widget(self): def _validate(self): text = self.base_widget.get_edit_text() if self._validate_func(text): - self._original_widget.set_attr_map({None: 'edit'}) - self._original_widget.set_focus_map({None: 'edit'}) + self._original_widget.set_attr_map({None: "edit"}) + self._original_widget.set_focus_map({None: "edit"}) return True else: - self._original_widget.set_attr_map({None: 'alert'}) - self._original_widget.set_focus_map({None: 'alert'}) + self._original_widget.set_attr_map({None: "alert"}) + self._original_widget.set_focus_map({None: "alert"}) return False def get_edit_text(self): @@ -434,10 +441,10 @@ def edit_text(self): def keypress(self, size, key): if ( - key in ['up', 'down', 'tab', 'shift tab', - 'page up', 'page down', 'meta enter'] or - (key in ['right'] and self.edit_pos >= len(self.edit_text)) or - (key in ['left'] and self.edit_pos == 0)): + key in ["up", "down", "tab", "shift tab", "page up", "page down", "meta enter"] + or (key in ["right"] and self.edit_pos >= len(self.edit_text)) + or (key in ["left"] and self.edit_pos == 0) + ): if not self._validate(): return return super().keypress(size, key) @@ -458,7 +465,6 @@ def _unsigned_int(number): class DurationWidget(urwid.WidgetWrap): - @staticmethod def unsigned_int(number): """test if `number` can be converted to a positive int""" @@ -480,65 +486,72 @@ def __init__(self, timedelta: dt.timedelta) -> None: days, hours, minutes, seconds = self._convert_timedelta(timedelta) self.days_edit = ValidatedEdit( - edit_text=str(days), validate=self.unsigned_int, align='right') + edit_text=str(days), validate=self.unsigned_int, align="right" + ) self.hours_edit = ValidatedEdit( - edit_text=str(hours), validate=self.unsigned_int, align='right') + edit_text=str(hours), validate=self.unsigned_int, align="right" + ) self.minutes_edit = ValidatedEdit( - edit_text=str(minutes), validate=self.unsigned_int, align='right') + edit_text=str(minutes), validate=self.unsigned_int, align="right" + ) self.seconds_edit = ValidatedEdit( - edit_text=str(seconds), validate=self.unsigned_int, align='right') - - self.columns = NColumns([ - (4, self.days_edit), - (2, urwid.Text('D')), - (3, self.hours_edit), - (2, urwid.Text('H')), - (3, self.minutes_edit), - (2, urwid.Text('M')), - (3, self.seconds_edit), - (2, urwid.Text('S')), - ]) + edit_text=str(seconds), validate=self.unsigned_int, align="right" + ) + + self.columns = NColumns( + [ + (4, self.days_edit), + (2, urwid.Text("D")), + (3, self.hours_edit), + (2, urwid.Text("H")), + (3, self.minutes_edit), + (2, urwid.Text("M")), + (3, self.seconds_edit), + (2, urwid.Text("S")), + ] + ) urwid.WidgetWrap.__init__(self, self.columns) def get_timedelta(self) -> dt.timedelta: return dt.timedelta( - seconds=int(self.seconds_edit.get_edit_text()) + - int(self.minutes_edit.get_edit_text()) * 60 + - int(self.hours_edit.get_edit_text()) * 60 * 60 + - int(self.days_edit.get_edit_text()) * 24 * 60 * 60) + seconds=int(self.seconds_edit.get_edit_text()) + + int(self.minutes_edit.get_edit_text()) * 60 + + int(self.hours_edit.get_edit_text()) * 60 * 60 + + int(self.days_edit.get_edit_text()) * 24 * 60 * 60 + ) class AlarmsEditor(urwid.WidgetWrap): - class AlarmEditor(urwid.WidgetWrap): - def __init__(self, alarm: tuple[dt.timedelta, str], delete_handler) -> None: duration, description = alarm if duration.total_seconds() > 0: - direction = 'after' + direction = "after" else: - direction = 'before' + direction = "before" duration = -1 * duration self.duration = DurationWidget(duration) self.description = ExtendedEdit( - edit_text=description if description is not None else "") - self.direction = Choice( - ['before', 'after'], active=direction, overlay_width=10) - self.columns = NColumns([ - (2, urwid.Text(' ')), - (21, self.duration), - (14, urwid.Padding(self.direction, right=1)), - self.description, - (10, button('Delete', on_press=delete_handler, user_data=self)), - ]) + edit_text=description if description is not None else "" + ) + self.direction = Choice(["before", "after"], active=direction, overlay_width=10) + self.columns = NColumns( + [ + (2, urwid.Text(" ")), + (21, self.duration), + (14, urwid.Padding(self.direction, right=1)), + self.description, + (10, button("Delete", on_press=delete_handler, user_data=self)), + ] + ) urwid.WidgetWrap.__init__(self, self.columns) def get_alarm(self): direction = self.direction.active - if direction == 'before': + if direction == "before": prefix = -1 else: prefix = 1 @@ -548,13 +561,13 @@ def __init__(self, event) -> None: self.event = event self.pile = NPile( - [urwid.Text('Alarms:')] + - [self.AlarmEditor(a, self.remove_alarm) for a in event.alarms] + - [urwid.Columns([(12, button('Add', on_press=self.add_alarm))])]) + [urwid.Text("Alarms:")] + + [self.AlarmEditor(a, self.remove_alarm) for a in event.alarms] + + [urwid.Columns([(12, button("Add", on_press=self.add_alarm))])] + ) urwid.WidgetWrap.__init__(self, self.pile) - def clear(self) -> None: """clear the alarm list""" self.pile.contents.clear() @@ -564,11 +577,11 @@ def add_alarm(self, button, timedelta: dt.timedelta | None = None): timedelta = dt.timedelta(0) self.pile.contents.insert( len(self.pile.contents) - 1, - (self.AlarmEditor((timedelta, self.event.summary), self.remove_alarm), - ('weight', 1))) + (self.AlarmEditor((timedelta, self.event.summary), self.remove_alarm), ("weight", 1)), + ) def remove_alarm(self, button, editor): - self.pile.contents.remove((editor, ('weight', 1))) + self.pile.contents.remove((editor, ("weight", 1))) def get_alarms(self): alarms = [] @@ -589,36 +602,44 @@ class FocusLineBoxWidth(urwid.WidgetDecoration, urwid.WidgetWrap): def __init__(self, widget) -> None: # we cheat here with the attrs, if we use thick dividers we apply the # focus attr group. We probably should fix this in render() - hline = urwid.AttrMap(urwid.Divider('─'), 'frame') - hline_focus = urwid.AttrMap(urwid.Divider('━'), 'frame focus') - self._vline = urwid.AttrMap(urwid.SolidFill('│'), 'frame') - self._vline_focus = urwid.AttrMap(urwid.SolidFill('┃'), 'frame focus') - self._topline = urwid.Columns([ - ('fixed', 1, urwid.AttrMap(urwid.Text('┌'), 'frame')), - hline, - ('fixed', 1, urwid.AttrMap(urwid.Text('┐'), 'frame')), - ]) - self._topline_focus = urwid.Columns([ - ('fixed', 1, urwid.AttrMap(urwid.Text('┏'), 'frame focus')), - hline_focus, - ('fixed', 1, urwid.AttrMap(urwid.Text('┓'), 'frame focus')), - ]) - self._bottomline = urwid.Columns([ - ('fixed', 1, urwid.AttrMap(urwid.Text('└'), 'frame')), - hline, - ('fixed', 1, urwid.AttrMap(urwid.Text('┘'), 'frame')), - ]) - self._bottomline_focus = urwid.Columns([ - ('fixed', 1, urwid.AttrMap(urwid.Text('┗'), 'frame focus')), - hline_focus, - ('fixed', 1, urwid.AttrMap(urwid.Text('┛'), 'frame focus')), - ]) + hline = urwid.AttrMap(urwid.Divider("─"), "frame") + hline_focus = urwid.AttrMap(urwid.Divider("━"), "frame focus") + self._vline = urwid.AttrMap(urwid.SolidFill("│"), "frame") + self._vline_focus = urwid.AttrMap(urwid.SolidFill("┃"), "frame focus") + self._topline = urwid.Columns( + [ + ("fixed", 1, urwid.AttrMap(urwid.Text("┌"), "frame")), + hline, + ("fixed", 1, urwid.AttrMap(urwid.Text("┐"), "frame")), + ] + ) + self._topline_focus = urwid.Columns( + [ + ("fixed", 1, urwid.AttrMap(urwid.Text("┏"), "frame focus")), + hline_focus, + ("fixed", 1, urwid.AttrMap(urwid.Text("┓"), "frame focus")), + ] + ) + self._bottomline = urwid.Columns( + [ + ("fixed", 1, urwid.AttrMap(urwid.Text("└"), "frame")), + hline, + ("fixed", 1, urwid.AttrMap(urwid.Text("┘"), "frame")), + ] + ) + self._bottomline_focus = urwid.Columns( + [ + ("fixed", 1, urwid.AttrMap(urwid.Text("┗"), "frame focus")), + hline_focus, + ("fixed", 1, urwid.AttrMap(urwid.Text("┛"), "frame focus")), + ] + ) self._middle = urwid.Columns( - [('fixed', 1, self._vline), widget, ('fixed', 1, self._vline)], + [("fixed", 1, self._vline), widget, ("fixed", 1, self._vline)], focus_column=1, ) self._all = urwid.Pile( - [('flow', self._topline), self._middle, ('flow', self._bottomline)], + [("flow", self._topline), self._middle, ("flow", self._bottomline)], focus_item=1, ) @@ -628,43 +649,49 @@ def __init__(self, widget) -> None: def render(self, size, focus): inner = self._all.contents[1][0] if focus: - self._all.contents[0] = (self._topline_focus, ('pack', None)) - inner.contents[0] = (self._vline_focus, ('given', 1, False)) - inner.contents[2] = (self._vline_focus, ('given', 1, False)) - self._all.contents[2] = (self._bottomline_focus, ('pack', None)) + self._all.contents[0] = (self._topline_focus, ("pack", None)) + inner.contents[0] = (self._vline_focus, ("given", 1, False)) + inner.contents[2] = (self._vline_focus, ("given", 1, False)) + self._all.contents[2] = (self._bottomline_focus, ("pack", None)) else: - self._all.contents[0] = (self._topline, ('pack', None)) - inner.contents[0] = (self._vline, ('given', 1, False)) - inner.contents[2] = (self._vline, ('given', 1, False)) - self._all.contents[2] = (self._bottomline, ('pack', None)) + self._all.contents[0] = (self._topline, ("pack", None)) + inner.contents[0] = (self._vline, ("given", 1, False)) + inner.contents[2] = (self._vline, ("given", 1, False)) + self._all.contents[2] = (self._bottomline, ("pack", None)) return super().render(size, focus) class FocusLineBoxColor(urwid.WidgetDecoration, urwid.WidgetWrap): def __init__(self, widget) -> None: - hline = urwid.Divider('─') - self._vline = urwid.AttrMap(urwid.SolidFill('│'), 'frame') + hline = urwid.Divider("─") + self._vline = urwid.AttrMap(urwid.SolidFill("│"), "frame") self._topline = urwid.AttrMap( - urwid.Columns([ - ('fixed', 1, urwid.Text('┌')), - hline, - ('fixed', 1, urwid.Text('┐')), - ]), - 'frame') + urwid.Columns( + [ + ("fixed", 1, urwid.Text("┌")), + hline, + ("fixed", 1, urwid.Text("┐")), + ] + ), + "frame", + ) self._bottomline = urwid.AttrMap( - urwid.Columns([ - ('fixed', 1, urwid.Text('└')), - hline, - ('fixed', 1, urwid.Text('┘')), - ]), - 'frame') + urwid.Columns( + [ + ("fixed", 1, urwid.Text("└")), + hline, + ("fixed", 1, urwid.Text("┘")), + ] + ), + "frame", + ) self._middle = urwid.Columns( - [('fixed', 1, self._vline), widget, ('fixed', 1, self._vline)], + [("fixed", 1, self._vline), widget, ("fixed", 1, self._vline)], focus_column=1, ) self._all = urwid.Pile( - [('flow', self._topline), self._middle, ('flow', self._bottomline)], + [("flow", self._topline), self._middle, ("flow", self._bottomline)], focus_item=1, ) @@ -673,42 +700,47 @@ def __init__(self, widget) -> None: def render(self, size, focus): if focus: - self._middle.contents[0][0].set_attr_map({None: 'frame focus color'}) - self._all.contents[0][0].set_attr_map({None: 'frame focus color'}) - self._all.contents[2][0].set_attr_map({None: 'frame focus color'}) + self._middle.contents[0][0].set_attr_map({None: "frame focus color"}) + self._all.contents[0][0].set_attr_map({None: "frame focus color"}) + self._all.contents[2][0].set_attr_map({None: "frame focus color"}) else: - self._middle.contents[0][0].set_attr_map({None: 'frame'}) - self._all.contents[0][0].set_attr_map({None: 'frame'}) - self._all.contents[2][0].set_attr_map({None: 'frame'}) + self._middle.contents[0][0].set_attr_map({None: "frame"}) + self._all.contents[0][0].set_attr_map({None: "frame"}) + self._all.contents[2][0].set_attr_map({None: "frame"}) return super().render(size, focus) class FocusLineBoxTop(urwid.WidgetDecoration, urwid.WidgetWrap): def __init__(self, widget) -> None: - topline = urwid.AttrMap(urwid.Divider('━'), 'frame') - self._all = urwid.Pile([('flow', topline), widget], focus_item=1) + topline = urwid.AttrMap(urwid.Divider("━"), "frame") + self._all = urwid.Pile([("flow", topline), widget], focus_item=1) urwid.WidgetWrap.__init__(self, self._all) urwid.WidgetDecoration.__init__(self, widget) def render(self, size, focus): if focus: - self._all.contents[0][0].set_attr_map({None: 'frame focus top'}) + self._all.contents[0][0].set_attr_map({None: "frame focus top"}) else: - self._all.contents[0][0].set_attr_map({None: 'frame'}) + self._all.contents[0][0].set_attr_map({None: "frame"}) return super().render(size, focus) linebox = { - 'color': FocusLineBoxColor, - 'top': FocusLineBoxTop, - 'width': FocusLineBoxWidth, - 'False': urwid.WidgetPlaceholder, + "color": FocusLineBoxColor, + "top": FocusLineBoxTop, + "width": FocusLineBoxWidth, + "False": urwid.WidgetPlaceholder, } -def button(*args, - attr_map: str='button', focus_map='button focus', - padding_left=0, padding_right=0, - **kwargs): + +def button( + *args, + attr_map: str = "button", + focus_map="button focus", + padding_left=0, + padding_right=0, + **kwargs, +): """wrapping an urwid button in attrmap and padding""" button_ = urwid.Button(*args, **kwargs) button_ = urwid.AttrMap(button_, attr_map=attr_map, focus_map=focus_map) @@ -718,12 +750,14 @@ def button(*args, class CAttrMap(urwid.AttrMap): """A variant of AttrMap that exposes all properties of the original widget""" + def __getattr__(self, name): return getattr(self.original_widget, name) class CPadding(urwid.Padding): """A variant of Patting that exposes some properties of the original widget""" + @property def active(self): return self.original_widget.active diff --git a/khal/utils.py b/khal/utils.py index da948c3f3..1a8af49cd 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -21,7 +21,6 @@ """collection of utility functions""" - import datetime as dt import json import random @@ -46,16 +45,18 @@ def generate_random_uid() -> str: when random isn't broken, getting a random UID from a pool of roughly 10^56 should be good enough""" choice = string.ascii_uppercase + string.digits - return ''.join([random.choice(choice) for _ in range(36)]) + return "".join([random.choice(choice) for _ in range(36)]) -RESET = '\x1b[0m' +RESET = "\x1b[0m" -ansi_reset = re.compile(r'\x1b\[0m') -ansi_sgr = re.compile(r'\x1b\[' - '(?!0m)' # negative lookahead, don't match 0m - '([0-9]+;?)+' - 'm') +ansi_reset = re.compile(r"\x1b\[0m") +ansi_sgr = re.compile( + r"\x1b\[" + "(?!0m)" # negative lookahead, don't match 0m + "([0-9]+;?)+" + "m" +) def find_last_reset(string: str) -> tuple[int, int, str]: @@ -63,7 +64,7 @@ def find_last_reset(string: str) -> tuple[int, int, str]: for m in re.finditer(ansi_reset, string): last = m if last is None: - return -2, -1, '' + return -2, -1, "" return last.start(), last.end(), last.group(0) @@ -72,7 +73,7 @@ def find_last_sgr(string: str) -> tuple[int, int, str]: for m in re.finditer(ansi_sgr, string): last = m if last is None: - return -2, -1, '' + return -2, -1, "" return last.start(), last.end(), last.group(0) @@ -122,7 +123,7 @@ def get_month_abbr_len() -> int: def localize_strip_tz(dates: list[dt.datetime], timezone: dt.tzinfo) -> Iterator[dt.datetime]: """converts a list of dates to timezone, than removes tz info""" for one_date in dates: - if getattr(one_date, 'tzinfo', None) is not None: + if getattr(one_date, "tzinfo", None) is not None: one_date = one_date.astimezone(timezone) one_date = one_date.replace(tzinfo=None) yield one_date @@ -130,7 +131,7 @@ def localize_strip_tz(dates: list[dt.datetime], timezone: dt.tzinfo) -> Iterator def to_unix_time(dtime: dt.datetime) -> float: """convert a datetime object to unix time in UTC (as a float)""" - if getattr(dtime, 'tzinfo', None) is not None: + if getattr(dtime, "tzinfo", None) is not None: dtime = dtime.astimezone(pytz.UTC) unix_time = timegm(dtime.timetuple()) return unix_time @@ -140,7 +141,7 @@ def to_naive_utc(dtime: dt.datetime) -> dt.datetime: """convert a datetime object to UTC and than remove the tzinfo, if datetime is naive already, return it """ - if not hasattr(dtime, 'tzinfo') or dtime.tzinfo is None: + if not hasattr(dtime, "tzinfo") or dtime.tzinfo is None: return dtime dtime_utc = dtime.astimezone(pytz.UTC) @@ -154,31 +155,30 @@ def is_aware(dtime: dt.datetime) -> bool: def relative_timedelta_str(day: dt.date) -> str: - """Converts the timespan from `day` to today into a human readable string. - """ + """Converts the timespan from `day` to today into a human readable string.""" days = (day - dt.date.today()).days if days < 0: - direction = 'ago' + direction = "ago" else: - direction = 'from now' - approx = '' + direction = "from now" + approx = "" if abs(days) < 7: - unit = 'day' + unit = "day" count = abs(days) elif abs(days) < 365: - unit = 'week' + unit = "week" count = int(abs(days) / 7) if abs(days) % 7 != 0: - approx = '~' + approx = "~" else: - unit = 'year' + unit = "year" count = int(abs(days) / 365) if abs(days) % 365 != 0: - approx = '~' + approx = "~" if count > 1: - unit += 's' + unit += "s" - return f'{approx}{count} {unit} {direction}' + return f"{approx}{count} {unit} {direction}" def get_wrapped_text(widget: urwid.AttrMap) -> str: @@ -187,19 +187,20 @@ def get_wrapped_text(widget: urwid.AttrMap) -> str: def human_formatter(format_string, width=None, colors=True): """Create a formatter that formats events to be human readable.""" + def fmt(rows): single = isinstance(rows, dict) if single: rows = [rows] results = [] for row in rows: - if 'calendar-color' in row: - row['calendar-color'] = get_color(row['calendar-color']) + if "calendar-color" in row: + row["calendar-color"] = get_color(row["calendar-color"]) s = format_string.format(**row) if colors: - s += style('', reset=True) + s += style("", reset=True) if width: results += color_wrap(s, width) @@ -209,24 +210,60 @@ def fmt(rows): return results[0] else: return results + return fmt -CONTENT_ATTRIBUTES = ['start', 'start-long', 'start-date', 'start-date-long', - 'start-time', 'end', 'end-long', 'end-date', 'end-date-long', 'end-time', - 'duration', 'start-full', 'start-long-full', 'start-date-full', - 'start-date-long-full', 'start-time-full', 'end-full', 'end-long-full', - 'end-date-full', 'end-date-long-full', 'end-time-full', 'duration-full', - 'start-style', 'end-style', 'to-style', 'start-end-time-style', - 'end-necessary', 'end-necessary-long', 'repeat-symbol', 'repeat-pattern', - 'title', 'organizer', 'description', 'location', 'all-day', 'categories', - 'uid', 'url', 'calendar', 'calendar-color', 'status', 'cancelled'] +CONTENT_ATTRIBUTES = [ + "start", + "start-long", + "start-date", + "start-date-long", + "start-time", + "end", + "end-long", + "end-date", + "end-date-long", + "end-time", + "duration", + "start-full", + "start-long-full", + "start-date-full", + "start-date-long-full", + "start-time-full", + "end-full", + "end-long-full", + "end-date-full", + "end-date-long-full", + "end-time-full", + "duration-full", + "start-style", + "end-style", + "to-style", + "start-end-time-style", + "end-necessary", + "end-necessary-long", + "repeat-symbol", + "repeat-pattern", + "title", + "organizer", + "description", + "location", + "all-day", + "categories", + "uid", + "url", + "calendar", + "calendar-color", + "status", + "cancelled", +] def json_formatter(fields): """Create a formatter that formats events in JSON.""" - if len(fields) == 1 and fields[0] == 'all': + if len(fields) == 1 and fields[0] == "all": fields = CONTENT_ATTRIBUTES def fmt(rows): @@ -238,11 +275,11 @@ def fmt(rows): for row in rows: f = dict(filter(lambda e: e[0] in fields and e[0] in CONTENT_ATTRIBUTES, row.items())) - if f.get('repeat-symbol', '') != '': + if f.get("repeat-symbol", "") != "": f["repeat-symbol"] = f["repeat-symbol"].strip() - if f.get('status', '') != '': + if f.get("status", "") != "": f["status"] = f["status"].strip() - if f.get('cancelled', '') != '': + if f.get("cancelled", "") != "": f["cancelled"] = f["cancelled"].strip() filtered.append(f) @@ -253,6 +290,7 @@ def fmt(rows): return results[0] else: return results + return fmt @@ -268,7 +306,7 @@ def str2alarm(alarms: str, description: str) -> Iterator[icalendar.Alarm]: """convert a comma separated list of alarm strings to icalendar.Alarm""" for alarm_trig in alarmstr2trigger(alarms): new_alarm = icalendar.Alarm() - new_alarm.add('ACTION', 'DISPLAY') - new_alarm.add('TRIGGER', alarm_trig) - new_alarm.add('DESCRIPTION', description) + new_alarm.add("ACTION", "DISPLAY") + new_alarm.add("TRIGGER", alarm_trig) + new_alarm.add("DESCRIPTION", description) yield new_alarm diff --git a/tests/backend_test.py b/tests/backend_test.py index 31a8e5c49..4f0207e21 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -9,26 +9,26 @@ from .utils import BERLIN, LOCALE_BERLIN, _get_text -calname = 'home' +calname = "home" def test_new_db_version(): - dbi = backend.SQLiteDb(calname, ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb(calname, ":memory:", locale=LOCALE_BERLIN) backend.DB_VERSION += 1 with pytest.raises(OutdatedDbVersionError): dbi._check_table_version() def test_event_rrule_recurrence_id(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert dbi.list(calname) == [] events = dbi.get_localized( BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)), ) assert list(events) == [] - dbi.update(_get_text('event_rrule_recuid'), href='12345.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('12345.ics', 'abcd')] + dbi.update(_get_text("event_rrule_recuid"), href="12345.ics", etag="abcd", calendar=calname) + assert dbi.list(calname) == [("12345.ics", "abcd")] events = dbi.get_localized( BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)), @@ -53,12 +53,17 @@ def test_event_rrule_recurrence_id(): def test_event_rrule_recurrence_id_invalid_tzid(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(_get_text('event_rrule_recuid_invalid_tzid'), href='12345.ics', etag='abcd', - calendar=calname) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + _get_text("event_rrule_recuid_invalid_tzid"), + href="12345.ics", + etag="abcd", + calendar=calname, + ) events = dbi.get_localized( BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) events = sorted(events, key=itemgetter(2)) assert len(events) == 6 @@ -94,17 +99,19 @@ def test_event_rrule_recurrence_id_reverse(): """as icalendar elements can be saved in arbitrary order, we also have to deal with `reverse` ordered icalendar files """ - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert dbi.list(calname) == [] events = dbi.get_localized( BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0))) + BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)), + ) assert list(events) == [] - dbi.update(event_rrule_recurrence_id_reverse, href='12345.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('12345.ics', 'abcd')] + dbi.update(event_rrule_recurrence_id_reverse, href="12345.ics", etag="abcd", calendar=calname) + assert dbi.list(calname) == [("12345.ics", "abcd")] events = dbi.get_localized( BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0))) + BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)), + ) events = sorted(events, key=itemgetter(2)) assert len(events) == 6 @@ -121,12 +128,15 @@ def test_event_rrule_recurrence_id_update_with_exclude(): test if updates work as they should. The updated event has the extra RECURRENCE-ID event removed and one recurrence date excluded via EXDATE """ - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(_get_text('event_rrule_recuid'), href='12345.ics', etag='abcd', calendar=calname) - dbi.update(_get_text('event_rrule_recuid_update'), - href='12345.ics', etag='abcd', calendar=calname) - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update(_get_text("event_rrule_recuid"), href="12345.ics", etag="abcd", calendar=calname) + dbi.update( + _get_text("event_rrule_recuid_update"), href="12345.ics", etag="abcd", calendar=calname + ) + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) events = sorted(events, key=itemgetter(2)) assert len(events) == 5 assert events[0][2] == BERLIN.localize(dt.datetime(2014, 6, 30, 7, 0)) @@ -141,17 +151,19 @@ def test_event_recuid_no_master(): test for events which have a RECUID component, but the master event is not present in the same file """ - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(_get_text('event_dt_recuid_no_master'), - href='12345.ics', etag='abcd', calendar=calname) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + _get_text("event_dt_recuid_no_master"), href="12345.ics", etag="abcd", calendar=calname + ) events = dbi.get_floating( - dt.datetime(2017, 3, 1, 0, 0), dt.datetime(2017, 4, 1, 0, 0), + dt.datetime(2017, 3, 1, 0, 0), + dt.datetime(2017, 4, 1, 0, 0), ) events = sorted(events, key=itemgetter(2)) assert len(events) == 1 assert events[0][2] == dt.datetime(2017, 3, 29, 16) assert events[0][3] == dt.datetime(2017, 3, 29, 16, 25) - assert 'SUMMARY:Infrastructure Planning' in events[0][0] + assert "SUMMARY:Infrastructure Planning" in events[0][0] def test_event_recuid_rrule_no_master(): @@ -159,13 +171,16 @@ def test_event_recuid_rrule_no_master(): test for events which have a RECUID and a RRULE component, but the master event is not present in the same file """ - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) dbi.update( - _get_text('event_dt_multi_recuid_no_master'), - href='12345.ics', etag='abcd', calendar=calname, + _get_text("event_dt_multi_recuid_no_master"), + href="12345.ics", + etag="abcd", + calendar=calname, ) events = dbi.get_floating( - dt.datetime(2010, 1, 1, 0, 0), dt.datetime(2020, 1, 1, 0, 0), + dt.datetime(2010, 1, 1, 0, 0), + dt.datetime(2020, 1, 1, 0, 0), ) events = sorted(events, key=itemgetter(2)) assert len(list(events)) == 2 @@ -173,16 +188,19 @@ def test_event_recuid_rrule_no_master(): assert events[0][3] == dt.datetime(2014, 6, 30, 12, 0) assert events[1][2] == dt.datetime(2014, 7, 7, 8, 30) assert events[1][3] == dt.datetime(2014, 7, 7, 12, 0) - events = dbi.search('VEVENT') + events = dbi.search("VEVENT") assert len(list(events)) == 2 def test_no_valid_timezone(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(_get_text('event_dt_local_missing_tz'), - href='12345.ics', etag='abcd', calendar=calname) - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 9, 0, 0)), - BERLIN.localize(dt.datetime(2014, 4, 10, 0, 0))) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + _get_text("event_dt_local_missing_tz"), href="12345.ics", etag="abcd", calendar=calname + ) + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 4, 9, 0, 0)), + BERLIN.localize(dt.datetime(2014, 4, 10, 0, 0)), + ) events = sorted(events) assert len(events) == 1 event = events[0] @@ -191,19 +209,25 @@ def test_no_valid_timezone(): def test_event_delete(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert dbi.list(calname) == [] - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0))) + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 8, 26, 0, 0)), + ) assert list(events) == [] - dbi.update(event_rrule_recurrence_id_reverse, href='12345.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('12345.ics', 'abcd')] - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + dbi.update(event_rrule_recurrence_id_reverse, href="12345.ics", etag="abcd", calendar=calname) + assert dbi.list(calname) == [("12345.ics", "abcd")] + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) assert len(list(events)) == 6 - dbi.delete('12345.ics', calendar=calname) - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + dbi.delete("12345.ics", calendar=calname) + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) assert len(list(events)) == 0 @@ -229,9 +253,9 @@ def test_event_delete(): def test_this_and_prior(): """we do not support THISANDPRIOR, therefore this should fail""" - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) with pytest.raises(UpdateFailed): - dbi.update(event_rrule_this_and_prior, href='12345.ics', etag='abcd', calendar=calname) + dbi.update(event_rrule_this_and_prior, href="12345.ics", etag="abcd", calendar=calname) event_rrule_this_and_future_temp = """ @@ -253,16 +277,19 @@ def test_this_and_prior(): END:VCALENDAR """ -event_rrule_this_and_future = \ - event_rrule_this_and_future_temp.format('20140707T090000', '20140707T180000') +event_rrule_this_and_future = event_rrule_this_and_future_temp.format( + "20140707T090000", "20140707T180000" +) def test_event_rrule_this_and_future(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(event_rrule_this_and_future, href='12345.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('12345.ics', 'abcd')] - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update(event_rrule_this_and_future, href="12345.ics", etag="abcd", calendar=calname) + assert dbi.list(calname) == [("12345.ics", "abcd")] + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) events = sorted(events, key=itemgetter(2)) assert len(events) == 6 @@ -280,22 +307,26 @@ def test_event_rrule_this_and_future(): assert events[4][3] == BERLIN.localize(dt.datetime(2014, 7, 28, 18, 0)) assert events[5][3] == BERLIN.localize(dt.datetime(2014, 8, 4, 18, 0)) - assert 'SUMMARY:Arbeit\n' in events[0][0] + assert "SUMMARY:Arbeit\n" in events[0][0] for event in events[1:]: - assert 'SUMMARY:Arbeit (lang)\n' in event[0] + assert "SUMMARY:Arbeit (lang)\n" in event[0] -event_rrule_this_and_future_multi_day_shift = \ - event_rrule_this_and_future_temp.format('20140708T090000', '20140709T150000') +event_rrule_this_and_future_multi_day_shift = event_rrule_this_and_future_temp.format( + "20140708T090000", "20140709T150000" +) def test_event_rrule_this_and_future_multi_day_shift(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(event_rrule_this_and_future_multi_day_shift, - href='12345.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('12345.ics', 'abcd')] - events = dbi.get_localized(BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0))) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + event_rrule_this_and_future_multi_day_shift, href="12345.ics", etag="abcd", calendar=calname + ) + assert dbi.list(calname) == [("12345.ics", "abcd")] + events = dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 4, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 9, 26, 0, 0)), + ) events = sorted(events, key=itemgetter(2)) assert len(events) == 6 @@ -313,9 +344,9 @@ def test_event_rrule_this_and_future_multi_day_shift(): assert events[4][3] == BERLIN.localize(dt.datetime(2014, 7, 30, 15, 0)) assert events[5][3] == BERLIN.localize(dt.datetime(2014, 8, 6, 15, 0)) - assert 'SUMMARY:Arbeit\n' in events[0][0] + assert "SUMMARY:Arbeit\n" in events[0][0] for event in events[1:]: - assert 'SUMMARY:Arbeit (lang)\n' in event[0] + assert "SUMMARY:Arbeit (lang)\n" in event[0] event_rrule_this_and_future_allday_temp = """ @@ -337,15 +368,20 @@ def test_event_rrule_this_and_future_multi_day_shift(): END:VCALENDAR """ -event_rrule_this_and_future_allday = \ - event_rrule_this_and_future_allday_temp.format(20140708, 20140709) +event_rrule_this_and_future_allday = event_rrule_this_and_future_allday_temp.format( + 20140708, 20140709 +) def test_event_rrule_this_and_future_allday(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(event_rrule_this_and_future_allday, - href='rrule_this_and_future_allday.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('rrule_this_and_future_allday.ics', 'abcd')] + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + event_rrule_this_and_future_allday, + href="rrule_this_and_future_allday.ics", + etag="abcd", + calendar=calname, + ) + assert dbi.list(calname) == [("rrule_this_and_future_allday.ics", "abcd")] events = list(dbi.get_floating(dt.datetime(2014, 4, 30, 0, 0), dt.datetime(2014, 9, 27, 0, 0))) assert len(events) == 6 @@ -363,18 +399,23 @@ def test_event_rrule_this_and_future_allday(): assert events[4][3] == dt.date(2014, 7, 30) assert events[5][3] == dt.date(2014, 8, 6) - assert 'SUMMARY:Arbeit\n' in events[0][0] + assert "SUMMARY:Arbeit\n" in events[0][0] for event in events[1:]: - assert 'SUMMARY:Arbeit (lang)\n' in event[0] + assert "SUMMARY:Arbeit (lang)\n" in event[0] def test_event_rrule_this_and_future_allday_prior(): - event_rrule_this_and_future_allday_prior = \ - event_rrule_this_and_future_allday_temp.format(20140705, 20140706) - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(event_rrule_this_and_future_allday_prior, - href='rrule_this_and_future_allday.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('rrule_this_and_future_allday.ics', 'abcd')] + event_rrule_this_and_future_allday_prior = event_rrule_this_and_future_allday_temp.format( + 20140705, 20140706 + ) + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + event_rrule_this_and_future_allday_prior, + href="rrule_this_and_future_allday.ics", + etag="abcd", + calendar=calname, + ) + assert dbi.list(calname) == [("rrule_this_and_future_allday.ics", "abcd")] events = list(dbi.get_floating(dt.datetime(2014, 4, 30, 0, 0), dt.datetime(2014, 9, 27, 0, 0))) assert len(events) == 6 @@ -393,9 +434,9 @@ def test_event_rrule_this_and_future_allday_prior(): assert events[4][3] == dt.date(2014, 7, 27) assert events[5][3] == dt.date(2014, 8, 3) - assert 'SUMMARY:Arbeit\n' in events[0][0] + assert "SUMMARY:Arbeit\n" in events[0][0] for event in events[1:]: - assert 'SUMMARY:Arbeit (lang)\n' in event[0] + assert "SUMMARY:Arbeit (lang)\n" in event[0] event_rrule_multi_this_and_future_allday = """BEGIN:VCALENDAR @@ -424,10 +465,14 @@ def test_event_rrule_this_and_future_allday_prior(): def test_event_rrule_multi_this_and_future_allday(): - dbi = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - dbi.update(event_rrule_multi_this_and_future_allday, - href='event_rrule_multi_this_and_future_allday.ics', etag='abcd', calendar=calname) - assert dbi.list(calname) == [('event_rrule_multi_this_and_future_allday.ics', 'abcd')] + dbi = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + dbi.update( + event_rrule_multi_this_and_future_allday, + href="event_rrule_multi_this_and_future_allday.ics", + etag="abcd", + calendar=calname, + ) + assert dbi.list(calname) == [("event_rrule_multi_this_and_future_allday.ics", "abcd")] events = sorted( dbi.get_floating(dt.datetime(2014, 4, 30, 0, 0), dt.datetime(2014, 9, 27, 0, 0)), ) @@ -447,11 +492,11 @@ def test_event_rrule_multi_this_and_future_allday(): assert events[4][3] == dt.date(2014, 7, 25) assert events[5][3] == dt.date(2014, 8, 1) - assert 'SUMMARY:Arbeit\n' in events[0][0] + assert "SUMMARY:Arbeit\n" in events[0][0] for event in [events[1], events[3]]: - assert 'SUMMARY:Arbeit (lang)\n' in event[0] + assert "SUMMARY:Arbeit (lang)\n" in event[0] for event in [events[2], events[4], events[5]]: - assert 'SUMMARY:Arbeit (neu)\n' in event[0] + assert "SUMMARY:Arbeit (neu)\n" in event[0] master = """BEGIN:VEVENT @@ -480,10 +525,12 @@ def test_event_rrule_multi_this_and_future_allday(): def test_calc_shift_deltas(): - assert (dt.timedelta(hours=2), dt.timedelta(hours=5)) == \ - backend.calc_shift_deltas(recuid_this_future) - assert (dt.timedelta(hours=2), dt.timedelta(hours=4, minutes=30)) == \ - backend.calc_shift_deltas(recuid_this_future_duration) + assert (dt.timedelta(hours=2), dt.timedelta(hours=5)) == backend.calc_shift_deltas( + recuid_this_future + ) + assert (dt.timedelta(hours=2), dt.timedelta(hours=4, minutes=30)) == backend.calc_shift_deltas( + recuid_this_future_duration + ) event_a = """BEGIN:VEVENT @@ -504,66 +551,90 @@ def test_calc_shift_deltas(): def test_two_calendars_same_uid(): - home = 'home' - work = 'work' - dbi = backend.SQLiteDb([home, work], ':memory:', locale=LOCALE_BERLIN) + home = "home" + work = "work" + dbi = backend.SQLiteDb([home, work], ":memory:", locale=LOCALE_BERLIN) assert dbi.list(home) == [] assert dbi.list(work) == [] - dbi.update(event_a, href='12345.ics', etag='abcd', calendar=home) - assert dbi.list(home) == [('12345.ics', 'abcd')] + dbi.update(event_a, href="12345.ics", etag="abcd", calendar=home) + assert dbi.list(home) == [("12345.ics", "abcd")] assert dbi.list(work) == [] - dbi.update(event_b, href='12345.ics', etag='abcd', calendar=work) - assert dbi.list(home) == [('12345.ics', 'abcd')] - assert dbi.list(work) == [('12345.ics', 'abcd')] + dbi.update(event_b, href="12345.ics", etag="abcd", calendar=work) + assert dbi.list(home) == [("12345.ics", "abcd")] + assert dbi.list(work) == [("12345.ics", "abcd")] dbi.calendars = [home] - events_a = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) + events_a = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) dbi.calendars = [work] - events_b = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) + events_b = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) assert len(events_a) == 4 assert len(events_b) == 4 dbi.calendars = [work, home] - events_c = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) + events_c = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) assert len(events_c) == 8 # count events from a given calendar assert [event[6] for event in events_c].count(home) == 4 assert [event[6] for event in events_c].count(work) == 4 - dbi.delete('12345.ics', calendar=home) + dbi.delete("12345.ics", calendar=home) dbi.calendars = [home] - events_a = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) + events_a = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) dbi.calendars = [work] - events_b = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) + events_b = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) assert len(events_a) == 0 assert len(events_b) == 4 dbi.calendars = [work, home] - events_c = list(dbi.get_localized(BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), - BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)))) - assert [event[6] for event in events_c].count('home') == 0 - assert [event[6] for event in events_c].count('work') == 4 + events_c = list( + dbi.get_localized( + BERLIN.localize(dt.datetime(2014, 6, 30, 0, 0)), + BERLIN.localize(dt.datetime(2014, 7, 26, 0, 0)), + ) + ) + assert [event[6] for event in events_c].count("home") == 0 + assert [event[6] for event in events_c].count("work") == 4 assert dbi.list(home) == [] - assert dbi.list(work) == [('12345.ics', 'abcd')] + assert dbi.list(work) == [("12345.ics", "abcd")] def test_update_one_should_not_affect_others(): """test if an THISANDFUTURE param effects other events as well""" - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - db.update(_get_text('event_d_15'), href='first', calendar=calname) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + db.update(_get_text("event_d_15"), href="first", calendar=calname) events = db.get_floating(dt.datetime(2015, 4, 9, 0, 0), dt.datetime(2015, 4, 10, 0, 0)) assert len(list(events)) == 1 - db.update(event_rrule_multi_this_and_future_allday, href='second', calendar=calname) + db.update(event_rrule_multi_this_and_future_allday, href="second", calendar=calname) events = list(db.get_floating(dt.datetime(2015, 4, 9, 0, 0), dt.datetime(2015, 4, 10, 0, 0))) assert len(events) == 1 def test_no_dtend(): """test support for events with no dtend""" - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - db.update(_get_text('event_dt_no_end'), href='event_dt_no_end', calendar=calname) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + db.update(_get_text("event_dt_no_end"), href="event_dt_no_end", calendar=calname) events = db.get_localized( BERLIN.localize(dt.datetime(2016, 1, 16, 0, 0)), BERLIN.localize(dt.datetime(2016, 1, 17, 0, 0)), @@ -583,27 +654,29 @@ def test_no_dtend(): supported_events = [ - event_a, event_b, event_rrule_this_and_future, + event_a, + event_b, + event_rrule_this_and_future, event_rrule_this_and_future_allday, - event_rrule_this_and_future_multi_day_shift + event_rrule_this_and_future_multi_day_shift, ] def test_check_support(): for cal_str in supported_events: ical = icalendar.Calendar.from_ical(cal_str) - [backend.check_support(event, '', '') for event in ical.walk()] + [backend.check_support(event, "", "") for event in ical.walk()] ical = icalendar.Calendar.from_ical(event_rrule_this_and_prior) with pytest.raises(UpdateFailed): - [backend.check_support(event, '', '') for event in ical.walk()] + [backend.check_support(event, "", "") for event in ical.walk()] def test_check_support_rdate_no_values(): """check if `check_support` doesn't choke on events with an RDATE property without a VALUE parameter""" - ical = icalendar.Calendar.from_ical(_get_text('event_rdate_no_value')) - [backend.check_support(event, '', '') for event in ical.walk()] + ical = icalendar.Calendar.from_ical(_get_text("event_rdate_no_value")) + [backend.check_support(event, "", "") for event in ical.walk()] card = """BEGIN:VCARD @@ -684,109 +757,111 @@ def test_check_support_rdate_no_values(): def test_birthdays(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 1 - assert 'SUMMARY:Unix\'s birthday' in events[0][0] + assert "SUMMARY:Unix's birthday" in events[0][0] events = list( - db.get_floating( - dt.datetime(2016, 3, 11, 0, 0), - dt.datetime(2016, 3, 11, 23, 59, 59, 999))) - assert 'SUMMARY:Unix\'s birthday' in events[0][0] + db.get_floating(dt.datetime(2016, 3, 11, 0, 0), dt.datetime(2016, 3, 11, 23, 59, 59, 999)) + ) + assert "SUMMARY:Unix's birthday" in events[0][0] def test_birthdays_update(): """test if we can update a birthday""" - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) - db.update_vcf_dates(card, 'unix.vcf', calendar=calname) - db.update_vcf_dates(card, 'unix.vcf', calendar=calname) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) + db.update_vcf_dates(card, "unix.vcf", calendar=calname) + db.update_vcf_dates(card, "unix.vcf", calendar=calname) def test_birthdays_no_fn(): - db = backend.SQLiteDb(['home'], ':memory:', locale=LOCALE_BERLIN) - assert list(db.get_floating(dt.datetime(1941, 9, 9, 0, 0), - dt.datetime(1941, 9, 9, 23, 59, 59, 9999))) == [] - db.update_vcf_dates(card_no_fn, 'unix.vcf', calendar=calname) - events = list(db.get_floating(dt.datetime(1941, 9, 9, 0, 0), - dt.datetime(1941, 9, 9, 23, 59, 59, 9999))) + db = backend.SQLiteDb(["home"], ":memory:", locale=LOCALE_BERLIN) + assert ( + list( + db.get_floating( + dt.datetime(1941, 9, 9, 0, 0), dt.datetime(1941, 9, 9, 23, 59, 59, 9999) + ) + ) + == [] + ) + db.update_vcf_dates(card_no_fn, "unix.vcf", calendar=calname) + events = list( + db.get_floating(dt.datetime(1941, 9, 9, 0, 0), dt.datetime(1941, 9, 9, 23, 59, 59, 9999)) + ) assert len(events) == 1 - assert 'SUMMARY:Dennis MacAlistair Ritchie\'s birthday' in events[0][0] + assert "SUMMARY:Dennis MacAlistair Ritchie's birthday" in events[0][0] def test_birthday_does_not_parse(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_does_not_parse, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_does_not_parse, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 0 def test_vcard_two_birthdays(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_two_birthdays, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_two_birthdays, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 0 def test_anniversary(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_anniversary, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_anniversary, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 1 - assert 'SUMMARY:Unix\'s anniversary' in events[0][0] + assert "SUMMARY:Unix's anniversary" in events[0][0] events = list( - db.get_floating( - dt.datetime(2016, 3, 11, 0, 0), - dt.datetime(2016, 3, 11, 23, 59, 59, 999))) - assert 'SUMMARY:Unix\'s anniversary' in events[0][0] + db.get_floating(dt.datetime(2016, 3, 11, 0, 0), dt.datetime(2016, 3, 11, 23, 59, 59, 999)) + ) + assert "SUMMARY:Unix's anniversary" in events[0][0] def test_abdate(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_abdate, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_abdate, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 1 - assert 'SUMMARY:Unix\'s spouse\'s birthday' in events[0][0] + assert "SUMMARY:Unix's spouse's birthday" in events[0][0] events = list( - db.get_floating( - dt.datetime(2016, 3, 11, 0, 0), - dt.datetime(2016, 3, 11, 23, 59, 59, 999))) - assert 'SUMMARY:Unix\'s spouse\'s birthday' in events[0][0] + db.get_floating(dt.datetime(2016, 3, 11, 0, 0), dt.datetime(2016, 3, 11, 23, 59, 59, 999)) + ) + assert "SUMMARY:Unix's spouse's birthday" in events[0][0] def test_abdate_nolabel(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_abdate_nolabel, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_abdate_nolabel, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 1 - assert 'SUMMARY:Unix\'s custom event from vcard' in events[0][0] + assert "SUMMARY:Unix's custom event from vcard" in events[0][0] events = list( - db.get_floating( - dt.datetime(2016, 3, 11, 0, 0), - dt.datetime(2016, 3, 11, 23, 59, 59, 999))) - assert 'SUMMARY:Unix\'s custom event from vcard' in events[0][0] + db.get_floating(dt.datetime(2016, 3, 11, 0, 0), dt.datetime(2016, 3, 11, 23, 59, 59, 999)) + ) + assert "SUMMARY:Unix's custom event from vcard" in events[0][0] def test_birthday_v3(): - db = backend.SQLiteDb([calname], ':memory:', locale=LOCALE_BERLIN) + db = backend.SQLiteDb([calname], ":memory:", locale=LOCALE_BERLIN) assert list(db.get_floating(start, end)) == [] - db.update_vcf_dates(card_v3, 'unix.vcf', calendar=calname) + db.update_vcf_dates(card_v3, "unix.vcf", calendar=calname) events = list(db.get_floating(start, end)) assert len(events) == 1 - assert 'SUMMARY:Unix\'s birthday' in events[0][0] + assert "SUMMARY:Unix's birthday" in events[0][0] events = list( - db.get_floating( - dt.datetime(2016, 3, 11, 0, 0), - dt.datetime(2016, 3, 11, 23, 59, 59, 999))) - assert 'SUMMARY:Unix\'s birthday' in events[0][0] + db.get_floating(dt.datetime(2016, 3, 11, 0, 0), dt.datetime(2016, 3, 11, 23, 59, 59, 999)) + ) + assert "SUMMARY:Unix's birthday" in events[0][0] diff --git a/tests/cal_display_test.py b/tests/cal_display_test.py index f59df410d..44803ce29 100644 --- a/tests/cal_display_test.py +++ b/tests/cal_display_test.py @@ -28,423 +28,432 @@ def test_getweeknumber(): def test_str_week(): aday = dt.date(2012, 6, 1) bday = dt.date(2012, 6, 8) - week = [dt.date(2012, 6, 6), - dt.date(2012, 6, 7), - dt.date(2012, 6, 8), - dt.date(2012, 6, 9), - dt.date(2012, 6, 10), - dt.date(2012, 6, 11), - dt.date(2012, 6, 12), - dt.date(2012, 6, 13)] - assert str_week(week, aday) == ' 6 7 8 9 10 11 12 13 ' - assert str_week(week, bday) == ' 6 7 \x1b[7m 8\x1b[0m 9 10 11 12 13 ' + week = [ + dt.date(2012, 6, 6), + dt.date(2012, 6, 7), + dt.date(2012, 6, 8), + dt.date(2012, 6, 9), + dt.date(2012, 6, 10), + dt.date(2012, 6, 11), + dt.date(2012, 6, 12), + dt.date(2012, 6, 13), + ] + assert str_week(week, aday) == " 6 7 8 9 10 11 12 13 " + assert str_week(week, bday) == " 6 7 \x1b[7m 8\x1b[0m 9 10 11 12 13 " class testCollection: def __init__(self) -> None: - self._calendars : dict[str, dict]= {} + self._calendars: dict[str, dict] = {} - def addCalendar(self, name: str , color: str, priority: int) -> None: - self._calendars[name] = {'color': color, 'priority': priority} + def addCalendar(self, name: str, color: str, priority: int) -> None: + self._calendars[name] = {"color": color, "priority": priority} def test_get_calendar_color(): - exampleCollection = testCollection() - exampleCollection.addCalendar('testCalendar1', 'dark red', 20) - exampleCollection.addCalendar('testCalendar2', 'light green', 10) - exampleCollection.addCalendar('testCalendar3', '', 10) + exampleCollection.addCalendar("testCalendar1", "dark red", 20) + exampleCollection.addCalendar("testCalendar2", "light green", 10) + exampleCollection.addCalendar("testCalendar3", "", 10) - assert get_calendar_color('testCalendar1', 'light blue', exampleCollection) == 'dark red' - assert get_calendar_color('testCalendar2', 'light blue', exampleCollection) == 'light green' + assert get_calendar_color("testCalendar1", "light blue", exampleCollection) == "dark red" + assert get_calendar_color("testCalendar2", "light blue", exampleCollection) == "light green" # test default color - assert get_calendar_color('testCalendar3', 'light blue', exampleCollection) == 'light blue' + assert get_calendar_color("testCalendar3", "light blue", exampleCollection) == "light blue" def test_get_color_list(): - - exampleCalendarList = ['testCalendar1', 'testCalendar2'] + exampleCalendarList = ["testCalendar1", "testCalendar2"] # test different priorities exampleCollection1 = testCollection() - exampleCollection1.addCalendar('testCalendar1', 'dark red', 20) - exampleCollection1.addCalendar('testCalendar2', 'light green', 10) + exampleCollection1.addCalendar("testCalendar1", "dark red", 20) + exampleCollection1.addCalendar("testCalendar2", "light green", 10) - testList1 = get_color_list(exampleCalendarList, 'light_blue', exampleCollection1) - assert 'dark red' in testList1 + testList1 = get_color_list(exampleCalendarList, "light_blue", exampleCollection1) + assert "dark red" in testList1 assert len(testList1) == 1 # test same priorities exampleCollection2 = testCollection() - exampleCollection2.addCalendar('testCalendar1', 'dark red', 20) - exampleCollection2.addCalendar('testCalendar2', 'light green', 20) + exampleCollection2.addCalendar("testCalendar1", "dark red", 20) + exampleCollection2.addCalendar("testCalendar2", "light green", 20) - testList2 = get_color_list(exampleCalendarList, 'light_blue', exampleCollection2) - assert 'dark red' in testList2 - assert 'light green' in testList2 + testList2 = get_color_list(exampleCalendarList, "light_blue", exampleCollection2) + assert "dark red" in testList2 + assert "light green" in testList2 assert len(testList2) == 2 # test duplicated colors exampleCollection3 = testCollection() - exampleCollection3.addCalendar('testCalendar1', 'dark red', 20) - exampleCollection3.addCalendar('testCalendar2', 'dark red', 20) + exampleCollection3.addCalendar("testCalendar1", "dark red", 20) + exampleCollection3.addCalendar("testCalendar2", "dark red", 20) - testList3 = get_color_list(exampleCalendarList, 'light_blue', exampleCollection3) + testList3 = get_color_list(exampleCalendarList, "light_blue", exampleCollection3) assert len(testList3) == 1 # test indexing operator (required by str_highlight_day()) exampleCollection4 = testCollection() - exampleCollection4.addCalendar('testCalendar1', 'dark red', 20) - exampleCollection4.addCalendar('testCalendar2', 'dark red', 20) + exampleCollection4.addCalendar("testCalendar1", "dark red", 20) + exampleCollection4.addCalendar("testCalendar2", "dark red", 20) - testList3 = get_color_list(exampleCalendarList, 'light_blue', exampleCollection4) - assert testList3[0] == 'dark red' + testList3 = get_color_list(exampleCalendarList, "light_blue", exampleCollection4) + assert testList3[0] == "dark red" example1 = [ - '\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m', - '\x1b[1mDec \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mMar \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m", + "\x1b[1mDec \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mMar \x1b[0m27 28 29 1 2 3 4 ", +] example2 = [ - '\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m', - ' 28 29 30 1 2 3 4 ', - '\x1b[1mDec \x1b[0m 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - ' 26 27 28 29 30 31 1 ', - '\x1b[1mJan \x1b[0m 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - ' 30 31 1 2 3 4 5 ', - '\x1b[1mFeb \x1b[0m 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - ' 27 28 29 1 2 3 4 '] + "\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m", + " 28 29 30 1 2 3 4 ", + "\x1b[1mDec \x1b[0m 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + " 26 27 28 29 30 31 1 ", + "\x1b[1mJan \x1b[0m 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + " 30 31 1 2 3 4 5 ", + "\x1b[1mFeb \x1b[0m 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + " 27 28 29 1 2 3 4 ", +] example_weno = [ - '\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m', - '\x1b[1mDec \x1b[0m28 29 30 1 2 3 4 \x1b[1m48\x1b[0m', - ' 5 6 7 8 9 10 11 \x1b[1m49\x1b[0m', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 \x1b[1m50\x1b[0m', - ' 19 20 21 22 23 24 25 \x1b[1m51\x1b[0m', - '\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 \x1b[1m52\x1b[0m', - ' 2 3 4 5 6 7 8 \x1b[1m 1\x1b[0m', - ' 9 10 11 12 13 14 15 \x1b[1m 2\x1b[0m', - ' 16 17 18 19 20 21 22 \x1b[1m 3\x1b[0m', - ' 23 24 25 26 27 28 29 \x1b[1m 4\x1b[0m', - '\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 \x1b[1m 5\x1b[0m', - ' 6 7 8 9 10 11 12 \x1b[1m 6\x1b[0m', - ' 13 14 15 16 17 18 19 \x1b[1m 7\x1b[0m', - ' 20 21 22 23 24 25 26 \x1b[1m 8\x1b[0m', - '\x1b[1mMar \x1b[0m27 28 29 1 2 3 4 \x1b[1m 9\x1b[0m'] + "\x1b[1m Mo Tu We Th Fr Sa Su \x1b[0m", + "\x1b[1mDec \x1b[0m28 29 30 1 2 3 4 \x1b[1m48\x1b[0m", + " 5 6 7 8 9 10 11 \x1b[1m49\x1b[0m", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 \x1b[1m50\x1b[0m", + " 19 20 21 22 23 24 25 \x1b[1m51\x1b[0m", + "\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 \x1b[1m52\x1b[0m", + " 2 3 4 5 6 7 8 \x1b[1m 1\x1b[0m", + " 9 10 11 12 13 14 15 \x1b[1m 2\x1b[0m", + " 16 17 18 19 20 21 22 \x1b[1m 3\x1b[0m", + " 23 24 25 26 27 28 29 \x1b[1m 4\x1b[0m", + "\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 \x1b[1m 5\x1b[0m", + " 6 7 8 9 10 11 12 \x1b[1m 6\x1b[0m", + " 13 14 15 16 17 18 19 \x1b[1m 7\x1b[0m", + " 20 21 22 23 24 25 26 \x1b[1m 8\x1b[0m", + "\x1b[1mMar \x1b[0m27 28 29 1 2 3 4 \x1b[1m 9\x1b[0m", +] example_we_start_su = [ - '\x1b[1m Su Mo Tu We Th Fr Sa \x1b[0m', - '\x1b[1mDec \x1b[0m27 28 29 30 1 2 3 ', - ' 4 5 6 7 8 9 10 ', - ' 11 \x1b[7m12\x1b[0m 13 14 15 16 17 ', - ' 18 19 20 21 22 23 24 ', - ' 25 26 27 28 29 30 31 ', - '\x1b[1mJan \x1b[0m 1 2 3 4 5 6 7 ', - ' 8 9 10 11 12 13 14 ', - ' 15 16 17 18 19 20 21 ', - ' 22 23 24 25 26 27 28 ', - '\x1b[1mFeb \x1b[0m29 30 31 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' 12 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mMar \x1b[0m26 27 28 29 1 2 3 '] + "\x1b[1m Su Mo Tu We Th Fr Sa \x1b[0m", + "\x1b[1mDec \x1b[0m27 28 29 30 1 2 3 ", + " 4 5 6 7 8 9 10 ", + " 11 \x1b[7m12\x1b[0m 13 14 15 16 17 ", + " 18 19 20 21 22 23 24 ", + " 25 26 27 28 29 30 31 ", + "\x1b[1mJan \x1b[0m 1 2 3 4 5 6 7 ", + " 8 9 10 11 12 13 14 ", + " 15 16 17 18 19 20 21 ", + " 22 23 24 25 26 27 28 ", + "\x1b[1mFeb \x1b[0m29 30 31 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " 12 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mMar \x1b[0m26 27 28 29 1 2 3 ", +] example_cz = [ - '\x1b[1m Po \xdat St \u010ct P\xe1 So Ne \x1b[0m', - '\x1b[1mpro \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mled \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1m\xfano \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mb\u0159e \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Po \xdat St \u010ct P\xe1 So Ne \x1b[0m", + "\x1b[1mpro \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mled \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1m\xfano \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mb\u0159e \x1b[0m27 28 29 1 2 3 4 ", +] example_gr = [ - '\x1b[1m δε τρ τε πε πα σα κυ \x1b[0m', - '\x1b[1mδεκ \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mιαν \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mφεβ \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mμαρ \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m δε τρ τε πε πα σα κυ \x1b[0m", + "\x1b[1mδεκ \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mιαν \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mφεβ \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mμαρ \x1b[0m27 28 29 1 2 3 4 ", +] example_gr_darwin = [ - '\x1b[1m δε τρ τε πε πα σα κυ \x1b[0m', - '\x1b[1mδεκ \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mιαν \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mφεβ \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mμαρ \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m δε τρ τε πε πα σα κυ \x1b[0m", + "\x1b[1mδεκ \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mιαν \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mφεβ \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mμαρ \x1b[0m27 28 29 1 2 3 4 ", +] example_de = [ - '\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m', - '\x1b[1mDez \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mMär \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m", + "\x1b[1mDez \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mJan \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mFeb \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mMär \x1b[0m27 28 29 1 2 3 4 ", +] example_de_freebsd = [ - '\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m', - '\x1b[1mDez. \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mJan. \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mFeb. \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mMärz \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m", + "\x1b[1mDez. \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mJan. \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mFeb. \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mMärz \x1b[0m27 28 29 1 2 3 4 ", +] example_de_netbsd = [ - '\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m', - '\x1b[1mDez. \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mJan. \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mFeb. \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mM\xe4r. \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Mo Di Mi Do Fr Sa So \x1b[0m", + "\x1b[1mDez. \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mJan. \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mFeb. \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mM\xe4r. \x1b[0m27 28 29 1 2 3 4 ", +] example_fr = [ - '\x1b[1m lu ma me je ve sa di \x1b[0m', - '\x1b[1mdéc. \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mjanv. \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mfévr. \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mmars \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m lu ma me je ve sa di \x1b[0m", + "\x1b[1mdéc. \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mjanv. \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mfévr. \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mmars \x1b[0m27 28 29 1 2 3 4 ", +] example_fr_darwin = [ - '\x1b[1m Lu Ma Me Je Ve Sa Di \x1b[0m', - '\x1b[1mdéc \x1b[0m28 29 30 1 2 3 4 ', - ' 5 6 7 8 9 10 11 ', - ' \x1b[7m12\x1b[0m 13 14 15 16 17 18 ', - ' 19 20 21 22 23 24 25 ', - '\x1b[1mjan \x1b[0m26 27 28 29 30 31 1 ', - ' 2 3 4 5 6 7 8 ', - ' 9 10 11 12 13 14 15 ', - ' 16 17 18 19 20 21 22 ', - ' 23 24 25 26 27 28 29 ', - '\x1b[1mfév \x1b[0m30 31 1 2 3 4 5 ', - ' 6 7 8 9 10 11 12 ', - ' 13 14 15 16 17 18 19 ', - ' 20 21 22 23 24 25 26 ', - '\x1b[1mmar \x1b[0m27 28 29 1 2 3 4 '] + "\x1b[1m Lu Ma Me Je Ve Sa Di \x1b[0m", + "\x1b[1mdéc \x1b[0m28 29 30 1 2 3 4 ", + " 5 6 7 8 9 10 11 ", + " \x1b[7m12\x1b[0m 13 14 15 16 17 18 ", + " 19 20 21 22 23 24 25 ", + "\x1b[1mjan \x1b[0m26 27 28 29 30 31 1 ", + " 2 3 4 5 6 7 8 ", + " 9 10 11 12 13 14 15 ", + " 16 17 18 19 20 21 22 ", + " 23 24 25 26 27 28 29 ", + "\x1b[1mfév \x1b[0m30 31 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "\x1b[1mmar \x1b[0m27 28 29 1 2 3 4 ", +] def test_vertical_month(): try: - locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12)) + locale.setlocale(locale.LC_ALL, "en_US.UTF-8") + vert_str = vertical_month(month=12, year=2011, today=dt.date(2011, 12, 12)) assert vert_str == example1 - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12), - monthdisplay='firstfullweek') + vert_str = vertical_month( + month=12, year=2011, today=dt.date(2011, 12, 12), monthdisplay="firstfullweek" + ) assert vert_str == example2 - weno_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12), - weeknumber='right') + weno_str = vertical_month( + month=12, year=2011, today=dt.date(2011, 12, 12), weeknumber="right" + ) assert weno_str == example_weno we_start_su_str = vertical_month( - month=12, year=2011, - today=dt.date(2011, 12, 12), - firstweekday=6) + month=12, year=2011, today=dt.date(2011, 12, 12), firstweekday=6 + ) assert we_start_su_str == example_we_start_su except locale.Error as error: - if str(error) == 'unsupported locale setting': + if str(error) == "unsupported locale setting": pytest.xfail( - 'To get this test to run, you need to add `en_US.utf-8` to ' - 'your locales. On Debian GNU/Linux 8 you do this by ' - 'uncommenting `de_DE.utf-8 in /etc/locale.gen and then run ' - '`locale-gen` (as root).' + "To get this test to run, you need to add `en_US.utf-8` to " + "your locales. On Debian GNU/Linux 8 you do this by " + "uncommenting `de_DE.utf-8 in /etc/locale.gen and then run " + "`locale-gen` (as root)." ) finally: - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") def test_vertical_month_unicode(): try: - locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12)) + locale.setlocale(locale.LC_ALL, "de_DE.UTF-8") + vert_str = vertical_month(month=12, year=2011, today=dt.date(2011, 12, 12)) # de_DE locale on at least Net and FreeBSD is different from the one # commonly used on linux systems - if platform.system() == 'FreeBSD': + if platform.system() == "FreeBSD": assert vert_str == example_de_freebsd - elif platform.system() == 'NetBSD': + elif platform.system() == "NetBSD": assert vert_str == example_de_netbsd else: assert vert_str == example_de - '\n'.join(vert_str) # issue 142 + "\n".join(vert_str) # issue 142 except locale.Error as error: - if str(error) == 'unsupported locale setting': + if str(error) == "unsupported locale setting": pytest.xfail( - 'To get this test to run, you need to add `de_DE.utf-8` to ' - 'your locales. On Debian GNU/Linux 8 you do this by ' - 'uncommenting `de_DE.utf-8 in /etc/locale.gen and then run ' - '`locale-gen` (as root).' + "To get this test to run, you need to add `de_DE.utf-8` to " + "your locales. On Debian GNU/Linux 8 you do this by " + "uncommenting `de_DE.utf-8 in /etc/locale.gen and then run " + "`locale-gen` (as root)." ) else: raise finally: - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") def test_vertical_month_unicode_weekdeays(): try: - locale.setlocale(locale.LC_ALL, 'cs_CZ.UTF-8') - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12)) + locale.setlocale(locale.LC_ALL, "cs_CZ.UTF-8") + vert_str = vertical_month(month=12, year=2011, today=dt.date(2011, 12, 12)) assert [line.lower() for line in vert_str] == [line.lower() for line in example_cz] - '\n'.join(vert_str) # issue 142/293 + "\n".join(vert_str) # issue 142/293 except locale.Error as error: - if str(error) == 'unsupported locale setting': + if str(error) == "unsupported locale setting": pytest.xfail( - 'To get this test to run, you need to add `cs_CZ.UTF-8` to ' - 'your locales. On Debian GNU/Linux 8 you do this by ' - 'uncommenting `cs_CZ.UTF-8` in /etc/locale.gen and then run ' - '`locale-gen` (as root).' + "To get this test to run, you need to add `cs_CZ.UTF-8` to " + "your locales. On Debian GNU/Linux 8 you do this by " + "uncommenting `cs_CZ.UTF-8` in /etc/locale.gen and then run " + "`locale-gen` (as root)." ) else: raise finally: - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") def strip_accents(string): """remove accents from unicode characters""" - return ''.join(c for c in unicodedata.normalize('NFD', string) - if unicodedata.category(c) != 'Mn') + return "".join( + c for c in unicodedata.normalize("NFD", string) if unicodedata.category(c) != "Mn" + ) def test_vertical_month_unicode_weekdeays_gr(): try: - locale.setlocale(locale.LC_ALL, 'el_GR.UTF-8') - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12)) + locale.setlocale(locale.LC_ALL, "el_GR.UTF-8") + vert_str = vertical_month(month=12, year=2011, today=dt.date(2011, 12, 12)) # on some OSes, Greek locale's abbreviated day of the week and # month names have accents, on some they haven't - if platform.system() == 'Darwin': - assert strip_accents('\n'.join([line.lower() for line in vert_str])) == \ - '\n'.join(example_gr_darwin) + if platform.system() == "Darwin": + assert strip_accents("\n".join([line.lower() for line in vert_str])) == "\n".join( + example_gr_darwin + ) else: - assert strip_accents('\n'.join([line.lower() for line in vert_str])) == \ - '\n'.join(example_gr) - '\n'.join(vert_str) # issue 142/293 + assert strip_accents("\n".join([line.lower() for line in vert_str])) == "\n".join( + example_gr + ) + "\n".join(vert_str) # issue 142/293 except locale.Error as error: - if str(error) == 'unsupported locale setting': + if str(error) == "unsupported locale setting": pytest.xfail( - 'To get this test to run, you need to add `el_GR.UTF-8` to ' - 'your locales. On Debian GNU/Linux 8 you do this by ' - 'uncommenting `el_GR.UTF-8` in /etc/locale.gen and then run ' - '`locale-gen` (as root).' + "To get this test to run, you need to add `el_GR.UTF-8` to " + "your locales. On Debian GNU/Linux 8 you do this by " + "uncommenting `el_GR.UTF-8` in /etc/locale.gen and then run " + "`locale-gen` (as root)." ) else: raise finally: - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") def test_vertical_month_abbr_fr(): # see issue #653 try: - locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') - vert_str = vertical_month(month=12, year=2011, - today=dt.date(2011, 12, 12)) - if platform.system() == 'Darwin': - assert '\n'.join(vert_str) == '\n'.join(example_fr_darwin) + locale.setlocale(locale.LC_ALL, "fr_FR.UTF-8") + vert_str = vertical_month(month=12, year=2011, today=dt.date(2011, 12, 12)) + if platform.system() == "Darwin": + assert "\n".join(vert_str) == "\n".join(example_fr_darwin) else: - assert '\n'.join(vert_str) == '\n'.join(example_fr) + assert "\n".join(vert_str) == "\n".join(example_fr) except locale.Error as error: - if str(error) == 'unsupported locale setting': + if str(error) == "unsupported locale setting": pytest.xfail( - 'To get this test to run, you need to add `fr_FR.UTF-8` to ' - 'your locales. On Debian GNU/Linux 8 you do this by ' - 'uncommenting `fr_FR.UTF-8` in /etc/locale.gen and then run ' - '`locale-gen` (as root).' + "To get this test to run, you need to add `fr_FR.UTF-8` to " + "your locales. On Debian GNU/Linux 8 you do this by " + "uncommenting `fr_FR.UTF-8` in /etc/locale.gen and then run " + "`locale-gen` (as root)." ) else: raise finally: - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, "C") diff --git a/tests/cli_test.py b/tests/cli_test.py index 79f96298b..6fbbc238b 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -16,9 +16,17 @@ class CustomCliRunner(CliRunner): - def __init__(self, config_file, db=None, calendars=None, - xdg_data_home=None, xdg_config_home=None, - xdg_cache_home=None, tmpdir=None, **kwargs) -> None: + def __init__( + self, + config_file, + db=None, + calendars=None, + xdg_data_home=None, + xdg_config_home=None, + xdg_cache_home=None, + tmpdir=None, + **kwargs, + ) -> None: self.config_file = config_file self.db = db self.calendars = calendars @@ -30,52 +38,63 @@ def __init__(self, config_file, db=None, calendars=None, super().__init__(**kwargs) def invoke(self, cli, args=None, *a, **kw): - args = ['-c', str(self.config_file)] + (args or []) + args = ["-c", str(self.config_file)] + (args or []) return super().invoke(cli, args, *a, **kw) @pytest.fixture def runner(tmpdir, monkeypatch): - db = tmpdir.join('khal.db') - calendar = tmpdir.mkdir('calendar') - calendar2 = tmpdir.mkdir('calendar2') - calendar3 = tmpdir.mkdir('calendar3') + db = tmpdir.join("khal.db") + calendar = tmpdir.mkdir("calendar") + calendar2 = tmpdir.mkdir("calendar2") + calendar3 = tmpdir.mkdir("calendar3") - xdg_data_home = tmpdir.join('vdirs') - xdg_cache_home = tmpdir.join('.cache') - xdg_config_home = tmpdir.join('.config') - config_file = xdg_config_home.join('khal').join('config') + xdg_data_home = tmpdir.join("vdirs") + xdg_cache_home = tmpdir.join(".cache") + xdg_config_home = tmpdir.join(".config") + config_file = xdg_config_home.join("khal").join("config") # TODO create a vdir config on disk and let vdirsyncer actually read it - monkeypatch.setattr('vdirsyncer.cli.config.load_config', lambda: Config()) - monkeypatch.setattr('xdg.BaseDirectory.xdg_data_home', str(xdg_data_home)) - monkeypatch.setattr('xdg.BaseDirectory.xdg_cache_home', str(xdg_cache_home)) - monkeypatch.setattr('xdg.BaseDirectory.xdg_config_home', str(xdg_config_home)) - monkeypatch.setattr('xdg.BaseDirectory.xdg_config_dirs', [str(xdg_config_home)]) + monkeypatch.setattr("vdirsyncer.cli.config.load_config", lambda: Config()) + monkeypatch.setattr("xdg.BaseDirectory.xdg_data_home", str(xdg_data_home)) + monkeypatch.setattr("xdg.BaseDirectory.xdg_cache_home", str(xdg_cache_home)) + monkeypatch.setattr("xdg.BaseDirectory.xdg_config_home", str(xdg_config_home)) + monkeypatch.setattr("xdg.BaseDirectory.xdg_config_dirs", [str(xdg_config_home)]) def inner(print_new=False, default_calendar=True, days=2, **kwargs): if default_calendar: - default_calendar = 'default_calendar = one' + default_calendar = "default_calendar = one" else: - default_calendar = '' - if not os.path.exists(str(xdg_config_home.join('khal'))): - os.makedirs(str(xdg_config_home.join('khal'))) - config_file.write(config_template.format( - delta=str(days) + 'd', - calpath=str(calendar), calpath2=str(calendar2), calpath3=str(calendar3), - default_calendar=default_calendar, - print_new=print_new, - dbpath=str(db), **kwargs)) + default_calendar = "" + if not os.path.exists(str(xdg_config_home.join("khal"))): + os.makedirs(str(xdg_config_home.join("khal"))) + config_file.write( + config_template.format( + delta=str(days) + "d", + calpath=str(calendar), + calpath2=str(calendar2), + calpath3=str(calendar3), + default_calendar=default_calendar, + print_new=print_new, + dbpath=str(db), + **kwargs, + ) + ) runner = CustomCliRunner( - config_file=config_file, db=db, calendars={"one": calendar}, - xdg_data_home=xdg_data_home, xdg_config_home=xdg_config_home, - xdg_cache_home=xdg_cache_home, tmpdir=tmpdir, + config_file=config_file, + db=db, + calendars={"one": calendar}, + xdg_data_home=xdg_data_home, + xdg_config_home=xdg_config_home, + xdg_cache_home=xdg_cache_home, + tmpdir=tmpdir, ) return runner + return inner -config_template = ''' +config_template = """ [calendars] [[one]] path = {calpath} @@ -106,100 +125,100 @@ def inner(print_new=False, default_calendar=True, days=2, **kwargs): [sqlite] path = {dbpath} -''' +""" def test_direct_modification(runner): runner = runner() - result = runner.invoke(main_khal, ['list']) - assert result.output == '' + result = runner.invoke(main_khal, ["list"]) + assert result.output == "" assert not result.exception - cal_dt = _get_text('event_dt_simple') - event = runner.calendars['one'].join('test.ics') + cal_dt = _get_text("event_dt_simple") + event = runner.calendars["one"].join("test.ics") event.write(cal_dt) - format = '{start-end-time-style}: {title}' - args = ['list', '--format', format, '--day-format', '', '09.04.2014'] + format = "{start-end-time-style}: {title}" + args = ["list", "--format", format, "--day-format", "", "09.04.2014"] result = runner.invoke(main_khal, args) assert not result.exception - assert result.output == '09:30-10:30: An Event\n' + assert result.output == "09:30-10:30: An Event\n" os.remove(str(event)) - result = runner.invoke(main_khal, ['list']) + result = runner.invoke(main_khal, ["list"]) assert not result.exception - assert result.output == '' + assert result.output == "" def test_simple(runner): runner = runner(days=2) - result = runner.invoke(main_khal, ['list']) + result = runner.invoke(main_khal, ["list"]) assert not result.exception - assert result.output == '' + assert result.output == "" - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke( - main_khal, f'new {now} 18:00 myevent'.split()) - assert result.output == '' + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + assert result.output == "" assert not result.exception - result = runner.invoke(main_khal, ['list']) + result = runner.invoke(main_khal, ["list"]) print(result.output) - assert 'myevent' in result.output - assert '18:00' in result.output + assert "myevent" in result.output + assert "18:00" in result.output # test show_all_days default value - assert 'Tomorrow:' not in result.output + assert "Tomorrow:" not in result.output assert not result.exception def test_simple_color(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke(main_khal, f'new {now} 18:00 myevent'.split()) - assert result.output == '' + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + assert result.output == "" assert not result.exception - result = runner.invoke(main_khal, ['list'], color=True) + result = runner.invoke(main_khal, ["list"], color=True) assert not result.exception - assert '\x1b[34m' in result.output + assert "\x1b[34m" in result.output def test_days(runner): runner = runner(days=9) - when = (dt.datetime.now() + dt.timedelta(days=7)).strftime('%d.%m.%Y') - result = runner.invoke(main_khal, f'new {when} 18:00 nextweek'.split()) - assert result.output == '' + when = (dt.datetime.now() + dt.timedelta(days=7)).strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {when} 18:00 nextweek".split()) + assert result.output == "" assert not result.exception - when = (dt.datetime.now() + dt.timedelta(days=30)).strftime('%d.%m.%Y') - result = runner.invoke(main_khal, f'new {when} 18:00 nextmonth'.split()) - assert result.output == '' + when = (dt.datetime.now() + dt.timedelta(days=30)).strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {when} 18:00 nextmonth".split()) + assert result.output == "" assert not result.exception - result = runner.invoke(main_khal, ['list']) - assert 'nextweek' in result.output - assert 'nextmonth' not in result.output - assert '18:00' in result.output + result = runner.invoke(main_khal, ["list"]) + assert "nextweek" in result.output + assert "nextmonth" not in result.output + assert "18:00" in result.output assert not result.exception def test_notstarted(runner): - with freeze_time('2015-6-1 15:00'): + with freeze_time("2015-6-1 15:00"): runner = runner(days=2) for command in [ - 'new 30.5.2015 5.6.2015 long event', - 'new 2.6.2015 4.6.2015 two day event', - 'new 1.6.2015 14:00 18:00 four hour event', - 'new 1.6.2015 16:00 17:00 one hour event', - 'new 2.6.2015 10:00 13:00 three hour event', + "new 30.5.2015 5.6.2015 long event", + "new 2.6.2015 4.6.2015 two day event", + "new 1.6.2015 14:00 18:00 four hour event", + "new 1.6.2015 16:00 17:00 one hour event", + "new 2.6.2015 10:00 13:00 three hour event", ]: result = runner.invoke(main_khal, command.split()) assert not result.exception - result = runner.invoke(main_khal, 'list now'.split()) - assert result.output == \ - """Today, 01.06.2015 + result = runner.invoke(main_khal, "list now".split()) + assert ( + result.output + == """Today, 01.06.2015 ↔ long event 14:00-18:00 four hour event 16:00-17:00 one hour event @@ -211,10 +230,12 @@ def test_notstarted(runner): ↔ long event ↔ two day event """ + ) assert not result.exception - result = runner.invoke(main_khal, 'list now --notstarted'.split()) - assert result.output == \ - """Today, 01.06.2015 + result = runner.invoke(main_khal, "list now --notstarted".split()) + assert ( + result.output + == """Today, 01.06.2015 16:00-17:00 one hour event Tomorrow, 02.06.2015 ↦ two day event @@ -222,11 +243,13 @@ def test_notstarted(runner): Wednesday, 03.06.2015 ↔ two day event """ + ) assert not result.exception - result = runner.invoke(main_khal, 'list now --once'.split()) - assert result.output == \ - """Today, 01.06.2015 + result = runner.invoke(main_khal, "list now --once".split()) + assert ( + result.output + == """Today, 01.06.2015 ↔ long event 14:00-18:00 four hour event 16:00-17:00 one hour event @@ -234,74 +257,81 @@ def test_notstarted(runner): ↦ two day event 10:00-13:00 three hour event """ + ) assert not result.exception - result = runner.invoke(main_khal, 'list now --once --notstarted'.split()) - assert result.output == \ - """Today, 01.06.2015 + result = runner.invoke(main_khal, "list now --once --notstarted".split()) + assert ( + result.output + == """Today, 01.06.2015 16:00-17:00 one hour event Tomorrow, 02.06.2015 ↦ two day event 10:00-13:00 three hour event """ + ) assert not result.exception def test_calendar(runner): - with freeze_time('2015-6-1'): + with freeze_time("2015-6-1"): runner = runner(days=0) - result = runner.invoke(main_khal, ['calendar']) + result = runner.invoke(main_khal, ["calendar"]) assert not result.exception assert result.exit_code == 0 - output = '\n'.join([ - " Mo Tu We Th Fr Sa Su No events", - "Jun 1 2 3 4 5 6 7 ", - " 8 9 10 11 12 13 14 ", - " 15 16 17 18 19 20 21 ", - " 22 23 24 25 26 27 28 ", - "Jul 29 30 1 2 3 4 5 ", - " 6 7 8 9 10 11 12 ", - " 13 14 15 16 17 18 19 ", - " 20 21 22 23 24 25 26 ", - "Aug 27 28 29 30 31 1 2 ", - " 3 4 5 6 7 8 9 ", - " 10 11 12 13 14 15 16 ", - " 17 18 19 20 21 22 23 ", - " 24 25 26 27 28 29 30 ", - "Sep 31 1 2 3 4 5 6 ", - "", - ]) + output = "\n".join( + [ + " Mo Tu We Th Fr Sa Su No events", + "Jun 1 2 3 4 5 6 7 ", + " 8 9 10 11 12 13 14 ", + " 15 16 17 18 19 20 21 ", + " 22 23 24 25 26 27 28 ", + "Jul 29 30 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "Aug 27 28 29 30 31 1 2 ", + " 3 4 5 6 7 8 9 ", + " 10 11 12 13 14 15 16 ", + " 17 18 19 20 21 22 23 ", + " 24 25 26 27 28 29 30 ", + "Sep 31 1 2 3 4 5 6 ", + "", + ] + ) assert result.output == output def test_long_calendar(runner): - with freeze_time('2015-6-1'): + with freeze_time("2015-6-1"): runner = runner(days=100) - result = runner.invoke(main_khal, ['calendar']) + result = runner.invoke(main_khal, ["calendar"]) assert not result.exception assert result.exit_code == 0 - output = '\n'.join([ - " Mo Tu We Th Fr Sa Su No events", - "Jun 1 2 3 4 5 6 7 ", - " 8 9 10 11 12 13 14 ", - " 15 16 17 18 19 20 21 ", - " 22 23 24 25 26 27 28 ", - "Jul 29 30 1 2 3 4 5 ", - " 6 7 8 9 10 11 12 ", - " 13 14 15 16 17 18 19 ", - " 20 21 22 23 24 25 26 ", - "Aug 27 28 29 30 31 1 2 ", - " 3 4 5 6 7 8 9 ", - " 10 11 12 13 14 15 16 ", - " 17 18 19 20 21 22 23 ", - " 24 25 26 27 28 29 30 ", - "Sep 31 1 2 3 4 5 6 ", - " 7 8 9 10 11 12 13 ", - " 14 15 16 17 18 19 20 ", - " 21 22 23 24 25 26 27 ", - "Oct 28 29 30 1 2 3 4 ", - "", - ]) + output = "\n".join( + [ + " Mo Tu We Th Fr Sa Su No events", + "Jun 1 2 3 4 5 6 7 ", + " 8 9 10 11 12 13 14 ", + " 15 16 17 18 19 20 21 ", + " 22 23 24 25 26 27 28 ", + "Jul 29 30 1 2 3 4 5 ", + " 6 7 8 9 10 11 12 ", + " 13 14 15 16 17 18 19 ", + " 20 21 22 23 24 25 26 ", + "Aug 27 28 29 30 31 1 2 ", + " 3 4 5 6 7 8 9 ", + " 10 11 12 13 14 15 16 ", + " 17 18 19 20 21 22 23 ", + " 24 25 26 27 28 29 30 ", + "Sep 31 1 2 3 4 5 6 ", + " 7 8 9 10 11 12 13 ", + " 14 15 16 17 18 19 20 ", + " 21 22 23 24 25 26 27 ", + "Oct 28 29 30 1 2 3 4 ", + "", + ] + ) assert result.output == output @@ -311,60 +341,64 @@ def test_default_command_empty(runner): result = runner.invoke(main_khal) assert result.exception assert result.exit_code == 2 - assert result.output.startswith('Usage: ') + assert result.output.startswith("Usage: ") def test_invalid_calendar(runner): runner = runner(days=2) - result = runner.invoke( - main_khal, ['new'] + '-a one 18:00 myevent'.split()) + result = runner.invoke(main_khal, ["new"] + "-a one 18:00 myevent".split()) assert not result.exception - result = runner.invoke( - main_khal, ['new'] + '-a inexistent 18:00 myevent'.split()) + result = runner.invoke(main_khal, ["new"] + "-a inexistent 18:00 myevent".split()) assert result.exception assert result.exit_code == 2 - assert 'Unknown calendar ' in result.output + assert "Unknown calendar " in result.output def test_attach_calendar(runner): runner = runner(days=2) - result = runner.invoke(main_khal, ['printcalendars']) - assert set(result.output.split('\n')[:3]) == {'one', 'two', 'three'} + result = runner.invoke(main_khal, ["printcalendars"]) + assert set(result.output.split("\n")[:3]) == {"one", "two", "three"} assert not result.exception - result = runner.invoke(main_khal, ['printcalendars', '-a', 'one']) - assert result.output == 'one\n' + result = runner.invoke(main_khal, ["printcalendars", "-a", "one"]) + assert result.output == "one\n" assert not result.exception - result = runner.invoke(main_khal, ['printcalendars', '-d', 'one']) - assert set(result.output.split('\n')[:2]) == {'two', 'three'} + result = runner.invoke(main_khal, ["printcalendars", "-d", "one"]) + assert set(result.output.split("\n")[:2]) == {"two", "three"} assert not result.exception # "see #905" @pytest.mark.xfail -@pytest.mark.parametrize('contents', [ - '', - 'BEGIN:VCALENDAR\nBEGIN:VTODO\nEND:VTODO\nEND:VCALENDAR\n' -]) +@pytest.mark.parametrize( + "contents", ["", "BEGIN:VCALENDAR\nBEGIN:VTODO\nEND:VTODO\nEND:VCALENDAR\n"] +) def test_no_vevent(runner, tmpdir, contents): runner = runner(days=2) - broken_item = runner.calendars['one'].join('broken_item.ics') - broken_item.write(contents.encode('utf-8'), mode='wb') + broken_item = runner.calendars["one"].join("broken_item.ics") + broken_item.write(contents.encode("utf-8"), mode="wb") - result = runner.invoke(main_khal, ['list']) + result = runner.invoke(main_khal, ["list"]) assert not result.exception - assert result.output == '' + assert result.output == "" def test_printformats(runner): runner = runner(days=2) - result = runner.invoke(main_khal, ['printformats']) - assert '\n'.join(['longdatetimeformat: 21.12.2013 21:45', - 'datetimeformat: 21.12. 21:45', - 'longdateformat: 21.12.2013', - 'dateformat: 21.12.', - 'timeformat: 21:45', - '']) == result.output + result = runner.invoke(main_khal, ["printformats"]) + assert ( + "\n".join( + [ + "longdatetimeformat: 21.12.2013 21:45", + "datetimeformat: 21.12. 21:45", + "longdateformat: 21.12.2013", + "dateformat: 21.12.", + "timeformat: 21:45", + "", + ] + ) + == result.output + ) assert not result.exception @@ -372,36 +406,36 @@ def test_printformats(runner): @pytest.mark.xfail def test_repeating(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') + now = dt.datetime.now().strftime("%d.%m.%Y") end_date = dt.datetime.now() + dt.timedelta(days=10) result = runner.invoke( - main_khal, (f"new {now} 18:00 myevent -r weekly -u " - f"{end_date.strftime('%d.%m.%Y')}").split()) + main_khal, (f"new {now} 18:00 myevent -r weekly -u {end_date.strftime('%d.%m.%Y')}").split() + ) assert not result.exception - assert result.output == '' + assert result.output == "" def test_at(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') + now = dt.datetime.now().strftime("%d.%m.%Y") end_date = dt.datetime.now() + dt.timedelta(days=10) result = runner.invoke( - main_khal, - f"new {now} {end_date.strftime('%d.%m.%Y')} 18:00 myevent".split()) - args = ['--color', 'at', '--format', '{start-time}{title}', '--day-format', '', '18:30'] + main_khal, f"new {now} {end_date.strftime('%d.%m.%Y')} 18:00 myevent".split() + ) + args = ["--color", "at", "--format", "{start-time}{title}", "--day-format", "", "18:30"] result = runner.invoke(main_khal, args) assert not result.exception - assert result.output.startswith('myevent') + assert result.output.startswith("myevent") def test_at_json(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') + now = dt.datetime.now().strftime("%d.%m.%Y") end_date = dt.datetime.now() + dt.timedelta(days=10) result = runner.invoke( - main_khal, - 'new {} {} 18:00 myevent'.format(now, end_date.strftime('%d.%m.%Y')).split()) - args = ['--color', 'at', '--json', 'start-time', '--json', 'title', '18:30'] + main_khal, "new {} {} 18:00 myevent".format(now, end_date.strftime("%d.%m.%Y")).split() + ) + args = ["--color", "at", "--json", "start-time", "--json", "title", "18:30"] result = runner.invoke(main_khal, args) assert not result.exception assert result.output.startswith('[{"start-time": "", "title": "myevent"}]') @@ -409,12 +443,12 @@ def test_at_json(runner): def test_at_json_default_fields(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') + now = dt.datetime.now().strftime("%d.%m.%Y") end_date = dt.datetime.now() + dt.timedelta(days=10) result = runner.invoke( - main_khal, - 'new {} {} 18:00 myevent'.format(now, end_date.strftime('%d.%m.%Y')).split()) - args = ['--color', 'at', '--json', 'all', '18:30'] + main_khal, "new {} {} 18:00 myevent".format(now, end_date.strftime("%d.%m.%Y")).split() + ) + args = ["--color", "at", "--json", "all", "18:30"] result = runner.invoke(main_khal, args) assert not result.exception output_fields = json.loads(result.output)[0].keys() @@ -423,52 +457,70 @@ def test_at_json_default_fields(runner): def test_at_json_strip(runner): runner = runner() - result = runner.invoke(main_khal, ['import', _get_ics_filepath( - 'event_rrule_recuid_cancelled')], input='0\ny\n') + result = runner.invoke( + main_khal, ["import", _get_ics_filepath("event_rrule_recuid_cancelled")], input="0\ny\n" + ) assert not result.exception - result = runner.invoke(main_khal, ['at', '--json', 'repeat-symbol', - '--json', 'status', '--json', 'cancelled', '14.07.2014', '07:00']) + result = runner.invoke( + main_khal, + [ + "at", + "--json", + "repeat-symbol", + "--json", + "status", + "--json", + "cancelled", + "14.07.2014", + "07:00", + ], + ) traceback.print_tb(result.exc_info[2]) assert not result.exception assert result.output.startswith( - '[{"repeat-symbol": "⟳", "status": "CANCELLED", "cancelled": "CANCELLED"}]') + '[{"repeat-symbol": "⟳", "status": "CANCELLED", "cancelled": "CANCELLED"}]' + ) def test_at_day_format(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') + now = dt.datetime.now().strftime("%d.%m.%Y") end_date = dt.datetime.now() + dt.timedelta(days=10) result = runner.invoke( - main_khal, - f"new {now} {end_date.strftime('%d.%m.%Y')} 18:00 myevent".split()) - args = ['--color', 'at', '--format', '{start-time}{title}', '--day-format', '{name}', '18:30'] + main_khal, f"new {now} {end_date.strftime('%d.%m.%Y')} 18:00 myevent".split() + ) + args = ["--color", "at", "--format", "{start-time}{title}", "--day-format", "{name}", "18:30"] result = runner.invoke(main_khal, args) assert not result.exception - assert result.output.startswith('Today\x1b[0m\nmyevent') + assert result.output.startswith("Today\x1b[0m\nmyevent") def test_list(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke( - main_khal, - f'new {now} 18:00 myevent'.split()) - format = '{red}{start-end-time-style}{reset} {title} :: {description}' - args = ['--color', 'list', '--format', format, '--day-format', 'header', '18:30'] + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + format = "{red}{start-end-time-style}{reset} {title} :: {description}" + args = ["--color", "list", "--format", format, "--day-format", "header", "18:30"] result = runner.invoke(main_khal, args) - expected = 'header\x1b[0m\n\x1b[31m18:00-19:00\x1b[0m myevent :: \x1b[0m\n' + expected = "header\x1b[0m\n\x1b[31m18:00-19:00\x1b[0m myevent :: \x1b[0m\n" assert not result.exception assert result.output.startswith(expected) def test_list_json(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke( - main_khal, - f'new {now} 18:00 myevent'.split()) - args = ['list', '--json', 'start-end-time-style', - '--json', 'title', '--json', 'description', '18:30'] + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + args = [ + "list", + "--json", + "start-end-time-style", + "--json", + "title", + "--json", + "description", + "18:30", + ] result = runner.invoke(main_khal, args) expected = '[{"start-end-time-style": "18:00-19:00", "title": "myevent", "description": ""}]' assert not result.exception @@ -477,42 +529,56 @@ def test_list_json(runner): def test_search(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke(main_khal, f'new {now} 18:00 myevent'.split()) - format = '{red}{start-end-time-style}{reset} {title} :: {description}' - result = runner.invoke(main_khal, ['--color', 'search', '--format', format, 'myevent']) + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + format = "{red}{start-end-time-style}{reset} {title} :: {description}" + result = runner.invoke(main_khal, ["--color", "search", "--format", format, "myevent"]) assert not result.exception - assert result.output.startswith('\x1b[34m\x1b[31m18:00') + assert result.output.startswith("\x1b[34m\x1b[31m18:00") def test_search_json(runner): runner = runner(days=2) - now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke(main_khal, f'new {now} 18:00 myevent'.split()) - result = runner.invoke(main_khal, ['search', '--json', 'start-end-time-style', - '--json', 'title', '--json', 'description', 'myevent']) + now = dt.datetime.now().strftime("%d.%m.%Y") + result = runner.invoke(main_khal, f"new {now} 18:00 myevent".split()) + result = runner.invoke( + main_khal, + [ + "search", + "--json", + "start-end-time-style", + "--json", + "title", + "--json", + "description", + "myevent", + ], + ) assert not result.exception assert result.output.startswith('[{"start-end-time-style": "18:00') def test_no_default_new(runner): runner = runner(default_calendar=False) - result = runner.invoke(main_khal, 'new 18:00 beer'.split()) - assert ("Error: Invalid value: No default calendar is configured, " - "please provide one explicitly.") in result.output + result = runner.invoke(main_khal, "new 18:00 beer".split()) + assert ( + "Error: Invalid value: No default calendar is configured, please provide one explicitly." + ) in result.output assert result.exit_code == 2 + def test_print_bad_ics(runner): """Attempt to print a .ics that is malformed, but does not have a DST-related error.""" runner = runner() - result = runner.invoke(main_khal, ['printics', _get_ics_filepath('non_dst_error')]) + result = runner.invoke(main_khal, ["printics", _get_ics_filepath("non_dst_error")]) assert result.exception assert isinstance(result.exception, ValueError) assert str(result.exception) == "Invalid iCalendar duration: PT-2H" + def test_import(runner, monkeypatch): runner = runner() - result = runner.invoke(main_khal, 'import -a one -a two import file.ics'.split()) + result = runner.invoke(main_khal, "import -a one -a two import file.ics".split()) assert result.exception assert result.exit_code == 2 assert 'Can\'t use "--include-calendar" / "-a" more than once' in result.output @@ -524,59 +590,62 @@ def clean(self): self.args, self.kwargs = None, None def import_ics(self, *args, **kwargs): - print('saving args') + print("saving args") print(args) self.args = args self.kwargs = kwargs fake = FakeImport() - monkeypatch.setattr('khal.controllers.import_ics', fake.import_ics) + monkeypatch.setattr("khal.controllers.import_ics", fake.import_ics) # as we are not actually parsing the file we want to import, we can use # any readable file at all, therefore re-using the configuration file - result = runner.invoke(main_khal, f'import -a one {runner.config_file}'.split()) + result = runner.invoke(main_khal, f"import -a one {runner.config_file}".split()) assert not result.exception - assert {cal['name'] for cal in fake.args[0].calendars} == {'one'} + assert {cal["name"] for cal in fake.args[0].calendars} == {"one"} fake.clean() - result = runner.invoke(main_khal, f'import {runner.config_file}'.split()) + result = runner.invoke(main_khal, f"import {runner.config_file}".split()) assert not result.exception - assert {cal['name'] for cal in fake.args[0].calendars} == {'one', 'two', 'three'} + assert {cal["name"] for cal in fake.args[0].calendars} == {"one", "two", "three"} def test_import_proper(runner): runner = runner() - result = runner.invoke(main_khal, ['import', _get_ics_filepath('cal_d')], input='0\ny\n') - assert result.output.startswith('09.04.-09.04. An Event') + result = runner.invoke(main_khal, ["import", _get_ics_filepath("cal_d")], input="0\ny\n") + assert result.output.startswith("09.04.-09.04. An Event") assert not result.exception - result = runner.invoke(main_khal, ['search', 'Event']) - assert result.output == '09.04.-09.04. An Event\n' + result = runner.invoke(main_khal, ["search", "Event"]) + assert result.output == "09.04.-09.04. An Event\n" def test_import_proper_invalid_timezone(runner): runner = runner() result = runner.invoke( - main_khal, ['import', _get_ics_filepath('invalid_tzoffset')], input='0\ny\n') + main_khal, ["import", _get_ics_filepath("invalid_tzoffset")], input="0\ny\n" + ) assert result.output.startswith( - 'warning: Invalid timezone offset encountered, timezone information may be wrong') + "warning: Invalid timezone offset encountered, timezone information may be wrong" + ) assert not result.exception - result = runner.invoke(main_khal, ['search', 'Event']) + result = runner.invoke(main_khal, ["search", "Event"]) assert result.output.startswith( - 'warning: Invalid timezone offset encountered, timezone information may be wrong') - assert '02.12. 08:00-02.12. 09:30 Some event' in result.output + "warning: Invalid timezone offset encountered, timezone information may be wrong" + ) + assert "02.12. 08:00-02.12. 09:30 Some event" in result.output def test_import_invalid_choice_and_prefix(runner): runner = runner() - result = runner.invoke(main_khal, ['import', _get_ics_filepath('cal_d')], input='9\nth\ny\n') - assert result.output.startswith('09.04.-09.04. An Event') - assert result.output.find('invalid choice') == 125 + result = runner.invoke(main_khal, ["import", _get_ics_filepath("cal_d")], input="9\nth\ny\n") + assert result.output.startswith("09.04.-09.04. An Event") + assert result.output.find("invalid choice") == 125 assert not result.exception - result = runner.invoke(main_khal, ['search', 'Event']) - assert result.output == '09.04.-09.04. An Event\n' + result = runner.invoke(main_khal, ["search", "Event"]) + assert result.output == "09.04.-09.04. An Event\n" def test_import_from_stdin(runner, monkeypatch): - ics_data = 'This is some really fake icalendar data' + ics_data = "This is some really fake icalendar data" class FakeImport: args, kwargs = None, None @@ -586,20 +655,20 @@ def clean(self): self.args, self.kwargs = None, None def import_ics(self, *args, **kwargs): - print('saving args') + print("saving args") print(args) self.call_count += 1 self.args = args self.kwargs = kwargs importer = FakeImport() - monkeypatch.setattr('khal.controllers.import_ics', importer.import_ics) + monkeypatch.setattr("khal.controllers.import_ics", importer.import_ics) runner = runner() - result = runner.invoke(main_khal, ['import'], input=ics_data) + result = runner.invoke(main_khal, ["import"], input=ics_data) assert not result.exception assert importer.call_count == 1 - assert importer.kwargs['ics'] == ics_data + assert importer.kwargs["ics"] == ics_data def test_interactive_command(runner, monkeypatch): @@ -610,13 +679,13 @@ def fake_ui(*a, **kw): print(token) sys.exit(0) - monkeypatch.setattr('khal.ui.start_pane', fake_ui) + monkeypatch.setattr("khal.ui.start_pane", fake_ui) - result = runner.invoke(main_ikhal, ['-a', 'one']) + result = runner.invoke(main_ikhal, ["-a", "one"]) assert not result.exception assert result.output.strip() == token - result = runner.invoke(main_khal, ['interactive', '-a', 'one']) + result = runner.invoke(main_khal, ["interactive", "-a", "one"]) assert not result.exception assert result.output.strip() == token @@ -624,25 +693,32 @@ def fake_ui(*a, **kw): def test_color_option(runner): runner = runner(days=2) - result = runner.invoke(main_khal, ['--no-color', 'list']) - assert result.output == '' - - result = runner.invoke(main_khal, ['--color', 'list']) - assert result.output == '' - - -def choices(dateformat=0, timeformat=0, - default_calendar='', - calendar_option=0, - accept_vdirsyncer_dir=True, - vdir='', - caldav_url='', caldav_user='', caldav_pw='', - write_config=True): + result = runner.invoke(main_khal, ["--no-color", "list"]) + assert result.output == "" + + result = runner.invoke(main_khal, ["--color", "list"]) + assert result.output == "" + + +def choices( + dateformat=0, + timeformat=0, + default_calendar="", + calendar_option=0, + accept_vdirsyncer_dir=True, + vdir="", + caldav_url="", + caldav_user="", + caldav_pw="", + write_config=True, +): """helper function to generate input for testing `configure`""" - confirm = {True: 'y', False: 'n'} + confirm = {True: "y", False: "n"} out = [ - str(dateformat), str(timeformat), str(calendar_option), + str(dateformat), + str(timeformat), + str(calendar_option), ] if calendar_option == 1: out.append(confirm[accept_vdirsyncer_dir]) @@ -654,50 +730,51 @@ def choices(dateformat=0, timeformat=0, out.append(caldav_pw) out.append(default_calendar) out.append(confirm[write_config]) - out.append('') - return '\n'.join(out) + out.append("") + return "\n".join(out) class Config: """helper class for mocking vdirsyncer's config objects""" + # TODO crate a vdir config on disk and let vdirsyncer actually read it storages = { - 'home_calendar_local': { - 'type': 'filesystem', - 'instance_name': 'home_calendar_local', - 'path': '~/.local/share/calendars/home/', - 'fileext': '.ics', + "home_calendar_local": { + "type": "filesystem", + "instance_name": "home_calendar_local", + "path": "~/.local/share/calendars/home/", + "fileext": ".ics", }, - 'events_local': { - 'type': 'filesystem', - 'instance_name': 'events_local', - 'path': '~/.local/share/calendars/events/', - 'fileext': '.ics', + "events_local": { + "type": "filesystem", + "instance_name": "events_local", + "path": "~/.local/share/calendars/events/", + "fileext": ".ics", }, - 'home_calendar_remote': { - 'type': 'caldav', - 'url': 'https://some.url/caldav', - 'username': 'foo', - 'password.fetch': ['command', 'get_secret'], - 'instance_name': 'home_calendar_remote', + "home_calendar_remote": { + "type": "caldav", + "url": "https://some.url/caldav", + "username": "foo", + "password.fetch": ["command", "get_secret"], + "instance_name": "home_calendar_remote", }, - 'home_contacts_remote': { - 'type': 'carddav', - 'url': 'https://another.url/caldav', - 'username': 'bar', - 'password.fetch': ['command', 'get_secret'], - 'instance_name': 'home_contacts_remote', + "home_contacts_remote": { + "type": "carddav", + "url": "https://another.url/caldav", + "username": "bar", + "password.fetch": ["command", "get_secret"], + "instance_name": "home_contacts_remote", }, - 'home_contacts_local': { - 'type': 'filesystem', - 'instance_name': 'home_contacts_local', - 'path': '~/.local/share/contacts/', - 'fileext': '.vcf', + "home_contacts_local": { + "type": "filesystem", + "instance_name": "home_contacts_local", + "path": "~/.local/share/contacts/", + "fileext": ".vcf", }, - 'events_remote': { - 'type': 'http', - 'instance_name': 'events_remote', - 'url': 'http://list.of/events/', + "events_remote": { + "type": "http", + "instance_name": "events_remote", + "url": "http://list.of/events/", }, } @@ -707,13 +784,15 @@ def test_configure_command(runner): runner = runner() runner.config_file.remove() - result = runner.invoke(main_khal, ['configure'], input=choices()) - assert f'Successfully wrote configuration to {runner.config_file}' in result.output + result = runner.invoke(main_khal, ["configure"], input=choices()) + assert f"Successfully wrote configuration to {runner.config_file}" in result.output assert result.exit_code == 0 with open(str(runner.config_file)) as f: - actual_config = ''.join(f.readlines()) + actual_config = "".join(f.readlines()) - assert actual_config == f'''[calendars] + assert ( + actual_config + == f"""[calendars] [[private]] path = {runner.tmpdir}/vdirs/khal/calendars/private @@ -728,7 +807,8 @@ def test_configure_command(runner): [default] default_calendar = private -''' +""" + ) # if aborting, no config file should be written runner = runner_factory() @@ -736,58 +816,61 @@ def test_configure_command(runner): runner.config_file.remove() assert not os.path.exists(str(runner.config_file)) - result = runner.invoke(main_khal, ['configure'], input=choices(write_config=False)) - assert 'aborted' in result.output + result = runner.invoke(main_khal, ["configure"], input=choices(write_config=False)) + assert "aborted" in result.output assert result.exit_code == 1 def test_print_ics_command(runner): runner = runner() # Input is empty and loading from stdin - result = runner.invoke(main_khal, ['printics', '-']) + result = runner.invoke(main_khal, ["printics", "-"]) assert result.exception # Non existing file - result = runner.invoke(main_khal, ['printics', 'nonexisting_file']) + result = runner.invoke(main_khal, ["printics", "nonexisting_file"]) assert result.exception - assert re.search(r'''Error: Invalid value for "?'?\[?(ICS|ics)\]?'?"?: ''' - r'''('nonexisting_file': No such file or directory\n|''' - r'Could not open file:)', result.output) + assert re.search( + r"""Error: Invalid value for "?'?\[?(ICS|ics)\]?'?"?: """ + r"""('nonexisting_file': No such file or directory\n|""" + r"Could not open file:)", + result.output, + ) # Run on test files - result = runner.invoke(main_khal, ['printics', _get_ics_filepath('cal_d')]) + result = runner.invoke(main_khal, ["printics", _get_ics_filepath("cal_d")]) assert not result.exception - result = runner.invoke(main_khal, ['printics', _get_ics_filepath('cal_dt_two_tz')]) + result = runner.invoke(main_khal, ["printics", _get_ics_filepath("cal_dt_two_tz")]) assert not result.exception # Test with some nice format strings - form = '{uid}\t{title}\t{description}\t{start}\t{start-long}\t{start-date}' \ - '\t{start-date-long}\t{start-time}\t{end}\t{end-long}\t{end-date}' \ - '\t{end-date-long}\t{end-time}\t{repeat-symbol}\t{description}' \ - '\t{description-separator}\t{location}\t{calendar}' \ - '\t{calendar-color}\t{start-style}\t{to-style}\t{end-style}' \ - '\t{start-end-time-style}\t{end-necessary}\t{end-necessary-long}' - result = runner.invoke(main_khal, [ - 'printics', '-f', form, _get_ics_filepath('cal_dt_two_tz')]) + form = ( + "{uid}\t{title}\t{description}\t{start}\t{start-long}\t{start-date}" + "\t{start-date-long}\t{start-time}\t{end}\t{end-long}\t{end-date}" + "\t{end-date-long}\t{end-time}\t{repeat-symbol}\t{description}" + "\t{description-separator}\t{location}\t{calendar}" + "\t{calendar-color}\t{start-style}\t{to-style}\t{end-style}" + "\t{start-end-time-style}\t{end-necessary}\t{end-necessary-long}" + ) + result = runner.invoke(main_khal, ["printics", "-f", form, _get_ics_filepath("cal_dt_two_tz")]) assert not result.exception - assert 25 == len(result.output.split('\t')) - result = runner.invoke(main_khal, [ - 'printics', '-f', form, _get_ics_filepath('cal_dt_two_tz')]) + assert 25 == len(result.output.split("\t")) + result = runner.invoke(main_khal, ["printics", "-f", form, _get_ics_filepath("cal_dt_two_tz")]) assert not result.exception - assert 25 == len(result.output.split('\t')) + assert 25 == len(result.output.split("\t")) def test_printics_read_from_stdin(runner): - runner = runner(command='printics') - result = runner.invoke(main_khal, ['printics'], input=_get_text('cal_d')) + runner = runner(command="printics") + result = runner.invoke(main_khal, ["printics"], input=_get_text("cal_d")) assert not result.exception - assert '1 events found in stdin input\n09.04.-09.04. An Event\n' in result.output + assert "1 events found in stdin input\n09.04.-09.04. An Event\n" in result.output def test_configure_command_config_exists(runner): runner = runner() - result = runner.invoke(main_khal, ['configure'], input=choices()) - assert 'Found an existing' in result.output + result = runner.invoke(main_khal, ["configure"], input=choices()) + assert "Found an existing" in result.output assert result.exit_code == 1 @@ -797,15 +880,18 @@ def test_configure_command_create_vdir(runner): runner.xdg_config_home.remove() result = runner.invoke( - main_khal, ['configure'], + main_khal, + ["configure"], input=choices(), ) - assert f'Successfully wrote configuration to {runner.config_file!s}' in result.output + assert f"Successfully wrote configuration to {runner.config_file!s}" in result.output assert result.exit_code == 0 with open(str(runner.config_file)) as f: - actual_config = ''.join(f.readlines()) + actual_config = "".join(f.readlines()) - assert actual_config == f'''[calendars] + assert ( + actual_config + == f"""[calendars] [[private]] path = {runner.xdg_data_home!s}/khal/calendars/private @@ -820,21 +906,23 @@ def test_configure_command_create_vdir(runner): [default] default_calendar = private -''' +""" + ) # running configure again, should yield another vdir path, as the old # one still exists runner.config_file.remove() result = runner.invoke( - main_khal, ['configure'], + main_khal, + ["configure"], input=choices(), ) - assert f'Successfully wrote configuration to {runner.config_file!s}' in result.output + assert f"Successfully wrote configuration to {runner.config_file!s}" in result.output assert result.exit_code == 0 with open(str(runner.config_file)) as f: - actual_config = ''.join(f.readlines()) + actual_config = "".join(f.readlines()) - assert f'{runner.xdg_data_home}/khal/calendars/private1' in actual_config + assert f"{runner.xdg_data_home}/khal/calendars/private1" in actual_config def cleanup(paths): @@ -852,7 +940,7 @@ def test_configure_command_cannot_write_config_file(runner): runner = runner() runner.config_file.remove() os.chmod(str(runner.xdg_config_home), 555) - result = runner.invoke(main_khal, ['configure'], input=choices()) + result = runner.invoke(main_khal, ["configure"], input=choices()) assert result.exit_code == 1 # make sure pytest can clean up behind us cleanup([runner.xdg_config_home]) @@ -863,10 +951,11 @@ def test_configure_command_cannot_create_vdir(runner): runner.config_file.remove() os.mkdir(str(runner.xdg_data_home), mode=555) result = runner.invoke( - main_khal, ['configure'], + main_khal, + ["configure"], input=choices(), ) - assert 'Exiting' in result.output + assert "Exiting" in result.output assert result.exit_code == 1 # make sure pytest can clean up behind us cleanup([runner.xdg_data_home]) @@ -874,122 +963,133 @@ def test_configure_command_cannot_create_vdir(runner): def test_edit(runner): runner = runner() - result = runner.invoke(main_khal, ['list']) + result = runner.invoke(main_khal, ["list"]) assert not result.exception - assert result.output == '' + assert result.output == "" - for name in ['event_dt_simple', 'event_d_15']: + for name in ["event_dt_simple", "event_d_15"]: cal_dt = _get_text(name) - event = runner.calendars['one'].join(f'{name}.ics') + event = runner.calendars["one"].join(f"{name}.ics") event.write(cal_dt) - format = '{start-end-time-style}: {title}' + format = "{start-end-time-style}: {title}" result = runner.invoke( - main_khal, ['edit', '--show-past', 'Event'], input='s\nGreat Event\nn\nn\n') + main_khal, ["edit", "--show-past", "Event"], input="s\nGreat Event\nn\nn\n" + ) assert not result.exception - args = ['list', '--format', format, '--day-format', '', '09.04.2014'] + args = ["list", "--format", format, "--day-format", "", "09.04.2014"] result = runner.invoke(main_khal, args) - assert '09:30-10:30: Great Event' in result.output + assert "09:30-10:30: Great Event" in result.output assert not result.exception - args = ['list', '--format', format, '--day-format', '', '09.04.2015'] + args = ["list", "--format", format, "--day-format", "", "09.04.2015"] result = runner.invoke(main_khal, args) - assert ': An Event' in result.output + assert ": An Event" in result.output assert not result.exception def test_new(runner): - runner = runner(print_new='path') + runner = runner(print_new="path") - result = runner.invoke(main_khal, 'new 13.03.2016 3d Visit'.split()) + result = runner.invoke(main_khal, "new 13.03.2016 3d Visit".split()) assert not result.exception - assert result.output.endswith('.ics\n') + assert result.output.endswith(".ics\n") assert result.output.startswith(str(runner.tmpdir)) def test_new_format(runner): - runner = runner(print_new='event') + runner = runner(print_new="event") - format = '{start-end-time-style}: {title}' - result = runner.invoke(main_khal, ['new', '13.03.2016 12:00', '3d', - '--format', format, 'Visit']) + format = "{start-end-time-style}: {title}" + result = runner.invoke( + main_khal, ["new", "13.03.2016 12:00", "3d", "--format", format, "Visit"] + ) assert not result.exception - assert result.output.startswith('→12:00: Visit') + assert result.output.startswith("→12:00: Visit") def test_new_json(runner): - runner = runner(print_new='event') + runner = runner(print_new="event") - result = runner.invoke(main_khal, ['new', '13.03.2016 12:00', '3d', - '--json', 'start-end-time-style', '--json', 'title', 'Visit']) + result = runner.invoke( + main_khal, + [ + "new", + "13.03.2016 12:00", + "3d", + "--json", + "start-end-time-style", + "--json", + "title", + "Visit", + ], + ) assert not result.exception - assert result.output.startswith( - '[{"start-end-time-style": "→12:00", "title": "Visit"}]') + assert result.output.startswith('[{"start-end-time-style": "→12:00", "title": "Visit"}]') -@ freeze_time('2015-6-1 8:00') +@freeze_time("2015-6-1 8:00") def test_new_interactive(runner): - runner = runner(print_new='path') + runner = runner(print_new="path") - result = runner.invoke( - main_khal, 'new -i'.split(), - 'Another event\n13:00 17:00\n\nNone\nn\n' - ) + result = runner.invoke(main_khal, "new -i".split(), "Another event\n13:00 17:00\n\nNone\nn\n") assert not result.exception assert result.exit_code == 0 def test_debug(runner): runner = runner() - result = runner.invoke(main_khal, ['-v', 'debug', 'printformats']) - assert result.output.startswith('debug: khal 0.') - assert 'using the config file at' in result.output - assert 'debug: Using config:\ndebug: [calendars]' in result.output + result = runner.invoke(main_khal, ["-v", "debug", "printformats"]) + assert result.output.startswith("debug: khal 0.") + assert "using the config file at" in result.output + assert "debug: Using config:\ndebug: [calendars]" in result.output assert not result.exception -@freeze_time('2015-6-1 8:00') +@freeze_time("2015-6-1 8:00") def test_new_interactive_extensive(runner): - runner = runner(print_new='path', default_calendar=False) + runner = runner(print_new="path", default_calendar=False) result = runner.invoke( - main_khal, 'new -i 15:00 15:30'.split(), - '?\ninvalid\ntwo\n' - 'Unicce Name\n' - '\n' - 'Europe/London\n' - 'bar\n' - 'l\non a boat\n' - 'p\nweekly\n' - '1.1.2018\n' - 'a\n30m\n' - 'c\nwork\n' - 'n\n' + main_khal, + "new -i 15:00 15:30".split(), + "?\ninvalid\ntwo\n" + "Unicce Name\n" + "\n" + "Europe/London\n" + "bar\n" + "l\non a boat\n" + "p\nweekly\n" + "1.1.2018\n" + "a\n30m\n" + "c\nwork\n" + "n\n", ) assert not result.exception assert result.exit_code == 0 -@freeze_time('2015-6-1 8:00') +@freeze_time("2015-6-1 8:00") def test_issue_1056(runner): """if an ansi escape sequence is contained in the output, we can't parse it properly""" - runner = runner(print_new='path', default_calendar=False) + runner = runner(print_new="path", default_calendar=False) result = runner.invoke( - main_khal, 'new -i'.split(), - 'two\n' - 'new event\n' - 'now\n' - 'Europe/London\n' - 'None\n' - 't\n' # edit datetime range - '\n' - 'n\n' + main_khal, + "new -i".split(), + "two\n" + "new event\n" + "now\n" + "Europe/London\n" + "None\n" + "t\n" # edit datetime range + "\n" + "n\n", ) - assert 'error parsing range' not in result.output + assert "error parsing range" not in result.output assert not result.exception assert result.exit_code == 0 @@ -998,9 +1098,10 @@ def test_list_now(runner, tmpdir): # reproduce #693 runner = runner() - xdg_config_home = tmpdir.join('.config') - config_file = xdg_config_home.join('khal').join('config') - config_file.write(""" + xdg_config_home = tmpdir.join(".config") + config_file = xdg_config_home.join("khal").join("config") + config_file.write( + """ [calendars] [[one]] path = {} @@ -1014,10 +1115,11 @@ def test_list_now(runner, tmpdir): longdateformat = %a %Y-%m-%d dateformat = %Y-%m-%d """.format( - tmpdir.join('calendar'), - tmpdir.join('calendar2'), - tmpdir.join('calendar3'), - )) + tmpdir.join("calendar"), + tmpdir.join("calendar2"), + tmpdir.join("calendar3"), + ) + ) - result = runner.invoke(main_khal, ['list', 'now']) + result = runner.invoke(main_khal, ["list", "now"]) assert not result.exception diff --git a/tests/configwizard_test.py b/tests/configwizard_test.py index 1b2a52a9b..084fe49ce 100644 --- a/tests/configwizard_test.py +++ b/tests/configwizard_test.py @@ -5,16 +5,25 @@ def test_validate_int(): - assert validate_int('3', 0, 3) == 3 + assert validate_int("3", 0, 3) == 3 with pytest.raises(click.UsageError): - validate_int('3', 0, 2) + validate_int("3", 0, 2) with pytest.raises(click.UsageError): - validate_int('two', 0, 2) + validate_int("two", 0, 2) def test_default_vdir(metavdirs): - names = get_collection_names_from_vdirs([('found', f'{metavdirs}/**/', 'discover')]) + names = get_collection_names_from_vdirs([("found", f"{metavdirs}/**/", "discover")]) assert names == [ - 'my private calendar', 'my calendar', 'public', 'home', 'public1', 'work', - 'cfgcolor', 'cfgcolor_again', 'cfgcolor_once_more', 'dircolor', 'singlecollection', + "my private calendar", + "my calendar", + "public", + "home", + "public1", + "work", + "cfgcolor", + "cfgcolor_again", + "cfgcolor_once_more", + "dircolor", + "singlecollection", ] diff --git a/tests/conftest.py b/tests/conftest.py index 14e5de695..56e9080f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,29 +15,29 @@ def metavdirs(tmpdir): tmpdir = str(tmpdir) dirstructure = [ - '/cal1/public/', - '/cal1/private/', - '/cal2/public/', - '/cal3/public/', - '/cal3/work/', - '/cal3/home/', - '/cal4/cfgcolor/', - '/cal4/dircolor/', - '/cal4/cfgcolor_again/', - '/cal4/cfgcolor_once_more/', - '/singlecollection/', + "/cal1/public/", + "/cal1/private/", + "/cal2/public/", + "/cal3/public/", + "/cal3/work/", + "/cal3/home/", + "/cal4/cfgcolor/", + "/cal4/dircolor/", + "/cal4/cfgcolor_again/", + "/cal4/cfgcolor_once_more/", + "/singlecollection/", ] for one in dirstructure: os.makedirs(tmpdir + one) filestructure = [ - ('/cal1/public/displayname', 'my calendar'), - ('/cal1/public/color', 'dark blue'), - ('/cal1/private/displayname', 'my private calendar'), - ('/cal1/private/color', '#FF00FF'), - ('/cal4/dircolor/color', 'dark blue'), + ("/cal1/public/displayname", "my calendar"), + ("/cal1/public/color", "dark blue"), + ("/cal1/private/displayname", "my private calendar"), + ("/cal1/private/color", "#FF00FF"), + ("/cal4/dircolor/color", "dark blue"), ] for filename, content in filestructure: - with open(tmpdir + filename, 'w') as metafile: + with open(tmpdir + filename, "w") as metafile: metafile.write(content) return tmpdir @@ -46,20 +46,20 @@ def metavdirs(tmpdir): def coll_vdirs(tmpdir) -> CollVdirType: calendars, vdirs = {}, {} for name in example_cals: - path = str(tmpdir) + '/' + name + path = str(tmpdir) + "/" + name os.makedirs(path, mode=0o770) - readonly = name == 'a_calendar' + readonly = name == "a_calendar" calendars[name] = CalendarConfiguration( name=name, path=path, readonly=readonly, - color='dark blue', + color="dark blue", priority=10, - ctype='calendar', - addresses='user@example.com', + ctype="calendar", + addresses="user@example.com", ) - vdirs[name] = Vdir(path, '.ics') - coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) + vdirs[name] = Vdir(path, ".ics") + coll = CalendarCollection(calendars=calendars, dbpath=":memory:", locale=LOCALE_BERLIN) coll.default_calendar_name = cal1 return coll, vdirs @@ -68,21 +68,27 @@ def coll_vdirs(tmpdir) -> CollVdirType: def coll_vdirs_birthday(tmpdir): calendars, vdirs = {}, {} for name in example_cals: - path = str(tmpdir) + '/' + name + path = str(tmpdir) + "/" + name os.makedirs(path, mode=0o770) - readonly = name == 'a_calendar' - calendars[name] = {'name': name, 'path': path, 'color': 'dark blue', - 'readonly': readonly, 'unicode_symbols': True, 'ctype': 'birthdays', - 'addresses': 'user@example.com'} - vdirs[name] = Vdir(path, '.vcf') - coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) + readonly = name == "a_calendar" + calendars[name] = { + "name": name, + "path": path, + "color": "dark blue", + "readonly": readonly, + "unicode_symbols": True, + "ctype": "birthdays", + "addresses": "user@example.com", + } + vdirs[name] = Vdir(path, ".vcf") + coll = CalendarCollection(calendars=calendars, dbpath=":memory:", locale=LOCALE_BERLIN) coll.default_calendar_name = cal1 return coll, vdirs @pytest.fixture(autouse=True) def never_echo_bytes(monkeypatch): - '''Click's echo function will not strip colorcodes if we call `click.echo` + """Click's echo function will not strip colorcodes if we call `click.echo` with a bytestring message. The reason for this that bytestrings may contain arbitrary binary data (such as images). @@ -90,24 +96,24 @@ def never_echo_bytes(monkeypatch): instances where it explicitly encodes its output into the configured locale. This in turn would break the functionality of the global `--color/--no-color` flag. - ''' + """ from click import echo as old_echo def echo(msg=None, *a, **kw): assert not isinstance(msg, bytes) return old_echo(msg, *a, **kw) - monkeypatch.setattr('click.echo', echo) + monkeypatch.setattr("click.echo", echo) class Result: @staticmethod def undo(): - monkeypatch.setattr('click.echo', old_echo) + monkeypatch.setattr("click.echo", old_echo) return Result -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def sleep_time(tmpdir_factory): """ Returns the filesystem's mtime precision @@ -118,12 +124,12 @@ def sleep_time(tmpdir_factory): This keeps test fast on systems with high precisions, but makes them pass on those that don't. """ - tmpfile = tmpdir_factory.mktemp('sleep').join('touch_me') + tmpfile = tmpdir_factory.mktemp("sleep").join("touch_me") def touch_and_mtime(): - tmpfile.open('w').close() + tmpfile.open("w").close() stat = os.stat(str(tmpfile)) - return getattr(stat, 'st_mtime_ns', stat.st_mtime) + return getattr(stat, "st_mtime_ns", stat.st_mtime) i = 0.00001 while i < 100: @@ -140,14 +146,14 @@ def touch_and_mtime(): # This should never happen, but oh, well: raise Exception( - 'Filesystem does not seem to save modified times of files. \n' - 'Cannot run tests that depend on this.' + "Filesystem does not seem to save modified times of files. \n" + "Cannot run tests that depend on this." ) @pytest.fixture def fix_caplog(monkeypatch): """Temporarily undoes the logging setup by click-log such that the caplog fixture can be used""" - logger = logging.getLogger('khal') - monkeypatch.setattr(logger, 'handlers', []) - monkeypatch.setattr(logger, 'propagate', True) + logger = logging.getLogger("khal") + monkeypatch.setattr(logger, "handlers", []) + monkeypatch.setattr(logger, "propagate", True) diff --git a/tests/controller_test.py b/tests/controller_test.py index dfe52ff94..629b6c80f 100644 --- a/tests/controller_test.py +++ b/tests/controller_test.py @@ -25,16 +25,16 @@ LOCATION:LDB Lobby END:VEVENT""" -event_today = event_allday_template.format(today.strftime('%Y%m%d'), - tomorrow.strftime('%Y%m%d')) +event_today = event_allday_template.format(today.strftime("%Y%m%d"), tomorrow.strftime("%Y%m%d")) item_today = Item(event_today) -event_format = '{calendar-color}{start-end-time-style:16} {title}' -event_format += '{repeat-symbol}{description-separator}{description}{calendar-color}' +event_format = "{calendar-color}{start-end-time-style:16} {title}" +event_format += "{repeat-symbol}{description-separator}{description}{calendar-color}" -conf = {'locale': utils.LOCALE_BERLIN, - 'default': {'timedelta': dt.timedelta(days=2), 'show_all_days': False} - } +conf = { + "locale": utils.LOCALE_BERLIN, + "default": {"timedelta": dt.timedelta(days=2), "show_all_days": False}, +} class TestGetAgenda: @@ -42,141 +42,150 @@ def test_new_event(self, coll_vdirs): coll, vdirs = coll_vdirs event = coll.create_event_from_ics(event_today, utils.cal1) coll.insert(event) - assert [' a meeting :: short description\x1b[0m'] == \ - khal_list(coll, [], conf, agenda_format=event_format, day_format="") + assert [" a meeting :: short description\x1b[0m"] == khal_list( + coll, [], conf, agenda_format=event_format, day_format="" + ) def test_new_event_day_format(self, coll_vdirs): coll, vdirs = coll_vdirs event = coll.create_event_from_ics(event_today, utils.cal1) coll.insert(event) - assert ['Today\x1b[0m', - ' a meeting :: short description\x1b[0m'] == \ - khal_list(coll, [], conf, agenda_format=event_format, day_format="{name}") + assert [ + "Today\x1b[0m", + " a meeting :: short description\x1b[0m", + ] == khal_list(coll, [], conf, agenda_format=event_format, day_format="{name}") def test_agenda_default_day_format(self, coll_vdirs): - with freeze_time('2016-04-10 12:33'): + with freeze_time("2016-04-10 12:33"): today = dt.date.today() event_today = event_allday_template.format( - today.strftime('%Y%m%d'), tomorrow.strftime('%Y%m%d')) + today.strftime("%Y%m%d"), tomorrow.strftime("%Y%m%d") + ) coll, vdirs = coll_vdirs event = coll.create_event_from_ics(event_today, utils.cal1) coll.insert(event) - out = khal_list( - coll, conf=conf, agenda_format=event_format, datepoint=[]) + out = khal_list(coll, conf=conf, agenda_format=event_format, datepoint=[]) assert [ - '\x1b[1m10.04.2016 12:33\x1b[0m\x1b[0m', - '↦ a meeting :: short description\x1b[0m'] == out + "\x1b[1m10.04.2016 12:33\x1b[0m\x1b[0m", + "↦ a meeting :: short description\x1b[0m", + ] == out def test_agenda_fail(self, coll_vdirs): - with freeze_time('2016-04-10 12:33'): + with freeze_time("2016-04-10 12:33"): coll, vdirs = coll_vdirs with pytest.raises(exceptions.FatalError): - khal_list(coll, conf=conf, agenda_format=event_format, datepoint=['xyz']) + khal_list(coll, conf=conf, agenda_format=event_format, datepoint=["xyz"]) with pytest.raises(exceptions.FatalError): - khal_list(coll, conf=conf, agenda_format=event_format, datepoint=['today']) + khal_list(coll, conf=conf, agenda_format=event_format, datepoint=["today"]) def test_empty_recurrence(self, coll_vdirs): coll, vidrs = coll_vdirs - coll.insert(coll.create_event_from_ics(dedent( - 'BEGIN:VEVENT\r\n' - 'UID:no_recurrences\r\n' - 'SUMMARY:No recurrences\r\n' - 'RRULE:FREQ=DAILY;COUNT=2;INTERVAL=1\r\n' - 'EXDATE:20110908T130000\r\n' - 'EXDATE:20110909T130000\r\n' - 'DTSTART:20110908T130000\r\n' - 'DTEND:20110908T170000\r\n' - 'END:VEVENT\r\n' - ), utils.cal1)) - assert '\n'.join(khal_list(coll, [], conf, - agenda_format=event_format, day_format="{name}")).lower() == '' + coll.insert( + coll.create_event_from_ics( + dedent( + "BEGIN:VEVENT\r\n" + "UID:no_recurrences\r\n" + "SUMMARY:No recurrences\r\n" + "RRULE:FREQ=DAILY;COUNT=2;INTERVAL=1\r\n" + "EXDATE:20110908T130000\r\n" + "EXDATE:20110909T130000\r\n" + "DTSTART:20110908T130000\r\n" + "DTEND:20110908T170000\r\n" + "END:VEVENT\r\n" + ), + utils.cal1, + ) + ) + assert ( + "\n".join( + khal_list(coll, [], conf, agenda_format=event_format, day_format="{name}") + ).lower() + == "" + ) class TestImport: def test_import(self, coll_vdirs): coll, vdirs = coll_vdirs - view = {'event_format': '{title}'} - conf = {'locale': utils.LOCALE_BERLIN, 'view': view} - import_ics(coll, conf, _get_text('event_rrule_recuid'), batch=True) + view = {"event_format": "{title}"} + conf = {"locale": utils.LOCALE_BERLIN, "view": view} + import_ics(coll, conf, _get_text("event_rrule_recuid"), batch=True) start_date = utils.BERLIN.localize(dt.datetime(2014, 4, 30)) end_date = utils.BERLIN.localize(dt.datetime(2014, 9, 26)) events = list(coll.get_localized(start_date, end_date)) assert len(events) == 6 events = sorted(events) assert events[1].start_local == utils.BERLIN.localize(dt.datetime(2014, 7, 7, 9, 0)) - assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) in \ - [ev.start for ev in events] + assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) in [ev.start for ev in events] - import_ics(coll, conf, _get_text('event_rrule_recuid_update'), batch=True) + import_ics(coll, conf, _get_text("event_rrule_recuid_update"), batch=True) events = list(coll.get_localized(start_date, end_date)) for ev in events: print(ev.start) - assert ev.calendar == 'foobar' + assert ev.calendar == "foobar" assert len(events) == 5 - assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) not in \ - [ev.start_local for ev in events] + assert utils.BERLIN.localize(dt.datetime(2014, 7, 14, 7, 0)) not in [ + ev.start_local for ev in events + ] def test_mix_datetime_types(self, coll_vdirs): """ Test importing events with mixed tz-aware and tz-naive datetimes. """ coll, vdirs = coll_vdirs - view = {'event_format': '{title}'} + view = {"event_format": "{title}"} import_ics( coll, - {'locale': utils.LOCALE_BERLIN, 'view': view}, - _get_text('event_dt_mixed_awareness'), - batch=True + {"locale": utils.LOCALE_BERLIN, "view": view}, + _get_text("event_dt_mixed_awareness"), + batch=True, ) start_date = utils.BERLIN.localize(dt.datetime(2015, 5, 29)) end_date = utils.BERLIN.localize(dt.datetime(2015, 6, 3)) events = list(coll.get_localized(start_date, end_date)) assert len(events) == 2 events = sorted(events) - assert events[0].start_local == \ - utils.BERLIN.localize(dt.datetime(2015, 5, 30, 12, 0)) - assert events[0].end_local == \ - utils.BERLIN.localize(dt.datetime(2015, 5, 30, 16, 0)) - assert events[1].start_local == \ - utils.BERLIN.localize(dt.datetime(2015, 6, 2, 12, 0)) - assert events[1].end_local == \ - utils.BERLIN.localize(dt.datetime(2015, 6, 2, 16, 0)) + assert events[0].start_local == utils.BERLIN.localize(dt.datetime(2015, 5, 30, 12, 0)) + assert events[0].end_local == utils.BERLIN.localize(dt.datetime(2015, 5, 30, 16, 0)) + assert events[1].start_local == utils.BERLIN.localize(dt.datetime(2015, 6, 2, 12, 0)) + assert events[1].end_local == utils.BERLIN.localize(dt.datetime(2015, 6, 2, 16, 0)) def test_start_end(): - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): start = dt.datetime(2016, 4, 10, 0, 0) end = dt.datetime(2016, 4, 11, 0, 0) - assert (start, end) == start_end_from_daterange(('today',), locale=utils.LOCALE_BERLIN) + assert (start, end) == start_end_from_daterange(("today",), locale=utils.LOCALE_BERLIN) def test_start_end_default_delta(): - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): start = dt.datetime(2016, 4, 10, 0, 0) end = dt.datetime(2016, 4, 11, 0, 0) - assert (start, end) == start_end_from_daterange(('today',), utils.LOCALE_BERLIN) + assert (start, end) == start_end_from_daterange(("today",), utils.LOCALE_BERLIN) def test_start_end_delta(): - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): start = dt.datetime(2016, 4, 10, 0, 0) end = dt.datetime(2016, 4, 12, 0, 0) - assert (start, end) == start_end_from_daterange(('today', '2d'), utils.LOCALE_BERLIN) + assert (start, end) == start_end_from_daterange(("today", "2d"), utils.LOCALE_BERLIN) def test_start_end_empty(): - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): start = dt.datetime(2016, 4, 10, 0, 0) end = dt.datetime(2016, 4, 11, 0, 0) assert (start, end) == start_end_from_daterange([], utils.LOCALE_BERLIN) def test_start_end_empty_default(): - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): start = dt.datetime(2016, 4, 10, 0, 0) end = dt.datetime(2016, 4, 13, 0, 0) assert (start, end) == start_end_from_daterange( - [], utils.LOCALE_BERLIN, + [], + utils.LOCALE_BERLIN, default_timedelta_date=dt.timedelta(days=3), default_timedelta_datetime=dt.timedelta(hours=1), ) diff --git a/tests/event_test.py b/tests/event_test.py index 8255a76bf..4cb7073b5 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -23,46 +23,52 @@ normalize_component, ) -EVENT_KWARGS = {'calendar': 'foobar', 'locale': LOCALE_BERLIN} +EVENT_KWARGS = {"calendar": "foobar", "locale": LOCALE_BERLIN} -LIST_FORMAT = '{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}' +LIST_FORMAT = "{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}" LIST_FORMATTER = human_formatter(LIST_FORMAT) -SEARCH_FORMAT = '{calendar-color}{cancelled}{start-long}{to-style}' + \ - '{end-necessary-long} {title}{repeat-symbol}' -CALENDAR_FORMAT = ('{calendar-color}{cancelled}{start-end-time-style} ({calendar}) ' - '{title} [{location}]{repeat-symbol}') +SEARCH_FORMAT = ( + "{calendar-color}{cancelled}{start-long}{to-style}" + + "{end-necessary-long} {title}{repeat-symbol}" +) +CALENDAR_FORMAT = ( + "{calendar-color}{cancelled}{start-end-time-style} ({calendar}) " + "{title} [{location}]{repeat-symbol}" +) CALENDAR_FORMATTER = human_formatter(CALENDAR_FORMAT) SEARCH_FORMATTER = human_formatter(SEARCH_FORMAT) def test_no_initialization(): with pytest.raises(ValueError, match="do not initialize this class directly"): - Event('', '') + Event("", "") def test_invalid_keyword_argument(): with pytest.raises(TypeError): - Event.fromString(_get_text('event_dt_simple'), keyword='foo') + Event.fromString(_get_text("event_dt_simple"), keyword="foo") def test_raw_dt(): - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event = Event.fromString(event_dt, start=start, end=end, **EVENT_KWARGS) - with freeze_time('2016-1-1'): - assert normalize_component(event.raw) == \ - normalize_component(_get_text('event_dt_simple_inkl_vtimezone')) + with freeze_time("2016-1-1"): + assert normalize_component(event.raw) == normalize_component( + _get_text("event_dt_simple_inkl_vtimezone") + ) event = Event.fromString(event_dt, **EVENT_KWARGS) - assert LIST_FORMATTER(event.attributes( - dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-10:30 An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-10:30 An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-10:30 An Event\x1b[0m" + ) assert event.recurring is False assert event.duration == dt.timedelta(hours=1) - assert event.uid == 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU' - assert event.organizer == '' + assert event.uid == "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU" + assert event.organizer == "" def test_calendar_in_format(): @@ -70,122 +76,128 @@ def test_calendar_in_format(): see #1121 """ - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event = Event.fromString(event_dt, start=start, end=end, **EVENT_KWARGS) - assert CALENDAR_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09:30-10:30 (foobar) An Event []\x1b[0m' + assert ( + CALENDAR_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09:30-10:30 (foobar) An Event []\x1b[0m" + ) def test_update_simple(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - event_updated = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS) - event.update_summary('A not so simple Event') - event.update_description('Everything has changed') - event.update_location('anywhere') - event.update_categories(['meeting']) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + event_updated = Event.fromString(_get_text("event_dt_simple_updated"), **EVENT_KWARGS) + event.update_summary("A not so simple Event") + event.update_description("Everything has changed") + event.update_location("anywhere") + event.update_categories(["meeting"]) assert normalize_component(event.raw) == normalize_component(event_updated.raw) def test_add_url(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - event.update_url('https://github.com/pimutils/khal') - assert 'URL:https://github.com/pimutils/khal' in event.raw + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + event.update_url("https://github.com/pimutils/khal") + assert "URL:https://github.com/pimutils/khal" in event.raw def test_get_url(): - event = Event.fromString(_get_text('event_dt_url'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_url"), **EVENT_KWARGS) assert event.url == "https://github.com/pimutils/khal" def test_no_end(): """reading an event with neither DTEND nor DURATION""" - event = Event.fromString(_get_text('event_dt_no_end'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_no_end"), **EVENT_KWARGS) # TODO make sure the event also gets converted to an all day event, as we # usually do - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == \ - '16.01.2016 08:00-17.01.2016 08:00 Test\x1b[0m' + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 12))) + == "16.01.2016 08:00-17.01.2016 08:00 Test\x1b[0m" + ) def test_do_not_save_empty_location(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - event.update_location('') - assert 'LOCATION' not in event.raw + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + event.update_location("") + assert "LOCATION" not in event.raw def test_do_not_save_empty_description(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - event.update_description('') - assert 'DESCRIPTION' not in event.raw + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + event.update_description("") + assert "DESCRIPTION" not in event.raw def test_do_not_save_empty_url(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - event.update_url('') - assert 'URL' not in event.raw + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + event.update_url("") + assert "URL" not in event.raw def test_remove_existing_location_if_set_to_empty(): - event = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS) - event.update_location('') - assert 'LOCATION' not in event.raw + event = Event.fromString(_get_text("event_dt_simple_updated"), **EVENT_KWARGS) + event.update_location("") + assert "LOCATION" not in event.raw def test_remove_existing_description_if_set_to_empty(): - event = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS) - event.update_description('') - assert 'DESCRIPTION' not in event.raw + event = Event.fromString(_get_text("event_dt_simple_updated"), **EVENT_KWARGS) + event.update_description("") + assert "DESCRIPTION" not in event.raw def test_remove_existing_url_if_set_to_empty(): - event = Event.fromString(_get_text('event_dt_url'), **EVENT_KWARGS) - event.update_url('') - assert 'URL' not in event.raw + event = Event.fromString(_get_text("event_dt_url"), **EVENT_KWARGS) + event.update_url("") + assert "URL" not in event.raw def test_update_remove_categories(): - event = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS) - event_nocat = Event.fromString(_get_text('event_dt_simple_nocat'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple_updated"), **EVENT_KWARGS) + event_nocat = Event.fromString(_get_text("event_dt_simple_nocat"), **EVENT_KWARGS) event.update_categories([]) assert normalize_component(event.raw) == normalize_component(event_nocat.raw) def test_raw_d(): - event_d = _get_text('event_d') + event_d = _get_text("event_d") event = Event.fromString(event_d, **EVENT_KWARGS) - assert event.raw.split('\r\n') == _get_text('cal_d').split('\n') - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == ' An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09.04.2014 An Event\x1b[0m' + assert event.raw.split("\r\n") == _get_text("cal_d").split("\n") + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == " An Event\x1b[0m" + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09.04.2014 An Event\x1b[0m" def test_update_sequence(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) event.increment_sequence() - assert event._vevents['PROTO']['SEQUENCE'] == 0 + assert event._vevents["PROTO"]["SEQUENCE"] == 0 event.increment_sequence() - assert event._vevents['PROTO']['SEQUENCE'] == 1 + assert event._vevents["PROTO"]["SEQUENCE"] == 1 def test_event_organizer(): - event = _get_text('event_dt_duration') + event = _get_text("event_dt_duration") event = Event.fromString(event, **EVENT_KWARGS) - assert event.organizer == 'Frank Nord (frank@nord.tld)' + assert event.organizer == "Frank Nord (frank@nord.tld)" def test_transform_event(): """test if transformation between different event types works""" - event_d = _get_text('event_d') + event_d = _get_text("event_d") event = Event.fromString(event_d, **EVENT_KWARGS) assert isinstance(event, AllDayEvent) start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event.update_start_end(start, end) assert isinstance(event, LocalizedEvent) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-10:30 An Event\x1b[0m' - analog_event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-10:30 An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-10:30 An Event\x1b[0m" + ) + analog_event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) assert normalize_component(event.raw) == normalize_component(analog_event.raw) with pytest.raises( @@ -196,37 +208,41 @@ def test_transform_event(): def test_update_event_d(): - event_d = _get_text('event_d') + event_d = _get_text("event_d") event = Event.fromString(event_d, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 20), dt.date(2014, 4, 22)) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 20))) == '↦ An Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 21))) == '↔ An Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 22))) == '⇥ An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 20))) == \ - '20.04.2014-22.04.2014 An Event\x1b[0m' - assert 'DTSTART;VALUE=DATE:20140420' in event.raw.split('\r\n') - assert 'DTEND;VALUE=DATE:20140423' in event.raw.split('\r\n') + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 20))) == "↦ An Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 21))) == "↔ An Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 22))) == "⇥ An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 20))) + == "20.04.2014-22.04.2014 An Event\x1b[0m" + ) + assert "DTSTART;VALUE=DATE:20140420" in event.raw.split("\r\n") + assert "DTEND;VALUE=DATE:20140423" in event.raw.split("\r\n") def test_update_event_duration(): - event_dur = _get_text('event_dt_duration') + event_dur = _get_text("event_dt_duration") event = Event.fromString(event_dur, **EVENT_KWARGS) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) assert event.duration == dt.timedelta(hours=1) - event.update_start_end(BERLIN.localize(dt.datetime(2014, 4, 9, 8, 0)), - BERLIN.localize(dt.datetime(2014, 4, 9, 12, 0))) + event.update_start_end( + BERLIN.localize(dt.datetime(2014, 4, 9, 8, 0)), + BERLIN.localize(dt.datetime(2014, 4, 9, 12, 0)), + ) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 8, 0)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 12, 0)) assert event.duration == dt.timedelta(hours=4) def test_dt_two_tz(): - event_dt_two_tz = _get_text('event_dt_two_tz') - cal_dt_two_tz = _get_text('cal_dt_two_tz') + event_dt_two_tz = _get_text("event_dt_two_tz") + cal_dt_two_tz = _get_text("cal_dt_two_tz") event = Event.fromString(event_dt_two_tz, **EVENT_KWARGS) - with freeze_time('2016-02-16 12:00:00'): + with freeze_time("2016-02-16 12:00:00"): assert normalize_component(cal_dt_two_tz) == normalize_component(event.raw) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) @@ -234,40 +250,48 @@ def test_dt_two_tz(): # local (Berlin) time! assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 16, 30)) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-16:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-16:30 An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-16:30 An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-16:30 An Event\x1b[0m" + ) def test_event_dt_duration(): """event has no end, but duration""" - event_dt_duration = _get_text('event_dt_duration') + event_dt_duration = _get_text("event_dt_duration") event = Event.fromString(event_dt_duration, **EVENT_KWARGS) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-10:30 An Event\x1b[0m' - assert human_formatter('{duration}')(event.attributes( - relative_to=dt.date.today())) == '1h\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-10:30 An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-10:30 An Event\x1b[0m" + ) + assert ( + human_formatter("{duration}")(event.attributes(relative_to=dt.date.today())) == "1h\x1b[0m" + ) def test_event_dt_floating(): """start and end time have no timezone, i.e. a floating event""" - event_str = _get_text('event_dt_floating') + event_str = _get_text("event_dt_floating") event = Event.fromString(event_str, **EVENT_KWARGS) assert isinstance(event, FloatingEvent) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert human_formatter('{duration}')(event.attributes( - relative_to=dt.date.today())) == '1h\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-10:30 An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-10:30 An Event\x1b[0m" + assert ( + human_formatter("{duration}")(event.attributes(relative_to=dt.date.today())) == "1h\x1b[0m" + ) + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-10:30 An Event\x1b[0m" + ) assert event.start == dt.datetime(2014, 4, 9, 9, 30) assert event.end == dt.datetime(2014, 4, 9, 10, 30) assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - event = Event.fromString(event_str, calendar='foobar', locale=LOCALE_MIXED) + event = Event.fromString(event_str, calendar="foobar", locale=LOCALE_MIXED) assert event.start == dt.datetime(2014, 4, 9, 9, 30) assert event.end == dt.datetime(2014, 4, 9, 10, 30) assert event.start_local == BOGOTA.localize(dt.datetime(2014, 4, 9, 9, 30)) @@ -276,16 +300,17 @@ def test_event_dt_floating(): def test_event_dt_tz_missing(): """localized event DTSTART;TZID=foo, but VTIMEZONE components missing""" - event_str = _get_text('event_dt_local_missing_tz') + event_str = _get_text("event_dt_local_missing_tz") event = Event.fromString(event_str, **EVENT_KWARGS) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert human_formatter('{duration}')(event.attributes( - relative_to=dt.date.today())) == '1h\x1b[0m' + assert ( + human_formatter("{duration}")(event.attributes(relative_to=dt.date.today())) == "1h\x1b[0m" + ) - event = Event.fromString(event_str, calendar='foobar', locale=LOCALE_MIXED) + event = Event.fromString(event_str, calendar="foobar", locale=LOCALE_MIXED) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) assert event.start_local == BOGOTA.localize(dt.datetime(2014, 4, 9, 2, 30)) @@ -293,306 +318,345 @@ def test_event_dt_tz_missing(): def test_event_dt_rr(): - event_dt_rr = _get_text('event_dt_rr') + event_dt_rr = _get_text("event_dt_rr") event = Event.fromString(event_dt_rr, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 09:30-10:30 An Event ⟳\x1b[0m' - assert human_formatter('{repeat-pattern}')(event.attributes(dt.date(2014, 4, 9)) - ) == 'FREQ=DAILY;COUNT=10\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30-10:30 An Event ⟳\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 09:30-10:30 An Event ⟳\x1b[0m" + ) + assert ( + human_formatter("{repeat-pattern}")(event.attributes(dt.date(2014, 4, 9))) + == "FREQ=DAILY;COUNT=10\x1b[0m" + ) def test_event_d_rr(): - event_d_rr = _get_text('event_d_rr') + event_d_rr = _get_text("event_d_rr") event = Event.fromString(event_d_rr, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == ' Another Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ - '09.04.2014 Another Event ⟳\x1b[0m' - assert human_formatter('{repeat-pattern}')(event.attributes(dt.date(2014, 4, 9)) - ) == 'FREQ=DAILY;COUNT=10\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == " Another Event ⟳\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) + == "09.04.2014 Another Event ⟳\x1b[0m" + ) + assert ( + human_formatter("{repeat-pattern}")(event.attributes(dt.date(2014, 4, 9))) + == "FREQ=DAILY;COUNT=10\x1b[0m" + ) start = dt.date(2014, 4, 10) end = dt.date(2014, 4, 11) event = Event.fromString(event_d_rr, start=start, end=end, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == ' Another Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ - '10.04.2014 Another Event ⟳\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == " Another Event ⟳\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) + == "10.04.2014 Another Event ⟳\x1b[0m" + ) def test_event_rd(): - event_dt_rd = _get_text('event_dt_rd') + event_dt_rd = _get_text("event_dt_rd") event = Event.fromString(event_dt_rd, **EVENT_KWARGS) assert event.recurring is True def test_status_confirmed(): - event = Event.fromString(_get_text('event_dt_status_confirmed'), **EVENT_KWARGS) - assert event.status == 'CONFIRMED' - FORMAT_CALENDAR = ('{calendar-color}{status-symbol}{start-end-time-style} ({calendar}) ' - '{title} [{location}]{repeat-symbol}') + event = Event.fromString(_get_text("event_dt_status_confirmed"), **EVENT_KWARGS) + assert event.status == "CONFIRMED" + FORMAT_CALENDAR = ( + "{calendar-color}{status-symbol}{start-end-time-style} ({calendar}) " + "{title} [{location}]{repeat-symbol}" + ) + + assert ( + human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) + == "✔09:30-10:30 (foobar) An Event []\x1b[0m" + ) - assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ - '✔09:30-10:30 (foobar) An Event []\x1b[0m' def test_event_d_long(): - event_d_long = _get_text('event_d_long') + event_d_long = _get_text("event_d_long") event = Event.fromString(event_d_long, **EVENT_KWARGS) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '↔ Another Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 11))) == '⇥ Another Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 16))) == \ - '09.04.2014-11.04.2014 Another Event\x1b[0m' - assert human_formatter('{duration}')(event.attributes( - relative_to=dt.date(2014, 4, 11))) == '3d\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "↦ Another Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == "↔ Another Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 11))) == "⇥ Another Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == " Another Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 16))) + == "09.04.2014-11.04.2014 Another Event\x1b[0m" + ) + assert ( + human_formatter("{duration}")(event.attributes(relative_to=dt.date(2014, 4, 11))) + == "3d\x1b[0m" + ) def test_event_d_two_days(): - event_d_long = _get_text('event_d_long') + event_d_long = _get_text("event_d_long") event = Event.fromString(event_d_long, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 9), dt.date(2014, 4, 10)) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '⇥ Another Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ - '09.04.2014-10.04.2014 Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "↦ Another Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == "⇥ Another Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == " Another Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) + == "09.04.2014-10.04.2014 Another Event\x1b[0m" + ) def test_event_dt_long(): - event_dt_long = _get_text('event_dt_long') + event_dt_long = _get_text("event_dt_long") event = Event.fromString(event_dt_long, **EVENT_KWARGS) - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30→ An Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '↔ An Event\x1b[0m' - assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == '→10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ - '09.04.2014 09:30-12.04.2014 10:30 An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == "09:30→ An Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == "↔ An Event\x1b[0m" + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == "→10:30 An Event\x1b[0m" + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) + == "09.04.2014 09:30-12.04.2014 10:30 An Event\x1b[0m" + ) def test_event_no_dst(): """test the creation of a corect VTIMEZONE for timezones with no dst""" - event_no_dst = _get_text('event_no_dst') - cal_no_dst = _get_text('cal_no_dst') - event = Event.fromString(event_no_dst, calendar='foobar', locale=LOCALE_BOGOTA) - if version.parse(pytz.__version__) > version.Version('2017.1'): - if version.parse(pytz.__version__) < version.Version('2022.7'): - cal_no_dst = cal_no_dst.replace( - 'TZNAME:COT', - 'RDATE:20380118T221407\r\nTZNAME:-05' - ) + event_no_dst = _get_text("event_no_dst") + cal_no_dst = _get_text("cal_no_dst") + event = Event.fromString(event_no_dst, calendar="foobar", locale=LOCALE_BOGOTA) + if version.parse(pytz.__version__) > version.Version("2017.1"): + if version.parse(pytz.__version__) < version.Version("2022.7"): + cal_no_dst = cal_no_dst.replace("TZNAME:COT", "RDATE:20380118T221407\r\nTZNAME:-05") else: - cal_no_dst = cal_no_dst.replace( - 'TZNAME:COT', - 'TZNAME:-05' - ) + cal_no_dst = cal_no_dst.replace("TZNAME:COT", "TZNAME:-05") assert normalize_component(event.raw) == normalize_component(cal_no_dst) - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ - '09.04.2014 09:30-10:30 An Event\x1b[0m' + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) + == "09.04.2014 09:30-10:30 An Event\x1b[0m" + ) def test_event_raw_UTC(): """test .raw() on events which are localized in UTC""" - event_utc = _get_text('event_dt_simple_zulu') + event_utc = _get_text("event_dt_simple_zulu") event = Event.fromString(event_utc, **EVENT_KWARGS) - assert event.raw == '\r\n'.join([ - '''BEGIN:VCALENDAR''', - '''VERSION:2.0''', - '''PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN''', - '''BEGIN:VEVENT''', - '''SUMMARY:An Event''', - '''DTSTART:20140409T093000Z''', - '''DTEND:20140409T103000Z''', - '''DTSTAMP:20140401T234817Z''', - '''UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU''', - '''END:VEVENT''', - '''END:VCALENDAR\r\n''']) + assert event.raw == "\r\n".join( + [ + """BEGIN:VCALENDAR""", + """VERSION:2.0""", + """PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN""", + """BEGIN:VEVENT""", + """SUMMARY:An Event""", + """DTSTART:20140409T093000Z""", + """DTEND:20140409T103000Z""", + """DTSTAMP:20140401T234817Z""", + """UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU""", + """END:VEVENT""", + """END:VCALENDAR\r\n""", + ] + ) def test_zulu_events(): """test if events in Zulu time are correctly recognized as localized events""" - event = Event.fromString(_get_text('event_dt_simple_zulu'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple_zulu"), **EVENT_KWARGS) assert type(event) is LocalizedEvent assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 11, 30)) def test_dtend_equals_dtstart(): - event = Event.fromString(_get_text('event_d_same_start_end'), - calendar='foobar', locale=LOCALE_BERLIN) + event = Event.fromString( + _get_text("event_d_same_start_end"), calendar="foobar", locale=LOCALE_BERLIN + ) assert event.end == event.start def test_multi_uid(): """test for support for events with consist of several sub events with the same uid""" - orig_event_str = _get_text('event_rrule_recuid') + orig_event_str = _get_text("event_rrule_recuid") event = Event.fromString(orig_event_str, **EVENT_KWARGS) - for line in orig_event_str.split('\n'): - assert line in event.raw.split('\r\n') + for line in orig_event_str.split("\n"): + assert line in event.raw.split("\r\n") def test_cancelled_instance(): - orig_event_str = _get_text('event_rrule_recuid_cancelled') - event = Event.fromString(orig_event_str, ref='1405314000', **EVENT_KWARGS) - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) == \ - 'CANCELLED 14.07.2014 07:00-12:00 Arbeit ⟳\x1b[0m' - event = Event.fromString(orig_event_str, ref='PROTO', **EVENT_KWARGS) - assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) == \ - '30.06.2014 07:00-12:00 Arbeit ⟳\x1b[0m' + orig_event_str = _get_text("event_rrule_recuid_cancelled") + event = Event.fromString(orig_event_str, ref="1405314000", **EVENT_KWARGS) + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) + == "CANCELLED 14.07.2014 07:00-12:00 Arbeit ⟳\x1b[0m" + ) + event = Event.fromString(orig_event_str, ref="PROTO", **EVENT_KWARGS) + assert ( + SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) + == "30.06.2014 07:00-12:00 Arbeit ⟳\x1b[0m" + ) def test_recur(): - event = Event.fromString(_get_text('event_dt_rr'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_rr"), **EVENT_KWARGS) assert event.recurring is True - assert event.recurpattern == 'FREQ=DAILY;COUNT=10' - assert event.recurobject == vRecur({'COUNT': [10], 'FREQ': ['DAILY']}) + assert event.recurpattern == "FREQ=DAILY;COUNT=10" + assert event.recurobject == vRecur({"COUNT": [10], "FREQ": ["DAILY"]}) def test_type_inference(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) assert type(event) is LocalizedEvent - event = Event.fromString(_get_text('event_dt_simple_zulu'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple_zulu"), **EVENT_KWARGS) assert type(event) is LocalizedEvent def test_duplicate_event(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) dupe = event.duplicate() - assert dupe._vevents['PROTO']['UID'].to_ical() != 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU' + assert dupe._vevents["PROTO"]["UID"].to_ical() != "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU" def test_remove_instance_from_rrule(): """removing an instance from a recurring event""" - event = Event.fromString(_get_text('event_dt_rr'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_rr"), **EVENT_KWARGS) event.delete_instance(dt.datetime(2014, 4, 10, 9, 30)) - assert 'EXDATE:20140410T093000' in event.raw.split('\r\n') + assert "EXDATE:20140410T093000" in event.raw.split("\r\n") event.delete_instance(dt.datetime(2014, 4, 12, 9, 30)) - assert 'EXDATE:20140410T093000,20140412T093000' in event.raw.split('\r\n') + assert "EXDATE:20140410T093000,20140412T093000" in event.raw.split("\r\n") def test_remove_instance_from_rdate(): """removing an instance from a recurring event""" - event = Event.fromString(_get_text('event_dt_rd'), **EVENT_KWARGS) - assert 'RDATE' in event.raw + event = Event.fromString(_get_text("event_dt_rd"), **EVENT_KWARGS) + assert "RDATE" in event.raw event.delete_instance(dt.datetime(2014, 4, 10, 9, 30)) - assert 'RDATE' not in event.raw + assert "RDATE" not in event.raw def test_remove_instance_from_two_rdate(): """removing an instance from a recurring event which has two RDATE props""" - event = Event.fromString(_get_text('event_dt_two_rd'), **EVENT_KWARGS) - assert event.raw.count('RDATE') == 2 + event = Event.fromString(_get_text("event_dt_two_rd"), **EVENT_KWARGS) + assert event.raw.count("RDATE") == 2 event.delete_instance(dt.datetime(2014, 4, 10, 9, 30)) - assert event.raw.count('RDATE') == 1 - assert 'RDATE:20140411T093000,20140412T093000' in event.raw.split('\r\n') + assert event.raw.count("RDATE") == 1 + assert "RDATE:20140411T093000,20140412T093000" in event.raw.split("\r\n") def test_remove_instance_from_recuid(): """remove an istance from an event which is specified via an additional VEVENT with the same UID (which we call `recuid` here""" - event = Event.fromString(_get_text('event_rrule_recuid'), **EVENT_KWARGS) - assert event.raw.split('\r\n').count('UID:event_rrule_recurrence_id') == 2 + event = Event.fromString(_get_text("event_rrule_recuid"), **EVENT_KWARGS) + assert event.raw.split("\r\n").count("UID:event_rrule_recurrence_id") == 2 event.delete_instance(BERLIN.localize(dt.datetime(2014, 7, 7, 7, 0))) - assert event.raw.split('\r\n').count('UID:event_rrule_recurrence_id') == 1 - assert 'EXDATE;TZID=Europe/Berlin:20140707T070000' in event.raw.split('\r\n') + assert event.raw.split("\r\n").count("UID:event_rrule_recurrence_id") == 1 + assert "EXDATE;TZID=Europe/Berlin:20140707T070000" in event.raw.split("\r\n") def test_format_24(): """test if events ending at 00:00/24:00 are displayed as ending the day before""" - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = BERLIN.localize(dt.datetime(2014, 4, 9, 19, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 10)) event = Event.fromString(event_dt, **EVENT_KWARGS) event.update_start_end(start, end) - format_ = '{start-end-time-style} {title}{repeat-symbol}' - assert human_formatter(format_)(event.attributes(dt.date(2014, 4, 9)) - ) == '19:30-24:00 An Event\x1b[0m' + format_ = "{start-end-time-style} {title}{repeat-symbol}" + assert ( + human_formatter(format_)(event.attributes(dt.date(2014, 4, 9))) + == "19:30-24:00 An Event\x1b[0m" + ) def test_invalid_format_string(): - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") event = Event.fromString(event_dt, **EVENT_KWARGS) - format_ = '{start-end-time-style} {title}{foo}' + format_ = "{start-end-time-style} {title}{foo}" with pytest.raises(KeyError): human_formatter(format_)(event.attributes(dt.date(2014, 4, 9))) def test_format_colors(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) - format_ = '{red}{title}{reset}' - assert human_formatter(format_)(event.attributes(dt.date(2014, 4, 9)) - ) == '\x1b[31mAn Event\x1b[0m\x1b[0m' - assert human_formatter(format_, colors=False)( - event.attributes(dt.date(2014, 4, 9), colors=False)) == 'An Event' + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) + format_ = "{red}{title}{reset}" + assert ( + human_formatter(format_)(event.attributes(dt.date(2014, 4, 9))) + == "\x1b[31mAn Event\x1b[0m\x1b[0m" + ) + assert ( + human_formatter(format_, colors=False)(event.attributes(dt.date(2014, 4, 9), colors=False)) + == "An Event" + ) def test_event_alarm(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) assert event.alarms == [] - event.update_alarms([(dt.timedelta(-1, 82800), 'new event')]) - assert event.alarms == [(dt.timedelta(-1, 82800), vText('new event'))] + event.update_alarms([(dt.timedelta(-1, 82800), "new event")]) + assert event.alarms == [(dt.timedelta(-1, 82800), vText("new event"))] def test_event_attendees(): - event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_simple"), **EVENT_KWARGS) assert event.attendees == "" - event.update_attendees(["this-does@not-exist.de", ]) + event.update_attendees( + [ + "this-does@not-exist.de", + ] + ) assert event.attendees == "this-does@not-exist.de" - assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list) - assert isinstance(event._vevents[event.ref].get('ATTENDEE', [])[0], vCalAddress) - assert str(event._vevents[event.ref].get('ATTENDEE', [])[0]) == "MAILTO:this-does@not-exist.de" + assert isinstance(event._vevents[event.ref].get("ATTENDEE", []), list) + assert isinstance(event._vevents[event.ref].get("ATTENDEE", [])[0], vCalAddress) + assert str(event._vevents[event.ref].get("ATTENDEE", [])[0]) == "MAILTO:this-does@not-exist.de" event.update_attendees(["this-does@not-exist.de", "also-does@not-exist.de"]) assert event.attendees == "this-does@not-exist.de, also-does@not-exist.de" - assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list) - assert len(event._vevents[event.ref].get('ATTENDEE', [])) == 2 - assert isinstance(event._vevents[event.ref].get('ATTENDEE', [])[0], vCalAddress) + assert isinstance(event._vevents[event.ref].get("ATTENDEE", []), list) + assert len(event._vevents[event.ref].get("ATTENDEE", [])) == 2 + assert isinstance(event._vevents[event.ref].get("ATTENDEE", [])[0], vCalAddress) # test if parameters from existing vCalAddress objects will be preserved new_address = vCalAddress("MAILTO:mail.address@not-exist.de") new_address.params = Parameters( - {'CN': 'Real Name', - 'PARTSTAT': 'NEEDS-ACTION', - 'ROLE': 'REQ-PARTICIPANT', - 'RSVP': 'TRUE'} + {"CN": "Real Name", "PARTSTAT": "NEEDS-ACTION", "ROLE": "REQ-PARTICIPANT", "RSVP": "TRUE"} ) - event._vevents[event.ref]['ATTENDEE'] = [new_address, ] + event._vevents[event.ref]["ATTENDEE"] = [ + new_address, + ] event.update_attendees(["another.mailaddress@not-exist.de", "mail.address@not-exist.de"]) assert event.attendees == "mail.address@not-exist.de, another.mailaddress@not-exist.de" - address = [a for a in event._vevents[event.ref].get('ATTENDEE', []) - if str(a) == "MAILTO:mail.address@not-exist.de"] + address = [ + a + for a in event._vevents[event.ref].get("ATTENDEE", []) + if str(a) == "MAILTO:mail.address@not-exist.de" + ] assert len(address) == 1 address = address[0] - assert address.params.get('CN', None) is not None - assert address.params['CN'] == "Real Name" + assert address.params.get("CN", None) is not None + assert address.params["CN"] == "Real Name" def test_create_timezone_static(): - gmt = pytz.timezone('Etc/GMT-8') + gmt = pytz.timezone("Etc/GMT-8") assert create_timezone(gmt).to_ical().split() == [ - b'BEGIN:VTIMEZONE', - b'TZID:Etc/GMT-8', - b'BEGIN:STANDARD', - b'DTSTART:16010101T000000', - b'RDATE:16010101T000000', - b'TZNAME:Etc/GMT-8', - b'TZOFFSETFROM:+0800', - b'TZOFFSETTO:+0800', - b'END:STANDARD', - b'END:VTIMEZONE', + b"BEGIN:VTIMEZONE", + b"TZID:Etc/GMT-8", + b"BEGIN:STANDARD", + b"DTSTART:16010101T000000", + b"RDATE:16010101T000000", + b"TZNAME:Etc/GMT-8", + b"TZOFFSETFROM:+0800", + b"TZOFFSETTO:+0800", + b"END:STANDARD", + b"END:VTIMEZONE", ] - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = GMTPLUS3.localize(dt.datetime(2014, 4, 9, 9, 30)) end = GMTPLUS3.localize(dt.datetime(2014, 4, 9, 10, 30)) event = Event.fromString(event_dt, **EVENT_KWARGS) event.update_start_end(start, end) - with freeze_time('2016-1-1'): + with freeze_time("2016-1-1"): assert normalize_component(event.raw) == normalize_component( """BEGIN:VCALENDAR VERSION:2.0 @@ -619,13 +683,13 @@ def test_create_timezone_static(): def test_sort_date_vs_datetime(): - event1 = Event.fromString(_get_text('event_d'), **EVENT_KWARGS) - event2 = Event.fromString(_get_text('event_dt_floating'), **EVENT_KWARGS) + event1 = Event.fromString(_get_text("event_d"), **EVENT_KWARGS) + event2 = Event.fromString(_get_text("event_dt_floating"), **EVENT_KWARGS) assert event1 < event2 def test_sort_event_start(): - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 45)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event1 = Event.fromString(event_dt, **EVENT_KWARGS) @@ -634,7 +698,7 @@ def test_sort_event_start(): def test_sort_event_end(): - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 45)) event1 = Event.fromString(event_dt, **EVENT_KWARGS) @@ -643,7 +707,7 @@ def test_sort_event_end(): def test_sort_event_summary(): - event_dt = _get_text('event_dt_simple') + event_dt = _get_text("event_dt_simple") event1 = Event.fromString(event_dt, **EVENT_KWARGS) event2 = Event.fromString(event_dt, **EVENT_KWARGS) event2.update_summary("ZZZ") @@ -653,82 +717,98 @@ def test_sort_event_summary(): def test_create_timezone_in_future(): """Events too far into the future (after the next DST transition) used to be created with invalid timezones""" - with freeze_time('2019-03-31'): + with freeze_time("2019-03-31"): assert create_timezone( - pytz.timezone('Europe/Amsterdam'), - dt.datetime(2022, 1, 1, 18, 0)).to_ical().split() == [ - b'BEGIN:VTIMEZONE', - b'TZID:Europe/Amsterdam', - b'BEGIN:STANDARD', - b'DTSTART:20211031T020000', - b'TZNAME:CET', - b'TZOFFSETFROM:+0200', - b'TZOFFSETTO:+0100', - b'END:STANDARD', - b'BEGIN:DAYLIGHT', - b'DTSTART:20220327T030000', - b'TZNAME:CEST', - b'TZOFFSETFROM:+0100', - b'TZOFFSETTO:+0200', - b'END:DAYLIGHT', - b'END:VTIMEZONE'] + pytz.timezone("Europe/Amsterdam"), dt.datetime(2022, 1, 1, 18, 0) + ).to_ical().split() == [ + b"BEGIN:VTIMEZONE", + b"TZID:Europe/Amsterdam", + b"BEGIN:STANDARD", + b"DTSTART:20211031T020000", + b"TZNAME:CET", + b"TZOFFSETFROM:+0200", + b"TZOFFSETTO:+0100", + b"END:STANDARD", + b"BEGIN:DAYLIGHT", + b"DTSTART:20220327T030000", + b"TZNAME:CEST", + b"TZOFFSETFROM:+0100", + b"TZOFFSETTO:+0200", + b"END:DAYLIGHT", + b"END:VTIMEZONE", + ] now = dt.datetime.now() min_value = now - dt.timedelta(days=3560) max_value = now + dt.timedelta(days=3560) -AMSTERDAM = pytz.timezone('Europe/Amsterdam') +AMSTERDAM = pytz.timezone("Europe/Amsterdam") -@given(datetimes(min_value=min_value, max_value=max_value), - datetimes(min_value=min_value, max_value=max_value)) +@given( + datetimes(min_value=min_value, max_value=max_value), + datetimes(min_value=min_value, max_value=max_value), +) def test_timezone_creation_with_arbitrary_dates(freeze_ts, event_time): """test if for arbitrary dates from the current date we produce a valid VTIMEZONE""" - event(f'freeze_ts == event_time: {freeze_ts == event_time}') + event(f"freeze_ts == event_time: {freeze_ts == event_time}") with freeze_time(freeze_ts): - vtimezone = create_timezone(AMSTERDAM, event_time).to_ical().decode('utf-8') + vtimezone = create_timezone(AMSTERDAM, event_time).to_ical().decode("utf-8") assert len(vtimezone) > 14 - assert 'BEGIN:STANDARD' in vtimezone - assert 'BEGIN:DAYLIGHT' in vtimezone + assert "BEGIN:STANDARD" in vtimezone + assert "BEGIN:DAYLIGHT" in vtimezone def test_parameters_description(): """test if we support DESCRIPTION properties with parameters""" - event = Event.fromString(_get_text('event_dt_description'), **EVENT_KWARGS) + event = Event.fromString(_get_text("event_dt_description"), **EVENT_KWARGS) assert event.description == ( - 'Hey, \n\nJust setting aside some dedicated time to talk about redacted.' + "Hey, \n\nJust setting aside some dedicated time to talk about redacted." ) + def test_partstat(): FORMAT_CALENDAR = ( - '{calendar-color}{partstat-symbol}{status-symbol}{start-end-time-style} ({calendar}) ' - '{title} [{location}]{repeat-symbol}' + "{calendar-color}{partstat-symbol}{status-symbol}{start-end-time-style} ({calendar}) " + "{title} [{location}]{repeat-symbol}" ) event = Event.fromString( - _get_text('event_dt_partstat'), addresses=['jdoe@example.com'], **EVENT_KWARGS) - assert event.partstat == 'ACCEPTED' - assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ - '✔09:30-10:30 (foobar) An Event []\x1b[0m' + _get_text("event_dt_partstat"), addresses=["jdoe@example.com"], **EVENT_KWARGS + ) + assert event.partstat == "ACCEPTED" + assert ( + human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) + == "✔09:30-10:30 (foobar) An Event []\x1b[0m" + ) event = Event.fromString( - _get_text('event_dt_partstat'), addresses=['another@example.com'], **EVENT_KWARGS) - assert event.partstat == 'DECLINED' - assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ - '❌09:30-10:30 (foobar) An Event []\x1b[0m' + _get_text("event_dt_partstat"), addresses=["another@example.com"], **EVENT_KWARGS + ) + assert event.partstat == "DECLINED" + assert ( + human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) + == "❌09:30-10:30 (foobar) An Event []\x1b[0m" + ) event = Event.fromString( - _get_text('event_dt_partstat'), addresses=['jqpublic@example.com'], **EVENT_KWARGS) - assert event.partstat == 'ACCEPTED' - assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ - '✔09:30-10:30 (foobar) An Event []\x1b[0m' + _get_text("event_dt_partstat"), addresses=["jqpublic@example.com"], **EVENT_KWARGS + ) + assert event.partstat == "ACCEPTED" + assert ( + human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) + == "✔09:30-10:30 (foobar) An Event []\x1b[0m" + ) + @pytest.mark.xfail def test_partstat_deligated(): event = Event.fromString( - _get_text('event_dt_partstat'), addresses=['hcabot@example.com'], **EVENT_KWARGS) - assert event.partstat == 'ACCEPTED' + _get_text("event_dt_partstat"), addresses=["hcabot@example.com"], **EVENT_KWARGS + ) + assert event.partstat == "ACCEPTED" event = Event.fromString( - _get_text('event_dt_partstat'), addresses=['iamboss@example.com'], **EVENT_KWARGS) - assert event.partstat == 'ACCEPTED' + _get_text("event_dt_partstat"), addresses=["iamboss@example.com"], **EVENT_KWARGS + ) + assert event.partstat == "ACCEPTED" diff --git a/tests/icalendar_test.py b/tests/icalendar_test.py index 392d09805..9810a5aba 100644 --- a/tests/icalendar_test.py +++ b/tests/icalendar_test.py @@ -12,53 +12,61 @@ def _get_TZIDs(lines): """from a list of strings, get all unique strings that start with TZID""" - return sorted(line for line in lines if line.startswith('TZID')) + return sorted(line for line in lines if line.startswith("TZID")) def test_normalize_component(): - assert normalize_component(textwrap.dedent(""" + assert normalize_component( + textwrap.dedent(""" BEGIN:VEVENT DTSTART;TZID=Europe/Berlin:20140409T093000 END:VEVENT - """)) != normalize_component(textwrap.dedent(""" + """) + ) != normalize_component( + textwrap.dedent(""" BEGIN:VEVENT DTSTART;TZID=Oyrope/Berlin:20140409T093000 END:VEVENT - """)) + """) + ) def test_new_vevent(): - with freeze_time('20220702T1400'): - vevent = _replace_uid(new_vevent( - LOCALE_BERLIN, - dt.date(2022, 7, 2), - dt.date(2022, 7, 3), - 'An Event', - allday=True, - repeat='weekly', - )) - assert vevent.to_ical().decode('utf-8') == '\r\n'.join([ - 'BEGIN:VEVENT', - 'SUMMARY:An Event', - 'DTSTART;VALUE=DATE:20220702', - 'DTEND;VALUE=DATE:20220703', - 'DTSTAMP:20220702T140000Z', - 'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA', - 'RRULE:FREQ=WEEKLY', - 'END:VEVENT', - '' - ]) + with freeze_time("20220702T1400"): + vevent = _replace_uid( + new_vevent( + LOCALE_BERLIN, + dt.date(2022, 7, 2), + dt.date(2022, 7, 3), + "An Event", + allday=True, + repeat="weekly", + ) + ) + assert vevent.to_ical().decode("utf-8") == "\r\n".join( + [ + "BEGIN:VEVENT", + "SUMMARY:An Event", + "DTSTART;VALUE=DATE:20220702", + "DTEND;VALUE=DATE:20220703", + "DTSTAMP:20220702T140000Z", + "UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA", + "RRULE:FREQ=WEEKLY", + "END:VEVENT", + "", + ] + ) def test_split_ics(): - cal = _get_text('cal_lots_of_timezones') + cal = _get_text("cal_lots_of_timezones") vevents = split_ics(cal) - vevents0 = vevents[0].split('\r\n') - vevents1 = vevents[1].split('\r\n') + vevents0 = vevents[0].split("\r\n") + vevents1 = vevents[1].split("\r\n") - part0 = _get_text('part0').split('\n') - part1 = _get_text('part1').split('\n') + part0 = _get_text("part0").split("\n") + part1 = _get_text("part1").split("\n") assert _get_TZIDs(vevents0) == _get_TZIDs(part0) assert _get_TZIDs(vevents1) == _get_TZIDs(part1) @@ -69,22 +77,22 @@ def test_split_ics(): def test_split_ics_random_uid(): random.seed(123) - cal = _get_text('cal_lots_of_timezones') + cal = _get_text("cal_lots_of_timezones") vevents = split_ics(cal, random_uid=True) - part0 = _get_text('part0').split('\n') - part1 = _get_text('part1').split('\n') + part0 = _get_text("part0").split("\n") + part1 = _get_text("part1").split("\n") for item in icalendar.Calendar.from_ical(vevents[0]).walk(): - if item.name == 'VEVENT': - assert item['UID'] == 'DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1' + if item.name == "VEVENT": + assert item["UID"] == "DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1" for item in icalendar.Calendar.from_ical(vevents[1]).walk(): - if item.name == 'VEVENT': - assert item['UID'] == '4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB' + if item.name == "VEVENT": + assert item["UID"] == "4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB" # after replacing the UIDs, everything should be as above - vevents0 = vevents[0].replace('DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1', '123').split('\r\n') - vevents1 = vevents[1].replace('4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB', 'abcde').split('\r\n') + vevents0 = vevents[0].replace("DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1", "123").split("\r\n") + vevents1 = vevents[1].replace("4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB", "abcde").split("\r\n") assert _get_TZIDs(vevents0) == _get_TZIDs(part0) assert _get_TZIDs(vevents1) == _get_TZIDs(part1) @@ -95,8 +103,8 @@ def test_split_ics_random_uid(): def test_split_ics_missing_timezone(): """testing if we detect the missing timezone in splitting""" - cal = _get_text('event_dt_local_missing_tz') - split_ics(cal, random_uid=True, default_timezone=LOCALE_BERLIN['default_timezone']) + cal = _get_text("event_dt_local_missing_tz") + split_ics(cal, random_uid=True, default_timezone=LOCALE_BERLIN["default_timezone"]) def test_windows_timezone(caplog): @@ -107,7 +115,7 @@ def test_windows_timezone(caplog): def test_split_ics_without_uid(): - cal = _get_text('without_uid') + cal = _get_text("without_uid") vevents = split_ics(cal) assert vevents vevents2 = split_ics(cal) diff --git a/tests/khalendar_test.py b/tests/khalendar_test.py index affdc93a6..6675334e4 100644 --- a/tests/khalendar_test.py +++ b/tests/khalendar_test.py @@ -49,14 +49,12 @@ LOCATION:LDB Lobby END:VEVENT""" -event_today = event_allday_template.format( - today.strftime('%Y%m%d'), tomorrow.strftime('%Y%m%d')) +event_today = event_allday_template.format(today.strftime("%Y%m%d"), tomorrow.strftime("%Y%m%d")) item_today = Item(event_today) -SIMPLE_EVENT_UID = 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU' +SIMPLE_EVENT_UID = "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU" class TestCalendar: - def test_create(self, coll_vdirs): assert True @@ -67,7 +65,7 @@ def test_new_event(self, coll_vdirs): coll.insert(event) events = list(coll.get_events_on(today)) assert len(events) == 1 - assert events[0].color == 'dark blue' + assert events[0].color == "dark blue" assert len(list(coll.get_events_on(tomorrow))) == 0 assert len(list(coll.get_events_on(yesterday))) == 0 assert len(list(vdirs[cal1].list())) == 1 @@ -86,49 +84,54 @@ def test_sanity(self, coll_vdirs): def test_db_needs_update(self, coll_vdirs, sleep_time): coll, vdirs = coll_vdirs - print('init') + print("init") for calendar in coll._calendars: - print(f'{calendar}: saved ctag: {coll._local_ctag(calendar)}, ' - f'vdir ctag: {coll._backend.get_ctag(calendar)}') + print( + f"{calendar}: saved ctag: {coll._local_ctag(calendar)}, " + f"vdir ctag: {coll._backend.get_ctag(calendar)}" + ) assert len(list(vdirs[cal1].list())) == 0 assert coll._needs_update(cal1) is False sleep(sleep_time) vdirs[cal1].upload(item_today) - print('upload') + print("upload") for calendar in coll._calendars: - print(f'{calendar}: saved ctag: {coll._local_ctag(calendar)}, ' - f'vdir ctag: {coll._backend.get_ctag(calendar)}') + print( + f"{calendar}: saved ctag: {coll._local_ctag(calendar)}, " + f"vdir ctag: {coll._backend.get_ctag(calendar)}" + ) assert len(list(vdirs[cal1].list())) == 1 assert coll._needs_update(cal1) is True coll.update_db() - print('updated') + print("updated") for calendar in coll._calendars: - print(f'{calendar}: saved ctag: {coll._local_ctag(calendar)}, ' - f'vdir ctag: {coll._backend.get_ctag(calendar)}') + print( + f"{calendar}: saved ctag: {coll._local_ctag(calendar)}, " + f"vdir ctag: {coll._backend.get_ctag(calendar)}" + ) assert coll._needs_update(cal1) is False class TestVdirsyncerCompat: def test_list(self, coll_vdirs): coll, vdirs = coll_vdirs - event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN) + event = Event.fromString(_get_text("event_d"), calendar=cal1, locale=LOCALE_BERLIN) assert event.etag is None assert event.href is None coll.insert(event) assert event.etag is not None - assert event.href == 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU.ics' + assert event.href == "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU.ics" event = Event.fromString(event_today, calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event) hrefs = sorted(href for href, etag in coll._backend.list(cal1)) assert {str(coll.get_event(href, calendar=cal1).uid) for href in hrefs} == { - 'uid3@host1.com', - 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU', + "uid3@host1.com", + "V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU", } class TestCollection: - astart = dt.datetime.combine(aday, dt.time.min) aend = dt.datetime.combine(aday, dt.time.max) bstart = dt.datetime.combine(bday, dt.time.min) @@ -140,46 +143,50 @@ class TestCollection: def test_default_calendar(self, tmpdir): calendars = { - 'foobar': {'name': 'foobar', 'path': str(tmpdir), 'readonly': True}, - 'home': {'name': 'home', 'path': str(tmpdir)}, - "Dad's Calendar": {'name': "Dad's calendar", 'path': str(tmpdir), 'readonly': True}, + "foobar": {"name": "foobar", "path": str(tmpdir), "readonly": True}, + "home": {"name": "home", "path": str(tmpdir)}, + "Dad's Calendar": {"name": "Dad's calendar", "path": str(tmpdir), "readonly": True}, } coll = CalendarCollection( - calendars=calendars, locale=LOCALE_BERLIN, dbpath=':memory:', + calendars=calendars, + locale=LOCALE_BERLIN, + dbpath=":memory:", ) assert coll.default_calendar_name is None with pytest.raises(ValueError, match="Unknown calendar: Dad's calendar"): coll.default_calendar_name = "Dad's calendar" assert coll.default_calendar_name is None with pytest.raises(ValueError, match="Unknown calendar: unknownstuff"): - coll.default_calendar_name = 'unknownstuff' + coll.default_calendar_name = "unknownstuff" assert coll.default_calendar_name is None - coll.default_calendar_name = 'home' - assert coll.default_calendar_name == 'home' - assert coll.writable_names == ['home'] + coll.default_calendar_name = "home" + assert coll.default_calendar_name == "home" + assert coll.writable_names == ["home"] def test_empty(self, coll_vdirs): coll, vdirs = coll_vdirs start = dt.datetime.combine(today, dt.time.min) end = dt.datetime.combine(today, dt.time.max) assert list(coll.get_floating(start, end)) == [] - assert list(coll.get_localized(utils.BERLIN.localize(start), - utils.BERLIN.localize(end))) == [] + assert ( + list(coll.get_localized(utils.BERLIN.localize(start), utils.BERLIN.localize(end))) == [] + ) def test_insert(self, coll_vdirs): """insert a localized event""" coll, vdirs = coll_vdirs coll.insert( - Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN), - cal1) + Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN), + cal1, + ) events = list(coll.get_localized(self.astart_berlin, self.aend_berlin)) assert len(events) == 1 - assert events[0].color == 'dark blue' + assert events[0].color == "dark blue" assert events[0].calendar == cal1 events = list(coll.get_events_on(aday)) assert len(events) == 1 - assert events[0].color == 'dark blue' + assert events[0].color == "dark blue" assert events[0].calendar == cal1 assert len(list(vdirs[cal1].list())) == 1 @@ -190,12 +197,12 @@ def test_insert(self, coll_vdirs): def test_insert_d(self, coll_vdirs): """insert a floating event""" coll, vdirs = coll_vdirs - event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN) + event = Event.fromString(_get_text("event_d"), calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) events = list(coll.get_events_on(aday)) assert len(events) == 1 assert events[0].calendar == cal1 - assert events[0].color == 'dark blue' + assert events[0].color == "dark blue" assert len(list(vdirs[cal1].list())) == 1 assert len(list(vdirs[cal2].list())) == 0 assert len(list(vdirs[cal3].list())) == 0 @@ -205,9 +212,9 @@ def test_insert_d_no_value(self, coll_vdirs): """insert a date event with no VALUE=DATE option""" coll, vdirs = coll_vdirs coll.insert( - Event.fromString( - _get_text('event_d_no_value'), calendar=cal1, locale=LOCALE_BERLIN), - cal1) + Event.fromString(_get_text("event_d_no_value"), calendar=cal1, locale=LOCALE_BERLIN), + cal1, + ) events = list(coll.get_events_on(aday)) assert len(events) == 1 assert events[0].calendar == cal1 @@ -220,19 +227,23 @@ def test_get(self, coll_vdirs): """test getting an event by its href""" coll, vdirs = coll_vdirs event = Event.fromString( - _get_text('event_dt_simple'), href='xyz.ics', calendar=cal1, locale=LOCALE_BERLIN, + _get_text("event_dt_simple"), + href="xyz.ics", + calendar=cal1, + locale=LOCALE_BERLIN, ) coll.insert(event, cal1) - event_from_db = coll.get_event(SIMPLE_EVENT_UID + '.ics', cal1) - with freeze_time('2016-1-1'): - assert normalize_component(event_from_db.raw) == \ - normalize_component(_get_text('event_dt_simple_inkl_vtimezone')) + event_from_db = coll.get_event(SIMPLE_EVENT_UID + ".ics", cal1) + with freeze_time("2016-1-1"): + assert normalize_component(event_from_db.raw) == normalize_component( + _get_text("event_dt_simple_inkl_vtimezone") + ) assert event_from_db.etag def test_change(self, coll_vdirs): """moving an event from one calendar to another""" coll, vdirs = coll_vdirs - event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN) + event = Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) event = list(coll.get_events_on(aday))[0] assert event.calendar == cal1 @@ -245,26 +256,28 @@ def test_change(self, coll_vdirs): def test_update_event(self, coll_vdirs): """updating one event""" coll, vdirs = coll_vdirs - event = Event.fromString( - _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN) + event = Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) events = coll.get_events_on(aday) event = list(events)[0] - event.update_summary('really simple event') + event.update_summary("really simple event") event.update_start_end(bday, bday) coll.update(event) events = list(coll.get_localized(self.astart_berlin, self.aend_berlin)) assert len(events) == 0 events = list(coll.get_floating(self.bstart, self.bend)) assert len(events) == 1 - assert events[0].summary == 'really simple event' + assert events[0].summary == "really simple event" def test_newevent(self, coll_vdirs): coll, vdirs = coll_vdirs bday = dt.datetime.combine(aday, dt.time.min) anend = bday + dt.timedelta(hours=1) event = icalendar_helpers.new_vevent( - dtstart=bday, dtend=anend, summary="hi", timezone=utils.BERLIN, + dtstart=bday, + dtend=anend, + summary="hi", + timezone=utils.BERLIN, locale=LOCALE_BERLIN, ) event = coll.create_event_from_ics(event.to_ical(), coll.default_calendar_name) @@ -272,62 +285,66 @@ def test_newevent(self, coll_vdirs): def test_modify_readonly_calendar(self, coll_vdirs): coll, vdirs = coll_vdirs - coll._calendars[cal1]['readonly'] = True - coll._calendars[cal3]['readonly'] = True - event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN) + coll._calendars[cal1]["readonly"] = True + coll._calendars[cal3]["readonly"] = True + event = Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN) with pytest.raises(khal.khalendar.exceptions.ReadOnlyCalendarError): coll.insert(event, cal1) with pytest.raises(khal.khalendar.exceptions.ReadOnlyCalendarError): # params don't really matter here - coll.delete('href', 'eteg', cal1) + coll.delete("href", "eteg", cal1) def test_search(self, coll_vdirs): coll, vdirs = coll_vdirs - assert len(list(coll.search('Event'))) == 0 - event = Event.fromString( - _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN) + assert len(list(coll.search("Event"))) == 0 + event = Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) - assert len(list(coll.search('Event'))) == 1 + assert len(list(coll.search("Event"))) == 1 event = Event.fromString( - _get_text('event_dt_floating'), calendar=cal1, locale=LOCALE_BERLIN) + _get_text("event_dt_floating"), calendar=cal1, locale=LOCALE_BERLIN + ) coll.insert(event, cal1) - assert len(list(coll.search('Search for me'))) == 1 - assert len(list(coll.search('Event'))) == 2 + assert len(list(coll.search("Search for me"))) == 1 + assert len(list(coll.search("Event"))) == 2 def test_search_recurrence_id_only(self, coll_vdirs): """test searching for recurring events which only have a recuid event, and no master""" coll, vdirs = coll_vdirs - assert len(list(coll.search('Event'))) == 0 + assert len(list(coll.search("Event"))) == 0 event = Event.fromString( - _get_text('event_dt_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN) + _get_text("event_dt_recuid_no_master"), calendar=cal1, locale=LOCALE_BERLIN + ) coll.insert(event, cal1) - assert len(list(coll.search('Event'))) == 1 + assert len(list(coll.search("Event"))) == 1 def test_search_recurrence_id_only_multi(self, coll_vdirs): """test searching for recurring events which only have a recuid event, and no master""" coll, vdirs = coll_vdirs - assert len(list(coll.search('Event'))) == 0 + assert len(list(coll.search("Event"))) == 0 event = Event.fromString( - _get_text('event_dt_multi_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN) + _get_text("event_dt_multi_recuid_no_master"), calendar=cal1, locale=LOCALE_BERLIN + ) coll.insert(event, cal1) - events = sorted(coll.search('Event')) + events = sorted(coll.search("Event")) assert len(events) == 2 - assert human_formatter('{start} {end} {title}')(events[0].attributes( - dt.date.today())) == '30.06. 07:30 30.06. 12:00 Arbeit\x1b[0m' - assert human_formatter('{start} {end} {title}')(events[1].attributes( - dt.date.today())) == '07.07. 08:30 07.07. 12:00 Arbeit\x1b[0m' + assert ( + human_formatter("{start} {end} {title}")(events[0].attributes(dt.date.today())) + == "30.06. 07:30 30.06. 12:00 Arbeit\x1b[0m" + ) + assert ( + human_formatter("{start} {end} {title}")(events[1].attributes(dt.date.today())) + == "07.07. 08:30 07.07. 12:00 Arbeit\x1b[0m" + ) def test_delete_two_events(self, coll_vdirs, sleep_time): """testing if we can delete any of two events in two different calendars with the same filename""" coll, vdirs = coll_vdirs - event1 = Event.fromString( - _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN) - event2 = Event.fromString( - _get_text('event_dt_simple'), calendar=cal2, locale=LOCALE_BERLIN) + event1 = Event.fromString(_get_text("event_dt_simple"), calendar=cal1, locale=LOCALE_BERLIN) + event2 = Event.fromString(_get_text("event_dt_simple"), calendar=cal2, locale=LOCALE_BERLIN) coll.insert(event1, cal1) sleep(sleep_time) # make sure the etags are different coll.insert(event2, cal2) @@ -345,74 +362,72 @@ def test_delete_two_events(self, coll_vdirs, sleep_time): def test_delete_recuid(self, coll_vdirs: CollVdirType): """Testing if we can delete a recuid (add it to exdate)""" coll, _ = coll_vdirs - event_str = _get_text('event_rrule_recuid') + event_str = _get_text("event_rrule_recuid") event = Event.fromString(event_str, calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) - event = coll.get_event('event_rrule_recurrence_id.ics', cal1) + event = coll.get_event("event_rrule_recurrence_id.ics", cal1) event = coll.delete_instance( - 'event_rrule_recurrence_id.ics', + "event_rrule_recurrence_id.ics", event.etag, calendar=cal1, rec_id=BERLIN.localize(dt.datetime(2014, 7, 14, 5)), ) - assert 'EXDATE;TZID=Europe/Berlin:20140714T050000' in event.raw.split() + assert "EXDATE;TZID=Europe/Berlin:20140714T050000" in event.raw.split() event = coll.delete_instance( - 'event_rrule_recurrence_id.ics', + "event_rrule_recurrence_id.ics", event.etag, calendar=cal1, rec_id=BERLIN.localize(dt.datetime(2014, 7, 21, 5)), ) - assert 'EXDATE;TZID=Europe/Berlin:20140714T050000,20140721T050000' in event.raw.split() + assert "EXDATE;TZID=Europe/Berlin:20140714T050000,20140721T050000" in event.raw.split() def test_invalid_timezones(self, coll_vdirs): """testing if we can delete any of two events in two different calendars with the same filename""" coll, vdirs = coll_vdirs - event = Event.fromString( - _get_text('invalid_tzoffset'), calendar=cal1, locale=LOCALE_BERLIN) + event = Event.fromString(_get_text("invalid_tzoffset"), calendar=cal1, locale=LOCALE_BERLIN) coll.insert(event, cal1) - events = sorted(coll.search('Event')) + events = sorted(coll.search("Event")) assert len(events) == 1 - assert human_formatter('{start} {end} {title}')(events[0].attributes(dt.date.today())) == \ - '02.12. 08:00 02.12. 09:30 Some event\x1b[0m' + assert ( + human_formatter("{start} {end} {title}")(events[0].attributes(dt.date.today())) + == "02.12. 08:00 02.12. 09:30 Some event\x1b[0m" + ) def test_multi_uid_vdir(self, coll_vdirs, caplog, fix_caplog, sleep_time): coll, vdirs = coll_vdirs caplog.set_level(logging.WARNING) sleep(sleep_time) # Make sure we get a new ctag on upload - vdirs[cal1].upload(DumbItem(_get_text('event_dt_multi_uid'), uid='12345')) + vdirs[cal1].upload(DumbItem(_get_text("event_dt_multi_uid"), uid="12345")) coll.update_db() - assert list(coll.search('')) == [] + assert list(coll.search("")) == [] messages = [rec.message for rec in caplog.records] - assert messages[0].startswith( - "The .ics file at foobar/12345.ics contains multiple UIDs.\n" - ) + assert messages[0].startswith("The .ics file at foobar/12345.ics contains multiple UIDs.\n") assert messages[1].startswith( "Skipping foobar/12345.ics: \nThis event will not be available in khal." ) class TestDbCreation: - def test_create_db(self, tmpdir): - vdirpath = str(tmpdir) + '/' + cal1 + vdirpath = str(tmpdir) + "/" + cal1 os.makedirs(vdirpath, mode=0o770) - dbdir = str(tmpdir) + '/subdir/' - dbpath = dbdir + 'khal.db' + dbdir = str(tmpdir) + "/subdir/" + dbpath = dbdir + "khal.db" assert not os.path.isdir(dbdir) - calendars = {cal1: {'name': cal1, 'path': vdirpath}} + calendars = {cal1: {"name": cal1, "path": vdirpath}} CalendarCollection(calendars, dbpath=dbpath, locale=LOCALE_BERLIN) assert os.path.isdir(dbdir) def test_failed_create_db(self, tmpdir): - dbdir = str(tmpdir) + '/subdir/' - dbpath = dbdir + 'khal.db' + dbdir = str(tmpdir) + "/subdir/" + dbpath = dbdir + "khal.db" os.chmod(str(tmpdir), 400) - calendars = {cal1: {'name': cal1, 'path': str(tmpdir)}} + calendars = {cal1: {"name": cal1, "path": str(tmpdir)}} with pytest.raises(CouldNotCreateDbDir): CalendarCollection(calendars, dbpath=dbpath, locale=LOCALE_BERLIN) os.chmod(str(tmpdir), 777) @@ -421,7 +436,7 @@ def test_failed_create_db(self, tmpdir): def test_event_different_timezones(coll_vdirs, sleep_time): coll, vdirs = coll_vdirs sleep(sleep_time) # Make sure we get a new ctag on upload - vdirs[cal1].upload(DumbItem(_get_text('event_dt_london'), uid='12345')) + vdirs[cal1].upload(DumbItem(_get_text("event_dt_london"), uid="12345")) coll.update_db() events = coll.get_localized( @@ -460,8 +475,10 @@ def test_event_different_timezones(coll_vdirs, sleep_time): # the event spans midnight Sydney, therefor it should also show up on the # next day - events = coll.get_localized(SYDNEY.localize(dt.datetime(2014, 4, 10, 0, 0)), - SYDNEY.localize(dt.datetime(2014, 4, 10, 23, 59))) + events = coll.get_localized( + SYDNEY.localize(dt.datetime(2014, 4, 10, 0, 0)), + SYDNEY.localize(dt.datetime(2014, 4, 10, 23, 59)), + ) events = list(events) assert len(events) == 1 assert event.start_local == SYDNEY.localize(dt.datetime(2014, 4, 9, 23)) @@ -471,8 +488,8 @@ def test_event_different_timezones(coll_vdirs, sleep_time): def test_default_calendar(coll_vdirs, sleep_time): """test if an update to the vdir is detected by the CalendarCollection""" coll, vdirs = coll_vdirs - vdir = vdirs['foobar'] - event = coll.create_event_from_ics(event_today, 'foobar') + vdir = vdirs["foobar"] + event = coll.create_event_from_ics(event_today, "foobar") assert len(list(coll.get_events_on(today))) == 0 @@ -498,25 +515,35 @@ def test_default_calendar(coll_vdirs, sleep_time): def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time): coll, vdirs = coll_vdirs - href_one, etag_one = vdirs[cal1].upload(coll.create_event_from_ics(dedent(""" + href_one, etag_one = vdirs[cal1].upload( + coll.create_event_from_ics( + dedent(""" BEGIN:VEVENT UID:meeting-one DTSTART;VALUE=DATE:20140909 DTEND;VALUE=DATE:20140910 SUMMARY:first meeting END:VEVENT - """), cal1)) + """), + cal1, + ) + ) sleep(sleep_time) # Make sure we get a new etag for meeting-two - href_two, etag_two = vdirs[cal1].upload(coll.create_event_from_ics(dedent(""" + href_two, etag_two = vdirs[cal1].upload( + coll.create_event_from_ics( + dedent(""" BEGIN:VEVENT UID:meeting-two DTSTART;VALUE=DATE:20140910 DTEND;VALUE=DATE:20140911 SUMMARY:second meeting END:VEVENT - """), cal1)) + """), + cal1, + ) + ) sleep(sleep_time) coll.update_db() @@ -529,16 +556,22 @@ def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time): def _update_vevent(href, calendar): updated_hrefs.append(href) return old_update_vevent(href, calendar) - monkeypatch.setattr(coll, '_update_vevent', _update_vevent) - href_three, etag_three = vdirs[cal1].upload(coll.create_event_from_ics(dedent(""" + monkeypatch.setattr(coll, "_update_vevent", _update_vevent) + + href_three, etag_three = vdirs[cal1].upload( + coll.create_event_from_ics( + dedent(""" BEGIN:VEVENT UID:meeting-three DTSTART;VALUE=DATE:20140911 DTEND;VALUE=DATE:20140912 SUMMARY:third meeting END:VEVENT - """), cal1)) + """), + cal1, + ) + ) sleep(sleep_time) assert coll._needs_update(cal1) @@ -571,62 +604,70 @@ def _update_vevent(href, calendar): def test_birthdays(coll_vdirs_birthday, sleep_time): coll, vdirs = coll_vdirs_birthday - assert list( - coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59)) - ) == [] + assert ( + list(coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59))) + == [] + ) sleep(sleep_time) # Make sure we get a new ctag on upload - vdirs[cal1].upload(DumbItem(card, 'unix')) + vdirs[cal1].upload(DumbItem(card, "unix")) coll.update_db() - assert 'Unix\'s 41st birthday' == list( - coll.get_floating(dt.datetime(2012, 3, 11), dt.datetime(2012, 3, 11)))[0].summary - assert 'Unix\'s 42nd birthday' == list( - coll.get_floating(dt.datetime(2013, 3, 11), dt.datetime(2013, 3, 11)))[0].summary - assert 'Unix\'s 43rd birthday' == list( - coll.get_floating(dt.datetime(2014, 3, 11), dt.datetime(2014, 3, 11)))[0].summary + assert ( + "Unix's 41st birthday" + == list(coll.get_floating(dt.datetime(2012, 3, 11), dt.datetime(2012, 3, 11)))[0].summary + ) + assert ( + "Unix's 42nd birthday" + == list(coll.get_floating(dt.datetime(2013, 3, 11), dt.datetime(2013, 3, 11)))[0].summary + ) + assert ( + "Unix's 43rd birthday" + == list(coll.get_floating(dt.datetime(2014, 3, 11), dt.datetime(2014, 3, 11)))[0].summary + ) def test_birthdays_29feb(coll_vdirs_birthday, sleep_time): """test how we deal with birthdays on 29th of feb in leap years""" coll, vdirs = coll_vdirs_birthday sleep(sleep_time) # Make sure we get a new ctag on upload - vdirs[cal1].upload(DumbItem(card_29thfeb, 'leap')) + vdirs[cal1].upload(DumbItem(card_29thfeb, "leap")) assert coll.needs_update() coll.update_db() - events = list( - coll.get_floating(dt.datetime(2004, 1, 1, 0, 0), dt.datetime(2004, 12, 31)) - ) + events = list(coll.get_floating(dt.datetime(2004, 1, 1, 0, 0), dt.datetime(2004, 12, 31))) assert len(events) == 1 - assert events[0].summary == 'leapyear\'s 4th birthday (29th of Feb.)' + assert events[0].summary == "leapyear's 4th birthday (29th of Feb.)" assert events[0].start == dt.date(2004, 2, 29) - events = list( - coll.get_floating(dt.datetime(2005, 1, 1, 0, 0), dt.datetime(2005, 12, 31)) - ) + events = list(coll.get_floating(dt.datetime(2005, 1, 1, 0, 0), dt.datetime(2005, 12, 31))) assert len(events) == 1 - assert events[0].summary == 'leapyear\'s 5th birthday (29th of Feb.)' + assert events[0].summary == "leapyear's 5th birthday (29th of Feb.)" assert events[0].start == dt.date(2005, 3, 1) - assert list( - coll.get_floating(dt.datetime(2001, 1, 1), dt.datetime(2001, 12, 31)) - )[0].summary == 'leapyear\'s 1st birthday (29th of Feb.)' - assert list( - coll.get_floating(dt.datetime(2002, 1, 1), dt.datetime(2002, 12, 31)) - )[0].summary == 'leapyear\'s 2nd birthday (29th of Feb.)' - assert list( - coll.get_floating(dt.datetime(2003, 1, 1), dt.datetime(2003, 12, 31)) - )[0].summary == 'leapyear\'s 3rd birthday (29th of Feb.)' - assert list( - coll.get_floating(dt.datetime(2023, 1, 1), dt.datetime(2023, 12, 31)) - )[0].summary == 'leapyear\'s 23rd birthday (29th of Feb.)' + assert ( + list(coll.get_floating(dt.datetime(2001, 1, 1), dt.datetime(2001, 12, 31)))[0].summary + == "leapyear's 1st birthday (29th of Feb.)" + ) + assert ( + list(coll.get_floating(dt.datetime(2002, 1, 1), dt.datetime(2002, 12, 31)))[0].summary + == "leapyear's 2nd birthday (29th of Feb.)" + ) + assert ( + list(coll.get_floating(dt.datetime(2003, 1, 1), dt.datetime(2003, 12, 31)))[0].summary + == "leapyear's 3rd birthday (29th of Feb.)" + ) + assert ( + list(coll.get_floating(dt.datetime(2023, 1, 1), dt.datetime(2023, 12, 31)))[0].summary + == "leapyear's 23rd birthday (29th of Feb.)" + ) assert events[0].start == dt.date(2005, 3, 1) def test_birthdays_no_year(coll_vdirs_birthday, sleep_time): coll, vdirs = coll_vdirs_birthday - assert list( - coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59)) - ) == [] + assert ( + list(coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59))) + == [] + ) sleep(sleep_time) # Make sure we get a new ctag on upload - vdirs[cal1].upload(DumbItem(card_no_year, 'vcard.vcf')) + vdirs[cal1].upload(DumbItem(card_no_year, "vcard.vcf")) coll.update_db() events = list(coll.get_floating(dt.datetime(1971, 3, 11), dt.datetime(1971, 3, 11, 23, 59, 59))) assert len(events) == 1 - assert 'Unix\'s birthday' == events[0].summary + assert "Unix's birthday" == events[0].summary diff --git a/tests/khalendar_utils_test.py b/tests/khalendar_utils_test.py index b9904f608..82c593926 100644 --- a/tests/khalendar_utils_test.py +++ b/tests/khalendar_utils_test.py @@ -10,8 +10,8 @@ # FIXME this file is in urgent need of a clean up -BERLIN = pytz.timezone('Europe/Berlin') -BOGOTA = pytz.timezone('America/Bogota') +BERLIN = pytz.timezone("Europe/Berlin") +BOGOTA = pytz.timezone("America/Bogota") # datetime event_dt = """BEGIN:VCALENDAR @@ -221,75 +221,238 @@ END:VCALENDAR """ -berlin = pytz.timezone('Europe/Berlin') -new_york = pytz.timezone('America/New_York') +berlin = pytz.timezone("Europe/Berlin") +new_york = pytz.timezone("America/New_York") def _get_vevent(event): ical = icalendar.Event.from_ical(event) for component in ical.walk(): - if component.name == 'VEVENT': + if component.name == "VEVENT": return component class TestExpand: dtstartend_berlin = [ - (berlin.localize(dt.datetime(2013, 3, 1, 14, 0, )), - berlin.localize(dt.datetime(2013, 3, 1, 16, 0, ))), - (berlin.localize(dt.datetime(2013, 5, 1, 14, 0, )), - berlin.localize(dt.datetime(2013, 5, 1, 16, 0, ))), - (berlin.localize(dt.datetime(2013, 7, 1, 14, 0, )), - berlin.localize(dt.datetime(2013, 7, 1, 16, 0, ))), - (berlin.localize(dt.datetime(2013, 9, 1, 14, 0, )), - berlin.localize(dt.datetime(2013, 9, 1, 16, 0, ))), - (berlin.localize(dt.datetime(2013, 11, 1, 14, 0,)), - berlin.localize(dt.datetime(2013, 11, 1, 16, 0,))), - (berlin.localize(dt.datetime(2014, 1, 1, 14, 0, )), - berlin.localize(dt.datetime(2014, 1, 1, 16, 0, ))) + ( + berlin.localize( + dt.datetime( + 2013, + 3, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2013, + 3, + 1, + 16, + 0, + ) + ), + ), + ( + berlin.localize( + dt.datetime( + 2013, + 5, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2013, + 5, + 1, + 16, + 0, + ) + ), + ), + ( + berlin.localize( + dt.datetime( + 2013, + 7, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2013, + 7, + 1, + 16, + 0, + ) + ), + ), + ( + berlin.localize( + dt.datetime( + 2013, + 9, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2013, + 9, + 1, + 16, + 0, + ) + ), + ), + ( + berlin.localize( + dt.datetime( + 2013, + 11, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2013, + 11, + 1, + 16, + 0, + ) + ), + ), + ( + berlin.localize( + dt.datetime( + 2014, + 1, + 1, + 14, + 0, + ) + ), + berlin.localize( + dt.datetime( + 2014, + 1, + 1, + 16, + 0, + ) + ), + ), ] dtstartend_utc = [ - (dt.datetime(2013, 3, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 3, 1, 16, 0, tzinfo=pytz.utc)), - (dt.datetime(2013, 5, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 5, 1, 16, 0, tzinfo=pytz.utc)), - (dt.datetime(2013, 7, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 7, 1, 16, 0, tzinfo=pytz.utc)), - (dt.datetime(2013, 9, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 9, 1, 16, 0, tzinfo=pytz.utc)), - (dt.datetime(2013, 11, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 11, 1, 16, 0, tzinfo=pytz.utc)), - (dt.datetime(2014, 1, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2014, 1, 1, 16, 0, tzinfo=pytz.utc)) + ( + dt.datetime(2013, 3, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 3, 1, 16, 0, tzinfo=pytz.utc), + ), + ( + dt.datetime(2013, 5, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 5, 1, 16, 0, tzinfo=pytz.utc), + ), + ( + dt.datetime(2013, 7, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 7, 1, 16, 0, tzinfo=pytz.utc), + ), + ( + dt.datetime(2013, 9, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 9, 1, 16, 0, tzinfo=pytz.utc), + ), + ( + dt.datetime(2013, 11, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 11, 1, 16, 0, tzinfo=pytz.utc), + ), + ( + dt.datetime(2014, 1, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2014, 1, 1, 16, 0, tzinfo=pytz.utc), + ), ] dtstartend_float = [ - (dt.datetime(2013, 3, 1, 14, 0), - dt.datetime(2013, 3, 1, 16, 0)), - (dt.datetime(2013, 5, 1, 14, 0), - dt.datetime(2013, 5, 1, 16, 0)), - (dt.datetime(2013, 7, 1, 14, 0), - dt.datetime(2013, 7, 1, 16, 0)), - (dt.datetime(2013, 9, 1, 14, 0), - dt.datetime(2013, 9, 1, 16, 0)), - (dt.datetime(2013, 11, 1, 14, 0), - dt.datetime(2013, 11, 1, 16, 0)), - (dt.datetime(2014, 1, 1, 14, 0), - dt.datetime(2014, 1, 1, 16, 0)) + (dt.datetime(2013, 3, 1, 14, 0), dt.datetime(2013, 3, 1, 16, 0)), + (dt.datetime(2013, 5, 1, 14, 0), dt.datetime(2013, 5, 1, 16, 0)), + (dt.datetime(2013, 7, 1, 14, 0), dt.datetime(2013, 7, 1, 16, 0)), + (dt.datetime(2013, 9, 1, 14, 0), dt.datetime(2013, 9, 1, 16, 0)), + (dt.datetime(2013, 11, 1, 14, 0), dt.datetime(2013, 11, 1, 16, 0)), + (dt.datetime(2014, 1, 1, 14, 0), dt.datetime(2014, 1, 1, 16, 0)), ] dstartend = [ - (dt.date(2013, 3, 1,), - dt.date(2013, 3, 2,)), - (dt.date(2013, 5, 1,), - dt.date(2013, 5, 2,)), - (dt.date(2013, 7, 1,), - dt.date(2013, 7, 2,)), - (dt.date(2013, 9, 1,), - dt.date(2013, 9, 2,)), - (dt.date(2013, 11, 1), - dt.date(2013, 11, 2)), - (dt.date(2014, 1, 1,), - dt.date(2014, 1, 2,)) + ( + dt.date( + 2013, + 3, + 1, + ), + dt.date( + 2013, + 3, + 2, + ), + ), + ( + dt.date( + 2013, + 5, + 1, + ), + dt.date( + 2013, + 5, + 2, + ), + ), + ( + dt.date( + 2013, + 7, + 1, + ), + dt.date( + 2013, + 7, + 2, + ), + ), + ( + dt.date( + 2013, + 9, + 1, + ), + dt.date( + 2013, + 9, + 2, + ), + ), + (dt.date(2013, 11, 1), dt.date(2013, 11, 2)), + ( + dt.date( + 2014, + 1, + 1, + ), + dt.date( + 2014, + 1, + 2, + ), + ), ] offset_berlin = [ dt.timedelta(0, 3600), @@ -297,7 +460,7 @@ class TestExpand: dt.timedelta(0, 7200), dt.timedelta(0, 7200), dt.timedelta(0, 3600), - dt.timedelta(0, 3600) + dt.timedelta(0, 3600), ] offset_utc = [ @@ -315,16 +478,14 @@ def test_expand_dt(self): vevent = _get_vevent(event_dt) dtstart = icalendar_helpers.expand(vevent, berlin) assert dtstart == self.dtstartend_berlin - assert [start.utcoffset() - for start, _ in dtstart] == self.offset_berlin + assert [start.utcoffset() for start, _ in dtstart] == self.offset_berlin assert [end.utcoffset() for _, end in dtstart] == self.offset_berlin def test_expand_dtb(self): vevent = _get_vevent(event_dtb) dtstart = icalendar_helpers.expand(vevent, berlin) assert dtstart == self.dtstartend_berlin - assert [start.utcoffset() - for start, _ in dtstart] == self.offset_berlin + assert [start.utcoffset() for start, _ in dtstart] == self.offset_berlin assert [end.utcoffset() for _, end in dtstart] == self.offset_berlin def test_expand_dttz(self): @@ -359,33 +520,42 @@ def test_expand_dtzb(self): def test_expand_invalid_exdate(self): """testing if we can expand an event with EXDATEs that do not much its RRULE""" - vevent = _get_vevent_file('event_invalid_exdate') + vevent = _get_vevent_file("event_invalid_exdate") dtstartl = icalendar_helpers.expand(vevent, berlin) # TODO test for logging message assert dtstartl == [ - (new_york.localize(dt.datetime(2011, 11, 12, 15, 50)), - new_york.localize(dt.datetime(2011, 11, 12, 17, 0))), - (new_york.localize(dt.datetime(2011, 11, 19, 15, 50)), - new_york.localize(dt.datetime(2011, 11, 19, 17, 0))), - (new_york.localize(dt.datetime(2011, 12, 3, 15, 50)), - new_york.localize(dt.datetime(2011, 12, 3, 17, 0))), + ( + new_york.localize(dt.datetime(2011, 11, 12, 15, 50)), + new_york.localize(dt.datetime(2011, 11, 12, 17, 0)), + ), + ( + new_york.localize(dt.datetime(2011, 11, 19, 15, 50)), + new_york.localize(dt.datetime(2011, 11, 19, 17, 0)), + ), + ( + new_york.localize(dt.datetime(2011, 12, 3, 15, 50)), + new_york.localize(dt.datetime(2011, 12, 3, 17, 0)), + ), ] class TestExpandNoRR: dtstartend_berlin = [ - (berlin.localize(dt.datetime(2013, 3, 1, 14, 0)), - berlin.localize(dt.datetime(2013, 3, 1, 16, 0))), + ( + berlin.localize(dt.datetime(2013, 3, 1, 14, 0)), + berlin.localize(dt.datetime(2013, 3, 1, 16, 0)), + ), ] dtstartend_utc = [ - (dt.datetime(2013, 3, 1, 14, 0, tzinfo=pytz.utc), - dt.datetime(2013, 3, 1, 16, 0, tzinfo=pytz.utc)), + ( + dt.datetime(2013, 3, 1, 14, 0, tzinfo=pytz.utc), + dt.datetime(2013, 3, 1, 16, 0, tzinfo=pytz.utc), + ), ] dtstartend_float = [ - (dt.datetime(2013, 3, 1, 14, 0), - dt.datetime(2013, 3, 1, 16, 0)), + (dt.datetime(2013, 3, 1, 14, 0), dt.datetime(2013, 3, 1, 16, 0)), ] offset_berlin = [ dt.timedelta(0, 3600), @@ -401,16 +571,14 @@ def test_expand_dt(self): vevent = _get_vevent(event_dt_norr) dtstart = icalendar_helpers.expand(vevent, berlin) assert dtstart == self.dtstartend_berlin - assert [start.utcoffset() - for start, _ in dtstart] == self.offset_berlin + assert [start.utcoffset() for start, _ in dtstart] == self.offset_berlin assert [end.utcoffset() for _, end in dtstart] == self.offset_berlin def test_expand_dtb(self): vevent = _get_vevent(event_dtb_norr) dtstart = icalendar_helpers.expand(vevent, berlin) assert dtstart == self.dtstartend_berlin - assert [start.utcoffset() - for start, _ in dtstart] == self.offset_berlin + assert [start.utcoffset() for start, _ in dtstart] == self.offset_berlin assert [end.utcoffset() for _, end in dtstart] == self.offset_berlin def test_expand_dttz(self): @@ -431,14 +599,24 @@ def test_expand_d(self): vevent = _get_vevent(event_d_norr) dtstart = icalendar_helpers.expand(vevent, berlin) assert dtstart == [ - (dt.date(2013, 3, 1,), - dt.date(2013, 3, 2,)), + ( + dt.date( + 2013, + 3, + 1, + ), + dt.date( + 2013, + 3, + 2, + ), + ), ] def test_expand_dtr_exdatez(self): """a recurring event with an EXDATE in Zulu time while DTSTART is localized""" - vevent = _get_vevent_file('event_dtr_exdatez') + vevent = _get_vevent_file("event_dtr_exdatez") dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 3 @@ -446,8 +624,8 @@ def test_expand_rrule_exdate_z(self): """event with not understood timezone for dtstart and zulu time form exdate """ - vevent = _get_vevent_file('event_dtr_no_tz_exdatez') - vevent = icalendar_helpers.sanitize(vevent, berlin, '', '') + vevent = _get_vevent_file("event_dtr_no_tz_exdatez") + vevent = icalendar_helpers.sanitize(vevent, berlin, "", "") dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 5 dtstarts = [start for start, end in dtstart] @@ -463,8 +641,8 @@ def test_expand_rrule_notz_until_z(self): """event with not understood timezone for dtstart and zulu time form exdate """ - vevent = _get_vevent_file('event_dtr_notz_untilz') - vevent = icalendar_helpers.sanitize(vevent, new_york, '', '') + vevent = _get_vevent_file("event_dtr_notz_untilz") + vevent = icalendar_helpers.sanitize(vevent, new_york, "", "") dtstart = icalendar_helpers.expand(vevent, new_york) assert len(dtstart) == 7 dtstarts = [start for start, end in dtstart] @@ -593,10 +771,8 @@ def test_until_notz(self): dtstart = icalendar_helpers.expand(vevent, berlin) starts = [start for start, _ in dtstart] assert len(starts) == 18 - assert dtstart[0][0] == berlin.localize( - dt.datetime(2014, 2, 3, 7, 0)) - assert dtstart[-1][0] == berlin.localize( - dt.datetime(2014, 2, 20, 7, 0)) + assert dtstart[0][0] == berlin.localize(dt.datetime(2014, 2, 3, 7, 0)) + assert dtstart[-1][0] == berlin.localize(dt.datetime(2014, 2, 20, 7, 0)) def test_until_d_notz(self): vevent = _get_vevent(event_until_d_notz) @@ -616,38 +792,31 @@ def test_recurrence_id_with_timezone(self): vevent = _get_vevent(recurrence_id_with_timezone) dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 1 - assert dtstart[0][0] == berlin.localize( - dt.datetime(2013, 11, 13, 19, 0)) + assert dtstart[0][0] == berlin.localize(dt.datetime(2013, 11, 13, 19, 0)) def test_event_exdate_dt(self): """recurring event, one date excluded via EXCLUDE""" vevent = _get_vevent(event_exdate_dt) dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 9 - assert dtstart[0][0] == berlin.localize( - dt.datetime(2014, 7, 2, 19, 0)) - assert dtstart[-1][0] == berlin.localize( - dt.datetime(2014, 7, 11, 19, 0)) + assert dtstart[0][0] == berlin.localize(dt.datetime(2014, 7, 2, 19, 0)) + assert dtstart[-1][0] == berlin.localize(dt.datetime(2014, 7, 11, 19, 0)) def test_event_exdates_dt(self): """recurring event, two dates excluded via EXCLUDE""" vevent = _get_vevent(event_exdates_dt) dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 8 - assert dtstart[0][0] == berlin.localize( - dt.datetime(2014, 7, 2, 19, 0)) - assert dtstart[-1][0] == berlin.localize( - dt.datetime(2014, 7, 11, 19, 0)) + assert dtstart[0][0] == berlin.localize(dt.datetime(2014, 7, 2, 19, 0)) + assert dtstart[-1][0] == berlin.localize(dt.datetime(2014, 7, 11, 19, 0)) def test_event_exdatesl_dt(self): """recurring event, three dates exclude via two EXCLUDEs""" vevent = _get_vevent(event_exdatesl_dt) dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 7 - assert dtstart[0][0] == berlin.localize( - dt.datetime(2014, 7, 2, 19, 0)) - assert dtstart[-1][0] == berlin.localize( - dt.datetime(2014, 7, 11, 19, 0)) + assert dtstart[0][0] == berlin.localize(dt.datetime(2014, 7, 2, 19, 0)) + assert dtstart[-1][0] == berlin.localize(dt.datetime(2014, 7, 11, 19, 0)) def test_event_exdates_remove(self): """check if we can remove one more instance""" @@ -668,33 +837,38 @@ def test_event_exdates_remove(self): def test_event_dt_rrule_invalid_until(self): """DTSTART and RRULE:UNTIL should be of the same type, but might not be""" - vevent = _get_vevent(_get_text('event_dt_rrule_invalid_until')) + vevent = _get_vevent(_get_text("event_dt_rrule_invalid_until")) dtstart = icalendar_helpers.expand(vevent, berlin) - assert dtstart == [(dt.date(2007, 12, 1), dt.date(2007, 12, 2)), - (dt.date(2008, 1, 1), dt.date(2008, 1, 2)), - (dt.date(2008, 2, 1), dt.date(2008, 2, 2))] + assert dtstart == [ + (dt.date(2007, 12, 1), dt.date(2007, 12, 2)), + (dt.date(2008, 1, 1), dt.date(2008, 1, 2)), + (dt.date(2008, 2, 1), dt.date(2008, 2, 2)), + ] def test_event_dt_rrule_invalid_until2(self): - """same as above, but now dtstart is of type date and until is datetime - """ - vevent = _get_vevent(_get_text('event_dt_rrule_invalid_until2')) + """same as above, but now dtstart is of type date and until is datetime""" + vevent = _get_vevent(_get_text("event_dt_rrule_invalid_until2")) dtstart = icalendar_helpers.expand(vevent, berlin) assert len(dtstart) == 35 - assert dtstart[0] == (berlin.localize(dt.datetime(2014, 4, 9, 9, 30)), - berlin.localize(dt.datetime(2014, 4, 9, 10, 30))) - assert dtstart[-1] == (berlin.localize(dt.datetime(2014, 12, 3, 9, 30)), - berlin.localize(dt.datetime(2014, 12, 3, 10, 30))) + assert dtstart[0] == ( + berlin.localize(dt.datetime(2014, 4, 9, 9, 30)), + berlin.localize(dt.datetime(2014, 4, 9, 10, 30)), + ) + assert dtstart[-1] == ( + berlin.localize(dt.datetime(2014, 12, 3, 9, 30)), + berlin.localize(dt.datetime(2014, 12, 3, 10, 30)), + ) def test_event_dt_rrule_until_before_start(self): """test handling if an RRULE's UNTIL is before the event's DTSTART""" - vevent = _get_vevent(_get_text('event_dt_rrule_until_before_start')) + vevent = _get_vevent(_get_text("event_dt_rrule_until_before_start")) dtstart = icalendar_helpers.expand(vevent, berlin) # TODO test for logging message assert dtstart is None def test_event_invalid_rrule(self): """test handling if an event with RRULE will never occur""" - vevent = _get_vevent(_get_text('event_rrule_no_occurence')) + vevent = _get_vevent(_get_text("event_rrule_no_occurence")) dtstart = icalendar_helpers.expand(vevent, berlin) # TODO test for logging message assert dtstart is None @@ -722,6 +896,7 @@ def test_event_invalid_rrule(self): class TestRDate: """Testing expanding of recurrence rules""" + def test_simple_rdate(self): vevent = _get_vevent(simple_rdate) dtstart = icalendar_helpers.expand(vevent, berlin) @@ -733,7 +908,7 @@ def test_rrule_and_rdate(self): assert len(dtstart) == 7 def test_rrule_past(self): - vevent = _get_vevent_file('event_r_past') + vevent = _get_vevent_file("event_r_past") assert vevent is not None dtstarts = icalendar_helpers.expand(vevent, berlin) assert len(dtstarts) == 73 @@ -741,13 +916,15 @@ def test_rrule_past(self): assert dtstarts[-1][0] == dt.date(2037, 4, 23) def test_rdate_date(self): - vevent = _get_vevent_file('event_d_rdate') + vevent = _get_vevent_file("event_d_rdate") dtstarts = icalendar_helpers.expand(vevent, berlin) assert len(dtstarts) == 4 - assert dtstarts == [(dt.date(2015, 8, 12), dt.date(2015, 8, 13)), - (dt.date(2015, 8, 13), dt.date(2015, 8, 14)), - (dt.date(2015, 8, 14), dt.date(2015, 8, 15)), - (dt.date(2015, 8, 15), dt.date(2015, 8, 16))] + assert dtstarts == [ + (dt.date(2015, 8, 12), dt.date(2015, 8, 13)), + (dt.date(2015, 8, 13), dt.date(2015, 8, 14)), + (dt.date(2015, 8, 14), dt.date(2015, 8, 15)), + (dt.date(2015, 8, 15), dt.date(2015, 8, 16)), + ] noend_date = """ @@ -783,28 +960,27 @@ def test_rdate_date(self): class TestSanitize: - def test_noend_date(self): vevent = _get_vevent(noend_date) - vevent = icalendar_helpers.sanitize(vevent, berlin, '', '') - assert vevent['DTSTART'].dt == dt.date(2014, 8, 29) - assert vevent['DTEND'].dt == dt.date(2014, 8, 30) + vevent = icalendar_helpers.sanitize(vevent, berlin, "", "") + assert vevent["DTSTART"].dt == dt.date(2014, 8, 29) + assert vevent["DTEND"].dt == dt.date(2014, 8, 30) def test_noend_datetime(self): vevent = _get_vevent(noend_datetime) - vevent = icalendar_helpers.sanitize(vevent, berlin, '', '') - assert vevent['DTSTART'].dt == BERLIN.localize(dt.datetime(2014, 8, 29, 8)) - assert vevent['DTEND'].dt == BERLIN.localize(dt.datetime(2014, 8, 29, 9)) + vevent = icalendar_helpers.sanitize(vevent, berlin, "", "") + assert vevent["DTSTART"].dt == BERLIN.localize(dt.datetime(2014, 8, 29, 8)) + assert vevent["DTEND"].dt == BERLIN.localize(dt.datetime(2014, 8, 29, 9)) def test_duration(self): - vevent = _get_vevent_file('event_dtr_exdatez') - vevent = icalendar_helpers.sanitize(vevent, berlin, '', '') + vevent = _get_vevent_file("event_dtr_exdatez") + vevent = icalendar_helpers.sanitize(vevent, berlin, "", "") def test_instant(self): vevent = _get_vevent(instant) - assert vevent['DTEND'].dt - vevent['DTSTART'].dt == dt.timedelta() - vevent = icalendar_helpers.sanitize(vevent, berlin, '', '') - assert vevent['DTEND'].dt - vevent['DTSTART'].dt == dt.timedelta(hours=1) + assert vevent["DTEND"].dt - vevent["DTSTART"].dt == dt.timedelta() + vevent = icalendar_helpers.sanitize(vevent, berlin, "", "") + assert vevent["DTEND"].dt - vevent["DTSTART"].dt == dt.timedelta(hours=1) class TestIsAware: diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 169e1b5a0..5cee0346f 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -26,26 +26,42 @@ def _create_testcases(*cases): - return [(userinput, ('\r\n'.join(output) + '\r\n').encode('utf-8')) - for userinput, output in cases] - - -def _construct_event(info, locale, - defaulttimelen=60, defaultdatelen=1, description=None, - location=None, categories=None, repeat=None, until=None, - alarm=None, **kwargs): - info = eventinfofstr(' '.join(info), locale, - default_event_duration=dt.timedelta(hours=1), - default_dayevent_duration=dt.timedelta(days=1), - adjust_reasonably=True, - ) + return [ + (userinput, ("\r\n".join(output) + "\r\n").encode("utf-8")) for userinput, output in cases + ] + + +def _construct_event( + info, + locale, + defaulttimelen=60, + defaultdatelen=1, + description=None, + location=None, + categories=None, + repeat=None, + until=None, + alarm=None, + **kwargs, +): + info = eventinfofstr( + " ".join(info), + locale, + default_event_duration=dt.timedelta(hours=1), + default_dayevent_duration=dt.timedelta(days=1), + adjust_reasonably=True, + ) if description is not None: info["description"] = description event = new_vevent( - locale=locale, location=location, - categories=categories, repeat=repeat, until=until, + locale=locale, + location=location, + categories=categories, + repeat=repeat, + until=until, alarms=alarm, - **info) + **info, + ) return event @@ -58,269 +74,286 @@ def _create_vevent(*args): appends the value otherwise. """ def_vevent = OrderedDict( - [('BEGIN', 'BEGIN:VEVENT'), - ('SUMMARY', 'SUMMARY:Äwesöme Event'), - ('DTSTART', 'DTSTART;VALUE=DATE:20131025'), - ('DTEND', 'DTEND;VALUE=DATE:20131026'), - ('DTSTAMP', 'DTSTAMP:20140216T120000Z'), - ('UID', 'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA')]) + [ + ("BEGIN", "BEGIN:VEVENT"), + ("SUMMARY", "SUMMARY:Äwesöme Event"), + ("DTSTART", "DTSTART;VALUE=DATE:20131025"), + ("DTEND", "DTEND;VALUE=DATE:20131026"), + ("DTSTAMP", "DTSTAMP:20140216T120000Z"), + ("UID", "UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA"), + ] + ) for row in args: - key = row.replace(':', ';').split(';')[0] + key = row.replace(":", ";").split(";")[0] def_vevent[key] = row - def_vevent['END'] = 'END:VEVENT' + def_vevent["END"] = "END:VEVENT" return list(def_vevent.values()) class TestTimeDelta2Str: - def test_single(self): - assert timedelta2str(dt.timedelta(minutes=10)) == '10m' + assert timedelta2str(dt.timedelta(minutes=10)) == "10m" def test_negative(self): - assert timedelta2str(dt.timedelta(minutes=-10)) == '-10m' + assert timedelta2str(dt.timedelta(minutes=-10)) == "-10m" def test_days(self): - assert timedelta2str(dt.timedelta(days=2)) == '2d' + assert timedelta2str(dt.timedelta(days=2)) == "2d" def test_multi(self): - assert timedelta2str( - dt.timedelta(days=6, hours=-3, minutes=10, seconds=-3) - ) == '5d 21h 9m 57s' + assert ( + timedelta2str(dt.timedelta(days=6, hours=-3, minutes=10, seconds=-3)) == "5d 21h 9m 57s" + ) def test_weekdaypstr(): for string, weekdayno in [ - ('monday', 0), - ('tue', 1), - ('wednesday', 2), - ('thursday', 3), - ('fri', 4), - ('saturday', 5), - ('sun', 6), + ("monday", 0), + ("tue", 1), + ("wednesday", 2), + ("thursday", 3), + ("fri", 4), + ("saturday", 5), + ("sun", 6), ]: assert weekdaypstr(string) == weekdayno def test_weekdaypstr_invalid(): with pytest.raises(ValueError, match="invalid weekday name `foobar`"): - weekdaypstr('foobar') + weekdaypstr("foobar") def test_construct_daynames(): - with freeze_time('2016-9-19'): - assert construct_daynames(dt.date(2016, 9, 19)) == 'Today' - assert construct_daynames(dt.date(2016, 9, 20)) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 21)) == 'Wednesday' + with freeze_time("2016-9-19"): + assert construct_daynames(dt.date(2016, 9, 19)) == "Today" + assert construct_daynames(dt.date(2016, 9, 20)) == "Tomorrow" + assert construct_daynames(dt.date(2016, 9, 21)) == "Wednesday" class TestGuessDatetimefstr: - - @freeze_time('2016-9-19T8:00') + @freeze_time("2016-9-19T8:00") def test_today(self): - assert (dt.datetime(2016, 9, 19, 13), False) == \ - guessdatetimefstr(['today', '13:00'], LOCALE_BERLIN) - assert dt.date.today() == guessdatetimefstr(['today'], LOCALE_BERLIN)[0].date() + assert (dt.datetime(2016, 9, 19, 13), False) == guessdatetimefstr( + ["today", "13:00"], LOCALE_BERLIN + ) + assert dt.date.today() == guessdatetimefstr(["today"], LOCALE_BERLIN)[0].date() - @freeze_time('2016-9-19T8:00') + @freeze_time("2016-9-19T8:00") def test_tomorrow(self): - assert (dt.datetime(2016, 9, 20, 16), False) == \ - guessdatetimefstr('tomorrow 16:00 16:00'.split(), locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 9, 20, 16), False) == guessdatetimefstr( + "tomorrow 16:00 16:00".split(), locale=LOCALE_BERLIN + ) - @freeze_time('2016-9-19T8:00') + @freeze_time("2016-9-19T8:00") def test_time_tomorrow(self): - assert (dt.datetime(2016, 9, 20, 16), False) == \ - guessdatetimefstr( - '16:00'.split(), locale=LOCALE_BERLIN, default_day=dt.date(2016, 9, 20)) + assert (dt.datetime(2016, 9, 20, 16), False) == guessdatetimefstr( + "16:00".split(), locale=LOCALE_BERLIN, default_day=dt.date(2016, 9, 20) + ) - @freeze_time('2016-9-19T8:00') + @freeze_time("2016-9-19T8:00") def test_time_yesterday(self): assert (dt.datetime(2016, 9, 18, 16), False) == guessdatetimefstr( - 'Yesterday 16:00'.split(), - locale=LOCALE_BERLIN, - default_day=dt.datetime.today()) + "Yesterday 16:00".split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today() + ) - @freeze_time('2016-9-19') + @freeze_time("2016-9-19") def test_time_weekday(self): assert (dt.datetime(2016, 9, 23, 16), False) == guessdatetimefstr( - 'Friday 16:00'.split(), - locale=LOCALE_BERLIN, - default_day=dt.datetime.today()) + "Friday 16:00".split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today() + ) - @freeze_time('2016-9-19 17:53') + @freeze_time("2016-9-19 17:53") def test_time_now(self): assert (dt.datetime(2016, 9, 19, 17, 53), False) == guessdatetimefstr( - 'now'.split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today()) + "now".split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today() + ) - @freeze_time('2016-12-30 17:53') + @freeze_time("2016-12-30 17:53") def test_long_not_configured(self): """long version is not configured, but short contains the year""" locale = { - 'timeformat': '%H:%M', - 'dateformat': '%Y-%m-%d', - 'longdateformat': '', - 'datetimeformat': '%Y-%m-%d %H:%M', - 'longdatetimeformat': '', + "timeformat": "%H:%M", + "dateformat": "%Y-%m-%d", + "longdateformat": "", + "datetimeformat": "%Y-%m-%d %H:%M", + "longdatetimeformat": "", } assert (dt.datetime(2017, 1, 1), True) == guessdatetimefstr( - '2017-1-1'.split(), locale=locale, default_day=dt.datetime.today()) + "2017-1-1".split(), locale=locale, default_day=dt.datetime.today() + ) assert (dt.datetime(2017, 1, 1, 16, 30), False) == guessdatetimefstr( - '2017-1-1 16:30'.split(), locale=locale, default_day=dt.datetime.today()) + "2017-1-1 16:30".split(), locale=locale, default_day=dt.datetime.today() + ) - @freeze_time('2016-12-30 17:53') + @freeze_time("2016-12-30 17:53") def test_short_format_contains_year(self): """if the non long versions of date(time)format contained a year, the current year would be used instead of the given one, see #545""" locale = { - 'timeformat': '%H:%M', - 'dateformat': '%Y-%m-%d', - 'longdateformat': '%Y-%m-%d', - 'datetimeformat': '%Y-%m-%d %H:%M', - 'longdatetimeformat': '%Y-%m-%d %H:%M', + "timeformat": "%H:%M", + "dateformat": "%Y-%m-%d", + "longdateformat": "%Y-%m-%d", + "datetimeformat": "%Y-%m-%d %H:%M", + "longdatetimeformat": "%Y-%m-%d %H:%M", } assert (dt.datetime(2017, 1, 1), True) == guessdatetimefstr( - '2017-1-1'.split(), locale=locale, default_day=dt.datetime.today()) + "2017-1-1".split(), locale=locale, default_day=dt.datetime.today() + ) assert (dt.datetime(2017, 1, 1, 16, 30), False) == guessdatetimefstr( - '2017-1-1 16:30'.split(), locale=locale, default_day=dt.datetime.today()) + "2017-1-1 16:30".split(), locale=locale, default_day=dt.datetime.today() + ) class TestGuessTimedeltafstr: - def test_single(self): - assert dt.timedelta(minutes=10) == guesstimedeltafstr('10m') + assert dt.timedelta(minutes=10) == guesstimedeltafstr("10m") def test_seconds(self): - assert dt.timedelta(seconds=10) == guesstimedeltafstr('10s') + assert dt.timedelta(seconds=10) == guesstimedeltafstr("10s") def test_single_plus(self): - assert dt.timedelta(minutes=10) == guesstimedeltafstr('+10m') + assert dt.timedelta(minutes=10) == guesstimedeltafstr("+10m") def test_seconds_plus(self): - assert dt.timedelta(seconds=10) == guesstimedeltafstr('+10s') + assert dt.timedelta(seconds=10) == guesstimedeltafstr("+10s") def test_days_plus(self): - assert dt.timedelta(days=10) == guesstimedeltafstr('+10days') + assert dt.timedelta(days=10) == guesstimedeltafstr("+10days") def test_negative(self): - assert dt.timedelta(minutes=-10) == guesstimedeltafstr('-10m') + assert dt.timedelta(minutes=-10) == guesstimedeltafstr("-10m") def test_multi(self): - assert dt.timedelta(days=1, hours=-3, minutes=10) == \ - guesstimedeltafstr(' 1d -3H 10min ') + assert dt.timedelta(days=1, hours=-3, minutes=10) == guesstimedeltafstr(" 1d -3H 10min ") def test_multi_plus(self): - assert dt.timedelta(days=1, hours=3, minutes=10) == \ - guesstimedeltafstr(' 1d +3H 10min ') + assert dt.timedelta(days=1, hours=3, minutes=10) == guesstimedeltafstr(" 1d +3H 10min ") def test_multi_plus_minus(self): - assert dt.timedelta(days=0, hours=21, minutes=10) == \ - guesstimedeltafstr('+1d -3H 10min ') + assert dt.timedelta(days=0, hours=21, minutes=10) == guesstimedeltafstr("+1d -3H 10min ") def test_multi_nospace(self): - assert dt.timedelta(days=1, hours=-3, minutes=10) == \ - guesstimedeltafstr('1D-3hour10m') + assert dt.timedelta(days=1, hours=-3, minutes=10) == guesstimedeltafstr("1D-3hour10m") def test_garbage(self): with pytest.raises( ValueError, match='Invalid unit in timedelta string "10mbar": "mbar"', ): - guesstimedeltafstr('10mbar') + guesstimedeltafstr("10mbar") def test_moregarbage(self): with pytest.raises( ValueError, match='Invalid beginning of timedelta string "foo10m": "foo"', ): - guesstimedeltafstr('foo10m') + guesstimedeltafstr("foo10m") def test_same(self): - assert dt.timedelta(minutes=20) == \ - guesstimedeltafstr('10min 10minutes') + assert dt.timedelta(minutes=20) == guesstimedeltafstr("10min 10minutes") class TestGuessRangefstr: - - @freeze_time('2016-9-19') + @freeze_time("2016-9-19") def test_today(self): - assert (dt.datetime(2016, 9, 19, 13), dt.datetime(2016, 9, 19, 14), False) == \ - guessrangefstr('13:00 14:00', locale=LOCALE_BERLIN) - assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21), True) == \ - guessrangefstr('today tomorrow', LOCALE_BERLIN) - - @freeze_time('2016-9-19 16:34') + assert ( + dt.datetime(2016, 9, 19, 13), + dt.datetime(2016, 9, 19, 14), + False, + ) == guessrangefstr("13:00 14:00", locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21), True) == guessrangefstr( + "today tomorrow", LOCALE_BERLIN + ) + + @freeze_time("2016-9-19 16:34") def test_tomorrow(self): # XXX remove this funtionality, we shouldn't support this anyway - assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21, 16), True) == \ - guessrangefstr('today tomorrow 16:00', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21, 16), True) == guessrangefstr( + "today tomorrow 16:00", locale=LOCALE_BERLIN + ) - @freeze_time('2016-9-19 13:34') + @freeze_time("2016-9-19 13:34") def test_time_tomorrow(self): - assert (dt.datetime(2016, 9, 19, 16), dt.datetime(2016, 9, 19, 17), False) == \ - guessrangefstr('16:00', locale=LOCALE_BERLIN) - assert (dt.datetime(2016, 9, 19, 16), dt.datetime(2016, 9, 19, 17), False) == \ - guessrangefstr('16:00 17:00', locale=LOCALE_BERLIN) + assert ( + dt.datetime(2016, 9, 19, 16), + dt.datetime(2016, 9, 19, 17), + False, + ) == guessrangefstr("16:00", locale=LOCALE_BERLIN) + assert ( + dt.datetime(2016, 9, 19, 16), + dt.datetime(2016, 9, 19, 17), + False, + ) == guessrangefstr("16:00 17:00", locale=LOCALE_BERLIN) def test_start_and_end_date(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2017, 1, 2), True) == \ - guessrangefstr('1.1.2016 1.1.2017', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1), dt.datetime(2017, 1, 2), True) == guessrangefstr( + "1.1.2016 1.1.2017", locale=LOCALE_BERLIN + ) def test_start_and_no_end_date(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == \ - guessrangefstr('1.1.2016', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == guessrangefstr( + "1.1.2016", locale=LOCALE_BERLIN + ) def test_start_and_end_date_time(self): - assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2017, 1, 1, 22), False) == \ - guessrangefstr( - '1.1.2016 10:00 1.1.2017 22:00', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2017, 1, 1, 22), False) == guessrangefstr( + "1.1.2016 10:00 1.1.2017 22:00", locale=LOCALE_BERLIN + ) def test_start_and_eod(self): start, end = dt.datetime(2016, 1, 1, 10), dt.datetime(2016, 1, 1, 23, 59, 59, 999999) - assert (start, end, False) == guessrangefstr('1.1.2016 10:00 eod', locale=LOCALE_BERLIN) + assert (start, end, False) == guessrangefstr("1.1.2016 10:00 eod", locale=LOCALE_BERLIN) def test_start_and_week(self): - assert (dt.datetime(2015, 12, 28), dt.datetime(2016, 1, 5), True) == \ - guessrangefstr('1.1.2016 week', locale=LOCALE_BERLIN) + assert (dt.datetime(2015, 12, 28), dt.datetime(2016, 1, 5), True) == guessrangefstr( + "1.1.2016 week", locale=LOCALE_BERLIN + ) def test_start_and_delta_1d(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == \ - guessrangefstr('1.1.2016 1d', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == guessrangefstr( + "1.1.2016 1d", locale=LOCALE_BERLIN + ) def test_start_and_delta_3d(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 4), True) == \ - guessrangefstr('1.1.2016 3d', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 4), True) == guessrangefstr( + "1.1.2016 3d", locale=LOCALE_BERLIN + ) def test_start_dt_and_delta(self): - assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2016, 1, 4, 10), False) == \ - guessrangefstr('1.1.2016 10:00 3d', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2016, 1, 4, 10), False) == guessrangefstr( + "1.1.2016 10:00 3d", locale=LOCALE_BERLIN + ) def test_start_allday_and_delta_datetime(self): with pytest.raises(FatalError): - guessrangefstr('1.1.2016 3d3m', locale=LOCALE_BERLIN) + guessrangefstr("1.1.2016 3d3m", locale=LOCALE_BERLIN) def test_start_zero_day_delta(self): with pytest.raises(FatalError): - guessrangefstr('1.1.2016 0d', locale=LOCALE_BERLIN) + guessrangefstr("1.1.2016 0d", locale=LOCALE_BERLIN) - @freeze_time('20160216') + @freeze_time("20160216") def test_week(self): - assert (dt.datetime(2016, 2, 15), dt.datetime(2016, 2, 23), True) == \ - guessrangefstr('week', locale=LOCALE_BERLIN) + assert (dt.datetime(2016, 2, 15), dt.datetime(2016, 2, 23), True) == guessrangefstr( + "week", locale=LOCALE_BERLIN + ) def test_invalid(self): with pytest.raises(DateTimeParseError): - guessrangefstr('3d', locale=LOCALE_BERLIN) + guessrangefstr("3d", locale=LOCALE_BERLIN) with pytest.raises(DateTimeParseError): - guessrangefstr('35.1.2016', locale=LOCALE_BERLIN) + guessrangefstr("35.1.2016", locale=LOCALE_BERLIN) with pytest.raises(DateTimeParseError): - guessrangefstr('1.1.2016 2x', locale=LOCALE_BERLIN) + guessrangefstr("1.1.2016 2x", locale=LOCALE_BERLIN) with pytest.raises(DateTimeParseError): - guessrangefstr('1.1.2016x', locale=LOCALE_BERLIN) + guessrangefstr("1.1.2016x", locale=LOCALE_BERLIN) with pytest.raises(DateTimeParseError): - guessrangefstr('xxx yyy zzz', locale=LOCALE_BERLIN) + guessrangefstr("xxx yyy zzz", locale=LOCALE_BERLIN) - @freeze_time('2016-12-30 17:53') + @freeze_time("2016-12-30 17:53") def test_short_format_contains_year(self): """if the non long versions of date(time)format contained a year, the current year would be used instead of the given one, see #545 @@ -328,95 +361,107 @@ def test_short_format_contains_year(self): same as above, but for guessrangefstr """ locale = { - 'timeformat': '%H:%M', - 'dateformat': '%Y-%m-%d', - 'longdateformat': '%Y-%m-%d', - 'datetimeformat': '%Y-%m-%d %H:%M', - 'longdatetimeformat': '%Y-%m-%d %H:%M', + "timeformat": "%H:%M", + "dateformat": "%Y-%m-%d", + "longdateformat": "%Y-%m-%d", + "datetimeformat": "%Y-%m-%d %H:%M", + "longdatetimeformat": "%Y-%m-%d %H:%M", } - assert (dt.datetime(2017, 1, 1), dt.datetime(2017, 1, 2), True) == \ - guessrangefstr('2017-1-1 2017-1-1', locale=locale) + assert (dt.datetime(2017, 1, 1), dt.datetime(2017, 1, 2), True) == guessrangefstr( + "2017-1-1 2017-1-1", locale=locale + ) test_set_format_de = _create_testcases( # all-day-events # one day only - ('25.10.2013 Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20131025', - 'DTEND;VALUE=DATE:20131026')), - + ( + "25.10.2013 Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20131025", "DTEND;VALUE=DATE:20131026"), + ), # 2 day - ('15.08.2014 16.08. Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20140815', - 'DTEND;VALUE=DATE:20140817')), # XXX - + ( + "15.08.2014 16.08. Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20140815", "DTEND;VALUE=DATE:20140817"), + ), # XXX # end date in next year and not specified - ('29.12.2014 03.01. Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20141229', - 'DTEND;VALUE=DATE:20150104')), - + ( + "29.12.2014 03.01. Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20141229", "DTEND;VALUE=DATE:20150104"), + ), # end date in next year - ('29.12.2014 03.01.2015 Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20141229', - 'DTEND;VALUE=DATE:20150104')), - + ( + "29.12.2014 03.01.2015 Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20141229", "DTEND;VALUE=DATE:20150104"), + ), # datetime events # start and end date same, no explicit end date given - ('25.10.2013 18:00 20:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131025T180000', - 'DTEND;TZID=Europe/Berlin:20131025T200000')), - + ( + "25.10.2013 18:00 20:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131025T180000", "DTEND;TZID=Europe/Berlin:20131025T200000" + ), + ), # start and end date same, ends 24:00 which should be 00:00 (start) of next # day - ('25.10.2013 18:00 24:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131025T180000', - 'DTEND;TZID=Europe/Berlin:20131026T000000')), - + ( + "25.10.2013 18:00 24:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131025T180000", "DTEND;TZID=Europe/Berlin:20131026T000000" + ), + ), # start and end date same, explicit end date (but no year) given - ('25.10.2013 18:00 26.10. 20:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131025T180000', - 'DTEND;TZID=Europe/Berlin:20131026T200000')), - - ('30.12.2013 18:00 2.1. 20:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131230T180000', - 'DTEND;TZID=Europe/Berlin:20140102T200000')), - + ( + "25.10.2013 18:00 26.10. 20:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131025T180000", "DTEND;TZID=Europe/Berlin:20131026T200000" + ), + ), + ( + "30.12.2013 18:00 2.1. 20:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131230T180000", "DTEND;TZID=Europe/Berlin:20140102T200000" + ), + ), # only start date given (no year, past day and month) - ('25.01. 18:00 20:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20150125T180000', - 'DTEND;TZID=Europe/Berlin:20150125T200000')), - + ( + "25.01. 18:00 20:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20150125T180000", "DTEND;TZID=Europe/Berlin:20150125T200000" + ), + ), # date ends next day, but end date not given - ('25.10.2013 23:00 0:30 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131025T230000', - 'DTEND;TZID=Europe/Berlin:20131026T003000')), - - ('2.2. 23:00 0:30 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20150202T230000', - 'DTEND;TZID=Europe/Berlin:20150203T003000')), - + ( + "25.10.2013 23:00 0:30 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131025T230000", "DTEND;TZID=Europe/Berlin:20131026T003000" + ), + ), + ( + "2.2. 23:00 0:30 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20150202T230000", "DTEND;TZID=Europe/Berlin:20150203T003000" + ), + ), # only start datetime given - ('25.10.2013 06:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20131025T060000', - 'DTEND;TZID=Europe/Berlin:20131025T070000')), - + ( + "25.10.2013 06:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20131025T060000", "DTEND;TZID=Europe/Berlin:20131025T070000" + ), + ), # timezone given - ('25.10.2013 06:00 America/New_York Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=America/New_York:20131025T060000', - 'DTEND;TZID=America/New_York:20131025T070000')) + ( + "25.10.2013 06:00 America/New_York Äwesöme Event", + _create_vevent( + "DTSTART;TZID=America/New_York:20131025T060000", + "DTEND;TZID=America/New_York:20131025T070000", + ), + ), ) -@freeze_time('20140216T120000') +@freeze_time("20140216T120000") def test_construct_event_format_de(): for data_list, vevent_expected in test_set_format_de: vevent = _construct_event(data_list.split(), locale=LOCALE_BERLIN) @@ -424,18 +469,21 @@ def test_construct_event_format_de(): test_set_format_us = _create_testcases( - ('1999/12/31-06:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=America/New_York:19991231T060000', - 'DTEND;TZID=America/New_York:19991231T070000')), - - ('2014/12/18 2014/12/20 Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20141218', - 'DTEND;VALUE=DATE:20141221')), + ( + "1999/12/31-06:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=America/New_York:19991231T060000", + "DTEND;TZID=America/New_York:19991231T070000", + ), + ), + ( + "2014/12/18 2014/12/20 Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20141218", "DTEND;VALUE=DATE:20141221"), + ), ) -@freeze_time('2014-02-16 12:00:00') +@freeze_time("2014-02-16 12:00:00") def test__construct_event_format_us(): for data_list, vevent in test_set_format_us: event = _construct_event(data_list.split(), locale=LOCALE_NEW_YORK) @@ -445,30 +493,34 @@ def test__construct_event_format_us(): test_set_format_de_complexer = _create_testcases( # now events where the start date has to be inferred, too # today - ('8:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T080000', - 'DTEND;TZID=Europe/Berlin:20140216T090000')), - + ( + "8:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T080000", "DTEND;TZID=Europe/Berlin:20140216T090000" + ), + ), # today until tomorrow - ('22:00 1:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T220000', - 'DTEND;TZID=Europe/Berlin:20140217T010000')), - + ( + "22:00 1:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T220000", "DTEND;TZID=Europe/Berlin:20140217T010000" + ), + ), # other timezone - ('22:00 1:00 Europe/London Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/London:20140216T220000', - 'DTEND;TZID=Europe/London:20140217T010000')), - - ('15.06. Äwesöme Event', - _create_vevent('DTSTART;VALUE=DATE:20140615', - 'DTEND;VALUE=DATE:20140616')), + ( + "22:00 1:00 Europe/London Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/London:20140216T220000", "DTEND;TZID=Europe/London:20140217T010000" + ), + ), + ( + "15.06. Äwesöme Event", + _create_vevent("DTSTART;VALUE=DATE:20140615", "DTEND;VALUE=DATE:20140616"), + ), ) -@freeze_time('2014-02-16 12:00:00') +@freeze_time("2014-02-16 12:00:00") def test__construct_event_format_de_complexer(): for data_list, vevent in test_set_format_de_complexer: event = _construct_event(data_list.split(), locale=LOCALE_BERLIN) @@ -476,20 +528,21 @@ def test__construct_event_format_de_complexer(): test_set_leap_year = _create_testcases( - ('29.02. Äwesöme Event', - _create_vevent( - 'DTSTART;VALUE=DATE:20160229', - 'DTEND;VALUE=DATE:20160301', - 'DTSTAMP:20160101T202122Z')), + ( + "29.02. Äwesöme Event", + _create_vevent( + "DTSTART;VALUE=DATE:20160229", "DTEND;VALUE=DATE:20160301", "DTSTAMP:20160101T202122Z" + ), + ), ) def test_leap_year(): for data_list, vevent in test_set_leap_year: - with freeze_time('1999-1-1'): + with freeze_time("1999-1-1"): with pytest.raises(DateTimeParseError): event = _construct_event(data_list.split(), locale=LOCALE_BERLIN) - with freeze_time('2016-1-1 20:21:22'): + with freeze_time("2016-1-1 20:21:22"): event = _construct_event(data_list.split(), locale=LOCALE_BERLIN) assert _replace_uid(event).to_ical() == vevent @@ -497,29 +550,35 @@ def test_leap_year(): test_set_description = _create_testcases( # now events where the start date has to be inferred, too # today - ('8:00 Äwesöme Event :: this is going to be awesome', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T080000', - 'DTEND;TZID=Europe/Berlin:20140216T090000', - 'DESCRIPTION:this is going to be awesome')), - + ( + "8:00 Äwesöme Event :: this is going to be awesome", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T080000", + "DTEND;TZID=Europe/Berlin:20140216T090000", + "DESCRIPTION:this is going to be awesome", + ), + ), # today until tomorrow - ('22:00 1:00 Äwesöme Event :: Will be even better', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T220000', - 'DTEND;TZID=Europe/Berlin:20140217T010000', - 'DESCRIPTION:Will be even better')), - - ('15.06. Äwesöme Event :: and again', - _create_vevent('DTSTART;VALUE=DATE:20140615', - 'DTEND;VALUE=DATE:20140616', - 'DESCRIPTION:and again')), + ( + "22:00 1:00 Äwesöme Event :: Will be even better", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T220000", + "DTEND;TZID=Europe/Berlin:20140217T010000", + "DESCRIPTION:Will be even better", + ), + ), + ( + "15.06. Äwesöme Event :: and again", + _create_vevent( + "DTSTART;VALUE=DATE:20140615", "DTEND;VALUE=DATE:20140616", "DESCRIPTION:and again" + ), + ), ) def test_description(): for data_list, vevent in test_set_description: - with freeze_time('2014-02-16 12:00:00'): + with freeze_time("2014-02-16 12:00:00"): event = _construct_event(data_list.split(), locale=LOCALE_BERLIN) assert _replace_uid(event).to_ical() == vevent @@ -527,94 +586,117 @@ def test_description(): test_set_repeat_floating = _create_testcases( # now events where the start date has to be inferred, too # today - ('8:00 Äwesöme Event', - _create_vevent( - 'DTSTART:20140216T080000', - 'DTEND:20140216T090000', - 'DESCRIPTION:please describe the event', - 'RRULE:FREQ=DAILY;UNTIL=20150604T000000'))) + ( + "8:00 Äwesöme Event", + _create_vevent( + "DTSTART:20140216T080000", + "DTEND:20140216T090000", + "DESCRIPTION:please describe the event", + "RRULE:FREQ=DAILY;UNTIL=20150604T000000", + ), + ) +) def test_repeat_floating(): for data_list, vevent in test_set_repeat_floating: - with freeze_time('2014-02-16 12:00:00'): - event = _construct_event(data_list.split(), - description='please describe the event', - repeat='daily', - until='04.06.2015', - locale=LOCALE_FLOATING) - assert normalize_component(_replace_uid(event).to_ical()) == \ - normalize_component(vevent) + with freeze_time("2014-02-16 12:00:00"): + event = _construct_event( + data_list.split(), + description="please describe the event", + repeat="daily", + until="04.06.2015", + locale=LOCALE_FLOATING, + ) + assert normalize_component(_replace_uid(event).to_ical()) == normalize_component(vevent) test_set_repeat_localized = _create_testcases( # now events where the start date has to be inferred, too # today - ('8:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T080000', - 'DTEND;TZID=Europe/Berlin:20140216T090000', - 'DESCRIPTION:please describe the event', - 'RRULE:FREQ=DAILY;UNTIL=20150604T230000Z'))) + ( + "8:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T080000", + "DTEND;TZID=Europe/Berlin:20140216T090000", + "DESCRIPTION:please describe the event", + "RRULE:FREQ=DAILY;UNTIL=20150604T230000Z", + ), + ) +) def test_repeat_localized(): for data_list, vevent in test_set_repeat_localized: - with freeze_time('2014-02-16 12:00:00'): - event = _construct_event(data_list.split(), - description='please describe the event', - repeat='daily', - until='05.06.2015', - locale=LOCALE_BERLIN) - assert normalize_component(_replace_uid(event).to_ical()) == \ - normalize_component(vevent) + with freeze_time("2014-02-16 12:00:00"): + event = _construct_event( + data_list.split(), + description="please describe the event", + repeat="daily", + until="05.06.2015", + locale=LOCALE_BERLIN, + ) + assert normalize_component(_replace_uid(event).to_ical()) == normalize_component(vevent) test_set_alarm = _create_testcases( - ('8:00 Äwesöme Event', - ['BEGIN:VEVENT', - 'SUMMARY:Äwesöme Event', - 'DTSTART;TZID=Europe/Berlin:20140216T080000', - 'DTEND;TZID=Europe/Berlin:20140216T090000', - 'DTSTAMP:20140216T120000Z', - 'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA', - 'DESCRIPTION:please describe the event', - 'BEGIN:VALARM', - 'ACTION:DISPLAY', - 'DESCRIPTION:please describe the event', - 'TRIGGER:-PT23M', - 'END:VALARM', - 'END:VEVENT'])) - - -@freeze_time('2014-02-16 12:00:00') + ( + "8:00 Äwesöme Event", + [ + "BEGIN:VEVENT", + "SUMMARY:Äwesöme Event", + "DTSTART;TZID=Europe/Berlin:20140216T080000", + "DTEND;TZID=Europe/Berlin:20140216T090000", + "DTSTAMP:20140216T120000Z", + "UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA", + "DESCRIPTION:please describe the event", + "BEGIN:VALARM", + "ACTION:DISPLAY", + "DESCRIPTION:please describe the event", + "TRIGGER:-PT23M", + "END:VALARM", + "END:VEVENT", + ], + ) +) + + +@freeze_time("2014-02-16 12:00:00") def test_alarm(): for data_list, vevent in test_set_alarm: - event = _construct_event(data_list.split(), - description='please describe the event', - alarm='23m', - locale=LOCALE_BERLIN) + event = _construct_event( + data_list.split(), + description="please describe the event", + alarm="23m", + locale=LOCALE_BERLIN, + ) assert _replace_uid(event).to_ical() == vevent test_set_description_and_location_and_categories = _create_testcases( # now events where the start date has to be inferred, too # today - ('8:00 Äwesöme Event', - _create_vevent( - 'DTSTART;TZID=Europe/Berlin:20140216T080000', - 'DTEND;TZID=Europe/Berlin:20140216T090000', - 'CATEGORIES:boring meeting', - 'DESCRIPTION:please describe the event', - 'LOCATION:in the office'))) + ( + "8:00 Äwesöme Event", + _create_vevent( + "DTSTART;TZID=Europe/Berlin:20140216T080000", + "DTEND;TZID=Europe/Berlin:20140216T090000", + "CATEGORIES:boring meeting", + "DESCRIPTION:please describe the event", + "LOCATION:in the office", + ), + ) +) -@freeze_time('2014-02-16 12:00:00') +@freeze_time("2014-02-16 12:00:00") def test_description_and_location_and_categories(): for data_list, vevent in test_set_description_and_location_and_categories: - event = _construct_event(data_list.split(), - description='please describe the event', - location='in the office', - categories=['boring meeting'], - locale=LOCALE_BERLIN) + event = _construct_event( + data_list.split(), + description="please describe the event", + location="in the office", + categories=["boring meeting"], + locale=LOCALE_BERLIN, + ) assert _replace_uid(event).to_ical() == vevent diff --git a/tests/settings_test.py b/tests/settings_test.py index 224b7db9b..acd2602fe 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -22,98 +22,118 @@ from .utils import LOCALE_BERLIN -PATH = __file__.rsplit('/', 1)[0] + '/configs/' +PATH = __file__.rsplit("/", 1)[0] + "/configs/" def get_localzone(): # this reproduces the code in settings.util for the time being import pytz + return pytz.timezone(str(_get_localzone())) class TestSettings: def test_simple_config(self): config = get_config( - PATH + 'simple.conf', + PATH + "simple.conf", _get_color_from_vdir=lambda x: None, - _get_vdir_type=lambda x: 'calendar', + _get_vdir_type=lambda x: "calendar", ) comp_config = { - 'calendars': { - 'home': { - 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, - 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + "calendars": { + "home": { + "path": os.path.expanduser("~/.calendars/home/"), + "readonly": False, + "color": None, + "priority": 10, + "type": "calendar", + "addresses": [""], }, - 'work': { - 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, - 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + "work": { + "path": os.path.expanduser("~/.calendars/work/"), + "readonly": False, + "color": None, + "priority": 10, + "type": "calendar", + "addresses": [""], }, }, - 'sqlite': {'path': os.path.expanduser('~/.cache/khal/khal.db')}, - 'locale': LOCALE_BERLIN, - 'default': { - 'default_calendar': None, - 'print_new': 'False', - 'highlight_event_days': False, - 'timedelta': dt.timedelta(days=2), - 'default_event_duration': dt.timedelta(hours=1), - 'default_dayevent_duration': dt.timedelta(days=1), - 'default_event_alarm': dt.timedelta(0), - 'default_dayevent_alarm': dt.timedelta(0), - 'show_all_days': False, - 'enable_mouse': True, - } + "sqlite": {"path": os.path.expanduser("~/.cache/khal/khal.db")}, + "locale": LOCALE_BERLIN, + "default": { + "default_calendar": None, + "print_new": "False", + "highlight_event_days": False, + "timedelta": dt.timedelta(days=2), + "default_event_duration": dt.timedelta(hours=1), + "default_dayevent_duration": dt.timedelta(days=1), + "default_event_alarm": dt.timedelta(0), + "default_dayevent_alarm": dt.timedelta(0), + "show_all_days": False, + "enable_mouse": True, + }, } for key in comp_config: assert config[key] == comp_config[key] def test_nocalendars(self): with pytest.raises(InvalidSettingsError): - get_config(PATH + 'nocalendars.conf') + get_config(PATH + "nocalendars.conf") def test_one_level_calendar(self): with pytest.raises(InvalidSettingsError): - get_config(PATH + 'one_level_calendars.conf') + get_config(PATH + "one_level_calendars.conf") def test_small(self): config = get_config( - PATH + 'small.conf', + PATH + "small.conf", _get_color_from_vdir=lambda x: None, - _get_vdir_type=lambda x: 'calendar', + _get_vdir_type=lambda x: "calendar", ) comp_config = { - 'calendars': { - 'home': {'path': os.path.expanduser('~/.calendars/home/'), - 'color': 'dark green', 'readonly': False, 'priority': 20, - 'type': 'calendar', 'addresses': ['']}, - 'work': {'path': os.path.expanduser('~/.calendars/work/'), - 'readonly': True, 'color': None, 'priority': 10, - 'type': 'calendar', 'addresses': ['user@example.com']}}, - 'sqlite': {'path': os.path.expanduser('~/.cache/khal/khal.db')}, - 'locale': { - 'local_timezone': get_localzone(), - 'default_timezone': get_localzone(), - 'timeformat': '%X', - 'dateformat': '%x', - 'longdateformat': '%x', - 'datetimeformat': '%c', - 'longdatetimeformat': '%c', - 'firstweekday': 0, - 'unicode_symbols': True, - 'weeknumbers': False, + "calendars": { + "home": { + "path": os.path.expanduser("~/.calendars/home/"), + "color": "dark green", + "readonly": False, + "priority": 20, + "type": "calendar", + "addresses": [""], + }, + "work": { + "path": os.path.expanduser("~/.calendars/work/"), + "readonly": True, + "color": None, + "priority": 10, + "type": "calendar", + "addresses": ["user@example.com"], + }, + }, + "sqlite": {"path": os.path.expanduser("~/.cache/khal/khal.db")}, + "locale": { + "local_timezone": get_localzone(), + "default_timezone": get_localzone(), + "timeformat": "%X", + "dateformat": "%x", + "longdateformat": "%x", + "datetimeformat": "%c", + "longdatetimeformat": "%c", + "firstweekday": 0, + "unicode_symbols": True, + "weeknumbers": False, + }, + "default": { + "default_calendar": None, + "print_new": "False", + "highlight_event_days": False, + "timedelta": dt.timedelta(days=2), + "default_event_duration": dt.timedelta(hours=1), + "default_dayevent_duration": dt.timedelta(days=1), + "show_all_days": False, + "enable_mouse": True, + "default_event_alarm": dt.timedelta(0), + "default_dayevent_alarm": dt.timedelta(0), }, - 'default': { - 'default_calendar': None, - 'print_new': 'False', - 'highlight_event_days': False, - 'timedelta': dt.timedelta(days=2), - 'default_event_duration': dt.timedelta(hours=1), - 'default_dayevent_duration': dt.timedelta(days=1), - 'show_all_days': False, - 'enable_mouse': True, - 'default_event_alarm': dt.timedelta(0), - 'default_dayevent_alarm': dt.timedelta(0), - } } for key in comp_config: assert config[key] == comp_config[key] @@ -131,8 +151,8 @@ def test_old_config(self, tmpdir): longdateformat: %d.%m.%Y [default] """ - conf_path = str(tmpdir.join('old.conf')) - with open(conf_path, 'w+') as conf: + conf_path = str(tmpdir.join("old.conf")) + with open(conf_path, "w+") as conf: conf.write(old_config) with pytest.raises(CannotParseConfigFileError): get_config(conf_path) @@ -147,8 +167,8 @@ def test_extra_sections(self, tmpdir): [unknownsection] foo = bar """ - conf_path = str(tmpdir.join('old.conf')) - with open(conf_path, 'w+') as conf: + conf_path = str(tmpdir.join("old.conf")) + with open(conf_path, "w+") as conf: conf.write(config) get_config(conf_path) # FIXME test for log entries @@ -163,8 +183,8 @@ def test_default_calendar_readonly(self, tmpdir): [default] default_calendar = home """ - conf_path = str(tmpdir.join('old.conf')) - with open(conf_path, 'w+') as conf: + conf_path = str(tmpdir.join("old.conf")) + with open(conf_path, "w+") as conf: conf.write(config) with pytest.raises(InvalidSettingsError): config_checks(get_config(conf_path)) @@ -172,142 +192,157 @@ def test_default_calendar_readonly(self, tmpdir): def test_broken_color(metavdirs): path = metavdirs - newvdir = path + '/cal5/' + newvdir = path + "/cal5/" os.makedirs(newvdir) - with open(newvdir + 'color', 'w') as metafile: - metafile.write('xxx') + with open(newvdir + "color", "w") as metafile: + metafile.write("xxx") assert get_color_from_vdir(newvdir) is None def test_discover(metavdirs): test_vdirs = { - '/cal1/public', '/cal1/private', '/cal2/public', - '/cal3/home', '/cal3/public', '/cal3/work', - '/cal4/cfgcolor', '/cal4/dircolor', '/cal4/cfgcolor_again', - '/cal4/cfgcolor_once_more', - '/singlecollection', + "/cal1/public", + "/cal1/private", + "/cal2/public", + "/cal3/home", + "/cal3/public", + "/cal3/work", + "/cal4/cfgcolor", + "/cal4/dircolor", + "/cal4/cfgcolor_again", + "/cal4/cfgcolor_once_more", + "/singlecollection", } path = metavdirs - assert test_vdirs == {vdir[len(path):] for vdir in get_all_vdirs(path + '/**/*/')} - assert test_vdirs == {vdir[len(path):] for vdir in get_all_vdirs(path + '/**/')} - assert test_vdirs == {vdir[len(path):] for vdir in get_all_vdirs(path + '/**/*')} + assert test_vdirs == {vdir[len(path) :] for vdir in get_all_vdirs(path + "/**/*/")} + assert test_vdirs == {vdir[len(path) :] for vdir in get_all_vdirs(path + "/**/")} + assert test_vdirs == {vdir[len(path) :] for vdir in get_all_vdirs(path + "/**/*")} def test_get_unique_name(metavdirs): path = metavdirs - vdirs = list(get_all_vdirs(path + '/**/')) + vdirs = list(get_all_vdirs(path + "/**/")) names = [] for vdir in sorted(vdirs): names.append(get_unique_name(vdir, names)) - assert sorted(names) == sorted([ - 'my private calendar', 'my calendar', 'public', 'home', 'public1', - 'work', 'cfgcolor', 'cfgcolor_again', 'cfgcolor_once_more', 'dircolor', - 'singlecollection', - ]) + assert sorted(names) == sorted( + [ + "my private calendar", + "my calendar", + "public", + "home", + "public1", + "work", + "cfgcolor", + "cfgcolor_again", + "cfgcolor_once_more", + "dircolor", + "singlecollection", + ] + ) def test_config_checks(metavdirs): path = metavdirs config = { - 'calendars': { - 'default': {'path': path + '/cal[1-3]/*', 'type': 'discover'}, - 'calendars_color': {'path': path + '/cal4/*', 'type': 'discover', 'color': 'dark blue'}, + "calendars": { + "default": {"path": path + "/cal[1-3]/*", "type": "discover"}, + "calendars_color": {"path": path + "/cal4/*", "type": "discover", "color": "dark blue"}, }, - 'sqlite': {'path': '/tmp'}, - 'locale': {'default_timezone': 'Europe/Berlin', 'local_timezone': 'Europe/Berlin'}, - 'default': {'default_calendar': None}, + "sqlite": {"path": "/tmp"}, + "locale": {"default_timezone": "Europe/Berlin", "local_timezone": "Europe/Berlin"}, + "default": {"default_calendar": None}, } config_checks(config) # cut off the part of the path that changes on each run - for cal in config['calendars']: - config['calendars'][cal]['path'] = config['calendars'][cal]['path'][len(metavdirs):] + for cal in config["calendars"]: + config["calendars"][cal]["path"] = config["calendars"][cal]["path"][len(metavdirs) :] test_config = { - 'calendars': { - 'home': { - 'color': None, - 'path': '/cal3/home', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "calendars": { + "home": { + "color": None, + "path": "/cal3/home", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'my calendar': { - 'color': 'dark blue', - 'path': '/cal1/public', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "my calendar": { + "color": "dark blue", + "path": "/cal1/public", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'my private calendar': { - 'color': '#FF00FF', - 'path': '/cal1/private', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "my private calendar": { + "color": "#FF00FF", + "path": "/cal1/private", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'public1': { - 'color': None, - 'path': '/cal3/public', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "public1": { + "color": None, + "path": "/cal3/public", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'public': { - 'color': None, - 'path': '/cal2/public', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "public": { + "color": None, + "path": "/cal2/public", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'work': { - 'color': None, - 'path': '/cal3/work', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "work": { + "color": None, + "path": "/cal3/work", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'cfgcolor': { - 'color': 'dark blue', - 'path': '/cal4/cfgcolor', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "cfgcolor": { + "color": "dark blue", + "path": "/cal4/cfgcolor", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'dircolor': { - 'color': 'dark blue', - 'path': '/cal4/dircolor', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "dircolor": { + "color": "dark blue", + "path": "/cal4/dircolor", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'cfgcolor_again': { - 'color': 'dark blue', - 'path': '/cal4/cfgcolor_again', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "cfgcolor_again": { + "color": "dark blue", + "path": "/cal4/cfgcolor_again", + "readonly": False, + "type": "calendar", + "priority": 10, }, - 'cfgcolor_once_more': { - 'color': 'dark blue', - 'path': '/cal4/cfgcolor_once_more', - 'readonly': False, - 'type': 'calendar', - 'priority': 10, + "cfgcolor_once_more": { + "color": "dark blue", + "path": "/cal4/cfgcolor_once_more", + "readonly": False, + "type": "calendar", + "priority": 10, }, - }, - 'default': {'default_calendar': None}, - 'locale': {'default_timezone': 'Europe/Berlin', 'local_timezone': 'Europe/Berlin'}, - 'sqlite': {'path': '/tmp'}, + "default": {"default_calendar": None}, + "locale": {"default_timezone": "Europe/Berlin", "local_timezone": "Europe/Berlin"}, + "sqlite": {"path": "/tmp"}, } - assert config['calendars'] == test_config['calendars'] + assert config["calendars"] == test_config["calendars"] assert config == test_config def test_is_color(): - assert is_color('dark blue') == 'dark blue' - assert is_color('#123456') == '#123456' - assert is_color('123') == '123' + assert is_color("dark blue") == "dark blue" + assert is_color("#123456") == "#123456" + assert is_color("123") == "123" with pytest.raises(VdtValueError): - assert is_color('red') == 'red' + assert is_color("red") == "red" diff --git a/tests/terminal_test.py b/tests/terminal_test.py index 1a3e6ffe7..def94133a 100644 --- a/tests/terminal_test.py +++ b/tests/terminal_test.py @@ -2,30 +2,28 @@ def test_colored(): - assert colored('test', 'light cyan') == '\33[1;36mtest\x1b[0m' - assert colored('täst', 'white') == '\33[37mtäst\x1b[0m' - assert colored('täst', 'white', 'dark green') == '\x1b[37m\x1b[42mtäst\x1b[0m' - assert colored('täst', 'light magenta', 'dark green', True) == '\x1b[1;35m\x1b[42mtäst\x1b[0m' - assert colored('täst', 'light magenta', 'dark green', False) == '\x1b[95m\x1b[42mtäst\x1b[0m' - assert colored('täst', 'light magenta', 'light green', True) == '\x1b[1;35m\x1b[42mtäst\x1b[0m' - assert colored('täst', 'light magenta', 'light green', False) == '\x1b[95m\x1b[102mtäst\x1b[0m' - assert colored('täst', '5', '20') == '\x1b[38;5;5m\x1b[48;5;20mtäst\x1b[0m' - assert colored('täst', '#F0F', '#00AABB') == \ - '\x1b[38;2;255;0;255m\x1b[48;2;0;170;187mtäst\x1b[0m' + assert colored("test", "light cyan") == "\33[1;36mtest\x1b[0m" + assert colored("täst", "white") == "\33[37mtäst\x1b[0m" + assert colored("täst", "white", "dark green") == "\x1b[37m\x1b[42mtäst\x1b[0m" + assert colored("täst", "light magenta", "dark green", True) == "\x1b[1;35m\x1b[42mtäst\x1b[0m" + assert colored("täst", "light magenta", "dark green", False) == "\x1b[95m\x1b[42mtäst\x1b[0m" + assert colored("täst", "light magenta", "light green", True) == "\x1b[1;35m\x1b[42mtäst\x1b[0m" + assert colored("täst", "light magenta", "light green", False) == "\x1b[95m\x1b[102mtäst\x1b[0m" + assert colored("täst", "5", "20") == "\x1b[38;5;5m\x1b[48;5;20mtäst\x1b[0m" + assert ( + colored("täst", "#F0F", "#00AABB") == "\x1b[38;2;255;0;255m\x1b[48;2;0;170;187mtäst\x1b[0m" + ) class TestMergeColumns: - def test_longer_right(self): - left = ['uiae', 'nrtd'] - right = ['123456', '234567', '345678'] - out = ['uiae 123456', - 'nrtd 234567', - ' 345678'] + left = ["uiae", "nrtd"] + right = ["123456", "234567", "345678"] + out = ["uiae 123456", "nrtd 234567", " 345678"] assert merge_columns(left, right, width=4) == out def test_longer_left(self): - left = ['uiae', 'nrtd', 'xvlc'] - right = ['123456', '234567'] - out = ['uiae 123456', 'nrtd 234567', 'xvlc '] + left = ["uiae", "nrtd", "xvlc"] + right = ["123456", "234567"] + out = ["uiae 123456", "nrtd 234567", "xvlc "] assert merge_columns(left, right, width=4) == out diff --git a/tests/ui/canvas_render.py b/tests/ui/canvas_render.py index 3915296a6..7ec0981d9 100644 --- a/tests/ui/canvas_render.py +++ b/tests/ui/canvas_render.py @@ -12,22 +12,22 @@ def __init__(self, canvas, palette: dict[str, str] | None = None) -> None: """currently only support foreground colors, so palette is a dictionary of attributes and foreground colors""" self._canvas = canvas - self._palette : dict[str, tuple[bool, str]]= {} + self._palette: dict[str, tuple[bool, str]] = {} if palette: for key, color in palette.items(): self.add_color(key, color) def add_color(self, key: str, color: str) -> None: - if color.startswith('#'): # RGB colour + if color.startswith("#"): # RGB colour r = color[1:3] g = color[3:5] b = color[5:8] rgb = int(r, 16), int(g, 16), int(b, 16) - value = True, '\33[38;2;{!s};{!s};{!s}m'.format(*rgb) + value = True, "\33[38;2;{!s};{!s};{!s}m".format(*rgb) else: - color = color.split(' ')[-1] - if color == 'gray': - color = 'white' # click will insist on US-english + color = color.split(" ")[-1] + if color == "gray": + color = "white" # click will insist on US-english value = False, color self._palette[key] = value # (is_ansi, color) @@ -43,7 +43,7 @@ def transform(self) -> str: col = row[-1] self._process_char(col[0], col[1], col[2].rstrip()) - self.output.write('\n') + self.output.write("\n") return self.output.getvalue() @@ -54,6 +54,6 @@ def _process_char(self, fmt, _, b): else: fmt = self._palette[fmt] if fmt[0]: - self.output.write(f'{fmt[1]}{click.style(text)}') + self.output.write(f"{fmt[1]}{click.style(text)}") else: self.output.write(click.style(text, fg=fmt[1])) diff --git a/tests/ui/test_calendarwidget.py b/tests/ui/test_calendarwidget.py index 82a76e46f..4c19f40e9 100644 --- a/tests/ui/test_calendarwidget.py +++ b/tests/ui/test_calendarwidget.py @@ -7,57 +7,64 @@ on_press: dict = {} keybindings = { - 'today': ['T'], - 'left': ['left', 'h', 'backspace'], - 'up': ['up', 'k'], - 'right': ['right', 'l', ' '], - 'down': ['down', 'j'], + "today": ["T"], + "left": ["left", "h", "backspace"], + "up": ["up", "k"], + "right": ["right", "l", " "], + "down": ["down", "j"], } def test_initial_focus_today(): today = dt.date.today() - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + weeknumbers="right", + ) assert frame.focus_date == today def test_set_focus_date(): today = dt.date.today() for diff in range(-10, 10, 1): - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + weeknumbers="right", + ) day = today + dt.timedelta(days=diff) frame.set_focus_date(day) assert frame.focus_date == day def test_set_focus_date_weekstart_6(): - - with freeze_time('2016-04-10'): + with freeze_time("2016-04-10"): today = dt.date.today() for diff in range(-21, 21, 1): - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - firstweekday=6, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + firstweekday=6, + weeknumbers="right", + ) day = today + dt.timedelta(days=diff) frame.set_focus_date(day) assert frame.focus_date == day - with freeze_time('2016-04-23'): + with freeze_time("2016-04-23"): today = dt.date.today() for diff in range(10): - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - firstweekday=6, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + firstweekday=6, + weeknumbers="right", + ) day = today + dt.timedelta(days=diff) frame.set_focus_date(day) assert frame.focus_date == day @@ -65,18 +72,23 @@ def test_set_focus_date_weekstart_6(): def test_set_focus_far_future(): future_date = dt.date.today() + dt.timedelta(days=1000) - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + weeknumbers="right", + ) frame.set_focus_date(future_date) assert frame.focus_date == future_date + def test_set_focus_far_past(): future_date = dt.date.today() - dt.timedelta(days=1000) - frame = CalendarWidget(on_date_change=lambda _: None, - keybindings=keybindings, - on_press=on_press, - weeknumbers='right') + frame = CalendarWidget( + on_date_change=lambda _: None, + keybindings=keybindings, + on_press=on_press, + weeknumbers="right", + ) frame.set_focus_date(future_date) assert frame.focus_date == future_date diff --git a/tests/ui/test_editor.py b/tests/ui/test_editor.py index 25845196a..13b6c2f57 100644 --- a/tests/ui/test_editor.py +++ b/tests/ui/test_editor.py @@ -7,17 +7,17 @@ from .canvas_render import CanvasTranslator -CONF = {'locale': LOCALE_BERLIN, 'keybindings': {}, 'view': {'monthdisplay': 'firstday'}} +CONF = {"locale": LOCALE_BERLIN, "keybindings": {}, "view": {"monthdisplay": "firstday"}} START = BERLIN.localize(dt.datetime(2015, 4, 26, 22, 23)) END = BERLIN.localize(dt.datetime(2015, 4, 27, 23, 23)) palette = { - 'date header focused': 'blue', - 'date header': 'green', - 'default': 'black', - 'edit focused': 'red', - 'edit': 'blue', + "date header focused": "blue", + "date header": "green", + "default": "black", + "edit focused": "red", + "edit": "blue", } @@ -26,54 +26,54 @@ def test_popup(monkeypatch): #405 """ + class FakeCalendar: def store(self, *args, **kwargs): self.args = args self.kwargs = kwargs fake = FakeCalendar() - monkeypatch.setattr( - 'khal.ui.calendarwidget.CalendarWidget.__init__', fake.store) + monkeypatch.setattr("khal.ui.calendarwidget.CalendarWidget.__init__", fake.store) see = StartEndEditor(START, END, CONF) - see.widgets.startdate.keypress((22, ), 'enter') - assert fake.kwargs['initial'] == dt.date(2015, 4, 26) - see.widgets.enddate.keypress((22, ), 'enter') - assert fake.kwargs['initial'] == dt.date(2015, 4, 27) + see.widgets.startdate.keypress((22,), "enter") + assert fake.kwargs["initial"] == dt.date(2015, 4, 26) + see.widgets.enddate.keypress((22,), "enter") + assert fake.kwargs["initial"] == dt.date(2015, 4, 27) def test_check_understood_rrule(): assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=1SU') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=1SU") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYMONTHDAY=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYMONTHDAY=1") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYSETPOS=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=TH;BYSETPOS=1") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TU,TH;BYSETPOS=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=TU,TH;BYSETPOS=1") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=1") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,SU,MO,TH,FR,TU,SA;BYSETPOS=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,SU,MO,TH,FR,TU,SA;BYSETPOS=1") ) assert RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,MO,TH,FR,TU,SA;BYSETPOS=1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;INTERVAL=2;BYDAY=WE,MO,TH,FR,TU,SA;BYSETPOS=1") ) assert not RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=-1SU') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=-1SU") ) assert not RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=1,2,3,4,5,6,7') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=1,2,3,4,5,6,7") ) assert not RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=-1') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=TH;BYMONTHDAY=-1") ) assert not RecurrenceEditor.check_understood_rrule( - icalendar.vRecur.from_ical('FREQ=MONTHLY;BYDAY=TH;BYSETPOS=3') + icalendar.vRecur.from_ical("FREQ=MONTHLY;BYDAY=TH;BYSETPOS=3") ) @@ -82,15 +82,15 @@ def test_editor(): editor = StartEndEditor( BERLIN.localize(dt.datetime(2017, 10, 2, 13)), BERLIN.localize(dt.datetime(2017, 10, 4, 18)), - conf=CONF + conf=CONF, ) assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) assert editor.enddt == BERLIN.localize(dt.datetime(2017, 10, 4, 18)) assert editor.changed is False for _ in range(3): - editor.keypress((10, ), 'tab') + editor.keypress((10,), "tab") for _ in range(3): - editor.keypress((10, ), 'shift tab') + editor.keypress((10,), "shift tab") assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) assert editor.enddt == BERLIN.localize(dt.datetime(2017, 10, 4, 18)) assert editor.changed is False @@ -101,12 +101,12 @@ def test_convert_to_date(): editor = StartEndEditor( BERLIN.localize(dt.datetime(2017, 10, 2, 13)), BERLIN.localize(dt.datetime(2017, 10, 4, 18)), - conf=CONF + conf=CONF, ) - canvas = editor.render((50, ), True) + canvas = editor.render((50,), True) assert CanvasTranslator(canvas, palette).transform() == ( - '[ ] Allday\nFrom: \x1b[31m2.10.2017 \x1b[0m \x1b[34m13:00 \x1b[0m\n' - 'To: \x1b[34m04.10.2017\x1b[0m \x1b[34m18:00 \x1b[0m\n' + "[ ] Allday\nFrom: \x1b[31m2.10.2017 \x1b[0m \x1b[34m13:00 \x1b[0m\n" + "To: \x1b[34m04.10.2017\x1b[0m \x1b[34m18:00 \x1b[0m\n" ) assert editor.startdt == BERLIN.localize(dt.datetime(2017, 10, 2, 13)) @@ -115,17 +115,16 @@ def test_convert_to_date(): assert editor.allday is False # set to all day event - editor.keypress((10, ), 'shift tab') - editor.keypress((10, ), ' ') + editor.keypress((10,), "shift tab") + editor.keypress((10,), " ") for _ in range(3): - editor.keypress((10, ), 'tab') + editor.keypress((10,), "tab") for _ in range(3): - editor.keypress((10, ), 'shift tab') + editor.keypress((10,), "shift tab") - canvas = editor.render((50, ), True) + canvas = editor.render((50,), True) assert CanvasTranslator(canvas, palette).transform() == ( - '[X] Allday\nFrom: \x1b[34m02.10.2017\x1b[0m \n' - 'To: \x1b[34m04.10.2017\x1b[0m \n' + "[X] Allday\nFrom: \x1b[34m02.10.2017\x1b[0m \nTo: \x1b[34m04.10.2017\x1b[0m \n" ) assert editor.changed is True diff --git a/tests/ui/test_walker.py b/tests/ui/test_walker.py index 6e77c32fc..a3f19a0ab 100644 --- a/tests/ui/test_walker.py +++ b/tests/ui/test_walker.py @@ -7,57 +7,66 @@ from .canvas_render import CanvasTranslator -CONF = {'locale': LOCALE_BERLIN, 'keybindings': {}, - 'view': {'monthdisplay': 'firstday'}, - 'default': {'timedelta': dt.timedelta(days=3)}, - } +CONF = { + "locale": LOCALE_BERLIN, + "keybindings": {}, + "view": {"monthdisplay": "firstday"}, + "default": {"timedelta": dt.timedelta(days=3)}, +} palette = { - 'date header focused': 'blue', - 'date header': 'green', - 'default': 'black', + "date header focused": "blue", + "date header": "green", + "default": "black", } -@freeze_time('2017-6-7') +@freeze_time("2017-6-7") def test_daywalker(coll_vdirs): collection, _ = coll_vdirs this_date = dt.date.today() daywalker = DayWalker(this_date, None, CONF, collection, delete_status={}) elistbox = DListBox( - daywalker, parent=None, conf=CONF, + daywalker, + parent=None, + conf=CONF, delete_status=lambda: False, toggle_delete_all=None, toggle_delete_instance=None, dynamic_days=True, ) canvas = elistbox.render((50, 6), True) - assert CanvasTranslator(canvas, palette).transform() == \ - """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m + assert ( + CanvasTranslator(canvas, palette).transform() + == """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m \x1b[32mTomorrow (Thursday, 08.06.2017)\x1b[0m \x1b[32mFriday, 09.06.2017 (2 days from now)\x1b[0m \x1b[32mSaturday, 10.06.2017 (3 days from now)\x1b[0m \x1b[32mSunday, 11.06.2017 (4 days from now)\x1b[0m \x1b[32mMonday, 12.06.2017 (5 days from now)\x1b[0m """ + ) -@freeze_time('2017-6-7') +@freeze_time("2017-6-7") def test_staticdaywalker(coll_vdirs): collection, _ = coll_vdirs this_date = dt.date.today() daywalker = StaticDayWalker(this_date, None, CONF, collection, delete_status={}) elistbox = DListBox( - daywalker, parent=None, conf=CONF, + daywalker, + parent=None, + conf=CONF, delete_status=lambda: False, toggle_delete_all=None, toggle_delete_instance=None, dynamic_days=False, ) canvas = elistbox.render((50, 10), True) - assert CanvasTranslator(canvas, palette).transform() == \ - """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m + assert ( + CanvasTranslator(canvas, palette).transform() + == """\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m \x1b[32mTomorrow (Thursday, 08.06.2017)\x1b[0m \x1b[32mFriday, 09.06.2017 (2 days from now)\x1b[0m @@ -68,23 +77,28 @@ def test_staticdaywalker(coll_vdirs): """ + ) -@freeze_time('2017-6-7') +@freeze_time("2017-6-7") def test_staticdaywalker_3(coll_vdirs): collection, _ = coll_vdirs this_date = dt.date.today() conf = {} conf.update(CONF) - conf['default'] = {'timedelta': dt.timedelta(days=1)} + conf["default"] = {"timedelta": dt.timedelta(days=1)} daywalker = StaticDayWalker(this_date, None, conf, collection, delete_status={}) elistbox = DListBox( - daywalker, parent=None, conf=conf, + daywalker, + parent=None, + conf=conf, delete_status=lambda: False, toggle_delete_all=None, toggle_delete_instance=None, dynamic_days=False, ) canvas = elistbox.render((50, 10), True) - assert CanvasTranslator(canvas, palette).transform() == \ - '\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m\n\n\n\n\n\n\n\n\n\n' + assert ( + CanvasTranslator(canvas, palette).transform() + == "\x1b[34mToday (Wednesday, 07.06.2017)\x1b[0m\n\n\n\n\n\n\n\n\n\n" + ) diff --git a/tests/ui/test_widgets.py b/tests/ui/test_widgets.py index 99ea922c9..b4e42796f 100644 --- a/tests/ui/test_widgets.py +++ b/tests/ui/test_widgets.py @@ -2,27 +2,27 @@ def test_delete_last_word(): - tests = [ - ('Fü1ü Bär!', 'Fü1ü Bär', 1), - ('Füü Bär1', 'Füü ', 1), - ('Füü1 Bär1', 'Füü1 ', 1), - (' Füü Bär', ' Füü ', 1), - ('Füü Bär.Füü', 'Füü Bär.', 1), - ('Füü Bär.(Füü)', 'Füü Bär.(Füü', 1), - ('Füü ', '', 1), - ('Füü ', '', 1), - ('Füü', '', 1), - ('', '', 1), - - ('Füü Bär.(Füü)', 'Füü Bär.', 3), - ('Füü Bär1', '', 2), - ('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, ' - 'sed diam nonumy eirmod tempor invidunt ut labore et dolore ' - 'magna aliquyam erat, sed diam volest.', - 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, ' - 'sed diam nonumy eirmod tempor invidunt ut labore ', - 10) + ("Fü1ü Bär!", "Fü1ü Bär", 1), + ("Füü Bär1", "Füü ", 1), + ("Füü1 Bär1", "Füü1 ", 1), + (" Füü Bär", " Füü ", 1), + ("Füü Bär.Füü", "Füü Bär.", 1), + ("Füü Bär.(Füü)", "Füü Bär.(Füü", 1), + ("Füü ", "", 1), + ("Füü ", "", 1), + ("Füü", "", 1), + ("", "", 1), + ("Füü Bär.(Füü)", "Füü Bär.", 3), + ("Füü Bär1", "", 2), + ( + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " + "sed diam nonumy eirmod tempor invidunt ut labore et dolore " + "magna aliquyam erat, sed diam volest.", + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " + "sed diam nonumy eirmod tempor invidunt ut labore ", + 10, + ), ] for org, short, number in tests: diff --git a/tests/utils.py b/tests/utils.py index 856da0f4d..9c66cd2d2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,66 +15,66 @@ CollVdirType = tuple[CalendarCollection, dict[str, Vdir]] -cal0 = 'a_calendar' -cal1 = 'foobar' +cal0 = "a_calendar" +cal1 = "foobar" cal2 = "Dad's calendar" -cal3 = 'private' +cal3 = "private" example_cals = [cal0, cal1, cal2, cal3] -BERLIN = pytz.timezone('Europe/Berlin') -NEW_YORK = pytz.timezone('America/New_York') -LONDON = pytz.timezone('Europe/London') -SAMOA = pytz.timezone('Pacific/Samoa') -SYDNEY = pytz.timezone('Australia/Sydney') -GMTPLUS3 = pytz.timezone('Etc/GMT+3') +BERLIN = pytz.timezone("Europe/Berlin") +NEW_YORK = pytz.timezone("America/New_York") +LONDON = pytz.timezone("Europe/London") +SAMOA = pytz.timezone("Pacific/Samoa") +SYDNEY = pytz.timezone("Australia/Sydney") +GMTPLUS3 = pytz.timezone("Etc/GMT+3") # the lucky people in Bogota don't know the pain that is DST -BOGOTA = pytz.timezone('America/Bogota') +BOGOTA = pytz.timezone("America/Bogota") LOCALE_BERLIN: LocaleConfiguration = { - 'default_timezone': BERLIN, - 'local_timezone': BERLIN, - 'dateformat': '%d.%m.', - 'longdateformat': '%d.%m.%Y', - 'timeformat': '%H:%M', - 'datetimeformat': '%d.%m. %H:%M', - 'longdatetimeformat': '%d.%m.%Y %H:%M', - 'unicode_symbols': True, - 'firstweekday': 0, - 'weeknumbers': False, + "default_timezone": BERLIN, + "local_timezone": BERLIN, + "dateformat": "%d.%m.", + "longdateformat": "%d.%m.%Y", + "timeformat": "%H:%M", + "datetimeformat": "%d.%m. %H:%M", + "longdatetimeformat": "%d.%m.%Y %H:%M", + "unicode_symbols": True, + "firstweekday": 0, + "weeknumbers": False, } LOCALE_NEW_YORK: LocaleConfiguration = { - 'default_timezone': NEW_YORK, - 'local_timezone': NEW_YORK, - 'timeformat': '%H:%M', - 'dateformat': '%Y/%m/%d', - 'longdateformat': '%Y/%m/%d', - 'datetimeformat': '%Y/%m/%d-%H:%M', - 'longdatetimeformat': '%Y/%m/%d-%H:%M', - 'firstweekday': 6, - 'unicode_symbols': True, - 'weeknumbers': False, + "default_timezone": NEW_YORK, + "local_timezone": NEW_YORK, + "timeformat": "%H:%M", + "dateformat": "%Y/%m/%d", + "longdateformat": "%Y/%m/%d", + "datetimeformat": "%Y/%m/%d-%H:%M", + "longdatetimeformat": "%Y/%m/%d-%H:%M", + "firstweekday": 6, + "unicode_symbols": True, + "weeknumbers": False, } LOCALE_SAMOA = { - 'local_timezone': SAMOA, - 'default_timezone': SAMOA, - 'unicode_symbols': True, + "local_timezone": SAMOA, + "default_timezone": SAMOA, + "unicode_symbols": True, } -LOCALE_SYDNEY = {'local_timezone': SYDNEY, 'default_timezone': SYDNEY} +LOCALE_SYDNEY = {"local_timezone": SYDNEY, "default_timezone": SYDNEY} LOCALE_BOGOTA = LOCALE_BERLIN.copy() -LOCALE_BOGOTA['local_timezone'] = BOGOTA -LOCALE_BOGOTA['default_timezone'] = BOGOTA +LOCALE_BOGOTA["local_timezone"] = BOGOTA +LOCALE_BOGOTA["default_timezone"] = BOGOTA LOCALE_MIXED = LOCALE_BERLIN.copy() -LOCALE_MIXED['local_timezone'] = BOGOTA +LOCALE_MIXED["local_timezone"] = BOGOTA LOCALE_FLOATING = LOCALE_BERLIN.copy() -LOCALE_FLOATING['default_timezone'] = None # type: ignore -LOCALE_FLOATING['local_timezone'] = None # type: ignore +LOCALE_FLOATING["default_timezone"] = None # type: ignore +LOCALE_FLOATING["local_timezone"] = None # type: ignore def normalize_component(x): @@ -84,50 +84,47 @@ def inner(c): contentlines = Contentlines() for name, value in c.property_items(sorted=True, recursive=False): contentlines.append(c.content_line(name, value, sorted=True)) - contentlines.append('') + contentlines.append("") - return (c.name, contentlines.to_ical(), - frozenset(inner(sub) for sub in c.subcomponents)) + return (c.name, contentlines.to_ical(), frozenset(inner(sub) for sub in c.subcomponents)) return inner(x) def _get_text(event_name): - directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' - if directory == '/ics/': - directory = './ics/' + directory = "/".join(__file__.split("/")[:-1]) + "/ics/" + if directory == "/ics/": + directory = "./ics/" - with open(os.path.join(directory, event_name + '.ics'), 'rb') as f: - rv = f.read().decode('utf-8') + with open(os.path.join(directory, event_name + ".ics"), "rb") as f: + rv = f.read().decode("utf-8") return rv def _get_vevent_file(event_path): - directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' - with open(os.path.join(directory, event_path + '.ics'), 'rb') as f: - ical = icalendar.Calendar.from_ical( - f.read() - ) + directory = "/".join(__file__.split("/")[:-1]) + "/ics/" + with open(os.path.join(directory, event_path + ".ics"), "rb") as f: + ical = icalendar.Calendar.from_ical(f.read()) for component in ical.walk(): - if component.name == 'VEVENT': + if component.name == "VEVENT": return component def _get_ics_filepath(event_name): - directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' - if directory == '/ics/': - directory = './ics/' - return os.path.join(directory, event_name + '.ics') + directory = "/".join(__file__.split("/")[:-1]) + "/ics/" + if directory == "/ics/": + directory = "./ics/" + return os.path.join(directory, event_name + ".ics") def _get_all_vevents_file(event_path): - directory = '/'.join(__file__.split('/')[:-1]) + '/ics/' + directory = "/".join(__file__.split("/")[:-1]) + "/ics/" ical = icalendar.Calendar.from_ical( - open(os.path.join(directory, event_path + '.ics'), 'rb').read() + open(os.path.join(directory, event_path + ".ics"), "rb").read() ) for component in ical.walk(): - if component.name == 'VEVENT': + if component.name == "VEVENT": yield component @@ -135,8 +132,8 @@ def _replace_uid(event): """ Replace an event's UID with E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA. """ - event.pop('uid') - event.add('uid', 'E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA') + event.pop("uid") + event.add("uid", "E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA") return event diff --git a/tests/utils_test.py b/tests/utils_test.py index d9f09bf36..4989b2583 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,4 +1,5 @@ """testing functions from the khal.utils""" + import datetime as dt from click import style @@ -8,43 +9,42 @@ def test_relative_timedelta_str(): - with freeze_time('2016-9-19'): - assert utils.relative_timedelta_str(dt.date(2016, 9, 24)) == '5 days from now' - assert utils.relative_timedelta_str(dt.date(2016, 9, 29)) == '~1 week from now' - assert utils.relative_timedelta_str(dt.date(2017, 9, 29)) == '~1 year from now' - assert utils.relative_timedelta_str(dt.date(2016, 7, 29)) == '~7 weeks ago' + with freeze_time("2016-9-19"): + assert utils.relative_timedelta_str(dt.date(2016, 9, 24)) == "5 days from now" + assert utils.relative_timedelta_str(dt.date(2016, 9, 29)) == "~1 week from now" + assert utils.relative_timedelta_str(dt.date(2017, 9, 29)) == "~1 year from now" + assert utils.relative_timedelta_str(dt.date(2016, 7, 29)) == "~7 weeks ago" weekheader = """ Mo Tu We Th Fr Sa Su """ today_line = """Today""" calendarline = ( - "Nov 31  1  2 " - " 3  4  5  6" + "Nov 31  1  2  3  4  5  6" ) def test_last_reset(): - assert utils.find_last_reset(weekheader) == (31, 35, '\x1b[0m') - assert utils.find_last_reset(today_line) == (13, 17, '\x1b[0m') - assert utils.find_last_reset(calendarline) == (99, 103, '\x1b[0m') - assert utils.find_last_reset('Hello World') == (-2, -1, '') + assert utils.find_last_reset(weekheader) == (31, 35, "\x1b[0m") + assert utils.find_last_reset(today_line) == (13, 17, "\x1b[0m") + assert utils.find_last_reset(calendarline) == (99, 103, "\x1b[0m") + assert utils.find_last_reset("Hello World") == (-2, -1, "") def test_last_sgr(): - assert utils.find_last_sgr(weekheader) == (0, 4, '\x1b[1m') - assert utils.find_last_sgr(today_line) == (0, 4, '\x1b[1m') - assert utils.find_last_sgr(calendarline) == (92, 97, '\x1b[32m') - assert utils.find_last_sgr('Hello World') == (-2, -1, '') + assert utils.find_last_sgr(weekheader) == (0, 4, "\x1b[1m") + assert utils.find_last_sgr(today_line) == (0, 4, "\x1b[1m") + assert utils.find_last_sgr(calendarline) == (92, 97, "\x1b[32m") + assert utils.find_last_sgr("Hello World") == (-2, -1, "") def test_find_unmatched_sgr(): assert utils.find_unmatched_sgr(weekheader) is None assert utils.find_unmatched_sgr(today_line) is None assert utils.find_unmatched_sgr(calendarline) is None - assert utils.find_unmatched_sgr('\x1b[31mHello World') == '\x1b[31m' - assert utils.find_unmatched_sgr('\x1b[31mHello\x1b[0m \x1b[32mWorld') == '\x1b[32m' - assert utils.find_unmatched_sgr('foo\x1b[1;31mbar') == '\x1b[1;31m' - assert utils.find_unmatched_sgr('\x1b[0mfoo\x1b[1;31m') == '\x1b[1;31m' + assert utils.find_unmatched_sgr("\x1b[31mHello World") == "\x1b[31m" + assert utils.find_unmatched_sgr("\x1b[31mHello\x1b[0m \x1b[32mWorld") == "\x1b[32m" + assert utils.find_unmatched_sgr("foo\x1b[1;31mbar") == "\x1b[1;31m" + assert utils.find_unmatched_sgr("\x1b[0mfoo\x1b[1;31m") == "\x1b[1;31m" def test_color_wrap(): @@ -70,7 +70,7 @@ def test_color_wrap_256(): "\x1b[38;2;17;255;0mLorem ipsum\x1b[0m", "\x1b[38;2;17;255;0mdolor sit amet, consetetur\x1b[0m", "\x1b[38;2;17;255;0msadipscing elitr, sed diam\x1b[0m", - "\x1b[38;2;17;255;0mnonumy\x1b[0m" + "\x1b[38;2;17;255;0mnonumy\x1b[0m", ] assert utils.color_wrap(text, 30) == expected @@ -85,15 +85,15 @@ def test_color_wrap_multiple_colors_and_tabs(): "\x1b[38;2;255;0m11:00-14:00 Rivers Street (noodles and pizza) (R) Calendar", ) expected = [ - '\x1b[31m14:00-14:50 AST-1002-102 INTRO AST II/STAR GALAX (R)\x1b[0m', - '\x1b[31mClasses\x1b[0m', - '15:30-16:45 PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R)', - 'Classes', - '\x1b[38;2;255;0m17:00-18:00 Pay Ticket Deadline Calendar\x1b[0m', - '09:30-10:45 PHL-1501-101 MIND, KNOWLEDGE & REALITY (R)', - 'Classes', - '\x1b[38;2;255;0m11:00-14:00 Rivers Street (noodles and\x1b[0m', - '\x1b[38;2;255;0mpizza) (R) Calendar\x1b[0m' + "\x1b[31m14:00-14:50 AST-1002-102 INTRO AST II/STAR GALAX (R)\x1b[0m", + "\x1b[31mClasses\x1b[0m", + "15:30-16:45 PHL-2000-104 PHILOSOPHY, SOCIETY & ETHICS (R)", + "Classes", + "\x1b[38;2;255;0m17:00-18:00 Pay Ticket Deadline Calendar\x1b[0m", + "09:30-10:45 PHL-1501-101 MIND, KNOWLEDGE & REALITY (R)", + "Classes", + "\x1b[38;2;255;0m11:00-14:00 Rivers Street (noodles and\x1b[0m", + "\x1b[38;2;255;0mpizza) (R) Calendar\x1b[0m", ] actual = [] for line in text: @@ -123,6 +123,6 @@ def test_get_weekday_occurrence(): def test_human_formatter_width(): - formatter = utils.human_formatter('{red}{title}', width=10) - output = formatter({'title': 'morethan10characters', 'red': style('', reset=False, fg='red')}) - assert output.startswith('\x1b[31mmoret\x1b[0m') + formatter = utils.human_formatter("{red}{title}", width=10) + output = formatter({"title": "morethan10characters", "red": style("", reset=False, fg="red")}) + assert output.startswith("\x1b[31mmoret\x1b[0m") diff --git a/tests/vdir_test.py b/tests/vdir_test.py index 2bddf1d2c..9eb34d1c1 100644 --- a/tests/vdir_test.py +++ b/tests/vdir_test.py @@ -26,17 +26,17 @@ def test_etag(tmpdir, sleep_time): - fpath = os.path.join(str(tmpdir), 'foo') + fpath = os.path.join(str(tmpdir), "foo") - file_ = open(fpath, 'w') - file_.write('foo') + file_ = open(fpath, "w") + file_.write("foo") file_.close() old_etag = vdir.get_etag_from_file(fpath) sleep(sleep_time) - file_ = open(fpath, 'w') - file_.write('foo') + file_ = open(fpath, "w") + file_.write("foo") file_.close() new_etag = vdir.get_etag_from_file(fpath) @@ -45,17 +45,17 @@ def test_etag(tmpdir, sleep_time): def test_etag_sync(tmpdir, sleep_time): - fpath = os.path.join(str(tmpdir), 'foo') + fpath = os.path.join(str(tmpdir), "foo") - file_ = open(fpath, 'w') - file_.write('foo') + file_ = open(fpath, "w") + file_.write("foo") file_.close() os.sync() old_etag = vdir.get_etag_from_file(fpath) sleep(sleep_time) - file_ = open(fpath, 'w') - file_.write('foo') + file_ = open(fpath, "w") + file_.write("foo") file_.close() new_etag = vdir.get_etag_from_file(fpath) diff --git a/tests/vtimezone_test.py b/tests/vtimezone_test.py index b59380be3..cb7d2daca 100644 --- a/tests/vtimezone_test.py +++ b/tests/vtimezone_test.py @@ -5,79 +5,89 @@ from khal.khalendar.event import create_timezone -berlin = pytz.timezone('Europe/Berlin') -bogota = pytz.timezone('America/Bogota') +berlin = pytz.timezone("Europe/Berlin") +bogota = pytz.timezone("America/Bogota") atime = dt.datetime(2014, 10, 28, 10, 10) btime = dt.datetime(2016, 10, 28, 10, 10) def test_berlin(): - vberlin_std = b'\r\n'.join( - [b'BEGIN:STANDARD', - b'DTSTART:20141026T020000', - b'TZNAME:CET', - b'TZOFFSETFROM:+0200', - b'TZOFFSETTO:+0100', - b'END:STANDARD', - ]) + vberlin_std = b"\r\n".join( + [ + b"BEGIN:STANDARD", + b"DTSTART:20141026T020000", + b"TZNAME:CET", + b"TZOFFSETFROM:+0200", + b"TZOFFSETTO:+0100", + b"END:STANDARD", + ] + ) - vberlin_dst = b'\r\n'.join( - [b'BEGIN:DAYLIGHT', - b'DTSTART:20150329T030000', - b'TZNAME:CEST', - b'TZOFFSETFROM:+0100', - b'TZOFFSETTO:+0200', - b'END:DAYLIGHT', - ]) + vberlin_dst = b"\r\n".join( + [ + b"BEGIN:DAYLIGHT", + b"DTSTART:20150329T030000", + b"TZNAME:CEST", + b"TZOFFSETFROM:+0100", + b"TZOFFSETTO:+0200", + b"END:DAYLIGHT", + ] + ) vberlin = create_timezone(berlin, atime, atime).to_ical() - assert b'TZID:Europe/Berlin' in vberlin + assert b"TZID:Europe/Berlin" in vberlin assert vberlin_std in vberlin assert vberlin_dst in vberlin def test_berlin_rdate(): - vberlin_std = b'\r\n'.join( - [b'BEGIN:STANDARD', - b'DTSTART:20141026T020000', - b'RDATE:20151025T020000,20161030T020000', - b'TZNAME:CET', - b'TZOFFSETFROM:+0200', - b'TZOFFSETTO:+0100', - b'END:STANDARD', - ]) + vberlin_std = b"\r\n".join( + [ + b"BEGIN:STANDARD", + b"DTSTART:20141026T020000", + b"RDATE:20151025T020000,20161030T020000", + b"TZNAME:CET", + b"TZOFFSETFROM:+0200", + b"TZOFFSETTO:+0100", + b"END:STANDARD", + ] + ) - vberlin_dst = b'\r\n'.join( - [b'BEGIN:DAYLIGHT', - b'DTSTART:20150329T030000', - b'RDATE:20160327T030000', - b'TZNAME:CEST', - b'TZOFFSETFROM:+0100', - b'TZOFFSETTO:+0200', - b'END:DAYLIGHT', - ]) + vberlin_dst = b"\r\n".join( + [ + b"BEGIN:DAYLIGHT", + b"DTSTART:20150329T030000", + b"RDATE:20160327T030000", + b"TZNAME:CEST", + b"TZOFFSETFROM:+0100", + b"TZOFFSETTO:+0200", + b"END:DAYLIGHT", + ] + ) vberlin = create_timezone(berlin, atime, btime).to_ical() - assert b'TZID:Europe/Berlin' in vberlin + assert b"TZID:Europe/Berlin" in vberlin assert vberlin_std in vberlin assert vberlin_dst in vberlin def test_bogota(): - vbogota = [b'BEGIN:VTIMEZONE', - b'TZID:America/Bogota', - b'BEGIN:STANDARD', - b'DTSTART:19930206T230000', - b'TZNAME:COT', - b'TZOFFSETFROM:-0400', - b'TZOFFSETTO:-0500', - b'END:STANDARD', - b'END:VTIMEZONE', - b''] - if version.parse(pytz.__version__) > version.Version('2017.1'): - vbogota[4] = b'TZNAME:-05' - if version.parse(pytz.__version__) < version.Version('2022.7'): - vbogota.insert(4, b'RDATE:20380118T221407') + vbogota = [ + b"BEGIN:VTIMEZONE", + b"TZID:America/Bogota", + b"BEGIN:STANDARD", + b"DTSTART:19930206T230000", + b"TZNAME:COT", + b"TZOFFSETFROM:-0400", + b"TZOFFSETTO:-0500", + b"END:STANDARD", + b"END:VTIMEZONE", + b"", + ] + if version.parse(pytz.__version__) > version.Version("2017.1"): + vbogota[4] = b"TZNAME:-05" + if version.parse(pytz.__version__) < version.Version("2022.7"): + vbogota.insert(4, b"RDATE:20380118T221407") - assert create_timezone(bogota, atime, atime).to_ical().split(b'\r\n') == vbogota + assert create_timezone(bogota, atime, atime).to_ical().split(b"\r\n") == vbogota