Skip to content

report

Base class for the whole report.

This corresponds to a mkdocs project. The class is mainly responsible for creating a mkdocs project if it doesn't exist already and ensuring that the neccessary settings are all included.

Report

Class representing a report.

Source code in mkreports/report.py
 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
class Report:
    """Class representing a report."""

    def __init__(
        self,
        path: Optional[Union[str, Path]] = None,
        project_root: Optional[Union[str, Path]] = None,
        md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
    ) -> None:
        """
        Initialize the report object. This relies on the report folder already
        existing, including necessary files for mkdocs. If this is not the case,
        use the **create** class-method.

        Args:
            path (Optional[Union[str, Path]]): Path to the top-level directory of the report.
            project_root (Optional[Union[str, Path]]):
                Directory that is the root of the project. If None, tries to use
                the root of the git-repository if there is one. Otherwise
                uses the root of the file-system.
            md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
                md objects (accessed from the proxy) to default keywords included when
                they are being called.
        """
        # need to ensure it is of type Path
        if path is None:
            path = get_mkreports_dir()

        self._path = Path(path).absolute()
        # first check if the path exists and is not empty and return error if that is not ok
        if not self.path.exists():
            raise ReportNotExistsError(f"{self.path} does not exist.")
        if not self.mkdocs_file.exists() or not self.mkdocs_file.is_file():
            raise ReportNotValidError(f"{self.mkdocs_file} does not exist")
        if not self.docs_dir.exists() or not self.docs_dir.is_dir():
            raise ReportNotValidError(f"{self.docs_dir} does not exist")
        if not self.index_file.exists() or not self.index_file.is_file():
            raise ReportNotValidError(f"{self.index_file} does not exist")

        if project_root is None:
            root = repo_root()
            if root is None:
                self.project_root = Path("/")
            else:
                self.project_root = root
        else:
            self.project_root = Path(project_root)

        self.md_defaults = md_defaults

    @property
    def path(self) -> Path:
        """
        Returns:
            Path: Path object that is the top-level of the report.
        """
        return self._path

    @property
    def mkdocs_file(self) -> Path:
        """
        Returns:
            Path: Location of the mkdocs file.

        """
        return self.path / "mkdocs.yml"

    @property
    def docs_dir(self) -> Path:
        """
        Returns:
            Path: Docs-folder in the report.

        """
        return self.path / "docs"

    @property
    def index_file(self) -> Path:
        """
        Returns:
            Path: Location of the index file.

        """
        return self.docs_dir / "index.md"

    @property
    def javascript_path(self) -> Path:
        """
        Returns:
            Path: Location of the javascript folder.

        """
        return self.docs_dir / "javascript"

    @property
    def settings(self):
        return ReportSettings(self.mkdocs_file)

    @classmethod
    def create(
        cls,
        path: Union[str, Path],
        report_name: str,
        project_root: Optional[Union[str, Path]] = None,
        md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
        settings: Optional[Mapping[str, str]] = default_settings,
        exist_ok: bool = False,
    ) -> "Report":
        """
        Create a new report.

        Args:
            path (Union[str, Path]): Top-level folder of the report.
            report_name (str): Name of the report (mkdocs site-name)
            project_root (Optional[Union[str, Path]]):
                Directory that is the root of the project. If None, tries to use
                the root of the git-repository if there is one. Otherwise
                uses the root of the file-system.
            md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
                md objects (accessed from the proxy) to default keywords included when
                they are being called.
            settings (Optional[Mapping[str, str]]): Settings of the report.
            exist_ok (bool): Is it ok if it already exists?

        Returns:
            Report: An instance of the class representing the project.

        """
        path = Path(path)
        # create the directory
        try:
            (path / "docs").mkdir(exist_ok=exist_ok, parents=True)
        except FileExistsError:
            raise ReportExistsError(f"{path / 'docs'} already exists.")

        # index.md created, but done nothing if it exists
        # if exist_ok=False, the previousalready failed otherwise
        (path / "docs" / "index.md").touch()

        # only do it if mkdocs_yml does not exist yet
        mkdocs_file = path / "mkdocs.yml"
        if not mkdocs_file.exists():
            # the settings are our serialized yaml
            # ensure settings is regular dict
            settings = dict(settings.items()) if settings is not None else {}
            settings["site_name"] = report_name
            with (path / "mkdocs.yml").open("w") as f:
                yaml.dump(settings, f, Dumper=yaml.Dumper, default_flow_style=False)

        # also create the overrides doc
        overrides_dir = path / "overrides"
        overrides_dir.mkdir(exist_ok=True, parents=True)
        with (overrides_dir / "main.html").open("w") as f:
            f.write(main_html_override)

        return cls(
            path,
            project_root=project_root,
            md_defaults=md_defaults,
        )

    def _add_nav_entry(self, nav_entry) -> None:
        # check that the nav-entry is relative; if absolute,
        # make it relative to the docs_dir
        loc = nav_entry.loc
        if isinstance(loc, str):
            loc = Path(loc)
        if loc.is_absolute():  # type: ignore
            loc = loc.relative_to(self.docs_dir)

        self.settings.append_nav_entry(NavEntry(nav_entry.hierarchy, loc))

    def get_nav_entry(self, path: Path) -> Optional[NavEntry]:
        """
        Get the NavEntry for a specific page.

        Args:
            path (Path): Path to the page, absolute or relative to docs_dir.

        Returns:
            The NavEntry if it exists or None.

        """
        if path.is_absolute():
            rel_path = path.relative_to(self.docs_dir)
        else:
            rel_path = path

        nav_list = self.settings.nav_list

        match_entries = [
            nav_entry for nav_entry in nav_list if nav_entry.loc == rel_path
        ]

        if len(match_entries) > 0:
            return match_entries[0]
        else:
            return None

    def page(
        self,
        page_name: Union[NavEntry, Path, str],
        truncate: bool = False,
        add_bottom: bool = True,
        md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
    ) -> "Page":
        """
        Create a page in the report.

        Args:
            page_name (Union[NavEntry, Path, str]): Name of the page and path. Using a
                **NavEntry**, a custom nav-entry and path can be specified. The path
                is always relative to the report-docs directory.
            truncate (bool): Should the page be truncated if it exists? Also deletes
                the *store_path*.
            add_bottom (bool): Should new entries be added at the bottom or at the
                top of the page. Top of the page is used for IPython.
            md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
                md objects (accessed from the proxy) to default keywords included when
                they are being called.

        Returns:
            Page: An object representing a new page.

        """

        nav_entry = normalize_nav_entry(page_name)
        path = nav_entry.loc
        assert isinstance(path, Path)

        # if the file already exists, just return a 'Page',
        # else create a new nav-entry and the file and return a 'Page'
        if (self.docs_dir / path).exists():
            if truncate:
                # delete the existing site
                (self.docs_dir / path).unlink()
                (self.docs_dir / path).touch()
                # we do not need to add en entry into the nav
        else:
            # create the file by touching it and create a nav-entry
            (self.docs_dir / path).parent.mkdir(exist_ok=True, parents=True)
            (self.docs_dir / path).touch()

            # update the report settings
            self._add_nav_entry(nav_entry)

        page = Page(
            self.docs_dir / path,
            report=self,
            add_bottom=add_bottom,
            md_defaults=md_defaults,
        )

        if truncate:
            if page.store_path.exists():
                shutil.rmtree(page.store_path)

        return page

    def insert_page(
        self,
        path_target: Union[str, Path, NavEntry],
        path_source: Path,
        mode: Literal["S", "T", "ST", "TS"] = "TS",
    ):
        """
        Insert a page into the report.

        This function can take an existing page (can also just be a markdown
            file) and inserts it into the page.

        Args:
            path_source (Path): The file to insert. Expected to be a markdown file.
            path_target (Union[Path, NavEntry]): Path or NavEntry where the page should be
                inserted.
            mode (Literal["S", "T", "ST", "TS"]): Insertion mode. If 'S', then only
                the target is overwritten with the source. If 'T', then the
                target is left as is, if it exists. For 'ST', the source is prepended,
                for 'TS', the source is appended to the target.
        """
        nav_entry = normalize_nav_entry(path_target)
        assert isinstance(nav_entry.loc, Path)

        if mode == "T" and not nav_entry.loc.exists():
            # force source being used
            mode = "S"

        target_page = self.page(path_target)  # initiating entries into the nav

        merge_pages(path_source=path_source, path_target=target_page.path, mode=mode)

    def __eq__(self, other):
        if type(self) != type(other):
            return False

        return self.__dict__ == other.__dict__

