File: main.py

package info (click to toggle)
python-sinfo 1.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 228 kB
  • sloc: python: 1,633; sh: 25; makefile: 5
file content (329 lines) | stat: -rw-r--r-- 13,712 bytes parent folder | download
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
'''
session_info finds and prints version information for loaded modules in the
current session, Python, and the OS.
'''

import sys
import types
import platform
import inspect
from datetime import datetime
from importlib import import_module
from inspect import cleandoc
from multiprocessing import cpu_count
from pathlib import Path

from stdlib_list import stdlib_list


def _imports(environ):
    '''Find modules in an environment.'''
    for name, val in environ:
        # If the module was directly imported
        if isinstance(val, types.ModuleType):
            yield val.__name__
        # If something was imported from the module
        else:
            try:
                yield val.__module__.split('.')[0]
            except AttributeError:
                pass


def _find_version(mod_version_attr):
    '''Find the version number of a module'''
    if (isinstance(mod_version_attr, str)
            or isinstance(mod_version_attr, int)):
        return mod_version_attr
    elif isinstance(mod_version_attr, tuple):
        joined_tuple = '.'.join([str(num) for num in mod_version_attr])
        return joined_tuple
    elif callable(mod_version_attr):
        try:
            return mod_version_attr()
        # A module might have a function that does something else than return
        # the version number altough it is named "version" or similar, e.g.
        # https://gitlab.com/joelostblom/session_info/-/issues/8. In these cases it
        # will likely fail to call without arguments, we're raising the same
        # type of error as in the parent try/except clause to continue looking
        # for more possible version strings.
        except Exception:
            raise AttributeError
    else:
        # print(f'Does not support module version of type {type(mod_ver_attr)}')
        return 'NA'


# From https://stackoverflow.com/a/52187331/2166823
# Slight modifications added
def _notebook_basename():
    """Returns the absolute path of the Notebook or None if it cannot be determined
    NOTE: works only when the security is token-based or there is also no password
    """
    import ipykernel
    import json
    import urllib
    from notebook import notebookapp

    connection_file = Path(ipykernel.get_connection_file()).name
    kernel_id = connection_file.split('-', 1)[1].split('.')[0]
    for srv in notebookapp.list_running_servers():
        if srv['token'] == '' and not srv['password']:  # No token and no password
            req = urllib.request.urlopen(srv['url']+'api/sessions')
        else:
            req = urllib.request.urlopen(srv['url']+'api/sessions?token='+srv['token'])
        sessions = json.load(req)
        for sess in sessions:
            if sess['kernel']['id'] == kernel_id:
                return Path(sess['notebook']['path']).stem
    return None


def _clean_modules(modules, private, excludes):
    # Keep module basename only. Filter duplicates and excluded modules.
    if private:
        clean_modules = set(module.split('.')[0] for module in modules
                            if module.split('.')[0] not in excludes)
    else:  # Also filter private modules
        clean_modules = set(module.split('.')[0] for module in modules
                            if module.split('.')[0] not in excludes
                            and not module.startswith('_'))
    return clean_modules


