File: context.py

package info (click to toggle)
mkdocs-macros-plugin 1.3.7%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 316 kB
  • sloc: python: 1,199; makefile: 4
file content (405 lines) | stat: -rw-r--r-- 12,824 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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
"""
Basic context for the jinja2 templates.

"Batteries included": It defines standard variables, macros and filters
that a template designer is likely to need.

It contains in particular documentation functions.

Laurent Franceschetti (c) 2020
"""
from urllib.parse import urlparse
import os
import sys
import subprocess
import platform
import traceback
from importlib.metadata import version as package_version
import datetime
from dateutil.parser import parse as date_parse
from functools import partial

import mkdocs
from mkdocs.structure.nav import get_navigation
from mkdocs.structure.files import File
from mkdocs.utils import normalize_url
import jinja2
from jinja2 import Template
from markdown import markdown


# ---------------------------------
# Initialization
# ---------------------------------
# Local directory
SOURCE_DIR = os.path.dirname(os.path.abspath(__file__))

# Name of the package (for version)
PACKAGE_NAME = 'mkdocs-macros-plugin'

# ---------------------------------
# Documentation utilities
# ---------------------------------


def list_items(obj):
    """
    Returns a list of key,value pairs for the content of an object
    Creates an abstraction layer so that we do not have to worry.
    """
    try:
        return obj.items()
    except AttributeError:
        # it's an object
        return obj.__dict__.items()
    except TypeError:
        # it's a list: enumerate
        return enumerate(list(obj))


def get_first_para(s) -> str:
    "Get the first para of a docstring"
    first_lines = []
    for row in s.strip().splitlines():
        if not row:
            break
        else:
            first_lines.append(row)
    r = ' '.join(first_lines).strip()
    # fix last character that ends with a semi-colon
    if r.endswith(':'):
        r = r[:-1] + '.'
    return r


def format_value(value):
    "Properly format the value, to make it descriptive"
    # those classes will be processed as "dictionary type"
    # NOTE: using the name does nto force us to import them
    LISTED_CLASSES = 'Config', 'File', 'Section'
    # those types will be printed without question
    SHORT_TYPES = int, float, str, list
    if callable(value):
        # for functions
        docstring = get_first_para(value.__doc__)
        # we interpret the markdown in the docstring,
        # since both jinja2 and ourselves use markdown,
        # and we need to produce a HTML table:
        docstring = markdown(docstring)
        try:
            varnames = ', '.join(value.__code__.co_varnames)
            return "(<i>%s</i>)<br/> %s" % (varnames, docstring)
        except AttributeError:
            # code not available
            return docstring
    elif (isinstance(value, dict) or
          type(value).__name__ in LISTED_CLASSES):
        # print("Processing:", type(value).__name__, isinstance(value, SHORT_TYPES))
        r_list = []
        for key, value in list_items(value):
            if isinstance(value, SHORT_TYPES):
                r_list.append("%s = %s" % (key, repr(value)))
            else:
                # object or dict: write minimal info:
                r_list.append("<b>%s</b> [<i>%s</i>]" %
                              (key, type(value).__name__))
        return ', '.join(r_list)
    else:
        return repr(value)


def make_html(rows, header=[], tb_class='macros-tb'):
    "Produce an HTML table"
    font_color = "#000000"  # black
    back_color = "#F0FFFF"  # light blue
    grid_color = "#DCDCDC"
    padding = "5px"
    style = f"color:{font_color}; border:1px solid {grid_color}; padding: {padding}"
    templ = Template("""
<table class="{{ tb_class }}" style="background-color: {{ back_color}}; {{ style }}">
    {% for item in header %}
    <th style="{{ style }}">{{ item }}</th>
    {% endfor %}
    {% for row in rows %}
        <tr>
        {% for item in row %}
            <td style="vertical-align:top; {{ style }}">{{ item }}</td>
        {% endfor %}
        </tr>
    {% endfor %}
</table>
    """)
    return templ.render(locals())