__init__(path=None, project_root=None, md_defaults=None)

Initialize the report object. This relies on the report folder already existing, including necessary files for mkdocs. If this is not the case, use the create class-method.

Parameters:

Name Type Description Default
path Optional[Union[str, Path]]

Path to the top-level directory of the report.

None
project_root Optional[Union[str, Path]]

Directory that is the root of the project. If None, tries to use the root of the git-repository if there is one. Otherwise uses the root of the file-system.

None
md_defaults Optional[Dict[str, Dict[str, Any]]

A dictionary mapping the names md objects (accessed from the proxy) to default keywords included when they are being called.

None
Source code in mkreports/report.py
 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
def __init__(
    self,
    path: Optional[Union[str, Path]] = None,
    project_root: Optional[Union[str, Path]] = None,
    md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
) -> None:
    """
    Initialize the report object. This relies on the report folder already
    existing, including necessary files for mkdocs. If this is not the case,
    use the **create** class-method.

    Args:
        path (Optional[Union[str, Path]]): Path to the top-level directory of the report.
        project_root (Optional[Union[str, Path]]):
            Directory that is the root of the project. If None, tries to use
            the root of the git-repository if there is one. Otherwise
            uses the root of the file-system.
        md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
            md objects (accessed from the proxy) to default keywords included when
            they are being called.
    """
    # need to ensure it is of type Path
    if path is None:
        path = get_mkreports_dir()

    self._path = Path(path).absolute()
    # first check if the path exists and is not empty and return error if that is not ok
    if not self.path.exists():
        raise ReportNotExistsError(f"{self.path} does not exist.")
    if not self.mkdocs_file.exists() or not self.mkdocs_file.is_file():
        raise ReportNotValidError(f"{self.mkdocs_file} does not exist")
    if not self.docs_dir.exists() or not self.docs_dir.is_dir():
        raise ReportNotValidError(f"{self.docs_dir} does not exist")
    if not self.index_file.exists() or not self.index_file.is_file():
        raise ReportNotValidError(f"{self.index_file} does not exist")

    if project_root is None:
        root = repo_root()
        if root is None:
            self.project_root = Path("/")
        else:
            self.project_root = root
    else:
        self.project_root = Path(project_root)

    self.md_defaults = md_defaults

create(path, report_name, project_root=None, md_defaults=None, settings=default_settings, exist_ok=False)

Create a new report.

Parameters:

Name Type Description Default
path Union[str, Path]

Top-level folder of the report.

required
report_name str

Name of the report (mkdocs site-name)

required
project_root Optional[Union[str, Path]]

Directory that is the root of the project. If None, tries to use the root of the git-repository if there is one. Otherwise uses the root of the file-system.

None
md_defaults Optional[Dict[str, Dict[str, Any]]

A dictionary mapping the names md objects (accessed from the proxy) to default keywords included when they are being called.

None
settings Optional[Mapping[str, str]]

Settings of the report.

default_settings
exist_ok bool

Is it ok if it already exists?

False

Returns:

Name Type Description
Report 'Report'

An instance of the class representing the project.

Source code in mkreports/report.py
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
@classmethod
def create(
    cls,
    path: Union[str, Path],
    report_name: str,
    project_root: Optional[Union[str, Path]] = None,
    md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
    settings: Optional[Mapping[str, str]] = default_settings,
    exist_ok: bool = False,
) -> "Report":
    """
    Create a new report.

    Args:
        path (Union[str, Path]): Top-level folder of the report.
        report_name (str): Name of the report (mkdocs site-name)
        project_root (Optional[Union[str, Path]]):
            Directory that is the root of the project. If None, tries to use
            the root of the git-repository if there is one. Otherwise
            uses the root of the file-system.
        md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
            md objects (accessed from the proxy) to default keywords included when
            they are being called.
        settings (Optional[Mapping[str, str]]): Settings of the report.
        exist_ok (bool): Is it ok if it already exists?

    Returns:
        Report: An instance of the class representing the project.

    """
    path = Path(path)
    # create the directory
    try:
        (path / "docs").mkdir(exist_ok=exist_ok, parents=True)
    except FileExistsError:
        raise ReportExistsError(f"{path / 'docs'} already exists.")

    # index.md created, but done nothing if it exists
    # if exist_ok=False, the previousalready failed otherwise
    (path / "docs" / "index.md").touch()

    # only do it if mkdocs_yml does not exist yet
    mkdocs_file = path / "mkdocs.yml"
    if not mkdocs_file.exists():
        # the settings are our serialized yaml
        # ensure settings is regular dict
        settings = dict(settings.items()) if settings is not None else {}
        settings["site_name"] = report_name
        with (path / "mkdocs.yml").open("w") as f:
            yaml.dump(settings, f, Dumper=yaml.Dumper, default_flow_style=False)

    # also create the overrides doc
    overrides_dir = path / "overrides"
    overrides_dir.mkdir(exist_ok=True, parents=True)
    with (overrides_dir / "main.html").open("w") as f:
        f.write(main_html_override)

    return cls(
        path,
        project_root=project_root,
        md_defaults=md_defaults,
    )

docs_dir()

Returns:

Name Type Description
Path Path

Docs-folder in the report.

Source code in mkreports/report.py
158
159
160
161
162
163
164
165
@property
def docs_dir(self) -> Path:
    """
    Returns:
        Path: Docs-folder in the report.

    """
    return self.path / "docs"

get_nav_entry(path)

Get the NavEntry for a specific page.

Parameters:

Name Type Description Default
path Path

Path to the page, absolute or relative to docs_dir.

required

Returns:

Type Description
Optional[NavEntry]

The NavEntry if it exists or None.

Source code in mkreports/report.py
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
def get_nav_entry(self, path: Path) -> Optional[NavEntry]:
    """
    Get the NavEntry for a specific page.

    Args:
        path (Path): Path to the page, absolute or relative to docs_dir.

    Returns:
        The NavEntry if it exists or None.

    """
    if path.is_absolute():
        rel_path = path.relative_to(self.docs_dir)
    else:
        rel_path = path

    nav_list = self.settings.nav_list

    match_entries = [
        nav_entry for nav_entry in nav_list if nav_entry.loc == rel_path
    ]

    if len(match_entries) > 0:
        return match_entries[0]
    else:
        return None

index_file()

Returns:

Name Type Description
Path Path

Location of the index file.

Source code in mkreports/report.py
167
168
169
170
171
172
173
174
@property
def index_file(self) -> Path:
    """
    Returns:
        Path: Location of the index file.

    """
    return self.docs_dir / "index.md"

insert_page(path_target, path_source, mode='TS')

Insert a page into the report.

This function can take an existing page (can also just be a markdown file) and inserts it into the page.

Parameters:

Name Type Description Default
path_source Path

The file to insert. Expected to be a markdown file.

required
path_target Union[Path, NavEntry]

Path or NavEntry where the page should be inserted.

required
mode Literal['S', 'T', 'ST', 'TS']

Insertion mode. If 'S', then only the target is overwritten with the source. If 'T', then the target is left as is, if it exists. For 'ST', the source is prepended, for 'TS', the source is appended to the target.

'TS'
Source code in mkreports/report.py
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
def insert_page(
    self,
    path_target: Union[str, Path, NavEntry],
    path_source: Path,
    mode: Literal["S", "T", "ST", "TS"] = "TS",
):
    """
    Insert a page into the report.

    This function can take an existing page (can also just be a markdown
        file) and inserts it into the page.

    Args:
        path_source (Path): The file to insert. Expected to be a markdown file.
        path_target (Union[Path, NavEntry]): Path or NavEntry where the page should be
            inserted.
        mode (Literal["S", "T", "ST", "TS"]): Insertion mode. If 'S', then only
            the target is overwritten with the source. If 'T', then the
            target is left as is, if it exists. For 'ST', the source is prepended,
            for 'TS', the source is appended to the target.
    """
    nav_entry = normalize_nav_entry(path_target)
    assert isinstance(nav_entry.loc, Path)

    if mode == "T" and not nav_entry.loc.exists():
        # force source being used
        mode = "S"

    target_page = self.page(path_target)  # initiating entries into the nav

    merge_pages(path_source=path_source, path_target=target_page.path, mode=mode)

javascript_path()

Returns:

Name Type Description
Path Path

Location of the javascript folder.

Source code in mkreports/report.py
176
177
178
179
180
181
182
183
@property
def javascript_path(self) -> Path:
    """
    Returns:
        Path: Location of the javascript folder.

    """
    return self.docs_dir / "javascript"

mkdocs_file()

Returns:

Name Type Description
Path Path

Location of the mkdocs file.

Source code in mkreports/report.py
149
150
151
152
153
154
155
156
@property
def mkdocs_file(self) -> Path:
    """
    Returns:
        Path: Location of the mkdocs file.

    """
    return self.path / "mkdocs.yml"

page(page_name, truncate=False, add_bottom=True, md_defaults=None)

Create a page in the report.

Parameters:

Name Type Description Default
page_name Union[NavEntry, Path, str]

Name of the page and path. Using a NavEntry, a custom nav-entry and path can be specified. The path is always relative to the report-docs directory.

required
truncate bool

Should the page be truncated if it exists? Also deletes the store_path.

False
add_bottom bool

Should new entries be added at the bottom or at the top of the page. Top of the page is used for IPython.

True
md_defaults Optional[Dict[str, Dict[str, Any]]

A dictionary mapping the names md objects (accessed from the proxy) to default keywords included when they are being called.

None

Returns:

Name Type Description
Page 'Page'

An object representing a new page.

Source code in mkreports/report.py
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
def page(
    self,
    page_name: Union[NavEntry, Path, str],
    truncate: bool = False,
    add_bottom: bool = True,
    md_defaults: Optional[Dict[str, Dict[str, Any]]] = None,
) -> "Page":
    """
    Create a page in the report.

    Args:
        page_name (Union[NavEntry, Path, str]): Name of the page and path. Using a
            **NavEntry**, a custom nav-entry and path can be specified. The path
            is always relative to the report-docs directory.
        truncate (bool): Should the page be truncated if it exists? Also deletes
            the *store_path*.
        add_bottom (bool): Should new entries be added at the bottom or at the
            top of the page. Top of the page is used for IPython.
        md_defaults (Optional[Dict[str, Dict[str, Any]]): A dictionary mapping the names
            md objects (accessed from the proxy) to default keywords included when
            they are being called.

    Returns:
        Page: An object representing a new page.

    """

    nav_entry = normalize_nav_entry(page_name)
    path = nav_entry.loc
    assert isinstance(path, Path)

    # if the file already exists, just return a 'Page',
    # else create a new nav-entry and the file and return a 'Page'
    if (self.docs_dir / path).exists():
        if truncate:
            # delete the existing site
            (self.docs_dir / path).unlink()
            (self.docs_dir / path).touch()
            # we do not need to add en entry into the nav
    else:
        # create the file by touching it and create a nav-entry
        (self.docs_dir / path).parent.mkdir(exist_ok=True, parents=True)
        (self.docs_dir / path).touch()

        # update the report settings
        self._add_nav_entry(nav_entry)

    page = Page(
        self.docs_dir / path,
        report=self,
        add_bottom=add_bottom,
        md_defaults=md_defaults,
    )

    if truncate:
        if page.store_path.exists():
            shutil.rmtree(page.store_path)

    return page

path()

Returns:

Name Type Description
Path Path

Path object that is the top-level of the report.

Source code in mkreports/report.py
141
142
143
144
145
146
147
@property
def path(self) -> Path:
    """
    Returns:
        Path: Path object that is the top-level of the report.
    """
    return self._path

normalize_nav_entry(nav_entry)

Normalize a nav entry

Ensures that if a string or Path is given, is turned into a NavEntry.

Parameters:

Name Type Description Default
nav_entry Union[str, Path, NavEntry]

The str, path or nav_entry to use.

required
Source code in mkreports/report.py
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
def normalize_nav_entry(nav_entry: Union[str, Path, NavEntry]) -> NavEntry:
    """
    Normalize a nav entry

    Ensures that if a string or Path is given, is turned into a NavEntry.

    Args:
        nav_entry (Union[str, Path, NavEntry]): The str, path or nav_entry to use.

    Returns:

    """
    if isinstance(nav_entry, (str, Path)):
        path = Path(nav_entry)
        if path.suffix == "":
            path = path.with_suffix(".md")
        nav_entry = path_to_nav_entry(path)
    else:
        path = nav_entry.loc
        assert isinstance(path, Path)

    if path.suffix != ".md":
        raise ValueError(f"{path} needs to have extension '.md'")

    return nav_entry