def show(na=True, os=True, cpu=False, jupyter=None, dependencies=None,
         std_lib=False, private=False, write_req_file=False, req_file_name=None,
         html=None, excludes=['builtins', 'stdlib_list']):
    '''
    Show version information for loaded modules in the current session,
    Python, and the OS.

    Parameters
    ----------
    na : bool
        Show module name even when no version number is found.
    os : bool
        Show OS information.
    cpu : bool
        Show number of logical CPU cores and info string (if available).
    jupyter : bool
        Show information about the jupyter environment. If `None`, output
        jupyter info only if inside a Jupyter notebook.
    dependencies : bool
        Show information about modules imported by the Python interpreter on
        startup and depency modules imported via other modules. If `None`,
        dependency modules will be included in the HTML output under a
        <details> tag, and excluded from the printed output. Setting `na` to
        `False` could be helpful to reduce verboseness.
    std_lib : bool
        Show information for modules imported from the standard library.
        Tries to detect the Python version to compare with the corresponding
        standard libarary, falls back to Python 3.7 if the version cannot be
        detected.
    private : bool
        Show information for private modules.
    write_req_file: bool
        Create a pip-compatible text file that lists all the module versions.
        If `None`, write dependency files for Jupyter notebooks only. If the
        `notebook` module is installed, `session_info` can automatically find the
        notebook name use it as a prefix for the requirments file. Otherwise
        it falls back to `session_info-requirements.txt`.
        This only writes explicitly imported modules.
    req_file_name : str
        Change the name of the requirements file.
    html: bool
        Format the output as HTML and collapse it in a <details> tag. If `None`,
        HTML will be used only if a Jupyter notebook environment is detected.
        Note that this will not be visible in notebooks shared on GitHub since
        they seemingly do not support the <details> tag. Requires IPython.
    excludes : list
        Do not show version information for these modules.
    '''
    # Exclude std lib packages
    if not std_lib:
        try:
            std_modules = stdlib_list(version=platform.python_version()[:-2])
        except ValueError:
            # Use 3.7 if the Python version cannot be found
            std_modules = stdlib_list('3.7')
        excludes = excludes + std_modules

    # Include jupyter info
    in_notebook = 'jupyter_core' in sys.modules.keys()
    if in_notebook:
        if html is None:
            html = True
    if jupyter or (jupyter is None and in_notebook):
        jupyter = True
        jup_mod_names = ['IPython', 'jupyter_client', 'jupyter_core',
                         'jupyterlab', 'notebook']
        jup_modules = []
        for jup_mod_name in jup_mod_names:
            try:
                jup_modules.append(import_module(jup_mod_name))
            except ModuleNotFoundError:
                pass
        # The length of `'jupyter_client'` is 14
        # The spaces are added to create uniform whitespace in the output
        # f-strings, which is needed to clean them with inspect.cleandoc
        jup_mod_and_ver = [f'            {module.__name__:14}\t{module.__version__}'
                           for module in jup_modules]
        output_jupyter = '\n'.join(jup_mod_and_ver)
    else:
        output_jupyter = None

    # Get `globals()` from the enviroment where the function is executed
    caller_globals = dict(
        inspect.getmembers(inspect.stack()[1][0]))["f_globals"]
    # Find imported modules in the global namespace
    imported_modules = _clean_modules(set(_imports(caller_globals.items())), private, excludes)
    # Wether to include dependency module not explicitly imported
    all_modules = {'imported': imported_modules}
    if dependencies is not False:  # Default with HTML is to include deps
        if html or dependencies:
            dependencies = True  # HTML default, used later for output strings
            sys_modules = _clean_modules(set(sys.modules.keys()), private, excludes)
            depend_modules = sys_modules.difference(imported_modules)
            if depend_modules:
                all_modules['depend'] = depend_modules
            else:
                dependencies = False

    output_modules = {}
    for mod_type in all_modules:
        clean_modules = all_modules[mod_type]

        # Don't duplicate jupyter module output
        if jupyter or in_notebook:
            for jup_module in jup_modules:
                if jup_module.__name__ in clean_modules:
                    clean_modules.remove(jup_module.__name__)

        # Find version number. Return NA if a version string can't be found.
        # This section is modified from the `py_session` package
        mod_and_ver = []
        mod_names = []
        mod_versions = []
        for mod_name in clean_modules:
            mod_names.append(mod_name)
            mod = sys.modules[mod_name]
            # Since modules use different attribute names to store version info,
            # try the most common ones.
            try:
                mod_version = _find_version(mod.__version__)
            except AttributeError:
                try:
                    mod_version = _find_version(mod.version)
                except AttributeError:
                    try:
                        mod_version = _find_version(mod.VERSION)
                    except AttributeError:
                        mod_version = 'NA'
                        # print(f'Cannot find a version attribute for {mod}.')
            mod_versions.append(mod_version)
        max_name_len = max([len(mod_name) for mod_name in mod_names])
        mod_and_ver = [f'{mod_name:{max_name_len}}\t{mod_version}'
                       for mod_name, mod_version in zip(mod_names, mod_versions)]
        if not na:
            mod_and_ver = [x for x in mod_and_ver if not x[-2:] == 'NA']
        mod_and_ver = sorted(mod_and_ver)
        output_modules[mod_type] = '\n            '.join(mod_and_ver)

    # Write requirements file for notebooks only by default
    if write_req_file or (write_req_file is None and in_notebook):
        if req_file_name is None:
            # Only import notebook libraries if we are in the notebook
            # Otherwise, running this multiple times would set `in_notebook=True`
            if in_notebook:
                try:
                    req_file_name = f'{_notebook_basename()}-requirements.txt'
                # If the notebook name cannot be found for, we want to fall
                # back to the default name. Usually this happens because the
                # notebook module is not found, but this is already mentioned
                # in the docstring so we're not differentiating between that
                # and other exceptions here.
                except Exception:
                    req_file_name = 'session_info-requirements.txt'
            else:
                req_file_name = 'session_info-requirements.txt'

        # For NA modules, just include the latest, so no version number.
        mods_na_removed = [mod_ver.replace('\tNA', '')
                           for mod_ver in output_modules['imported'].split('\n')]
        if jupyter:
            mods_req_file = mods_na_removed + jup_mod_and_ver
        else:
            mods_req_file = mods_na_removed
        clean_mods_req_file = [mod_ver.replace(' ', '').replace('\t', '==')
                               for mod_ver in mods_req_file]
        with open(req_file_name, 'w') as f:
            for mod_to_req in clean_mods_req_file:
                f.write('{}\n'.format(mod_to_req))

    # Sys info
    sys_output = 'Python ' + sys.version.replace('\n', '')
    os_output = platform.platform() if os else ''
    if cpu:
        if platform.processor() != '':
            cpu_output = f'{cpu_count()} logical CPU cores, {platform.processor()}'
        else:
            cpu_output = f'{cpu_count()} logical CPU cores'
    else:
        cpu_output = ''
    date_output = 'Session information updated at {}'.format(
        datetime.now().strftime('%Y-%m-%d %H:%M'))

    # Output
    # For proper formatting and exclusion of `-----` when `jupyter=False`.
    nl = '\n'
    output_jup_str = ('' if output_jupyter is None
                      else f'{nl}            -----{nl}{output_jupyter}')
    if html:
        from IPython.display import HTML
        if dependencies:
            # Must be dedented to line up with the returned HTML.
            # Otherwise `cleandoc()` does not work.
            output_depend_str = f"""
            -----
            </pre>
            <details>
            <summary>Click to view modules imported as dependencies</summary>
            <pre>
            {output_modules['depend']}
            </pre>
            </details> <!-- seems like this ends pre, so might as well be explicit -->
            <pre>"""
        else:
            output_depend_str = ''
        return HTML(cleandoc(f"""
            <details>
            <summary>Click to view session information</summary>
            <pre>
            -----
            {output_modules['imported']}{output_depend_str}{output_jup_str}
            -----
            {sys_output}
            {os_output}
            {cpu_output}
            -----
            {date_output}
            </pre>
            </details>""").replace('\n\n', '\n'))  # Clean empty output fields
    else:
        if dependencies:
            # Must be dedented to line up with the returned HTML.
            # Otherwise `cleandoc()` does not work.
            output_depend_str = f"""
            -----
            {output_modules['depend']}"""
        else:
            output_depend_str = ''
        print(cleandoc(f"""
            -----
            {output_modules['imported']}{output_depend_str}{output_jup_str}
            -----
            {sys_output}
            {os_output}
            {cpu_output}
            -----
            {date_output}""").replace('\n\n', '\n'))  # Clean empty output fields