We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Report pynndescent version in sc.logging.print_header
Hi,

Thank you for the great tool. I think this is not a bug. 

Recently I upgraded some packages and found my results were different from the previous runs. I figured out that it is caused by different versions of `pynndescent` (0.4.7 vs 0.5.1), which is recommended to use in UMAP. So I think `pynndescent` should be included in the output of `sc.logging.print_header()`.

#### Versions

<details>

-----
anndata     0.7.5
scanpy      1.6.1
sinfo       0.3.1
-----
PIL                 8.1.0
anndata             0.7.5
constants           NA
cycler              0.10.0
cython_runtime      NA
dateutil            2.8.1
get_version         2.1
h5py                3.1.0
highs_wrapper       NA
igraph              0.8.3
joblib              1.0.0
kiwisolver          1.3.1
legacy_api_wrap     1.2
leidenalg           0.8.3
llvmlite            0.35.0
louvain             0.7.0
matplotlib          3.3.3
mpl_toolkits        NA
natsort             7.1.1
numba               0.52.0
numexpr             2.7.2
numpy               1.19.5
packaging           20.8
pandas              1.2.1
pkg_resources       NA
pynndescent         0.5.1
pyparsing           2.4.7
pytz                2020.5
scanpy              1.6.1
scipy               1.6.0
setuptools_scm      NA
sinfo               0.3.1
six                 1.15.0
sklearn             0.24.1
statsmodels         0.12.1
tables              3.6.1
texttable           1.6.3
umap                0.4.6
-----
Python 3.8.5 (default, Sep  4 2020, 07:30:14) [GCC 7.3.0]
Linux-3.10.0-1160.11.1.el7.x86_64-x86_64-with-glibc2.10
40 logical CPU cores, x86_64

