prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Persist log file
  When running the cuber, there usually is a lot of output (overflowing terminal buffers). It would be great if all of that output would be persisted in a log file as well to allow to easily scan and search through it, afterwards.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `webknossos/webknossos/utils.py`
  Content:
  ```
  1 import argparse
  2 import calendar
  3 import functools
  4 import json
  5 import logging
  6 import os
  7 import time
  8 from concurrent.futures import as_completed
  9 from concurrent.futures._base import Future
  10 from datetime import datetime
  11 from multiprocessing import cpu_count
  12 from pathlib import Path
  13 from typing import Any, Callable, Iterable, List, Optional, Union
  14 
  15 import rich
  16 from cluster_tools import WrappedProcessPoolExecutor, get_executor
  17 from cluster_tools.schedulers.cluster_executor import ClusterExecutor
  18 from rich.progress import Progress
  19 
  20 times = {}
  21 
  22 
  23 def time_start(identifier: str) -> None:
  24     times[identifier] = time.time()
  25     logging.debug("{} started".format(identifier))
  26 
  27 
  28 def time_stop(identifier: str) -> None:
  29     _time = times.pop(identifier)
  30     logging.debug("{} took {:.8f}s".format(identifier, time.time() - _time))
  31 
  32 
  33 def get_executor_for_args(
  34     args: Optional[argparse.Namespace],
  35 ) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:
  36     executor = None
  37     if args is None:
  38         # For backwards compatibility with code from other packages
  39         # we allow args to be None. In this case we are defaulting
  40         # to these values:
  41         jobs = cpu_count()
  42         executor = get_executor("multiprocessing", max_workers=jobs)
  43         logging.info("Using pool of {} workers.".format(jobs))
  44     elif args.distribution_strategy == "multiprocessing":
  45         # Also accept "processes" instead of job to be compatible with segmentation-tools.
  46         # In the long run, the args should be unified and provided by the clustertools.
  47         if "jobs" in args:
  48             jobs = args.jobs
  49         elif "processes" in args:
  50             jobs = args.processes
  51         else:
  52             jobs = cpu_count()
  53 
  54         executor = get_executor("multiprocessing", max_workers=jobs)
  55         logging.info("Using pool of {} workers.".format(jobs))
  56     elif args.distribution_strategy in ("slurm", "kubernetes"):
  57         if args.job_resources is None:
  58             resources_example = (
  59                 '{"mem": "1G"}'
  60                 if args.distribution_strategy == "slurm"
  61                 else '{"memory": "1G"}'
  62             )
  63             raise argparse.ArgumentTypeError(
  64                 f"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'"
  65             )
  66 
  67         executor = get_executor(
  68             args.distribution_strategy,
  69             debug=True,
  70             keep_logs=True,
  71             job_resources=json.loads(args.job_resources),
  72         )
  73         logging.info(f"Using {args.distribution_strategy} cluster.")
  74     else:
  75         logging.error(
  76             "Unknown distribution strategy: {}".format(args.distribution_strategy)
  77         )
  78 
  79     return executor
  80 
  81 
  82 F = Callable[..., Any]
  83 
  84 
  85 def named_partial(func: F, *args: Any, **kwargs: Any) -> F:
  86     # Propagate __name__ and __doc__ attributes to partial function
  87     partial_func = functools.partial(func, *args, **kwargs)
  88     functools.update_wrapper(partial_func, func)
  89     if hasattr(func, "__annotations__"):
  90         # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511
  91         partial_func.__annotations__ = {}
  92     return partial_func
  93 
  94 
  95 def wait_and_ensure_success(
  96     futures: List[Future], progress_desc: Optional[str] = None
  97 ) -> List[Any]:
  98     """Waits for all futures to complete and raises an exception
  99     as soon as a future resolves with an error."""
  100 
  101     results = []
  102     if progress_desc is None:
  103         for fut in as_completed(futures):
  104             results.append(fut.result())
  105     else:
  106         with get_rich_progress() as progress:
  107             task = progress.add_task(progress_desc, total=len(futures))
  108             for fut in as_completed(futures):
  109                 results.append(fut.result())
  110                 progress.update(task, advance=1)
  111     return results
  112 
  113 
  114 def snake_to_camel_case(snake_case_name: str) -> str:
  115     parts = snake_case_name.split("_")
  116     return parts[0] + "".join(part.title() for part in parts[1:])
  117 
  118 
  119 def get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:
  120     for i in range(0, len(arr), chunk_size):
  121         yield arr[i : i + chunk_size]
  122 
  123 
  124 def time_since_epoch_in_ms() -> int:
  125     d = datetime.utcnow()
  126     unixtime = calendar.timegm(d.utctimetuple())
  127     return unixtime * 1000
  128 
  129 
  130 def copy_directory_with_symlinks(
  131     src_path: Path,
  132     dst_path: Path,
  133     ignore: Iterable[str] = tuple(),
  134     make_relative: bool = False,
  135 ) -> None:
  136     """
  137     Links all directories in src_path / dir_name to dst_path / dir_name.
  138     """
  139     for item in src_path.iterdir():
  140         if item.name not in ignore:
  141             symlink_path = dst_path / item.name
  142             if make_relative:
  143                 rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))
  144             else:
  145                 rel_or_abspath = item.resolve()
  146             symlink_path.symlink_to(rel_or_abspath)
  147 
  148 
  149 def setup_logging(args: argparse.Namespace) -> None:
  150     logging.basicConfig(
  151         level=(logging.DEBUG if args.verbose else logging.INFO),
  152         format="%(asctime)s %(levelname)s %(message)s",
  153     )
  154 
  155 
  156 def add_verbose_flag(parser: argparse.ArgumentParser) -> None:
  157     parser.add_argument(
  158         "--verbose", help="Verbose output", dest="verbose", action="store_true"
  159     )
  160 
  161     parser.set_defaults(verbose=False)
  162 
  163 
  164 def get_rich_progress() -> Progress:
  165     return Progress(
  166         "[progress.description]{task.description}",
  167         rich.progress.BarColumn(),
  168         "[progress.percentage]{task.percentage:>3.0f}%",
  169         rich.progress.TimeElapsedColumn(),
  170         "|",
  171         rich.progress.TimeRemainingColumn(),
  172     )
  173
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/webknossos/webknossos/utils.py b/webknossos/webknossos/utils.py
  --- a/webknossos/webknossos/utils.py
  +++ b/webknossos/webknossos/utils.py
  @@ -4,6 +4,7 @@
   import json
   import logging
   import os
  +import sys
   import time
   from concurrent.futures import as_completed
   from concurrent.futures._base import Future
  @@ -147,10 +148,28 @@
   
   
   def setup_logging(args: argparse.Namespace) -> None:
  -    logging.basicConfig(
  -        level=(logging.DEBUG if args.verbose else logging.INFO),
  -        format="%(asctime)s %(levelname)s %(message)s",
  -    )
  +    log_path = Path(f"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt")
  +
  +    console_log_level = logging.DEBUG if args.verbose else logging.INFO
  +    file_log_level = logging.DEBUG
  +
  +    logging_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
  +
  +    # Always set the global log level to the more verbose of console_log_level and
  +    # file_log_level to allow to log with different log levels to console and files.
  +    root_logger = logging.getLogger()
  +    root_logger.setLevel(min(console_log_level, file_log_level))
  +
  +    console = logging.StreamHandler(sys.stdout)
  +    console.setLevel(console_log_level)
  +    console.setFormatter(logging_formatter)
  +    root_logger.addHandler(console)
  +
  +    log_path.parent.mkdir(parents=True, exist_ok=True)
  +    file_handler = logging.FileHandler(log_path, mode="w", encoding="UTF-8")
  +    file_handler.setLevel(file_log_level)
  +    file_handler.setFormatter(logging_formatter)
  +    root_logger.addHandler(file_handler)
   
   
   def add_verbose_flag(parser: argparse.ArgumentParser) -> None:
  
verification_info: {"golden_diff": "diff --git a/webknossos/webknossos/utils.py b/webknossos/webknossos/utils.py\n--- a/webknossos/webknossos/utils.py\n+++ b/webknossos/webknossos/utils.py\n@@ -4,6 +4,7 @@\n import json\n import logging\n import os\n+import sys\n import time\n from concurrent.futures import as_completed\n from concurrent.futures._base import Future\n@@ -147,10 +148,28 @@\n \n \n def setup_logging(args: argparse.Namespace) -> None:\n-    logging.basicConfig(\n-        level=(logging.DEBUG if args.verbose else logging.INFO),\n-        format=\"%(asctime)s %(levelname)s %(message)s\",\n-    )\n+    log_path = Path(f\"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt\")\n+\n+    console_log_level = logging.DEBUG if args.verbose else logging.INFO\n+    file_log_level = logging.DEBUG\n+\n+    logging_formatter = logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\")\n+\n+    # Always set the global log level to the more verbose of console_log_level and\n+    # file_log_level to allow to log with different log levels to console and files.\n+    root_logger = logging.getLogger()\n+    root_logger.setLevel(min(console_log_level, file_log_level))\n+\n+    console = logging.StreamHandler(sys.stdout)\n+    console.setLevel(console_log_level)\n+    console.setFormatter(logging_formatter)\n+    root_logger.addHandler(console)\n+\n+    log_path.parent.mkdir(parents=True, exist_ok=True)\n+    file_handler = logging.FileHandler(log_path, mode=\"w\", encoding=\"UTF-8\")\n+    file_handler.setLevel(file_log_level)\n+    file_handler.setFormatter(logging_formatter)\n+    root_logger.addHandler(file_handler)\n \n \n def add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n", "issue": "Persist log file\nWhen running the cuber, there usually is a lot of output (overflowing terminal buffers). It would be great if all of that output would be persisted in a log file as well to allow to easily scan and search through it, afterwards.\n", "before_files": [{"content": "import argparse\nimport calendar\nimport functools\nimport json\nimport logging\nimport os\nimport time\nfrom concurrent.futures import as_completed\nfrom concurrent.futures._base import Future\nfrom datetime import datetime\nfrom multiprocessing import cpu_count\nfrom pathlib import Path\nfrom typing import Any, Callable, Iterable, List, Optional, Union\n\nimport rich\nfrom cluster_tools import WrappedProcessPoolExecutor, get_executor\nfrom cluster_tools.schedulers.cluster_executor import ClusterExecutor\nfrom rich.progress import Progress\n\ntimes = {}\n\n\ndef time_start(identifier: str) -> None:\n    times[identifier] = time.time()\n    logging.debug(\"{} started\".format(identifier))\n\n\ndef time_stop(identifier: str) -> None:\n    _time = times.pop(identifier)\n    logging.debug(\"{} took {:.8f}s\".format(identifier, time.time() - _time))\n\n\ndef get_executor_for_args(\n    args: Optional[argparse.Namespace],\n) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:\n    executor = None\n    if args is None:\n        # For backwards compatibility with code from other packages\n        # we allow args to be None. In this case we are defaulting\n        # to these values:\n        jobs = cpu_count()\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy == \"multiprocessing\":\n        # Also accept \"processes\" instead of job to be compatible with segmentation-tools.\n        # In the long run, the args should be unified and provided by the clustertools.\n        if \"jobs\" in args:\n            jobs = args.jobs\n        elif \"processes\" in args:\n            jobs = args.processes\n        else:\n            jobs = cpu_count()\n\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy in (\"slurm\", \"kubernetes\"):\n        if args.job_resources is None:\n            resources_example = (\n                '{\"mem\": \"1G\"}'\n                if args.distribution_strategy == \"slurm\"\n                else '{\"memory\": \"1G\"}'\n            )\n            raise argparse.ArgumentTypeError(\n                f\"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'\"\n            )\n\n        executor = get_executor(\n            args.distribution_strategy,\n            debug=True,\n            keep_logs=True,\n            job_resources=json.loads(args.job_resources),\n        )\n        logging.info(f\"Using {args.distribution_strategy} cluster.\")\n    else:\n        logging.error(\n            \"Unknown distribution strategy: {}\".format(args.distribution_strategy)\n        )\n\n    return executor\n\n\nF = Callable[..., Any]\n\n\ndef named_partial(func: F, *args: Any, **kwargs: Any) -> F:\n    # Propagate __name__ and __doc__ attributes to partial function\n    partial_func = functools.partial(func, *args, **kwargs)\n    functools.update_wrapper(partial_func, func)\n    if hasattr(func, \"__annotations__\"):\n        # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511\n        partial_func.__annotations__ = {}\n    return partial_func\n\n\ndef wait_and_ensure_success(\n    futures: List[Future], progress_desc: Optional[str] = None\n) -> List[Any]:\n    \"\"\"Waits for all futures to complete and raises an exception\n    as soon as a future resolves with an error.\"\"\"\n\n    results = []\n    if progress_desc is None:\n        for fut in as_completed(futures):\n            results.append(fut.result())\n    else:\n        with get_rich_progress() as progress:\n            task = progress.add_task(progress_desc, total=len(futures))\n            for fut in as_completed(futures):\n                results.append(fut.result())\n                progress.update(task, advance=1)\n    return results\n\n\ndef snake_to_camel_case(snake_case_name: str) -> str:\n    parts = snake_case_name.split(\"_\")\n    return parts[0] + \"\".join(part.title() for part in parts[1:])\n\n\ndef get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:\n    for i in range(0, len(arr), chunk_size):\n        yield arr[i : i + chunk_size]\n\n\ndef time_since_epoch_in_ms() -> int:\n    d = datetime.utcnow()\n    unixtime = calendar.timegm(d.utctimetuple())\n    return unixtime * 1000\n\n\ndef copy_directory_with_symlinks(\n    src_path: Path,\n    dst_path: Path,\n    ignore: Iterable[str] = tuple(),\n    make_relative: bool = False,\n) -> None:\n    \"\"\"\n    Links all directories in src_path / dir_name to dst_path / dir_name.\n    \"\"\"\n    for item in src_path.iterdir():\n        if item.name not in ignore:\n            symlink_path = dst_path / item.name\n            if make_relative:\n                rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))\n            else:\n                rel_or_abspath = item.resolve()\n            symlink_path.symlink_to(rel_or_abspath)\n\n\ndef setup_logging(args: argparse.Namespace) -> None:\n    logging.basicConfig(\n        level=(logging.DEBUG if args.verbose else logging.INFO),\n        format=\"%(asctime)s %(levelname)s %(message)s\",\n    )\n\n\ndef add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\n        \"--verbose\", help=\"Verbose output\", dest=\"verbose\", action=\"store_true\"\n    )\n\n    parser.set_defaults(verbose=False)\n\n\ndef get_rich_progress() -> Progress:\n    return Progress(\n        \"[progress.description]{task.description}\",\n        rich.progress.BarColumn(),\n        \"[progress.percentage]{task.percentage:>3.0f}%\",\n        rich.progress.TimeElapsedColumn(),\n        \"|\",\n        rich.progress.TimeRemainingColumn(),\n    )\n", "path": "webknossos/webknossos/utils.py"}], "after_files": [{"content": "import argparse\nimport calendar\nimport functools\nimport json\nimport logging\nimport os\nimport sys\nimport time\nfrom concurrent.futures import as_completed\nfrom concurrent.futures._base import Future\nfrom datetime import datetime\nfrom multiprocessing import cpu_count\nfrom pathlib import Path\nfrom typing import Any, Callable, Iterable, List, Optional, Union\n\nimport rich\nfrom cluster_tools import WrappedProcessPoolExecutor, get_executor\nfrom cluster_tools.schedulers.cluster_executor import ClusterExecutor\nfrom rich.progress import Progress\n\ntimes = {}\n\n\ndef time_start(identifier: str) -> None:\n    times[identifier] = time.time()\n    logging.debug(\"{} started\".format(identifier))\n\n\ndef time_stop(identifier: str) -> None:\n    _time = times.pop(identifier)\n    logging.debug(\"{} took {:.8f}s\".format(identifier, time.time() - _time))\n\n\ndef get_executor_for_args(\n    args: Optional[argparse.Namespace],\n) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:\n    executor = None\n    if args is None:\n        # For backwards compatibility with code from other packages\n        # we allow args to be None. In this case we are defaulting\n        # to these values:\n        jobs = cpu_count()\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy == \"multiprocessing\":\n        # Also accept \"processes\" instead of job to be compatible with segmentation-tools.\n        # In the long run, the args should be unified and provided by the clustertools.\n        if \"jobs\" in args:\n            jobs = args.jobs\n        elif \"processes\" in args:\n            jobs = args.processes\n        else:\n            jobs = cpu_count()\n\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy in (\"slurm\", \"kubernetes\"):\n        if args.job_resources is None:\n            resources_example = (\n                '{\"mem\": \"1G\"}'\n                if args.distribution_strategy == \"slurm\"\n                else '{\"memory\": \"1G\"}'\n            )\n            raise argparse.ArgumentTypeError(\n                f\"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'\"\n            )\n\n        executor = get_executor(\n            args.distribution_strategy,\n            debug=True,\n            keep_logs=True,\n            job_resources=json.loads(args.job_resources),\n        )\n        logging.info(f\"Using {args.distribution_strategy} cluster.\")\n    else:\n        logging.error(\n            \"Unknown distribution strategy: {}\".format(args.distribution_strategy)\n        )\n\n    return executor\n\n\nF = Callable[..., Any]\n\n\ndef named_partial(func: F, *args: Any, **kwargs: Any) -> F:\n    # Propagate __name__ and __doc__ attributes to partial function\n    partial_func = functools.partial(func, *args, **kwargs)\n    functools.update_wrapper(partial_func, func)\n    if hasattr(func, \"__annotations__\"):\n        # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511\n        partial_func.__annotations__ = {}\n    return partial_func\n\n\ndef wait_and_ensure_success(\n    futures: List[Future], progress_desc: Optional[str] = None\n) -> List[Any]:\n    \"\"\"Waits for all futures to complete and raises an exception\n    as soon as a future resolves with an error.\"\"\"\n\n    results = []\n    if progress_desc is None:\n        for fut in as_completed(futures):\n            results.append(fut.result())\n    else:\n        with get_rich_progress() as progress:\n            task = progress.add_task(progress_desc, total=len(futures))\n            for fut in as_completed(futures):\n                results.append(fut.result())\n                progress.update(task, advance=1)\n    return results\n\n\ndef snake_to_camel_case(snake_case_name: str) -> str:\n    parts = snake_case_name.split(\"_\")\n    return parts[0] + \"\".join(part.title() for part in parts[1:])\n\n\ndef get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:\n    for i in range(0, len(arr), chunk_size):\n        yield arr[i : i + chunk_size]\n\n\ndef time_since_epoch_in_ms() -> int:\n    d = datetime.utcnow()\n    unixtime = calendar.timegm(d.utctimetuple())\n    return unixtime * 1000\n\n\ndef copy_directory_with_symlinks(\n    src_path: Path,\n    dst_path: Path,\n    ignore: Iterable[str] = tuple(),\n    make_relative: bool = False,\n) -> None:\n    \"\"\"\n    Links all directories in src_path / dir_name to dst_path / dir_name.\n    \"\"\"\n    for item in src_path.iterdir():\n        if item.name not in ignore:\n            symlink_path = dst_path / item.name\n            if make_relative:\n                rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))\n            else:\n                rel_or_abspath = item.resolve()\n            symlink_path.symlink_to(rel_or_abspath)\n\n\ndef setup_logging(args: argparse.Namespace) -> None:\n    log_path = Path(f\"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt\")\n\n    console_log_level = logging.DEBUG if args.verbose else logging.INFO\n    file_log_level = logging.DEBUG\n\n    logging_formatter = logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\")\n\n    # Always set the global log level to the more verbose of console_log_level and\n    # file_log_level to allow to log with different log levels to console and files.\n    root_logger = logging.getLogger()\n    root_logger.setLevel(min(console_log_level, file_log_level))\n\n    console = logging.StreamHandler(sys.stdout)\n    console.setLevel(console_log_level)\n    console.setFormatter(logging_formatter)\n    root_logger.addHandler(console)\n\n    log_path.parent.mkdir(parents=True, exist_ok=True)\n    file_handler = logging.FileHandler(log_path, mode=\"w\", encoding=\"UTF-8\")\n    file_handler.setLevel(file_log_level)\n    file_handler.setFormatter(logging_formatter)\n    root_logger.addHandler(file_handler)\n\n\ndef add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\n        \"--verbose\", help=\"Verbose output\", dest=\"verbose\", action=\"store_true\"\n    )\n\n    parser.set_defaults(verbose=False)\n\n\ndef get_rich_progress() -> Progress:\n    return Progress(\n        \"[progress.description]{task.description}\",\n        rich.progress.BarColumn(),\n        \"[progress.percentage]{task.percentage:>3.0f}%\",\n        rich.progress.TimeElapsedColumn(),\n        \"|\",\n        rich.progress.TimeRemainingColumn(),\n    )\n", "path": "webknossos/webknossos/utils.py"}]}