def get_git_info():
    """
    Get the abbreviated commit version (not provided by get_git_info())
    Returns a dictionary
    """
    LAST_COMMIT = ['git', 'log', '-1']
    COMMANDS = {
        'short_commit': ['git', 'rev-parse', '--short', 'HEAD'],
        'commit': ['git', 'rev-parse', 'HEAD'],
        'tag': ['git', 'describe', '--tags'],
        # With --abbrev set to 0, git will find the closest tagname without any suffix
        'short_tag': ['git', 'describe', '--tags', '--abbrev=0'],
        'author': LAST_COMMIT + ["--pretty=format:%an"],
        'author_email': LAST_COMMIT + ["--pretty=format:%ae"],
        'committer': LAST_COMMIT + ["--pretty=format:%cn"],
        'committer_email': LAST_COMMIT + ["--pretty=format:%ce"],
        # %cd is the commit date
        'date_ISO': LAST_COMMIT + ['--pretty=format:%cd'],
        'message': LAST_COMMIT + ["--pretty=format:%B"],
        'raw': LAST_COMMIT,
        'root_dir': ['git', 'rev-parse', '--show-toplevel']
    }

    # always return a date, even in case of failure
    r = {'status': False, 'date': None}
    try:
        for var, command in COMMANDS.items():
            # NOTE: The 'text' argument is clearer,
            #       but for Python < 3.7, only `universal_newlines`
            #       is accepted
            try:
                r[var] = subprocess.check_output(command,
                                                 universal_newlines=True,
                                                 stderr=subprocess.DEVNULL).strip()
                if var == 'date_ISO':
                    r['date'] = date_parse(r[var])
                r['status'] = True
            except subprocess.CalledProcessError as e:
                if e.returncode == 128:
                    # generally means "unexpected error"
                    # git status (no repo),
                    # git tag (no tag)
                    r[var] = ''
                else:
                    # should be 1, type whatever that is
                    r[var] = "# Cannot execute '%s': %s" % (command, e)
            except Exception as e:
                # any other error, it's probably meaningless at this point
                r[var] = "# Unexpected error '%s': %s" % (command, e)
        # convert
        return r
    except FileNotFoundError as e:
        # not git command
        return r.update(
            {'status': False,
             'diagnosis': 'Git command not found',
             'error': str(e)})


def python_version():
    "Get the python version"
    try:
        return sys.version.split('(')[0].rstrip()
    except (AttributeError, IndexError) as e:
        return str(e)


def system_name():
    "Get the system name"
    r = platform.system()
    if not r:
        # you never know
        return "<UNKNOWN>"
    # print("Found:", r)
    CONV = {'Win': 'Windows', 'Darwin': 'MacOs'}
    return CONV.get(r, r)


def system_version():
    "Get the system version"
    try:
        return platform.mac_ver()[0] or platform.release()
    except (AttributeError, IndexError) as e:
        return str(e)


# for the navigation


class Files(object):
    "This helper class is needed to rebuild the navigation"

    def __init__(self, config):
        self.config = config
        self._filenames = []

    @property
    def filenames(self):
        "The list of filenames (not used at the moment"
        return self._filenames

    def get_file_from_path(self, path):
        "Build the filenames"
        self._filenames.append(path)
        file = File(os.path.basename(path),
                    os.path.dirname(path),
                    os.path.dirname(path), True)
        return file

    def documentation_pages(self):
        return []


# ---------------------------------
# Urls
# ---------------------------------


def is_relative(url):
    """
    Check whether a url is relative


    >>> urlparse("http://www.google.com")
    ParseResult(scheme='http', netloc='www.google.com', path='', params='', query='', fragment='')
    >>> urlparse("../foo")
    ParseResult(scheme='', netloc='', path='../foo', params='', query='', fragment='')
    """
    p = urlparse(url)
    return (not p.scheme) and p.path