</details>


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `scanpy/logging.py`
Content:
```
1 """Logging and Profiling
2 """
3 import io
4 import logging
5 import sys
6 from functools import update_wrapper, partial
7 from logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET
8 from datetime import datetime, timedelta, timezone
9 from typing import Optional
10 
11 import anndata.logging
12 from sinfo import sinfo
13 
14 
15 HINT = (INFO + DEBUG) // 2
16 logging.addLevelName(HINT, 'HINT')
17 
18 
19 class _RootLogger(logging.RootLogger):
20     def __init__(self, level):
21         super().__init__(level)
22         self.propagate = False
23         _RootLogger.manager = logging.Manager(self)
24 
25     def log(
26         self,
27         level: int,
28         msg: str,
29         *,
30         extra: Optional[dict] = None,
31         time: datetime = None,
32         deep: Optional[str] = None,
33     ) -> datetime:
34         from . import settings
35 
36         now = datetime.now(timezone.utc)
37         time_passed: timedelta = None if time is None else now - time
38         extra = {
39             **(extra or {}),
40             'deep': deep if settings.verbosity.level < level else None,
41             'time_passed': time_passed,
42         }
43         super().log(level, msg, extra=extra)
44         return now
45 
46     def critical(self, msg, *, time=None, deep=None, extra=None) -> datetime:
47         return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra)
48 
49     def error(self, msg, *, time=None, deep=None, extra=None) -> datetime:
50         return self.log(ERROR, msg, time=time, deep=deep, extra=extra)
51 
52     def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime:
53         return self.log(WARNING, msg, time=time, deep=deep, extra=extra)
54 
55     def info(self, msg, *, time=None, deep=None, extra=None) -> datetime:
56         return self.log(INFO, msg, time=time, deep=deep, extra=extra)
57 
58     def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime:
59         return self.log(HINT, msg, time=time, deep=deep, extra=extra)
60 
61     def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime:
62         return self.log(DEBUG, msg, time=time, deep=deep, extra=extra)
63 
64 
65 def _set_log_file(settings):
66     file = settings.logfile
67     name = settings.logpath
68     root = settings._root_logger
69     h = logging.StreamHandler(file) if name is None else logging.FileHandler(name)
70     h.setFormatter(_LogFormatter())
71     h.setLevel(root.level)
72     if len(root.handlers) == 1:
73         root.removeHandler(root.handlers[0])
74     elif len(root.handlers) > 1:
75         raise RuntimeError('Scanpy’s root logger somehow got more than one handler')
76     root.addHandler(h)
77 
78 
79 def _set_log_level(settings, level: int):
80     root = settings._root_logger
81     root.setLevel(level)
82     (h,) = root.handlers  # may only be 1
83     h.setLevel(level)
84 
85 
86 class _LogFormatter(logging.Formatter):
87     def __init__(
88         self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'
89     ):
90         super().__init__(fmt, datefmt, style)
91 
92     def format(self, record: logging.LogRecord):
93         format_orig = self._style._fmt
94         if record.levelno == INFO:
95             self._style._fmt = '{message}'
96         elif record.levelno == HINT:
97             self._style._fmt = '--> {message}'
98         elif record.levelno == DEBUG:
99             self._style._fmt = '    {message}'
100         if record.time_passed:
101             # strip microseconds
102             if record.time_passed.microseconds:
103                 record.time_passed = timedelta(
104                     seconds=int(record.time_passed.total_seconds())
105                 )
106             if '{time_passed}' in record.msg:
107                 record.msg = record.msg.replace(
108                     '{time_passed}', str(record.time_passed)
109                 )
110             else:
111                 self._style._fmt += ' ({time_passed})'
112         if record.deep:
113             record.msg = f'{record.msg}: {record.deep}'
114         result = logging.Formatter.format(self, record)
115         self._style._fmt = format_orig
116         return result
117 
118 
119 print_memory_usage = anndata.logging.print_memory_usage
120 get_memory_usage = anndata.logging.get_memory_usage
121 
122 
123 _DEPENDENCIES_NUMERICS = [
124     'anndata',  # anndata actually shouldn't, but as long as it's in development
125     'umap',
126     'numpy',
127     'scipy',
128     'pandas',
129     ('sklearn', 'scikit-learn'),
130     'statsmodels',
131     ('igraph', 'python-igraph'),
132     'louvain',
133     'leidenalg',
134 ]
135 
136 
137 def _versions_dependencies(dependencies):
138     # this is not the same as the requirements!
139     for mod in dependencies:
140         mod_name, dist_name = mod if isinstance(mod, tuple) else (mod, mod)
141         try:
142             imp = __import__(mod_name)
143             yield dist_name, imp.__version__
144         except (ImportError, AttributeError):
145             pass
146 
147 
148 def print_header(*, file=None):
149     """\
150     Versions that might influence the numerical results.
151     Matplotlib and Seaborn are excluded from this.
152     """
153 
154     modules = ['scanpy'] + _DEPENDENCIES_NUMERICS
155     print(
156         ' '.join(f'{mod}=={ver}' for mod, ver in _versions_dependencies(modules)),
157         file=file or sys.stdout,
158     )
159 
160 
161 def print_versions(*, file=None):
162     """Print print versions of imported packages"""
163     if file is None:  # Inform people about the behavior change
164         warning('If you miss a compact list, please try `print_header`!')
165     stdout = sys.stdout
166     try:
167         buf = sys.stdout = io.StringIO()
168         sinfo(
169             dependencies=True,
170             excludes=[
171                 'builtins',
172                 'stdlib_list',
173                 'importlib_metadata',
174                 # Special module present if test coverage being calculated
175                 # https://gitlab.com/joelostblom/sinfo/-/issues/10
176                 "$coverage",
177             ],
178         )
179     finally:
180         sys.stdout = stdout
181     output = buf.getvalue()
182     print(output, file=file)
183 
184 
185 def print_version_and_date(*, file=None):
186     """\
187     Useful for starting a notebook so you see when you started working.
188     """
189     from . import __version__
190 
191     if file is None:
192         file = sys.stdout
193     print(
194         f'Running Scanpy {__version__}, ' f'on {datetime.now():%Y-%m-%d %H:%M}.',
195         file=file,
196     )
197 
198 
199 def _copy_docs_and_signature(fn):
200     return partial(update_wrapper, wrapped=fn, assigned=['__doc__', '__annotations__'])
201 
202 
203 def error(
204     msg: str,
205     *,
206     time: datetime = None,
207     deep: Optional[str] = None,
208     extra: Optional[dict] = None,
209 ) -> datetime:
210     """\
211     Log message with specific level and return current time.
212 
213     Parameters
214     ==========
215     msg
216         Message to display.
217     time
218         A time in the past. If this is passed, the time difference from then
219         to now is appended to `msg` as ` (HH:MM:SS)`.
220         If `msg` contains `{time_passed}`, the time difference is instead
221         inserted at that position.
222     deep
223         If the current verbosity is higher than the log function’s level,
224         this gets displayed as well
225     extra
226         Additional values you can specify in `msg` like `{time_passed}`.
227     """
228     from ._settings import settings
229 
230     return settings._root_logger.error(msg, time=time, deep=deep, extra=extra)
231 
232 
233 @_copy_docs_and_signature(error)
234 def warning(msg, *, time=None, deep=None, extra=None) -> datetime:
235     from ._settings import settings
236 
237     return settings._root_logger.warning(msg, time=time, deep=deep, extra=extra)
238 
239 
240 @_copy_docs_and_signature(error)
241 def info(msg, *, time=None, deep=None, extra=None) -> datetime:
242     from ._settings import settings
243 
244     return settings._root_logger.info(msg, time=time, deep=deep, extra=extra)
245 
246 
247 @_copy_docs_and_signature(error)
248 def hint(msg, *, time=None, deep=None, extra=None) -> datetime:
249     from ._settings import settings
250 
251     return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra)
252 
253 
254 @_copy_docs_and_signature(error)
255 def debug(msg, *, time=None, deep=None, extra=None) -> datetime:
256     from ._settings import settings
257 
258     return settings._root_logger.debug(msg, time=time, deep=deep, extra=extra)
259
```
--- 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()

