pax_global_header00006660000000000000000000000064147077612700014525gustar00rootroot0000000000000052 comment=009a4207304b8dfdb5cdfe0398eaa9cccb00cf6a mkdocs-test-0.5.3/000077500000000000000000000000001470776127000137675ustar00rootroot00000000000000mkdocs-test-0.5.3/.gitignore000066400000000000000000000007251470776127000157630ustar00rootroot00000000000000# Temporary files *~ .py~ .python-version #Byte-compiled led / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # MkDocs site/ # Mkdocs-Test, Mkdocs-Macros and others __*/ # Other files (generated by mkdocs-macros or others) cache* # JetBrains PyCharm and other IntelliJ based IDEs .idea/ mkdocs-test-0.5.3/LICENSE000066400000000000000000000020651470776127000147770ustar00rootroot00000000000000MIT License Copyright (c) 2024 Laurent Franceschetti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.mkdocs-test-0.5.3/README.md000066400000000000000000000134331470776127000152520ustar00rootroot00000000000000
![MkDocs-Test](logo.png) # A testing framework (plugin + test fixture)
for MkDocs projects
- [A testing framework (plugin + test fixture)for MkDocs projects](#a-testing-framework-plugin--test-fixturefor-mkdocs-projects) - [Description](#description) - [What problem does it solve?](#what-problem-does-it-solve) - [MkDocs-Test](#mkdocs-test) - [Usage](#usage) - [Installation](#installation) - [From pypi](#from-pypi) - [Locally (Github)](#locally-github) - [Installing the plugin](#installing-the-plugin) - [Performing basic tests](#performing-basic-tests) - [Tests on a page](#tests-on-a-page) - [Testing the HTML](#testing-the-html) - [Performing advanced tests](#performing-advanced-tests) - [Reading the configuration file](#reading-the-configuration-file) - [Accessing page metadata](#accessing-page-metadata) - [Reading the log](#reading-the-log) - [License](#license) ## Description ### What problem does it solve? Currently the quickest way for maintainers of an [MkDocs](https://www.mkdocs.org/) website project (or developers of an [MkDocs plugin](https://www.mkdocs.org/dev-guide/plugins/)) to check whether an MkDocs project builds correctly, is to run `mkdocs build` (possibly with the `--strict` option). It leaves the following issues open: - This is a binary proposition: it worked or it didn't. - It doesn't perform integrity tests on the pages; if something started to go wrong, the issue might emerge only later. - If something went already wrong, it doesn't necessarily say where, or why. One solution is to write an ad-hoc program to make tests on the target (HTML) pages; this requires knowing in advance where those files will be stored. Manually keeping track of those target files is doable for small documentation projects; but for larger ones, or for conducting systematic tests, it becomes quickly impractical. ### MkDocs-Test The purpose of Mkdocs-Test is to facilitate the comparison of source pages (Markdown files) and destination pages (HTML) in an MkDocs project. MkDocs-Test is a framework composed of two parts: - an MkDocs plugin (`test`): it creates a `__test__` directory, which contains the data necessary to map the pages of the website. - a framework for conducting the test. The `DocProject` class groups together all the information necessary for the tests on a specific MkDocs project. > 📝 **Technical Note**
The plugin exports the `nav` object, > in the form of a dictionary of Page objects. ## Usage ### Installation #### From pypi ```sh pip install mkdocs-test ``` #### Locally (Github) ```sh pip install . ``` Or, to install the test dependencies (for testing _this_ package, not MkDocs projects): ```sh pip install .[test] ``` ### Installing the plugin > ⚠️ **The plugin is a pre-requisite**
The framework will not work > without the plugin (it exports the pages map into the > `__test__` directory). Declare the `test` plugin in your config file (typically `mkdocs.yml`): ```yaml plugins: - search - ... - test ``` ### Performing basic tests The choice of testing tool is up to you (the examples in this package were tested with [pytest](https://docs.pytest.org/en/stable/)). ```python from mkdocs_test import DocProject project = DocProject() # declare the project # (by default, the directory where the program resides) project.build(strict=False, verbose=False) # build the project; these are the default values for arguments assert project.success # return code of the build is zero (success) ? print(project.build_result.returncode) # return code from the build # perform automatic integrity checks (do pages exist?) project.self_check() ``` ### Tests on a page Each page of the MkDocs project can be tested separately ```python # find the page by relative pathname: page = project.pages['index.md'] # find the page by name, filename or relative pathname: page = project.get_page('index') # easy, and naïve search on the target html page assert "hello world" in page.html # find the words "second page", under the header that contains "subtitle" # at level 2; arguments header and header_level are optional # the method returns the text so found (if found) # the search is case insensitive assert page.find_text_text('second page', header="subtitle", header_level=2) ``` > ⚠️ **Two markdown versions**
`page.markdown` > contains the markdown after possible transformations > by plugins; whereas `page.source.markdown` contains the exact > markdown in the file. > > If you wish to have the complete source file (including the frontmatter), > use `page.source.text`. ### Testing the HTML You can directly access the `.find()` and `.find_all()` methods offered by [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find-all). ```python page = project.get_page('foo') headers = page.find_all('h2') # get all headers of level 2 for header in headers: print(header.string) script = page.find('script', type="module") assert 'import foo' in script.string ``` ## Performing advanced tests ### Reading the configuration file ```python print(project.config.site_name) ``` ### Accessing page metadata ```python page = project.get_page('index') assert page.meta.foo = 'hello' # key-value pair in the page's frontmatter ``` ### Reading the log ```python # search in the trace (naïve) assert "Cleaning site" in project.trace # get all WARNING log entries entries = myproject.find_entries(severity='WARNING') # get the entry from source 'test', containing 'debug file' (case-insensitive) entry = project.find_entry('debug file', source='test') assert entry, "Entry not found" ``` ## License MIT Licensemkdocs-test-0.5.3/cleanup.sh000077500000000000000000000007301470776127000157550ustar00rootroot00000000000000#!/bin/bash # This is necessary to start from a clean version for test # Find and delete all __pycache__ directories find . -type d -name "__pycache__" -exec rm -rf {} + # Find and delete all directories starting with __ find . -type d -name "__*" -exec rm -rf {} + # Find and delete all .egg-info directories find . -type d -name "*.egg-info" -exec rm -rf {} + # Find and delete all .egg files find . -type f -name "*.egg" -exec rm -f {} + echo "Cleanup complete!" mkdocs-test-0.5.3/logo.png000066400000000000000000000263431470776127000154450ustar00rootroot00000000000000PNG  IHDRAbo pHYs+iTXtXML:com.adobe.xmp Mkdocs-Test - 1 2024-10-08 32b1917c-8997-4549-8456-cfb6169cf6c3 525265914179580 2 Fralau Canva (Renderer) O1'IDATxwxTe?LIB$Ti(,M)*`k}AW]ۺ.XTVP EzIH2df13!ɔ$0 a\W. '{TUU@P|' AKC@ u(A8 @P!@ A@ u(A8 @P!@ A{]F'$'E*AvK QEUT0U$#齩 EQtZjՓ$ UHV@ H:okb)@j#TUr"~>ƪTX4ȲXLfKɄXPmM&{g%IB[;ؽ7Ѫm[뀦m>o(1$%%w^e<<#@ h~nh75'˙`,CݚEyhTU seلZRT-IU UfU=ZQUus'BQ吙bBd_?;vO8-{8  FUUSz1Oǃcdz % N_^h֩rCUEE%<Ћú1g$YUDDes^xm۶ĉzիWSZZ~;ڵkwcxsĉzɲ7DEEѭ[7!-L&o߾ݻjyq %ʙHrJx wPt%{ ;w ,<_dYTeYM9G.Qx ;Tra$6ŘXO8ABB:zlFFӧOz~h1f4F!..#F0i$ $Lוg{_ (h6<+{Va,%`7Bx/3ەhߥ3AȲmЛoCeE%9u^۸{xkM jY,;XeTUUeNׯWSX,8q'N+„ H n8***ή,I;v3xX&eMwX01%!,4Yxx>2mڂRVQKÜ`ݺuWn͚5ɍCRR&Mbܸqdee]btܹO>{JxHUTUAMÒXY,*<$S1@NpLmEX1_TQ-fsXG5>Alݺب6999ڵfti&ϩST*+V3⨪PYrRUyW3,eTeV@ѶUG9 mH-biuƩNmDUPRPrlN;bv٨6ׯl67ьn,.\ȑ#9@P//s-9^I^0/Td) m?>ł)0ń0  D5ա&u:-a|5MNm۶LY~=7nT[˗/3evڅ^oiҷo_Μ9NBB`>>ӣGbcc]#''D)--E$$66;6ѨD.\@QQBBB&&&!Cn Ym2339v/_ NGxx8ǣ鮺oEQHNN&%%l"00mңGڴiӠ***ꫯ\ O{x[ULnzyW&#I(eTe_@ wIb!rB,$Tg40]X[y8z6m@K7A/]Ç۷o흭zE\\\cjj*&33ӡLeVXŊg),3r&,ee{ m[XXO<ıロs9-_Ÿ&?44y,o?VQf͚œO>IYYYƮd޼yL>Ƌ/fر\|Qcմ\w/aÆջg ԨTKcÆ :g6l7`ƌnMO=W5?oooڷo_dٲencFf.U&*l>r ]p8C}MkmHIIeX,\w:K*pOC^ɬ%1\`FΧP2ű] M6QUUUZ\\zjq4 ?9sxכ|>}ݻw}K,[_z5?x֮]SO=u)SWoQ}6m\Sɑ#G6mZCj_Gx篺o?O[uxdQUU c((d1q2G* )\Gpsٺ'x8 r>,Vǘu3Ъҳa 1n4zoYF[ R%L)*9^,UW(*,QuB=?u];\6lSO=իM4 d21m4a''OnSѵ-ʪHJJns(_?۾ˆ >nBϜ9s ^}h4233>裵>/NǴi۷/z\ٽ{w-wBBǝs#<>뮻HHHK.sN> ocDGG׺vZу(HOO_СCaU̘1Vmbʝ˲ҩ'$$ug8+(..eQP,TE%¹B:m2AE?ymMUU0UR~79PZ(3̗o*Xo}8X8N2u?a"(J*++LJ#G:dY~Kq,..t>y犊 γnte>|8+WtT;G4 /|[y0`@}?2s=ǸqdѢE9mëʬYSmܸUV!I~{v8W^^pnXصk+V`ڵ̞=iΪUHJJrZO;z1>|8>(C ŋL&.\G}T+HHHw>++5kְl2'3ٳYtCWevxb>dcnL2#'rH7k$).\XUfFUAʱ$ $hgm ZTl6_ASc3?~Cٞ={\>P;S߷fXų>2>o߾]ooo_rQ:K)w)!0ݺudX֬YØ1cKMMef3˗/w9+Wszj4z͋/ȉ'ؾ}* +h4 :˗Yk?F?vz=..;Xtԉ?i!!w6m??;vE'hxx esQ챎XŅb_.|F1iI\Bb.a./\EB +[(f^ֶ3d痣qf`1[Zb$G1cVZf1@L^䵠-Z>::M6I&my.h4,Xe[g@t;zZnΐ$ɩGnhh6O>]trrr_=NWvƌd[RRjQ^l6RU^Ŷ/JHNGl TyQl2sH610zQza GEB F,$Ij}Q^ki gРA\KKKMٍ$IbΝ̝;im? hjܙ˝"㴮Ncܸqnٳ'={tZ?;\۷o˾~ac#$$e|EEf">>e˖y~=ٷoK~m+IzrZvZo߾ݻР3I7KI*9UU4?_t-]ۇ9d EFefRKTvɾCgibp` :I!ʋyv],^XK6>|j6Va Loz=~H-Pkظq_jo۶Zo[lqp֭7|sgOWds-44[ҩS'X96uVAh'::zǻgddPPPPk.C}x):uΝ̙3ӟD.]6,ߧOFݨ9_+~s]Dߎ;PN<`]l˖-cȐ!h0[MI+"ZѾTEU)4*$=C*/aթKk!++&њ17xkE$҃sU+V|*jLJаP$cMƎ۷׊tf8qby0e<˷C֭ӧsNvIΝyxGܚw-U5ȶm5̛*UUU:{]($ZY"[ÝmK@Fv l8.3qI<ѳ cn #:HVB_zI$IUD8~Ȏ+111NkV ti5Mرc.͐`=M :tPbqUރV=Ö-[:th[,VZE|| Vrp"EEҵJ4ތ;͛ͯ[nux8tܹAyXNz<\i4k]e٥(5T,ի[jS/jZ?vFuܒqѦ)x9~8'On١C4hM<;AS5nhъ"l)M9Kx02с:t!9ᢘ 3<;hiI)cUBB'{u.7nC`zVVgnRm(Gcɒ%>w}GIIӲnag?ء~CW%کtRZZ걤ٳgC石xb{uy=z4Æ 94@׮]Yf III,ZUV2̙3}>=GT7q&f^ ߃`gCVzt2Ff~pxOXjKЪu+tE. _~i!6or-NҤZ~g&CI, org1$Ww,J.]XCf͚EBB۶mv,&-%QQQ 6/ 'i*ܝ";dcdz|rz->#,YB~~ [om9 Z,5Fra?ΪD)UX3儆0wnap-Ӽk'N,)6nGu>PXXȴi<`^|EuOtĈN[,c?~e( ˿/cyw^EEEnCaZ:>c\Yk4َ+OjWߡTx*/w B${yyXIN`6vWvq( %{j~y7H$~4A>|SC9\k.jݷީu5tl6uV ܹs]h]Ռ;^o2gbq̤|wt߼ysj5Izq C>+7RAdd$tZj*~F{999i\3F*ǰ+璒IMGQ$dY&&.F)SUQx> Õ$U`iajF^iebȺf(BK|l~\v=cG=5___^o(  3rDղrJi-Zƍ ++/3f"##=z[S$I,]T֭6mӲ˗/3tPo^+IéS8q"[nuڮm۶L0zxx95y衇xk3ʙ3gO6lõѣO搵(Kǐm6{.k &_] 6mԩo6:ubܹ޽!jM=JQs|WfH=SUdVX᲍sxUQBB1vRϦofٝO |c.^HgȡDk:ܰ D)J_b(.G=K.QU2N$ƺI>C̜9#G86kIڼp_uk*#F ((JJJ܊XWDfܹ.F^~e׿ҪU+Z- :PQn֭[exnVwN֭$K.~z8ഏH$&Mw%xW_}aÆѡCdY N}:?s?v*ΈbҥL6ͥym؆ϥXM<=FUU]s(?~Ǐ7,<W=B$VXA]fnJII!%%ccw߹|q=CLLӲ8Lc08zUUpm4ɒCvґG} Ÿn~z逸~VdAˠI7$[?aVj;sN5ZFV|,tډُ%0~jaWvL54hKUEQ1cUn.2~~~DDDлwoƍNj/͛嫯rFӧ?<ookn=x 3f $$m}V˰aoOkmۖ;v0{l5ԩO='Od޼y'|W͹sh4 ]vٳAGh􏔔))) F?z4OKEUUΞ=˙3gΦI%,,:Өբh$99/RPP`@Q C~- $''STT,Ӯ]j+A<ՁvÞ_~%+#Ғ2'4 list[LogEntry]: """ Parse the log entries, e.g.: DEBUG - Running 1 `page_markdown` events INFO - [macros] - Rendering source page: index.md DEBUG - [macros] - Page title: Home WARNING - [macros] - ERROR # _Macro Rendering Error_ _File_: `second.md` _UndefinedError_: 'foo' is undefined ``` Traceback (most recent call last): File "snip/site-packages/mkdocs_macros/plugin.py", line 665, in render DEBUG - Copying static assets. RULES: 1. Every entry starts with a severity code (Uppercase). 2. The message is then divided into: - source: between brackets, e.g. [macros] - title: the remnant of the first line, e.g. "Page title: Home" - payload: the rest of the message """ log_entries = [] current_entry = None mkdocs_log = mkdocs_log.strip() for line in mkdocs_log.split('\n'): match = re.match(r'^([A-Z]+)\s+-\s+(.*)', line) if match: if current_entry: log_entries.append(current_entry) severity = match.group(1) message = match.group(2) source_match = re.match(r'^\[(.*?)\]\s+-\s+(.*)', message) if source_match: source = source_match.group(1) title = source_match.group(2) else: source = '' title = message current_entry = {'severity': severity, 'source': source, 'title': title, 'payload': []} elif current_entry: # current_entry['payload'] += '\n' + line current_entry['payload'].append(line) if current_entry: log_entries.append(current_entry) # Transform the payloads into str: for entry in log_entries: entry['payload'] = '\n'.join(entry['payload']).strip() return [SuperDict(item) for item in log_entries] # --------------------------- # An Mkdocs Documentation project # --------------------------- class MkDocsPage(SuperDict): "A markdown page from MkDocs, with all its information (source, target)" MANDATORY_ATTRS = ['title', 'markdown', 'content', 'meta', 'file'] def __init__(self, page:dict): # Call the superclass's __init__ method super().__init__(page) for field in self.MANDATORY_ATTRS: if field not in self: raise AttributeError(f"Missing attribute '{field}'") @property def h1(self): "First h1 in the markdown" return get_first_h1(self.markdown) @property def plain_text(self): """ The content, as plain text """ try: return self._plain_text except AttributeError: soup = BeautifulSoup(self.content, "html.parser") self._plain_text = soup.get_text() return self._plain_text @property def html(self): """ The final HTML code that will be displayed, complete with javascript, etc. (the end product). """ try: return self._html except AttributeError: try: with open(self.file.abs_dest_path, 'r') as f: s = f.read() self._html = s return self._html except AttributeError as e: # to make sure we don't go into a weird recovery # with SuperDict, in case of AttributeError (get_attr) raise Exception(e) @property def source(self) -> SuperDict: """ The source information, drawn from the source file (it contains the markdown) """ try: return self._source except AttributeError: try: # get the source file and decompose it src_filename = self.file.abs_src_path assert os.path.isfile(src_filename), f"'{src_filename}' does not exist" with open(src_filename, 'r') as f: source_text = f.read() markdown, frontmatter, meta = get_frontmatter(source_text) source = { 'text': source_text, 'markdown': markdown, 'frontmatter': frontmatter, 'meta': meta } self._source = SuperDict(source) return self._source except AttributeError as e: # to make sure we don't go into a weird recovery # with SuperDict, in case of AttributeError (get_attr) raise Exception(e) # ---------------------------------- # Smart functions # ---------------------------------- def find_text(self, pattern: str, header: str = None, header_level: int = None) -> str | None: """ Find a text or regex pattern in the html page (case-insensitive). Arguments --------- - html: the html string - pattern: the text or regex - header (text or regex): if specified, it finds it first, and then looks for the text between that header and the next one (any level). - header_level: you can speciy it, if there is a risk of ambiguity. Returns ------- The line where the pattern was found, or None """ # it operates on the html return find_in_html(self.html, pattern=pattern, header=header, header_level=header_level) @property def soup(self): "Soup from BeautifulSoup" try: return self._soup except AttributeError: self._soup = BeautifulSoup(self.html, 'html.parser') return self._soup def find_all(self, tag: str, *args, **kwargs) -> list[HTMLTag]: """ Extract tags from the HTML source and return them with their attributes and content. It wraps the soup.find_all() function of BeautifulSoup. See: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find-all Arguments --------- - tag is the string argument of soup.find_all(), i.e. the tag Returns ------- Each tag returned in the list contains in particular: - attrs: A dictionary of the attributes - string: The text within the tag (None if there are nexted tags) Note ---- For various ways of formulating the query: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#kinds-of-filters """ tags = self.soup.find_all(tag, *args, **kwargs) return tags def find(self, tag: str, *args, **kwargs) -> HTMLTag|None: """ Extracts the first tag from the HTML source. It wraps the soup.find() function of BeautifulSoup. See: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find """ return self.soup.find(tag, *args, **kwargs) def find_header(self, pattern: str, header_level:int=None) -> str | None: """ Returns the first header (h1, h2, h3...) that matches a pattern; otherwise None """ soup = BeautifulSoup(self.html, 'html.parser') if header_level is None: criterion = re.compile(r'h[1-6]') else: criterion = f'h{header_level}' headers = soup.find_all(criterion, string=re.compile(pattern)) r = [header.text for header in headers] if len(r): return r[0] # ---------------------------------- # Predicate functions # ---------------------------------- def is_src_file(self) -> bool: """ Predicate: does the source (Markdown) file exist? """ return os.path.isfile(self.file.abs_src_path) def is_dest_file(self) -> bool: """ Predicate: does the destination file (HTML) exist? """ return os.path.isfile(self.file.abs_dest_path) def is_markdown_rendered(self) -> bool: """ Predicate: "Rendered" means that the raw Markdown is different from the source markdown; more accurately, that the source markdown is not contained in the target markdown. Please do NOT confuse this with the rendering of Markdown into HTML (with templates containing navigation, header and footer, etc.). Hence "not rendered" is a "nothing happened". It covers these cases: 1. No rendering of the markdown has taken place at all (no plugin, or plugin inactive, or not instructions within the page). 2. A header and/or footer were added to the Markdown code (in `on_pre_page_macros() or in `on_post_page_macro()` in Mkdocs-Macros) but the Markdown itself was not modified. 3. An order to render was given, but there was actually NO rendering of the markdown, for some reason (error case). """ # make sure that the source is stripped, to be sure. return self.source.markdown.strip() not in self.markdown class DocProject(object): """ An object that describes the current MkDocs project being tested (any plugin). """ def __init__(self, project_dir:str='', path:str=''): """ Initialize the documentation project. Designed for pytest: if the path is not defined, it will take the path of the calling program. """ if not path: # get the caller's directory caller_frame = inspect.stack()[1] path = os.path.dirname(caller_frame.filename) path = os.path.abspath(path) project_dir = os.path.join(path, project_dir) self._project_dir = project_dir # test existence of YAML file or fail self.config_file @property def project_dir(self) -> str: "The source directory of the MkDocs project (abs or relative path)" return self._project_dir @property def docs_dir(self): "The target directory of markdown files (full path)" return os.path.join(self.project_dir, self.config.get('docs_dir', DOCS_DEFAULT_DIRNAME)) @property def test_dir(self): "The test directory (full path)" return os.path.join(self.project_dir, TEST_DIRNAME) # ---------------------------------- # Config file # ---------------------------------- @property def config_file(self) -> str: "The config file" try: return self._config_file except AttributeError: # List of possible mkdocs configuration filenames CANDIDATES = ['mkdocs.yaml', 'mkdocs.yml'] for filename in os.listdir(self.project_dir): if filename in CANDIDATES: self._config_file = os.path.join(self.project_dir, filename) return self._config_file raise FileNotFoundError("This is not an MkDocs directory") @property def config(self) -> SuperDict: """ Get the configuration from the config file. All main items of the config are accessible with the dot notation. (config.site_name, config.theme, etc.) """ try: return self._config except AttributeError: with open(self.config_file, 'r', encoding='utf-8') as file: self._config = SuperDict(yaml.safe_load(file)) return self._config def get_plugin(self, name:str) -> SuperDict: "Get a plugin config (from the Config file) by its plugin name" for el in self.config.plugins: if name in el: if isinstance(el, str): return SuperDict() elif isinstance(el, dict): plugin = el[name] return SuperDict(plugin) else: raise ValueError(f"Unexpected content of plugin {name}!") return SuperDict(self.config.plugins.get(name)) # ---------------------------------- # Build # ---------------------------------- def build(self, strict:bool=False, verbose:bool=False) -> subprocess.CompletedProcess: """ Build the documentation, to perform the tests Arguments: - strict (default: False) to make the build fail in case of warnings - verbose (default: True), to generate the target_files directory Returns: (if desired) the low level result of the process (return code and stderr). This is not needed, since, those values are stored, and parsed. """ os.chdir(self.project_dir) command = MKDOCS_BUILD.copy() assert '--strict' not in command if strict: command.append('--strict') if verbose: command.append('--verbose') print("BUILD COMMAND:", command) self._build_result = run_command(*command) return self.build_result # ---------------------------------- # Post-build properties # Will fail if called before build # ---------------------------------- @property def build_result(self) -> subprocess.CompletedProcess: """ Result of the build (low level) """ try: return self._build_result except AttributeError: raise AttributeError("No build result yet (not run)") @property def success(self) -> bool: "Was the execution of the build a success?" return self.build_result.returncode == 0 # ---------------------------------- # Get the Markdown pages # ---------------------------------- @property def page_map_file(self): "The page map file exported by the Test plugin" filename = os.path.join(self.test_dir, PAGE_MAP) if not os.path.isfile(filename): raise FileNotFoundError("The pagemap file was not found. " "Did you forget to declare the `test` plugin " "in the MkDocs config file?") return filename @property def pages(self) -> dict[MkDocsPage]: "The dictionary of Markdown pages + the HTML produced by the build" try: return self._pages except AttributeError: # build the pages with open(self.page_map_file, 'r') as file: pages = json.load(file) self._pages = {key: MkDocsPage(value) for key, value in pages.items()} return self._pages def get_page(self, name:str) -> MkDocsPage | None: """ Find a name in the list of Markdown pages (filenames) using a name (full or partial, with or without extension). """ # get all the filenames of pages: filenames = [filename for filename in self.pages.keys()] # get the filename we want, from that list: filename = find_page(name, filenames) # return the corresponding page: return self.pages.get(filename) # ---------------------------------- # Log # ---------------------------------- @property def trace(self) -> str: "Return the trace of the execution (log as text)" return self.build_result.stderr @property def log(self) -> List[SuperDict]: """ The parsed trace """ try: return self._log except AttributeError: self._log = parse_log(self.trace) # print("BUILT:", self.log) return self._log @property def log_severities(self) -> List[str]: """ List of severities (DEBUG, INFO, WARNING) found """ try: return self._log_severities except AttributeError: self._log_severities = list({entry.get('severity', '#None') for entry in self.log}) return self._log_severities def find_entries(self, title:str='', source:str='', severity:str='') -> List[LogEntry]: """ Filter entries according to criteria of title and severity; all criteria are case-insensitive. Arguments: - title: regex - source: regex, for which entity issued it (macros, etc.) - severity: one of the existing sevirities """ if not title and not severity and not source: return self.log severity = severity.upper() # if severity and severity not in self.log_severities: # raise ValueError(f"{severity} not in the list") filtered_entries = [] # Compile the title regex pattern once (if provided) title_pattern = re.compile(title, re.IGNORECASE) if title else None source_pattern = re.compile(source, re.IGNORECASE) if source else None for entry in self.log: # Check if the entry matches the title regex (if provided) if title_pattern: title_match = re.search(title_pattern, entry.get('title', '')) else: title_match = True # Check if the entry matches the source regex (if provided) if source_pattern: source_match = re.search(source_pattern, entry.get('source', '')) else: source_match = True # Check if the entry matches the severity (if provided) if severity: severity_match = (entry['severity'] == severity) # print("Decision:", severity_match) else: severity_match = True # If both conditions are met, add the entry to the filtered list if title_match and severity_match and source_match: filtered_entries.append(entry) assert isinstance(filtered_entries, list) return filtered_entries def find_entry(self, title:str='', source:str = '', severity:str='') -> SuperDict | None: """ Find the first entry according to criteria of title and severity Arguments: - title: regex - source: regex - severity """ found = self.find_entries(title, source=source, severity=severity) if len(found): return found[0] else: return None # ---------------------------------- # Self-check # ---------------------------------- def self_check(self): "Performs a number of post-build self-checks (integrity)" for page in self.pages.values(): name = page.file.name assert page.markdown, f"'{name}' is empty" assert page.is_src_file(), f"source (Markdown) of '{name}' is missing" assert page.is_dest_file(), f"destination (HTML) of '{name}' is missing"mkdocs-test-0.5.3/mkdocs_test/common.py000066400000000000000000000241541470776127000201560ustar00rootroot00000000000000""" Fixtures utilities for the testing of Mkdocs-Macros (pytest) Part of the test package. Not all are used, but they are maintained here for future reference. (C) Laurent Franceschetti 2024 """ import os import re from io import StringIO import inspect import subprocess import yaml from typing import List import markdown import pandas as pd from bs4 import BeautifulSoup from super_collections import SuperDict # ------------------------------------------ # Initialization # ------------------------------------------ # the directory where the export files must go TEST_DIRNAME = '__test__' "The default docs directory" DOCS_DEFAULT_DIRNAME = 'docs' "The mapping file (communication between plugin and test)" PAGE_MAP = 'page_map.json' # --------------------------- # Print functions # --------------------------- std_print = print from rich import print from rich.panel import Panel from rich.table import Table TITLE_COLOR = 'green' def h1(s:str, color:str=TITLE_COLOR): "Color print a 1st level title to the console" print() print(Panel(f"[{color} bold]{s}", style=color, width=80)) def h2(s:str, color:str=TITLE_COLOR): "Color print a 2nd level title to the consule" print() print(f"[green bold underline]{s}") def h3(s:str, color:str=TITLE_COLOR): "Color print a 2nd level title to the consule" print() print(f"[green underline]{s}") # --------------------------- # Low-level functions # --------------------------- def find_after(s:str, word:str, pattern:str): """ Find the the first occurence of a pattern after a word (Both word and pattern can be regex, and the matching is case insensitive.) """ word_pattern = re.compile(word, re.IGNORECASE) parts = word_pattern.split(s, maxsplit=1) # parts = s.split(word, 1) if len(parts) > 1: # Strip the remainder and search for the pattern remainder = parts[1].strip() match = re.search(pattern, remainder, flags=re.IGNORECASE) return match.group(0) if match else None else: return None def find_page(name:str, filenames:List) -> str: """ Find a name in list of filenames using a name (full or partial, with or without extension). """ for filename in filenames: # give priority to exact matches # print("Checking:", filename) if name == filename: return filename # try without extension stem, _ = os.path.splitext(filename) # print("Checking:", stem) if name == stem: return filename # try again without full path for filename in filenames: if filename.endswith(name): return filename stem, _ = os.path.splitext(filename) # print("Checking:", stem) if stem.endswith(name): return filename def list_markdown_files(directory:str): """ Makes a list of markdown files in a directory """ markdown_files = [] for root, dirs, files in os.walk(directory): for file in files: if file.endswith('.md') or file.endswith('.markdown'): relative_path = os.path.relpath(os.path.join(root, file), directory) markdown_files.append(relative_path) return markdown_files def markdown_to_html(markdown_text): """Convert markdown text to HTML.""" html = markdown.markdown(markdown_text, extensions=["tables"]) return html def style_dataframe(df:pd.DataFrame): """ Apply beautiful and colorful styling to any dataframe (patches the dataframe). """ def _rich_str(self): table = Table(show_header=True, header_style="bold magenta") # Add columns for col in self.columns: table.add_column(col, style="dim", width=12) # Add rows for row in self.itertuples(index=False): table.add_row(*map(str, row)) return table # reassign str to rich (to avoid messing up when rich.print is used) df.__rich__ = _rich_str.__get__(df) # -------------------------------------------- # Smart find/extraction functions (HTML) # -------------------------------------------- def extract_tables_from_html(html:str, formatter:callable=None): """ Extract tables from an HTML source and convert them into dataframes """ soup = BeautifulSoup(html, 'html.parser') tables = soup.find_all('table') dataframes = {} unnamed_table_count = 0 for table in tables: print("Found a table") # Find the nearest header header = table.find_previous(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) if header: header_text = header.get_text() else: unnamed_table_count += 1 header_text = f"Unnamed Table {unnamed_table_count}" # Convert HTML table to DataFrame df = pd.read_html(StringIO(str(table)))[0] if formatter: formatter(df) # Add DataFrame to dictionary with header as key dataframes[header_text] = df return dataframes def find_in_html(html: str, pattern: str, header: str = None, header_level: int = None) -> str | None: """ Find a text or regex pattern in a HTML document (case-insensitive) Arguments --------- - html: the html string - pattern: the text or regex - header (text or regex): if specified, it finds it first, and then looks for the text between that header and the next one (any level). - header_level: you can speciy it, if there is a risk of ambiguity. Returns ------- The line where the pattern was found, or None """ if not isinstance(pattern, str): pattern = str(pattern) soup = BeautifulSoup(html, 'html.parser') # Compile regex patterns with case-insensitive flag pattern_regex = re.compile(pattern, re.IGNORECASE) if header: header_regex = re.compile(header, re.IGNORECASE) # Find all headers (h1 to h6) headers = soup.find_all(re.compile('^h[1-6]$', re.IGNORECASE)) for hdr in headers: if header_regex.search(hdr.text): # Check if header level is specified and matches if header_level and hdr.name != f'h{header_level}': continue # Extract text until the next header text = [] for sibling in hdr.find_next_siblings(): if sibling.name and re.match('^h[1-6]$', sibling.name, re.IGNORECASE): break text.append(sibling.get_text(separator='\n', strip=True)) full_text = '\n'.join(text) # Search for the pattern in the extracted text match = pattern_regex.search(full_text) if match: # Find the full line containing the match lines = full_text.split('\n') for line in lines: if pattern_regex.search(line): return line else: # Extract all text from the document full_text = soup.get_text(separator='\n', strip=True) # Search for the pattern in the full text match = pattern_regex.search(full_text) if match: # Find the full line containing the match lines = full_text.split('\n') for line in lines: if pattern_regex.search(line): return line return None # -------------------------------------------- # Smart find/extraction functions (Markdown) # -------------------------------------------- def get_frontmatter(text:str) -> tuple[str, dict]: """ Get the front matter from a markdown file. Returns ------- - markdown - frontmatter - metadata """ # Split the content to extract the YAML front matter parts = text.split('---',maxsplit=2) if len(parts) > 1: frontmatter = parts[1].strip() metadata = SuperDict(yaml.safe_load(frontmatter)) try: markdown = parts[2] except IndexError: markdown = '' return (markdown.strip(), frontmatter, metadata) else: return (text, '', {}) def get_first_h1(markdown_text: str): """ Get the first h1 in a markdown file, ignoring YAML frontmatter and comments. """ # Remove YAML frontmatter yaml_frontmatter_pattern = re.compile(r'^---\s*\n(.*?\n)?---\s*\n', re.DOTALL) markdown_text = yaml_frontmatter_pattern.sub('', markdown_text) # Regular expression to match both syntaxes for level 1 headers h1_pattern = re.compile(r'^(# .+|.+\n=+)', re.MULTILINE) match = h1_pattern.search(markdown_text) if match: header = match.group(0) # Remove formatting if header.startswith('#'): return header.lstrip('# ').strip() else: return header.split('\n')[0].strip() return None def get_tables(markdown_text:str) -> dict[pd.DataFrame]: """ Convert markdown text to HTML, extract tables, and convert them to dataframes. """ html = markdown_to_html(markdown_text) dataframes = extract_tables_from_html(html, formatter=style_dataframe) return dataframes # --------------------------- # OS Functions # --------------------------- def run_command(command, *args) -> subprocess.CompletedProcess: "Execute a command" full_command = [command] + list(args) return subprocess.run(full_command, capture_output=True, text=True) def get_caller_directory(): "Get the caller's directory name (to be called from a function)" # Get the current frame current_frame = inspect.currentframe() # Get the caller's frame caller_frame = inspect.getouterframes(current_frame, 2) # Get the file name of the caller caller_file = caller_frame[1].filename # Get the absolute path of the directory containing the caller file directory_abspath = os.path.abspath(os.path.dirname(caller_file)) return directory_abspathmkdocs-test-0.5.3/mkdocs_test/plugin.py000066400000000000000000000072701470776127000201640ustar00rootroot00000000000000# -------------------------------------------- # Main part of the plugin # Defines the MacrosPlugin class # # Laurent Franceschetti (c) 2018 # MIT License # -------------------------------------------- import os import json import logging from bs4 import BeautifulSoup from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import Files from super_collections import SuperDict from mkdocs.plugins import BasePlugin from mkdocs.structure.pages import Page from mkdocs.structure.nav import Navigation try: from mkdocs.plugins import event_priority except ImportError: event_priority = lambda priority: lambda f: f # No-op fallback from .common import TEST_DIRNAME, DOCS_DEFAULT_DIRNAME, PAGE_MAP, get_frontmatter LOWEST_PRIORITY = -90 # ------------------------------------------ # Utilities # ------------------------------------------ log = logging.getLogger(f"mkdocs.plugins.test") def fmt(*args): "Format text for the log" items = ['[test] - '] + [str(arg) for arg in args] return ' '.join(items) def convert_object(object) -> SuperDict: "Convert an object to a dictionary" d = {key: value for key, value in object.__dict__.items() if not key.startswith('_') and isinstance(value, (str, int, float, dict))} return SuperDict(d) def check_dir(dest_file:str): "Check that the directories of a destination file exist" os.makedirs(os.path.dirname(dest_file), exist_ok=True) # ------------------------------------------ # Plugin # ------------------------------------------ class TestPlugin(BasePlugin): """ This plugin generates information necessary for testing MkDocs project """ # ---------------------------- # Directories # ---------------------------- @property def docs_dir(self) -> str: "The docs directory (relative to project dir)" return self.config.get('docs_dir', DOCS_DEFAULT_DIRNAME) @property def test_dir(self) -> str: "Return the test dir" return TEST_DIRNAME @property def nav(self): "Get the nav" try: return self._nav except AttributeError: raise AttributeError("Trying to access the nav attribute too early") @property def source_markdown(self) -> SuperDict: "The table raw (target) markdown (used to complement the page table)" try: return self._source_markdown except AttributeError: self._source_markdown = SuperDict() return self._source_markdown # ---------------------------- # Pages # ---------------------------- def get_page_map(self) -> SuperDict: """ Recursively build the mapping of pages from self.nav: all pages, created by on_nav(). """ pages = [] for page in self.nav.pages: d = convert_object(page) d.file = convert_object(page.file) pages.append(d) return SuperDict({page.file.src_uri: page for page in pages}) # ---------------------------- # Handling events # ---------------------------- @event_priority(LOWEST_PRIORITY) def on_nav(self, nav, config, files): "Set the nav" self._nav = nav @event_priority(LOWEST_PRIORITY) def on_post_build(self, config): """ The most important action: export all pages This method is called at the end of the build process """ mapping = self.get_page_map() out_file = os.path.join(self.test_dir, PAGE_MAP) log.info(fmt("Debug file:", out_file)) check_dir(out_file) with open(out_file, 'w') as f: json.dump(mapping, f, indent=4) mkdocs-test-0.5.3/pyproject.toml000066400000000000000000000017111470776127000167030ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "mkdocs-test" version = "0.5.3" description = "A test framework for MkDocs projects" authors = [ { name = "Laurent Franceschetti" } ] license = { text = "LICENSE" } readme = "README.md" dependencies = [ "beautifulsoup4", "markdown", "pandas" ] classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11" ] [project.urls] Source = "https://github.com/fralau/mkdocs-test" [project.optional-dependencies] test = [ "pytest>=7.0", "toml-cli" ] [tool.mkdocs] site_name = "MkDoc Test Documentation" [tool.mkdocs.plugins] test = {} [project.entry-points."mkdocs.plugins"] test = "mkdocs_test.plugin:TestPlugin" mkdocs-test-0.5.3/test/000077500000000000000000000000001470776127000147465ustar00rootroot00000000000000mkdocs-test-0.5.3/test/advanced/000077500000000000000000000000001470776127000165135ustar00rootroot00000000000000mkdocs-test-0.5.3/test/advanced/__init__.py000066400000000000000000000001241470776127000206210ustar00rootroot00000000000000""" This __init__.py file is indispensable for pytest to recognize its packages. """mkdocs-test-0.5.3/test/advanced/docs/000077500000000000000000000000001470776127000174435ustar00rootroot00000000000000mkdocs-test-0.5.3/test/advanced/docs/index.md000066400000000000000000000001461470776127000210750ustar00rootroot00000000000000# Main Page Hello world! This is to test a very simple case of an MkDocs project, with two pages. mkdocs-test-0.5.3/test/advanced/docs/other/000077500000000000000000000000001470776127000205645ustar00rootroot00000000000000mkdocs-test-0.5.3/test/advanced/docs/other/third.md000066400000000000000000000000631470776127000222170ustar00rootroot00000000000000# This is a third file This is a file in a subdir.mkdocs-test-0.5.3/test/advanced/docs/second.md000066400000000000000000000001301470776127000212320ustar00rootroot00000000000000--- foo: "Hello world" --- # Second page ## This is a subtitle This is a second page. mkdocs-test-0.5.3/test/advanced/mkdocs.yml000066400000000000000000000002451470776127000205170ustar00rootroot00000000000000site_name: Simple MkDocs Website theme: mkdocs nav: - Home: index.md - Next page: second.md - Third page: other/third.md plugins: - search - test mkdocs-test-0.5.3/test/advanced/test_advanced.py000066400000000000000000000032501470776127000216710ustar00rootroot00000000000000""" Testing the project (C) Laurent Franceschetti 2024 """ import pytest from mkdocs_test import DocProject from mkdocs_test.common import h1, h2, h3 def test_pages(): project = DocProject() project.build(strict=False) h1(f"Testing project: {project.config.site_name}") # did not fail return_code = project.build_result.returncode assert not return_code, "Failed when it should not" # ---------------- # Test log # ---------------- print(project.log) entry = project.find_entry(source='test') print("---") print("Confirming export:", entry.title) # ---------------- # First page # ---------------- pagename = 'index' h2(f"Testing: {pagename}") page = project.get_page(pagename) print("Plain text:", page.plain_text) # ---------------- # Second page # ---------------- # there is intentionally an error (`foo` does not exist) pagename = 'second' h2(f"Testing: {pagename}") page = project.get_page(pagename) assert page.meta.foo == "Hello world" assert "second page" in page.plain_text assert page.find_text('second page',header="subtitle", header_level=2) # ---------------- # Third page # check that it handles subdirs correctly # ---------------- page_path = 'other/third.md' page = project.pages[page_path] # it is found by its pathname assert "# This is a third file" in page.markdown def test_strict(): "This project must fail" project = DocProject() # it must not fail with the --strict option, project.build(strict=True) assert not project.build_result.returncode, "Failed when it should not" mkdocs-test-0.5.3/test/alter_markdown/000077500000000000000000000000001470776127000177575ustar00rootroot00000000000000mkdocs-test-0.5.3/test/alter_markdown/__init__.py000066400000000000000000000001241470776127000220650ustar00rootroot00000000000000""" This __init__.py file is indispensable for pytest to recognize its packages. """mkdocs-test-0.5.3/test/alter_markdown/docs/000077500000000000000000000000001470776127000207075ustar00rootroot00000000000000mkdocs-test-0.5.3/test/alter_markdown/docs/index.md000066400000000000000000000002051470776127000223350ustar00rootroot00000000000000# Main Page This is to show the alteration. ## Values Show the values of {x} and {y}. ## Message Here is the message: {message}mkdocs-test-0.5.3/test/alter_markdown/docs/second.md000066400000000000000000000001361470776127000225040ustar00rootroot00000000000000# Second page ## This is a subtitle This is a second page that doesn't do anything special. mkdocs-test-0.5.3/test/alter_markdown/hooks.py000066400000000000000000000007471470776127000214640ustar00rootroot00000000000000""" Hook script for altering the code """ MY_VARIABLES = {"x": 5, "y": 12, "message": 'hello world'} def on_page_markdown(markdown:str, *args, **kwargs) -> str | None: """ Process the markdown template, by interpolating the variables e.g. "{x} and {y}" -> "5 and 12" Note: ----- .format(), contrary to f-strings, does not allow inline expressions: the expression "{x + y}" won't work. """ raw_markdown = markdown.format(**MY_VARIABLES) return raw_markdown mkdocs-test-0.5.3/test/alter_markdown/mkdocs.yml000066400000000000000000000003451470776127000217640ustar00rootroot00000000000000site_name: Alteration of source Markdown theme: readthedocs nav: - Home: index.md - Next page: second.md hooks: # Mkdocs hook for doing the alteration (instead of a plugin) - hooks.py plugins: - search - test mkdocs-test-0.5.3/test/alter_markdown/test_site.py000066400000000000000000000033031470776127000223330ustar00rootroot00000000000000""" Testing the project (C) Laurent Franceschetti 2024 """ import pytest from mkdocs_test import DocProject from mkdocs_test.common import h1, h2, h3 from .hooks import MY_VARIABLES def test_pages(): project = DocProject() project.build(strict=True) h1(f"Testing project: {project.config.site_name}") # did not fail return_code = project.build_result.returncode assert not return_code, "Failed when it should not" # ---------------- # Test log # ---------------- print(project.log) entry = project.find_entry(source='test') print("---") print("Confirming export:", entry.title) # ---------------- # First page # ---------------- pagename = 'index' h2(f"Testing: {pagename}") page = project.get_page(pagename) print("Plain text:", page.plain_text) # it has been altered assert page.markdown.strip() != page.source.markdown.strip() assert page.is_markdown_rendered() # check that markdown is rendered # null test assert "foobar" not in page.markdown # brute-force testing assert "hello world" in page.markdown.lower() # check that the values of the variables have been properly rendered: assert page.find_text(MY_VARIABLES['x'], header="Values") assert page.find_text(MY_VARIABLES['y'], header="Values") assert page.find_text(MY_VARIABLES['message'], header="Message") # ---------------- # Second page # ---------------- pagename = 'second' h2(f"Testing: {pagename}") page = project.get_page(pagename) assert page # not altered assert page.markdown.strip() == page.source.markdown.strip() assert not page.is_markdown_rendered() mkdocs-test-0.5.3/test/simple/000077500000000000000000000000001470776127000162375ustar00rootroot00000000000000mkdocs-test-0.5.3/test/simple/__init__.py000066400000000000000000000001241470776127000203450ustar00rootroot00000000000000""" This __init__.py file is indispensable for pytest to recognize its packages. """mkdocs-test-0.5.3/test/simple/docs/000077500000000000000000000000001470776127000171675ustar00rootroot00000000000000mkdocs-test-0.5.3/test/simple/docs/index.md000066400000000000000000000001461470776127000206210ustar00rootroot00000000000000# Main Page Hello world! This is to test a very simple case of an MkDocs project, with two pages. mkdocs-test-0.5.3/test/simple/docs/second.md000066400000000000000000000002141470776127000207610ustar00rootroot00000000000000--- foo: "Hello world" --- # Second page ## This is a subtitle This is a second page. ## Second header of level two This is more text. mkdocs-test-0.5.3/test/simple/mkdocs.yml000066400000000000000000000002041470776127000202360ustar00rootroot00000000000000site_name: Simple MkDocs Website theme: mkdocs nav: - Home: index.md - Next page: second.md plugins: - search - test mkdocs-test-0.5.3/test/simple/test_site.py000066400000000000000000000035721470776127000206230ustar00rootroot00000000000000""" Testing the project (C) Laurent Franceschetti 2024 """ import pytest from mkdocs_test import DocProject from mkdocs_test.common import h1, h2, h3 def test_pages(): project = DocProject() project.build(strict=False) h1(f"Testing project: {project.config.site_name}") # did not fail return_code = project.build_result.returncode assert not return_code, "Failed when it should not" # ---------------- # Test log # ---------------- print(project.log) entry = project.find_entry(source='test') print("---") print("Confirming export:", entry.title) # ---------------- # First page # ---------------- pagename = 'index' h2(f"Testing: {pagename}") page = project.get_page(pagename) print("Plain text:", page.plain_text) # ---------------- # Second page # ---------------- # there is intentionally an error (`foo` does not exist) pagename = 'second' h2(f"Testing: {pagename}") page = project.get_page(pagename) assert page.meta.foo == "Hello world" assert "second page" in page.plain_text assert page.find_text('second page',header="subtitle", header_level=2) # test find_header() method assert page.find_header('subtitle', 2) # by level assert not page.find_header('subtitle', 3) assert page.find_header('subtitle') # all levels # test find_all; all headers of level 2: headers = page.find_all('h2') assert len(headers) == 2 print("Headers found:", headers) assert "Second header" in headers[1].string # check that find also works: assert page.find('h2').string == headers[0].string def test_strict(): "This project must fail" project = DocProject() # it must not fail with the --strict option, project.build(strict=True) assert not project.build_result.returncode, "Failed when it should not" mkdocs-test-0.5.3/test/test_simple.py000066400000000000000000000113221470776127000176470ustar00rootroot00000000000000""" Testing the tester (C) Laurent Franceschetti 2024 """ import pytest import os from mkdocs_test import DocProject, parse_log, list_doc_projects from mkdocs_test.common import ( h1, h2, h3, std_print, get_tables, list_markdown_files, find_in_html, find_page) # --------------------------- # Initialization # --------------------------- "The directory of this file" REF_DIR = os.path.dirname(os.path.abspath(__file__)) "All subdirectories containing mkdocs.yml" PROJECTS = list_doc_projects(REF_DIR) def test_functions(): "Test the low level fixtures" h1("Unit tests") # Print the list of directories h2("Directories containing mkdocs.yml") for directory in PROJECTS: print(directory) print(PROJECTS) print() # Example usage h2("Parse tables") SOURCE_DOCUMENT = """ # Header 1 Some text. ## Table 1 | Column 1 | Column 2 | |----------|----------| | Value 1 | Value 2 | | Value 3 | Value 4 | ## Table 2 | Column A | Column B | |----------|----------| | Value A | Value B | | Value C | Value D | ## Another Section Some more text. | Column X | Column Y | |----------|----------| | Value X1 | Value Y1 | | Value X2 | Value Y2 | """ dfs = get_tables(SOURCE_DOCUMENT) # Print the list of directories print("Dataframes:") for header, df in dfs.items(): print(f"Table under '{header}':") print(df) # -------------------- # Test parsing # -------------------- h2("Parsing logs") TEST_CODE = """ DEBUG - Running 1 `page_markdown` events INFO - [macros] - Rendering source page: index.md DEBUG - [macros] - Page title: Home DEBUG - No translations found here: '(...)/mkdocs/themes/mkdocs/locales' WARNING - [macros] - ERROR # _Macro Rendering Error_ _File_: `second.md` _UndefinedError_: 'foo' is undefined ``` Traceback (most recent call last): File "snip/site-packages/mkdocs_macros/plugin.py", line 665, in render DEBUG - Copying static assets. FOOBAR - This is a title with a new severity Payload here. DEBUG - Copying static assets. INFO - [macros - MAIN] - This means `on_post_build(env)` works """ log = parse_log(TEST_CODE) print(log) h2("Search in HTML (advanced)") # Example usage html_doc = """ Example

Main Header

This is some text under the main header.

More text under the main header.

Sub Header

Text under the sub header.

Another Main Header

Text under another main header.

""" print(html_doc) print(find_in_html(html_doc, 'more text')) print(find_in_html(html_doc, 'MORE TEXT')) print(find_in_html(html_doc, 'under the main', header='Main header')) print(find_in_html(html_doc, 'under the main', header='Main header')) print(find_in_html(html_doc, 'under the', header='sub header')) assert 'More text' in find_in_html(html_doc, 'more text') def test_find_pages(): """ Low level tests for search """ h2("Search pages") PAGES = ['foo.md', 'hello/world.md', 'no_foo/bar.md', 'foo/bar.md'] for name in ('foo', 'world', 'hello/world', 'foo/bar'): print(f"{name} -> {find_page(name, PAGES)}") assert find_page('foo.md', PAGES) == 'foo.md' assert find_page('world', PAGES) == 'hello/world.md' assert find_page('world.md', PAGES) == 'hello/world.md' assert find_page('hello/world', PAGES) == 'hello/world.md' assert find_page('hello/world.md', PAGES) == 'hello/world.md' # doesn't accidentally mismatch directory: assert find_page('foo/bar.md', PAGES) != 'no_foo/bar.md' def test_doc_project(): """ Test a project """ PROJECT_NAME = 'simple' # MYPROJECT = 'simple' h1(f"TESTING MKDOCS PROJECT ({PROJECT_NAME})") h2("Config") myproject = DocProject(PROJECT_NAME) config = myproject.config print(config) h2("Build") result = myproject.build() assert result == myproject.build_result myproject.self_check() # perform an integrity check h2("Log") assert myproject.trace == result.stderr std_print(myproject.trace) h2("Filtering the log by severity") infos = myproject.find_entries(severity='INFO') print(f"There are {len(infos)} info items.") print('\n'.join(f" {i} - {item.title}" for i, item in enumerate(infos))) h2("Page objects") for filename, page in myproject.pages.items(): h3(f"PAGE: {filename}") print("- Title:", page.title) print("- Heading 1:", page.h1) print("- Markdown(start):", page.markdown[:50]) if __name__ == '__main__': pytest.main()mkdocs-test-0.5.3/update_pypi.sh000077500000000000000000000024411470776127000166520ustar00rootroot00000000000000# ------------------------------------------------------------- # update the package on pypi # 2024-10-12 # # Tip: if you don't want to retype pypi's username every time # define it as an environment variable (TWINE_USERNAME) # # ------------------------------------------------------------- function warn { GREEN='\033[0;32m' NORMAL='\033[0m' echo -e "${GREEN}$1${NORMAL}" } function get_value { # get the value from the config file toml get --toml-path pyproject.toml $1 } # Clean the subdirs, for safety and to guarantee integrity ./cleanup.sh # Check for changes in the files compared to the repository if ! git diff --quiet; then warn "Won't do it: there are changes in the repository. Please commit first!" exit 1 fi # get the project inform package_name=$(get_value project.name) package_version=v$(get_value project.version) # add a 'v' in front (git convention) # update Pypi warn "Rebuilding $package_name..." rm -rf build dist *.egg-info # necessary to guarantee integrity python3 -m build if twine upload dist/* ; then git push # just in case warn "... create tag $package_version, and push to remote git repo..." git tag $package_version git push --tags warn "Done ($package_version)!" else warn "Failed ($package_version)!" exit 1 fi