Coverage for .nox/test/lib/python3.12/site-packages/mkdocs_licenseinfo/extension.py: 90%
59 statements
« prev ^ index » next coverage.py v7.3.4, created at 2023-12-27 09:44 +0000
« prev ^ index » next coverage.py v7.3.4, created at 2023-12-27 09:44 +0000
1"""Defines the extension for processing a markdown block to get the license information.
3Uses a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockprocessors)
4that looks for on '::licenseinfo'.
6The specifics can be configured with YAML configuration in the block, and include !ENV flags:
8```yaml
9::license_check
11 # The information on what to use for requirments (see the [licensecheck docs](https://pypi.org/project/licensecheck/#configuration-example)) - default is PEP631 (pyproject.toml)
12 using: <PEP631:dev;dev-test>
13 # Packages to remove (see the [licensecheck docs](https://pypi.org/project/licensecheck/#configuration-example)) - default is None, the packages in this are not shown in the section
14 diff: <PEP631>
15 # A list of packages to ignore
16 ignore_packages: <list of packages>
17 # A list of packages to fail on
18 fail_packages: <list of packages>
19 # A list of packages to skip
20 skip_packages: <list of packages>
21 # A list of licenses to ignore
22 ignore_licenses: <list of licenses>
23 # A list of licenses to skip
24 fail_licenses: <list of licenses>
25 # Set the base indent to work from - optional, can use the heading of the block instead.
26 base_indent: 2
27 # Set the package template to process the package into as an Jinja2 template (optional) (the github api response is passed in as package)
28 package_template: "{{package.name}}"
29 # Path to requirements containing folder relative to docs_dir - if not set the working dir is used
30 requirements_path: <path string>
31```
32"""
34from __future__ import annotations
36from pathlib import Path
37import re
38from typing import Any, MutableSequence, TYPE_CHECKING
39from xml.etree.ElementTree import Element # nosec: B405
41from markdown.blockprocessors import BlockProcessor
42from markdown.extensions import Extension
43from mkdocs.utils.yaml import get_yaml_loader, yaml_load
45from mkdocs_licenseinfo.render_markdown import get_licenses_as_markdown
47if TYPE_CHECKING:
48 from markdown import Markdown
49 from markdown.blockparser import BlockParser
52class LicenseInfoProcessor(BlockProcessor):
53 """License info Markdown block processor."""
55 regex = re.compile(r"^(?P<heading>#{1,6} *|)::licenseinfo*$", flags=re.MULTILINE)
57 def __init__(
58 self,
59 parser: BlockParser,
60 config: dict,
61 ) -> None:
62 """Initialize the processor."""
63 super().__init__(parser=parser)
64 self._config = config
66 def test(self, parent: Element, block: str) -> bool: # noqa: U100
67 """Match the extension instructions."""
68 return bool(self.regex.search(block))
70 def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
71 """Run code on the matched blocks to get the markdown."""
72 block = blocks.pop(0)
73 match = self.regex.search(block)
75 if match:
76 if match.start() > 0:
77 self.parser.parseBlocks(parent, [block[: match.start()]])
78 # removes the first line
79 block = block[match.end() :]
81 block, the_rest = self.detab(block)
82 if the_rest:
83 # This block contained unindented line(s) after the first indented
84 # line. Insert these lines as the first block of the master blocks
85 # list for future processing.
86 blocks.insert(0, the_rest)
88 if match:
89 heading_level = match["heading"].count("#")
90 # We are going to process the markdown from the releases and then
91 # insert it back into the blocks to be processed as markdown
92 block = self._process_block(block, heading_level)
93 blocks.insert(0, block)
95 def _process_block(
96 self,
97 yaml_block: str,
98 heading_level: int = 0,
99 ) -> str:
100 """Process a block."""
101 config = yaml_load(yaml_block, loader=get_yaml_loader()) or {}
102 if heading_level is None:
103 heading_level = 0
104 base_indent = config.get('base_indent', heading_level)
105 using = config.get('using', None)
106 diff = config.get('diff', None)
107 ignore_packages = config.get('ignore_packages', self._config.get('ignore_packages', None))
108 fail_packages = config.get('fail_packages', self._config.get('fail_packages', None))
109 skip_packages = config.get('skip_packages', self._config.get('skip_packages', None))
110 ignore_licenses = config.get('ignore_licenses', self._config.get('ignore_licenses', None))
111 fail_licenses = config.get('fail_licenses', self._config.get('fail_licenses', None))
112 package_template = config.get('package_template', self._config.get('package_template', None))
113 requirements_path = config.get('requirements_path', self._config.get('requirements_path', None))
114 if requirements_path:
115 requirements_path = (Path(self._config.get('docs_dir', '.')) / Path(requirements_path)).resolve()
116 block = '\n\n'.join(get_licenses_as_markdown(
117 using=using,
118 ignore_packages=ignore_packages,
119 fail_packages=fail_packages,
120 skip_packages=skip_packages,
121 ignore_licenses=ignore_licenses,
122 fail_licenses=fail_licenses,
123 diff=diff,
124 package_template=package_template,
125 path=requirements_path
126 ))
127 # We need to decrease/increase the base indent level
128 if base_indent > 0:
129 block = block.replace('# ', ('#'*base_indent)+'# ')
130 return block
133class LicenseInfoExtension(Extension):
134 """The Markdown extension."""
136 def __init__(self, config: dict, **kwargs: Any) -> None:
137 """Initialize the object."""
138 super().__init__(**kwargs)
139 self._config = config
141 def extendMarkdown(self, md: Markdown) -> None:
142 """Register the extension.
144 Add an instance of [`LicenseInfoProcessor`][mkdocs_licenseinfo.extension.LicenseInfoProcessor]
145 to the Markdown parser.
146 """
147 md.parser.blockprocessors.register(
148 LicenseInfoProcessor(md.parser, self._config),
149 "license_check",
150 priority=75, # Right before markdown.blockprocessors.HashHeaderProcessor
151 )