```

diff --git a/scanpy/logging.py b/scanpy/logging.py
--- a/scanpy/logging.py
+++ b/scanpy/logging.py
@@ -131,6 +131,7 @@
     ('igraph', 'python-igraph'),
     'louvain',
     'leidenalg',
+    'pynndescent',
 ]

{"golden_diff": "diff --git a/scanpy/logging.py b/scanpy/logging.py\n--- a/scanpy/logging.py\n+++ b/scanpy/logging.py\n@@ -131,6 +131,7 @@\n     ('igraph', 'python-igraph'),\n     'louvain',\n     'leidenalg',\n+    'pynndescent',\n ]\n", "issue": "Report pynndescent version in sc.logging.print_header\nHi,\r\n\r\nThank you for the great tool. I think this is not a bug. \r\n\r\nRecently I upgraded some packages and found my results were different from the previous runs. I figured out that it is caused by different versions of `pynndescent` (0.4.7 vs 0.5.1), which is recommended to use in UMAP. So I think `pynndescent` should be included in the output of `sc.logging.print_header()`.\r\n\r\n#### Versions\r\n\r\n<details>\r\n\r\n-----\r\nanndata     0.7.5\r\nscanpy      1.6.1\r\nsinfo       0.3.1\r\n-----\r\nPIL                 8.1.0\r\nanndata             0.7.5\r\nconstants           NA\r\ncycler              0.10.0\r\ncython_runtime      NA\r\ndateutil            2.8.1\r\nget_version         2.1\r\nh5py                3.1.0\r\nhighs_wrapper       NA\r\nigraph              0.8.3\r\njoblib              1.0.0\r\nkiwisolver          1.3.1\r\nlegacy_api_wrap     1.2\r\nleidenalg           0.8.3\r\nllvmlite            0.35.0\r\nlouvain             0.7.0\r\nmatplotlib          3.3.3\r\nmpl_toolkits        NA\r\nnatsort             7.1.1\r\nnumba               0.52.0\r\nnumexpr             2.7.2\r\nnumpy               1.19.5\r\npackaging           20.8\r\npandas              1.2.1\r\npkg_resources       NA\r\npynndescent         0.5.1\r\npyparsing           2.4.7\r\npytz                2020.5\r\nscanpy              1.6.1\r\nscipy               1.6.0\r\nsetuptools_scm      NA\r\nsinfo               0.3.1\r\nsix                 1.15.0\r\nsklearn             0.24.1\r\nstatsmodels         0.12.1\r\ntables              3.6.1\r\ntexttable           1.6.3\r\numap                0.4.6\r\n-----\r\nPython 3.8.5 (default, Sep  4 2020, 07:30:14) [GCC 7.3.0]\r\nLinux-3.10.0-1160.11.1.el7.x86_64-x86_64-with-glibc2.10\r\n40 logical CPU cores, x86_64\r\n\r\n</details>\r\n\n", "before_files": [{"content": "\"\"\"Logging and Profiling\n\"\"\"\nimport io\nimport logging\nimport sys\nfrom functools import update_wrapper, partial\nfrom logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Optional\n\nimport anndata.logging\nfrom sinfo import sinfo\n\n\nHINT = (INFO + DEBUG) // 2\nlogging.addLevelName(HINT, 'HINT')\n\n\nclass _RootLogger(logging.RootLogger):\n    def __init__(self, level):\n        super().__init__(level)\n        self.propagate = False\n        _RootLogger.manager = logging.Manager(self)\n\n    def log(\n        self,\n        level: int,\n        msg: str,\n        *,\n        extra: Optional[dict] = None,\n        time: datetime = None,\n        deep: Optional[str] = None,\n    ) -> datetime:\n        from . import settings\n\n        now = datetime.now(timezone.utc)\n        time_passed: timedelta = None if time is None else now - time\n        extra = {\n            **(extra or {}),\n            'deep': deep if settings.verbosity.level < level else None,\n            'time_passed': time_passed,\n        }\n        super().log(level, msg, extra=extra)\n        return now\n\n    def critical(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra)\n\n    def error(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(ERROR, msg, time=time, deep=deep, extra=extra)\n\n    def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(WARNING, msg, time=time, deep=deep, extra=extra)\n\n    def info(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(INFO, msg, time=time, deep=deep, extra=extra)\n\n    def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(HINT, msg, time=time, deep=deep, extra=extra)\n\n    def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(DEBUG, msg, time=time, deep=deep, extra=extra)\n\n\ndef _set_log_file(settings):\n    file = settings.logfile\n    name = settings.logpath\n    root = settings._root_logger\n    h = logging.StreamHandler(file) if name is None else logging.FileHandler(name)\n    h.setFormatter(_LogFormatter())\n    h.setLevel(root.level)\n    if len(root.handlers) == 1:\n        root.removeHandler(root.handlers[0])\n    elif len(root.handlers) > 1:\n        raise RuntimeError('Scanpy\u2019s root logger somehow got more than one handler')\n    root.addHandler(h)\n\n\ndef _set_log_level(settings, level: int):\n    root = settings._root_logger\n    root.setLevel(level)\n    (h,) = root.handlers  # may only be 1\n    h.setLevel(level)\n\n\nclass _LogFormatter(logging.Formatter):\n    def __init__(\n        self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'\n    ):\n        super().__init__(fmt, datefmt, style)\n\n    def format(self, record: logging.LogRecord):\n        format_orig = self._style._fmt\n        if record.levelno == INFO:\n            self._style._fmt = '{message}'\n        elif record.levelno == HINT:\n            self._style._fmt = '--> {message}'\n        elif record.levelno == DEBUG:\n            self._style._fmt = '    {message}'\n        if record.time_passed:\n            # strip microseconds\n            if record.time_passed.microseconds:\n                record.time_passed = timedelta(\n                    seconds=int(record.time_passed.total_seconds())\n                )\n            if '{time_passed}' in record.msg:\n                record.msg = record.msg.replace(\n                    '{time_passed}', str(record.time_passed)\n                )\n            else:\n                self._style._fmt += ' ({time_passed})'\n        if record.deep:\n            record.msg = f'{record.msg}: {record.deep}'\n        result = logging.Formatter.format(self, record)\n        self._style._fmt = format_orig\n        return result\n\n\nprint_memory_usage = anndata.logging.print_memory_usage\nget_memory_usage = anndata.logging.get_memory_usage\n\n\n_DEPENDENCIES_NUMERICS = [\n    'anndata',  # anndata actually shouldn't, but as long as it's in development\n    'umap',\n    'numpy',\n    'scipy',\n    'pandas',\n    ('sklearn', 'scikit-learn'),\n    'statsmodels',\n    ('igraph', 'python-igraph'),\n    'louvain',\n    'leidenalg',\n]\n\n\ndef _versions_dependencies(dependencies):\n    # this is not the same as the requirements!\n    for mod in dependencies:\n        mod_name, dist_name = mod if isinstance(mod, tuple) else (mod, mod)\n        try:\n            imp = __import__(mod_name)\n            yield dist_name, imp.__version__\n        except (ImportError, AttributeError):\n            pass\n\n\ndef print_header(*, file=None):\n    \"\"\"\\\n    Versions that might influence the numerical results.\n    Matplotlib and Seaborn are excluded from this.\n    \"\"\"\n\n    modules = ['scanpy'] + _DEPENDENCIES_NUMERICS\n    print(\n        ' '.join(f'{mod}=={ver}' for mod, ver in _versions_dependencies(modules)),\n        file=file or sys.stdout,\n    )\n\n\ndef print_versions(*, file=None):\n    \"\"\"Print print versions of imported packages\"\"\"\n    if file is None:  # Inform people about the behavior change\n        warning('If you miss a compact list, please try `print_header`!')\n    stdout = sys.stdout\n    try:\n        buf = sys.stdout = io.StringIO()\n        sinfo(\n            dependencies=True,\n            excludes=[\n                'builtins',\n                'stdlib_list',\n                'importlib_metadata',\n                # Special module present if test coverage being calculated\n                # https://gitlab.com/joelostblom/sinfo/-/issues/10\n                \"$coverage\",\n            ],\n        )\n    finally:\n        sys.stdout = stdout\n    output = buf.getvalue()\n    print(output, file=file)\n\n\ndef print_version_and_date(*, file=None):\n    \"\"\"\\\n    Useful for starting a notebook so you see when you started working.\n    \"\"\"\n    from . import __version__\n\n    if file is None:\n        file = sys.stdout\n    print(\n        f'Running Scanpy {__version__}, ' f'on {datetime.now():%Y-%m-%d %H:%M}.',\n        file=file,\n    )\n\n\ndef _copy_docs_and_signature(fn):\n    return partial(update_wrapper, wrapped=fn, assigned=['__doc__', '__annotations__'])\n\n\ndef error(\n    msg: str,\n    *,\n    time: datetime = None,\n    deep: Optional[str] = None,\n    extra: Optional[dict] = None,\n) -> datetime:\n    \"\"\"\\\n    Log message with specific level and return current time.\n\n    Parameters\n    ==========\n    msg\n        Message to display.\n    time\n        A time in the past. If this is passed, the time difference from then\n        to now is appended to `msg` as ` (HH:MM:SS)`.\n        If `msg` contains `{time_passed}`, the time difference is instead\n        inserted at that position.\n    deep\n        If the current verbosity is higher than the log function\u2019s level,\n        this gets displayed as well\n    extra\n        Additional values you can specify in `msg` like `{time_passed}`.\n    \"\"\"\n    from ._settings import settings\n\n    return settings._root_logger.error(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef warning(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.warning(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef info(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.info(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef hint(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef debug(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.debug(msg, time=time, deep=deep, extra=extra)\n", "path": "scanpy/logging.py"}], "after_files": [{"content": "\"\"\"Logging and Profiling\n\"\"\"\nimport io\nimport logging\nimport sys\nfrom functools import update_wrapper, partial\nfrom logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Optional\n\nimport anndata.logging\nfrom sinfo import sinfo\n\n\nHINT = (INFO + DEBUG) // 2\nlogging.addLevelName(HINT, 'HINT')\n\n\nclass _RootLogger(logging.RootLogger):\n    def __init__(self, level):\n        super().__init__(level)\n        self.propagate = False\n        _RootLogger.manager = logging.Manager(self)\n\n    def log(\n        self,\n        level: int,\n        msg: str,\n        *,\n        extra: Optional[dict] = None,\n        time: datetime = None,\n        deep: Optional[str] = None,\n    ) -> datetime:\n        from . import settings\n\n        now = datetime.now(timezone.utc)\n        time_passed: timedelta = None if time is None else now - time\n        extra = {\n            **(extra or {}),\n            'deep': deep if settings.verbosity.level < level else None,\n            'time_passed': time_passed,\n        }\n        super().log(level, msg, extra=extra)\n        return now\n\n    def critical(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra)\n\n    def error(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(ERROR, msg, time=time, deep=deep, extra=extra)\n\n    def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(WARNING, msg, time=time, deep=deep, extra=extra)\n\n    def info(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(INFO, msg, time=time, deep=deep, extra=extra)\n\n    def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(HINT, msg, time=time, deep=deep, extra=extra)\n\n    def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime:\n        return self.log(DEBUG, msg, time=time, deep=deep, extra=extra)\n\n\ndef _set_log_file(settings):\n    file = settings.logfile\n    name = settings.logpath\n    root = settings._root_logger\n    h = logging.StreamHandler(file) if name is None else logging.FileHandler(name)\n    h.setFormatter(_LogFormatter())\n    h.setLevel(root.level)\n    if len(root.handlers) == 1:\n        root.removeHandler(root.handlers[0])\n    elif len(root.handlers) > 1:\n        raise RuntimeError('Scanpy\u2019s root logger somehow got more than one handler')\n    root.addHandler(h)\n\n\ndef _set_log_level(settings, level: int):\n    root = settings._root_logger\n    root.setLevel(level)\n    (h,) = root.handlers  # may only be 1\n    h.setLevel(level)\n\n\nclass _LogFormatter(logging.Formatter):\n    def __init__(\n        self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'\n    ):\n        super().__init__(fmt, datefmt, style)\n\n    def format(self, record: logging.LogRecord):\n        format_orig = self._style._fmt\n        if record.levelno == INFO:\n            self._style._fmt = '{message}'\n        elif record.levelno == HINT:\n            self._style._fmt = '--> {message}'\n        elif record.levelno == DEBUG:\n            self._style._fmt = '    {message}'\n        if record.time_passed:\n            # strip microseconds\n            if record.time_passed.microseconds:\n                record.time_passed = timedelta(\n                    seconds=int(record.time_passed.total_seconds())\n                )\n            if '{time_passed}' in record.msg:\n                record.msg = record.msg.replace(\n                    '{time_passed}', str(record.time_passed)\n                )\n            else:\n                self._style._fmt += ' ({time_passed})'\n        if record.deep:\n            record.msg = f'{record.msg}: {record.deep}'\n        result = logging.Formatter.format(self, record)\n        self._style._fmt = format_orig\n        return result\n\n\nprint_memory_usage = anndata.logging.print_memory_usage\nget_memory_usage = anndata.logging.get_memory_usage\n\n\n_DEPENDENCIES_NUMERICS = [\n    'anndata',  # anndata actually shouldn't, but as long as it's in development\n    'umap',\n    'numpy',\n    'scipy',\n    'pandas',\n    ('sklearn', 'scikit-learn'),\n    'statsmodels',\n    ('igraph', 'python-igraph'),\n    'louvain',\n    'leidenalg',\n    'pynndescent',\n]\n\n\ndef _versions_dependencies(dependencies):\n    # this is not the same as the requirements!\n    for mod in dependencies:\n        mod_name, dist_name = mod if isinstance(mod, tuple) else (mod, mod)\n        try:\n            imp = __import__(mod_name)\n            yield dist_name, imp.__version__\n        except (ImportError, AttributeError):\n            pass\n\n\ndef print_header(*, file=None):\n    \"\"\"\\\n    Versions that might influence the numerical results.\n    Matplotlib and Seaborn are excluded from this.\n    \"\"\"\n\n    modules = ['scanpy'] + _DEPENDENCIES_NUMERICS\n    print(\n        ' '.join(f'{mod}=={ver}' for mod, ver in _versions_dependencies(modules)),\n        file=file or sys.stdout,\n    )\n\n\ndef print_versions(*, file=None):\n    \"\"\"Print print versions of imported packages\"\"\"\n    if file is None:  # Inform people about the behavior change\n        warning('If you miss a compact list, please try `print_header`!')\n    stdout = sys.stdout\n    try:\n        buf = sys.stdout = io.StringIO()\n        sinfo(\n            dependencies=True,\n            excludes=[\n                'builtins',\n                'stdlib_list',\n                'importlib_metadata',\n                # Special module present if test coverage being calculated\n                # https://gitlab.com/joelostblom/sinfo/-/issues/10\n                \"$coverage\",\n            ],\n        )\n    finally:\n        sys.stdout = stdout\n    output = buf.getvalue()\n    print(output, file=file)\n\n\ndef print_version_and_date(*, file=None):\n    \"\"\"\\\n    Useful for starting a notebook so you see when you started working.\n    \"\"\"\n    from . import __version__\n\n    if file is None:\n        file = sys.stdout\n    print(\n        f'Running Scanpy {__version__}, ' f'on {datetime.now():%Y-%m-%d %H:%M}.',\n        file=file,\n    )\n\n\ndef _copy_docs_and_signature(fn):\n    return partial(update_wrapper, wrapped=fn, assigned=['__doc__', '__annotations__'])\n\n\ndef error(\n    msg: str,\n    *,\n    time: datetime = None,\n    deep: Optional[str] = None,\n    extra: Optional[dict] = None,\n) -> datetime:\n    \"\"\"\\\n    Log message with specific level and return current time.\n\n    Parameters\n    ==========\n    msg\n        Message to display.\n    time\n        A time in the past. If this is passed, the time difference from then\n        to now is appended to `msg` as ` (HH:MM:SS)`.\n        If `msg` contains `{time_passed}`, the time difference is instead\n        inserted at that position.\n    deep\n        If the current verbosity is higher than the log function\u2019s level,\n        this gets displayed as well\n    extra\n        Additional values you can specify in `msg` like `{time_passed}`.\n    \"\"\"\n    from ._settings import settings\n\n    return settings._root_logger.error(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef warning(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.warning(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef info(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.info(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef hint(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra)\n\n\n@_copy_docs_and_signature(error)\ndef debug(msg, *, time=None, deep=None, extra=None) -> datetime:\n    from ._settings import settings\n\n    return settings._root_logger.debug(msg, time=time, deep=deep, extra=extra)\n", "path": "scanpy/logging.py"}]}