def fix_url(url):
    """
    If url is relative, fix it so that it points to the docs directory.
    This is necessary because relative links in markdown must be adapted
    in html ('img/foo.png' => '../img/img.png').
    """
    if is_relative(url):
        r = "../" + url
    else:
        r = url
    return r

# ---------------------------------
# Exports to the environment
# ---------------------------------


def define_env(env):
    """
    This is the hook for declaring variables, macros and filters
    """

    # Get data on the environment (versions)
    try:
        environment = {
            'system': system_name(),
            'system_version': system_version(),
            'python_version': python_version(),
            'mkdocs_version': mkdocs.__version__,
            'macros_plugin_version': package_version(PACKAGE_NAME),
            'jinja2_version': jinja2.__version__,
            # 'site_git_version': site_git_version(),
        }
    except Exception as e:
        # Avoid breaking the system if error in reading the system info:
        environment = ("<i><b>Cannot read system info!</b> %s: %s</i>" %
                       (type(e).__name__, str(e)))
    env.variables['environment'] = environment

    # configuration of the plugin, in the yaml file:
    env.variables['plugin'] = env.config

    # git information:
    env.variables['git'] = get_git_info()

    def render_file(filename):
        """
        Render an external page (filename) containing jinja2 code
        Do not declare as macro, as this is pointless.
        """
        SOURCE_FILE = os.path.join(SOURCE_DIR, filename)
        with open(SOURCE_FILE) as f:
            s = f.read()
        # now we need to render the jinja2 directives,
        # always rendering (to skip reasoning about page header)
        return env.render(s, force_rendering=True)

    @env.macro
    def context(obj:dict=None):
        """
        *Default Mkdocs-Macro*: List an object
        (by default the variables)
        """
        if not obj:
            obj = env.variables
        try:
            return [(var, type(value).__name__, format_value(value))
                    for var, value in list_items(obj)]
        except jinja2.exceptions.UndefinedError as e:
            return [("<i>Error!</i>", type(e).__name__, str(e))]
        except AttributeError:
            # Not an object or dictionary (int, str, etc.)
            return [(obj, type(obj).__name__, repr(obj))]

    @env.filter
    def pretty(var_list):
        """
        *Default Mkdocs-Macro*: Prettify a dictionary or object 
        (used for environment documentation, or debugging).

        Note: it will work only on the product of the `context()` macro

        To prettify any object `obj`, thus use: `context(obj) | pretty`
        """
        if not var_list:
            return ''
        else:
            try:
                rows = [("<b>%s</b>" % var, "<i>%s</i>" % var_type,
                        content.replace('\n', '<br/>'))
                        for var, var_type, content in var_list]
                header = ['Variable', 'Type', 'Content']
                return make_html(rows, header)
            except Exception as e:
                # dont make the whole page fail:
                return "#%s: %s\n%s" % (type(e).__name__, e,
                                        traceback.format_exc())

    @env.macro
    def macros_info():
        """
        *Test/debug function*:
        list useful documentation on the mkdocs_macro environment.
        """
        # NOTE: this is template
        return render_file('macros_info.md')

    @env.macro
    def now():
        """
        *Default Mkdocs-Macro*:
        Get the current time (at the moment of the project build).
        It returns a datetime object. 
        Used alone, it provides a timestamp.
        To get the year use `now().year`, for the month number 
        `now().month`, etc.
        """
        return datetime.datetime.now()

    # add fix url function as macro
    env.macro(fix_url)




    # add the normal mkdocs url function
    # env.filter(normalize_url)

    @env.filter
    def relative_url(path: str):
        """
        *Default Mkdocs-Macro*:
        convert the path of any page according to MkDoc's internal logic,
        into a URL relative to the current page
        (implements the `normalize_url()` function from `mkdocs.util`).
        Typically used to manage custom navigation:
        `{{ page.url | relative_url }}`.
        """
        return normalize_url(path=path, page=env.page)