├── examples ├── some-libs │ ├── __init__.py │ └── mylib │ │ └── mypy.py ├── some-package │ ├── __init__.py │ ├── libs │ │ └── package_lib.py │ ├── resources │ │ └── sample.resource │ └── pyproject.toml ├── some-resources │ ├── SUT Y │ │ ├── Common.robot │ │ ├── .libtoc │ │ └── SettingsMenu.robot │ └── SUT X │ │ ├── robotframework │ │ ├── Common.robot │ │ └── LoginPage.robot │ │ └── python │ │ └── NavigationMenu.py ├── generate_docs.sh └── .libtoc ├── robotframework_libtoc ├── __init__.py ├── homepage_template.html ├── toc_template.html └── libtoc.py ├── Screenshot.png ├── pyproject.toml ├── poetry.lock ├── .gitignore ├── README.md └── LICENSE /examples/some-libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/some-package/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /robotframework_libtoc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/some-package/libs/package_lib.py: -------------------------------------------------------------------------------- 1 | def keyword_inside_package_lib(): 2 | pass -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amochin/robotframework-libtoc/HEAD/Screenshot.png -------------------------------------------------------------------------------- /examples/some-resources/SUT Y/Common.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Start SUT Y 3 | [Documentation] Launches the SUT Y 4 | No Operation -------------------------------------------------------------------------------- /examples/generate_docs.sh: -------------------------------------------------------------------------------- 1 | # Run this script from the 'examples' folder.. 2 | # .. or adjust the paths if running from another dir 3 | libtoc -P . . -------------------------------------------------------------------------------- /examples/some-resources/SUT Y/.libtoc: -------------------------------------------------------------------------------- 1 | [paths] 2 | # Use glob patterns 3 | **/*.robot 4 | **/*.resource 5 | **/*.py 6 | 7 | [libs] 8 | # Use RF library names with params - like for libdoc -------------------------------------------------------------------------------- /examples/some-libs/mylib/mypy.py: -------------------------------------------------------------------------------- 1 | def my_python_keyword(): 2 | """ 3 | This keyword is imported using a Python fully qualified name - 4 | see that setting the PYTHONPATH works 5 | """ 6 | pass -------------------------------------------------------------------------------- /examples/some-package/resources/sample.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation This is an example resource file inside of an installable package. 3 | ... The package must be installed / available in the PYTHONPATH 4 | 5 | 6 | *** Keywords *** 7 | Start Sample Package Application 8 | No Operation -------------------------------------------------------------------------------- /robotframework_libtoc/homepage_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Docs for Robot Framework keywords 6 |

7 |

8 | Please select a library in the navigation sidebar 9 |

10 |

11 | Created: 12 | 13 | {0} 14 | 15 |

16 | 17 | -------------------------------------------------------------------------------- /examples/some-package/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "some-package" 3 | version = "0.0.1" 4 | description = "This is just an example package to demonstrate the package loading feature of libtoc" 5 | authors = [] 6 | packages = [{include = "example_package", from = "src"}] 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.7" 10 | robotframework = ">=4" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /examples/.libtoc: -------------------------------------------------------------------------------- 1 | [paths] 2 | # Use glob patterns 3 | some-resources/**/*.robot 4 | some-resources/**/*.resource 5 | some-resources/**/*.py 6 | 7 | [libs] 8 | # Use RF library names with params - like for libdoc 9 | # SeleniumLibrary 10 | # Remote::http://10.0.0.42:8270 11 | # Import a library from PYTHONPATH using fully qualified name 12 | some-libs.mylib.mypy 13 | # You can use environment variables in lib params 14 | # SomeLib::$some_env_var/somepath 15 | 16 | [packages] 17 | # Use package name in python path and glob pattern separated by : 18 | some-package:resources/**/*.resource 19 | some-package:libs/**/*.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "robotframework-libtoc" 3 | version = "1.4.2" 4 | description = "Docs and TOC generator for Robot Framework resources and libs" 5 | authors = ["Andre Mochinin"] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/amochin/robotframework-libtoc" 9 | include = ["robotframework_libtoc/*template.html"] 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.7" 13 | robotframework = ">=4" 14 | importlib-resources = "~5.12" 15 | 16 | [tool.poetry.scripts] 17 | libtoc = "robotframework_libtoc.libtoc:main" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /examples/some-resources/SUT X/robotframework/Common.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Start Firefox with no proxy 3 | [Arguments] ${URL} 4 | ${proxy}= Evaluate sys.modules['selenium.webdriver'].Proxy() sys, selenium.webdriver 5 | ${direct}= Evaluate sys.modules['selenium.webdriver'].common.proxy.ProxyType.DIRECT sys, selenium.webdriver 6 | ${proxy.proxyType}= Set Variable ${direct} 7 | ${caps}= Evaluate sys.modules['selenium.webdriver'].DesiredCapabilities.FIREFOX sys, selenium.webdriver 8 | Evaluate $proxy.add_to_capabilities($caps) 9 | Open Browser ${URL} Firefox 10 | 11 | Open SUT X in Browser 12 | [Documentation] Starts the browser und opens the SUT X start page 13 | No Operation -------------------------------------------------------------------------------- /examples/some-resources/SUT X/robotframework/LoginPage.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource Common.robot 3 | 4 | *** Variables *** 5 | 6 | # Locators 7 | ${LoginDummy URL} /login 8 | ${Login Button} //button[text()='Login'] 9 | 10 | *** Keywords *** 11 | 12 | Open 13 | [Documentation] Navigates to the 'Login' page and waits for the 'Login' button to appear 14 | ... 15 | ... _Parameters_: 16 | ... - *Base_URL* - Base address of the SUT X app incl. prefix und port number 17 | [Arguments] ${Base_URL} 18 | 19 | Go To ${Base_URL}${LoginDummy URL} 20 | Wait Until Page Contains Element ${Login Button} 21 | 22 | Click Login 23 | [Documentation] Clicks the 'Login' button 24 | Click Element ${Login Button} -------------------------------------------------------------------------------- /examples/some-resources/SUT Y/SettingsMenu.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource Common.robot 3 | 4 | *** Variables *** 5 | 6 | # Locators 7 | ${Settings Page URL} /settings 8 | ${Save Button} //button[text()='Save'] 9 | 10 | *** Keywords *** 11 | 12 | Navigate 13 | [Documentation] Navigates to the Settings pages and waits for the "Save" button to appear 14 | ... 15 | ... _Parameters_: 16 | ... - *Base_URL* - Base URL of the SUT Y app including prefix und port number 17 | [Arguments] ${Base_URL} 18 | 19 | Go To ${Base_URL}${Settings Page URL} 20 | Wait Until Page Contains Element ${Save Button} 21 | 22 | Click Save 23 | [Documentation] Clicks the 'Save' button 24 | Click Element ${Save Button} -------------------------------------------------------------------------------- /examples/some-resources/SUT X/python/NavigationMenu.py: -------------------------------------------------------------------------------- 1 | from robot.libraries.BuiltIn import BuiltIn 2 | 3 | PAGE_TITLE = 'Some title' 4 | 5 | 6 | def _s(): 7 | return BuiltIn().get_library_instance('SeleniumLibrary') 8 | 9 | locators = { 10 | "some_element": "//label[contains(text(), 'Blahblah')]/../..//input", 11 | } 12 | 13 | # ------ RF keywords ----------------------- 14 | 15 | 16 | def wait_page_loaded(timeout): 17 | """ 18 | Waits for the page to load (meaning the element XYZ is visible), maximum the specified timeout. 19 | If the element is still not visible after timeout, an error is reported. 20 | 21 | _Parameters:_ 22 | - *timeout* - Max. time to wait 23 | """ 24 | _s().wait_until_page_contains_element(locators["some_element"], timeout=timeout) -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "importlib-resources" 5 | version = "5.12.0" 6 | description = "Read resources from Python packages" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, 11 | {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, 12 | ] 13 | 14 | [package.dependencies] 15 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 16 | 17 | [package.extras] 18 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 19 | testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 20 | 21 | [[package]] 22 | name = "robotframework" 23 | version = "6.1.1" 24 | description = "Generic automation framework for acceptance testing and robotic process automation (RPA)" 25 | optional = false 26 | python-versions = ">=3.6" 27 | files = [ 28 | {file = "robotframework-6.1.1-py3-none-any.whl", hash = "sha256:ee0d512d557e72ed760dd075525f6226baaab309010a48f9c9bf1f416ca434f7"}, 29 | {file = "robotframework-6.1.1.zip", hash = "sha256:3fa18f2596a4df2418c4b59abf43248327c15ed38ad8665f6a9a9c75c95d7789"}, 30 | ] 31 | 32 | [[package]] 33 | name = "zipp" 34 | version = "3.15.0" 35 | description = "Backport of pathlib-compatible object wrapper for zip files" 36 | optional = false 37 | python-versions = ">=3.7" 38 | files = [ 39 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 40 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 41 | ] 42 | 43 | [package.extras] 44 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 45 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 46 | 47 | [metadata] 48 | lock-version = "2.0" 49 | python-versions = ">=3.7" 50 | content-hash = "a67b7ce05acdcdffea591b182e7cc41c0f29a662a44bbdf011f620c6b99bd878" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created docs except templates 2 | **/*.html 3 | !**/*template.html 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | .DS_Store 135 | -------------------------------------------------------------------------------- /robotframework_libtoc/toc_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 | 31 |
32 | 33 |
34 | 35 | Robot Framework Documentation 36 | 37 |
38 | {} 39 |
40 |

41 | Created: {} 42 |

43 |
44 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Robot Framework LibTOC 2 | 3 | ## What it does 4 | This tool generates docs using Robot Framework [Libdoc](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#libdoc) for an entire folder (or multiple folders) with Robot Framework resources/libs and creates a TOC (table of contents) file for them 5 | 6 | ## Why use it 7 | The Robot Framework Libdoc tool normally generates a HTML file for a single keyword library or a resource file. 8 | If you have several keyword libraries or resources, you just get several separate HTML files. 9 | 10 | This tool collects separate keyword documentation files in one place and creates a TOC (table of contents) page 11 | with links to these files. 12 | The result is a folder with several static HTML pages which can be placed somewhere 13 | in the intranet or uploaded as CI artifact - so everybody can easily access the keywords docs. 14 | 15 | ### Here is the example screenshot 16 | ![](Screenshot.png) 17 | 18 | ## How it works 19 | - The tool goes through the specified folders with RF resources and it's **direct** subfolders 20 | - It looks for the **config files** named `.libtoc` which contain items you would like to create docs for: 21 | 1. Paths to resource/lib files in [glob format](https://en.wikipedia.org/wiki/Glob_(programming)) 22 | 2. RF libraries, installed or available in PYTHONPATH using the provided fully qualified name 23 | > Librariy import params (if necessary) like described in [libdoc user guide](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#general-usage) 24 | > Other libdoc CLI options (e.g. version or name of the output file) are not supported 25 | 3. Paths to resource/lib files in [glob format](https://en.wikipedia.org/wiki/Glob_(programming)) inside Python packages, loaded from the PYTHONPATH 26 | > See more about bundling RF resources in Python packages in [RF User Guide](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#taking-resource-files-into-use) 27 | 28 | - Then it generates the docs using `libdoc` - both for files paths, resolved from the glob patterns, and for the installed libraries. The created HTML files are placed in the **libtoc output_dir** - keeping the original subfolder structure of resources 29 | - Finally it generates a **TOC (Table of Contents)** HTML page with links to all the generated HTML files. 30 | The navigation tree structure in the TOC repeats the folder tree structure. 31 | ## Example of a `.libtoc` config file 32 | ``` 33 | [paths] 34 | # Use glob patterns 35 | some-resources/**/*.robot 36 | some-resources/**/*.resource 37 | some-resources/**/*.py 38 | 39 | [libs] 40 | # Use RF library names with params - like for libdoc 41 | SeleniumLibrary 42 | Remote::http://10.0.0.42:8270 43 | # Import a library from PYTHONPATH using fully qualified name 44 | some-libs.mylib.mypy 45 | # You can use environment variables in lib params 46 | SomeLib::$some_env_var/somepath 47 | 48 | [packages] 49 | # Use package name in python path and glob patterns separated by : 50 | some-package:resources/**/*.resource 51 | some-package:libs/**/*.py 52 | ``` 53 | > The config file must contain at least one of the sections - `[paths]`, `[libs]`, `[packages]` 54 | ## How to install it 55 | ### System requirements 56 | - Python >=3.7 57 | - Robot Framework 58 | ### Installation using pip 59 | ```shell 60 | pip install robotframework-libtoc 61 | ``` 62 | 63 | ## How to use it 64 | - Create the `.libtoc` config files in the *root of the resources folder* and/or in *direct subfolders* where you need docs to be created. 65 | - Run `libtoc`. The last `resources_dirs` parameter is mandatory, it takes any number of paths. Other params are optional: 66 | - `-d, --output_dir` 67 | - `--config_file` 68 | - `--toc_file` 69 | - `--toc_template` 70 | - `--homepage_template` 71 | - `-P, --pythonpath` 72 | 73 | Examples: 74 | ```shell 75 | libtoc example_resources 76 | libtoc 'example_resources/SUT X' 'example_resources/SUT Y' 77 | libtoc --output_dir docs example_resources 78 | libtoc --output_dir docs --toc_file MY_SPECIAL_NAME_FOR_DOCS.html example_resources 79 | libtoc --toc_template MY_CUSTOM_TOC.html --homepage_template MY_CUSTOM_HOMEPAGE.html example_resources 80 | ``` 81 | 82 | - Open the created file, e.g. `docs/keyword_docs.html` 83 | 84 | ## How to change the TOC and the homepage HTML templates 85 | The default HTML template files are located in the python installation directory (usually something like `/lib/site-packages/robotframework_libtoc`) and can be changed if necessary. 86 | It's also possible to provide custom HTML template files using the `--toc_template` and `--homepage_template` options. 87 | 88 | ## How to set the Python Path 89 | There are two ways to extend the list of paths where the libraries are searched for: 90 | 1. Using the `--pythonpath` option 91 | 2. Set the **PYTHONPATH** environment variable 92 | 93 | See more in [Robot Framework User Guide](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#pythonpath). 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /robotframework_libtoc/libtoc.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import glob 3 | import os 4 | import shutil 5 | import sys 6 | from datetime import datetime 7 | from pathlib import Path 8 | 9 | import importlib_resources 10 | import robot.libdoc 11 | 12 | 13 | class LibdocException(Exception): 14 | def __init__(self, broken_file): 15 | self.broken_file = broken_file 16 | 17 | 18 | def toc(links, timestamp, home_page_path, template_file=""): 19 | """ 20 | Returns a HTML source code for TOC (table of contents) page, based on the template and including 21 | the provided `links`, generation `timestamp` and the `home_page_path` HTML file as a landing page. 22 | """ 23 | if template_file == "": 24 | template_file = os.path.join(os.path.dirname(__file__), "toc_template.html") 25 | with open(template_file, encoding="utf8") as f: 26 | html_template = f.read() 27 | 28 | # double all brackets to make the further formatting work 29 | html_with_escaped_braces = html_template.replace("{", "{{") 30 | html_with_escaped_braces = html_with_escaped_braces.replace("}", "}}") 31 | 32 | # and convert the formatting brackets back 33 | html_with_escaped_braces = html_with_escaped_braces.replace("{{}}", "{}") 34 | 35 | return html_with_escaped_braces.format(home_page_path, links, timestamp) 36 | 37 | 38 | def homepage(timestamp, template_file=""): 39 | """ 40 | Returns a HTML source code for a landing page, based on the template and includig the provided `timestamp`. 41 | """ 42 | if template_file == "": 43 | template_file = os.path.join( 44 | os.path.dirname(__file__), "homepage_template.html" 45 | ) 46 | with open(template_file, encoding="utf_8") as f: 47 | html_template = f.read() 48 | return html_template.format(timestamp) 49 | 50 | 51 | def read_config(config_file): 52 | """ 53 | Parses the content of the `config_file` and returns a dictionary `{"paths":[values], "libs":[values]}`. 54 | 55 | The `paths` values are glob patterns, which can be resolved in real paths and used for generating docs using `libdoc`. 56 | The `libs` values are names of Robot Framework libraries with necessary import params - in the way to be also used for docs generation using `libdoc`. 57 | 58 | The config file must be formatted like this: 59 | ``` 60 | # Comments starting with # are ignored 61 | [Paths] 62 | *.resource 63 | **/my_subfolder/*.py 64 | 65 | [Libs] 66 | SeleniumLibrary 67 | SomeLibrary::some_import_param 68 | ``` 69 | """ 70 | sections = { 71 | "paths": {"markers": ["[paths]"], "values": []}, 72 | "packages": {"markers": ["[packages]"], "values": []}, 73 | "libs": {"markers": ["[libs]", "[libraries]"], "values": []}, 74 | } 75 | 76 | with open(config_file, encoding="utf8") as f: 77 | section_to_add = "" 78 | lines = f.readlines() 79 | for line in lines: 80 | stripped_line = line.strip() 81 | if len(stripped_line) > 0: 82 | if not stripped_line.startswith("#"): # comments 83 | skip_line = False 84 | for section_name, section_content in sections.items(): 85 | if stripped_line.lower() in section_content["markers"]: 86 | section_to_add = section_name 87 | skip_line = True 88 | break 89 | if not skip_line and section_to_add: 90 | sections[section_to_add]["values"].append(stripped_line) 91 | 92 | return { 93 | "paths": sections["paths"]["values"], 94 | "packages": sections["packages"]["values"], 95 | "libs": sections["libs"]["values"], 96 | } 97 | 98 | 99 | def add_files_from_folder(folder, base_dir_path, root=True): 100 | """ 101 | Creates a HTML source code with links to all HTML files in the `folder` and all it's subfolders. 102 | The links contain file paths relative to the `base_dir_path`. 103 | 104 | The `root` parameter is needed for internal usage only - it's set to False during deeper recursive calls. 105 | """ 106 | result_str = "" 107 | if not root: # means we're in the root - no collapsible need in this case 108 | result_str += """ 109 | """.format( 110 | os.path.basename(folder) 111 | ) 112 | 113 | result_str += """
114 | """ 115 | 116 | for item in sorted(os.listdir(folder)): 117 | item_path = os.path.abspath(os.path.join(folder, item)) 118 | if item.endswith(".html"): 119 | name_without_ext = os.path.splitext(item)[0] 120 | result_str += """{} 121 | """.format( 122 | os.path.relpath(item_path, base_dir_path), name_without_ext 123 | ) 124 | else: 125 | if os.path.isdir(item_path): 126 | result_str += add_files_from_folder( 127 | item_path, base_dir_path, root=False 128 | ) 129 | 130 | if not root: 131 | # end of the "collapsible_content" 132 | result_str += """
133 | """ 134 | return result_str 135 | 136 | 137 | def create_docs_for_dir(resource_dir, output_dir, config_file): 138 | """ 139 | Creates HTML docs using Robot Framework module `libdoc` for all resources and libraries in the `resource_dir`. 140 | Generated files are placed inside the `output_dir`, keeping the original subfolder tree structure. 141 | 142 | Paths of resource/python files and libraries, which the docs should be generated for, are configured using the `config_file`. 143 | 144 | The `config_file` must be formatted like this: 145 | ``` 146 | # Comments starting with # are ignored 147 | [Paths] 148 | *.resource 149 | **/my_subfolder/*.py 150 | 151 | [Libs] 152 | SeleniumLibrary 153 | SomeLibrary::some_import_param 154 | ``` 155 | """ 156 | target_dir = os.path.join( 157 | os.path.abspath(output_dir), os.path.basename(resource_dir) 158 | ) 159 | doc_config = read_config(config_file) 160 | 161 | resource_path_patterns = doc_config["paths"] 162 | if resource_path_patterns: 163 | print(">> Processing paths") 164 | broken_files = [] 165 | for path_pattern in resource_path_patterns: 166 | for real_path in glob.glob( 167 | os.path.join(resource_dir, path_pattern), recursive=True 168 | ): 169 | relative_path = os.path.relpath(real_path, resource_dir) 170 | target_path = os.path.join( 171 | target_dir, relative_path.rpartition(".")[0] + ".html" 172 | ) 173 | print(f">>> Processing file: {relative_path}") 174 | return_code = robot.libdoc.libdoc(real_path, target_path, quiet=True) 175 | if return_code > 0: 176 | broken_files.append(relative_path) 177 | 178 | package_definitions = doc_config["packages"] 179 | if package_definitions: 180 | print("---") 181 | packages = {} 182 | broken_packages = [] 183 | for package_definition in package_definitions: 184 | package_name, path_pattern = package_definition.split(":", 1) 185 | if package_name not in packages: 186 | packages[package_name] = [] 187 | packages[package_name].append(path_pattern) 188 | 189 | for package_name, paths_patterns in packages.items(): 190 | print(f">> Processing package: {package_name}") 191 | try: 192 | package_anchor = importlib_resources.files(package_name) 193 | except ModuleNotFoundError as e: 194 | print(f"Importing package '{package_name}' failed: {e}") 195 | broken_packages.append(package_name) 196 | else: 197 | with importlib_resources.as_file(package_anchor) as package_path: 198 | for path_pattern in paths_patterns: 199 | package_resource_files = package_path.glob(path_pattern) 200 | for real_path in package_resource_files: 201 | relative_path = Path(package_name) / real_path.relative_to( 202 | package_path 203 | ) 204 | target_path = os.path.join( 205 | target_dir, relative_path.with_suffix(".html") 206 | ) 207 | print(f">>> Processing file: {relative_path}") 208 | return_code = robot.libdoc.libdoc( 209 | real_path, target_path, quiet=True 210 | ) 211 | if return_code > 0: 212 | broken_packages.append(relative_path) 213 | 214 | libs = doc_config["libs"] 215 | if libs: 216 | print("---") 217 | print(">> Processing libraries") 218 | broken_libs = [] 219 | for lib in libs: 220 | lib_str_with_resolved_vars = os.path.expandvars(lib) 221 | target_path = os.path.join( 222 | target_dir, lib_str_with_resolved_vars.partition("::")[0] + ".html" 223 | ) 224 | print(f">>> Processing lib: {lib_str_with_resolved_vars}") 225 | return_code = robot.libdoc.libdoc( 226 | lib_str_with_resolved_vars, target_path, quiet=True 227 | ) 228 | if return_code > 0: 229 | broken_libs.append(lib_str_with_resolved_vars) 230 | return broken_files, broken_packages, broken_libs 231 | 232 | 233 | def create_toc( 234 | html_docs_dir, 235 | toc_file="keyword_docs.html", 236 | homepage_file="homepage.html", 237 | toc_template="", 238 | homepage_template="", 239 | ): 240 | """ 241 | Generates a `toc_file` (Table of Contents) HTML page with links to all HTML files inside the `html_docs_dir` and all it's subfolders. 242 | 243 | The navigation tree structure in the TOC repeats the folder tree structure. 244 | It also creates a `homepage_file` shown as a landing page when opening the TOC. 245 | 246 | All the content of the `html_docs_dir` will be moved in the new `src` subfolder, leaving only the `toc_file` directly inside. 247 | """ 248 | print(f"> Creating TOC in: {os.path.abspath(html_docs_dir)}") 249 | # move all subfolders and files into "src" 250 | src_subdir = os.path.join(html_docs_dir, "src") 251 | os.makedirs(src_subdir, exist_ok=True) 252 | all_docs = os.listdir(html_docs_dir) 253 | for doc_element in all_docs: 254 | if doc_element == "src": 255 | continue 256 | src = os.path.join(html_docs_dir, doc_element) 257 | target = os.path.join(src_subdir, doc_element) 258 | shutil.move(src, target) 259 | 260 | # create homepage in "src" 261 | homepage_path = os.path.join(src_subdir, homepage_file) 262 | current_date_time = datetime.now().strftime("%d.%m.%Y %H:%M:%S") 263 | doc_files_links = add_files_from_folder(src_subdir, os.path.abspath(html_docs_dir)) 264 | with open(homepage_path, "w", encoding="utf8") as f: 265 | f.write(homepage(current_date_time, homepage_template)) 266 | 267 | # create TOC 268 | toc_file_path = os.path.join(html_docs_dir, toc_file) 269 | with open(toc_file_path, "w", encoding="utf8") as f: 270 | f.write( 271 | toc( 272 | doc_files_links, 273 | current_date_time, 274 | os.path.relpath(homepage_path, os.path.abspath(html_docs_dir)), 275 | toc_template, 276 | ) 277 | ) 278 | 279 | print("---") 280 | print("TOC finished. Output file: {}".format(os.path.abspath(toc_file_path))) 281 | 282 | 283 | def main(): 284 | parser = argparse.ArgumentParser( 285 | description="Generates keyword docs using libdoc based on config files in direct subfolders of the resources dir and creates a TOC" 286 | ) 287 | parser.add_argument( 288 | "resources_dirs", nargs="+", help="Folders with resources and keywords files" 289 | ) 290 | parser.add_argument( 291 | "-d", "--output_dir", default="docs", help="Folder to create the docs in" 292 | ) 293 | parser.add_argument( 294 | "--config_file", 295 | default=".libtoc", 296 | help="File in each folder with docs generation configs", 297 | ) 298 | parser.add_argument( 299 | "--toc_file", default="keyword_docs.html", help="Name of the TOC file generated" 300 | ) 301 | parser.add_argument( 302 | "--toc_template", default="", help="Custom HTML template for the TOC file" 303 | ) 304 | parser.add_argument( 305 | "--homepage_template", 306 | default="", 307 | help="Custom HTML template for the homepage file", 308 | ) 309 | parser.add_argument( 310 | "-P", 311 | "--pythonpath", 312 | default="", 313 | help="Additional locations where to search for libraries and resources similarly as when running tests", 314 | ) 315 | 316 | args = parser.parse_args() 317 | 318 | if args.pythonpath: 319 | sys.path.insert(0, args.pythonpath) 320 | 321 | if os.path.isdir(args.output_dir): 322 | print(f"Output dir already exists, deleting it: {args.output_dir}") 323 | shutil.rmtree(args.output_dir) 324 | total_broken_files = [] 325 | total_broken_packages = [] 326 | total_broken_libs = [] 327 | 328 | for resources_dir in args.resources_dirs: 329 | print("") 330 | print(f"> Creating docs for dir: {os.path.abspath(resources_dir)}") 331 | for child_element in os.listdir(resources_dir): 332 | child_element_path = os.path.join(resources_dir, child_element) 333 | current_broken_files = [] 334 | current_broken_packages = [] 335 | current_broken_libs = [] 336 | if os.path.isdir(child_element_path): 337 | config_file = os.path.join(child_element_path, args.config_file) 338 | if os.path.isfile(config_file): 339 | ( 340 | current_broken_files, 341 | current_broken_packages, 342 | current_broken_libs, 343 | ) = create_docs_for_dir( 344 | child_element_path, 345 | args.output_dir, 346 | os.path.abspath(config_file), 347 | ) 348 | elif child_element == args.config_file: 349 | current_broken_files, current_broken_packages, current_broken_libs = ( 350 | create_docs_for_dir( 351 | resources_dir, 352 | args.output_dir, 353 | os.path.abspath(os.path.join(resources_dir, args.config_file)), 354 | ) 355 | ) 356 | 357 | total_broken_files += current_broken_files 358 | total_broken_packages += current_broken_packages 359 | total_broken_libs += current_broken_libs 360 | 361 | if total_broken_files: 362 | print("") 363 | print( 364 | f"---> !!! Errors occurred while generating docs for {len(total_broken_files)} files (see details above):" 365 | ) 366 | for f in total_broken_files: 367 | print(f" - {f}") 368 | 369 | if total_broken_packages: 370 | print("") 371 | print( 372 | f"---> !!! Errors occurred while generating docs for {len(total_broken_packages)} packages (see details above):" 373 | ) 374 | for f in total_broken_packages: 375 | print(f" - {f}") 376 | 377 | if total_broken_libs: 378 | print("") 379 | print( 380 | f"---> !!! Errors occurred while generating docs for {len(total_broken_libs)} libs (see details above):" 381 | ) 382 | for l in total_broken_libs: 383 | print(f" - {l}") 384 | 385 | if os.path.isdir(args.output_dir): 386 | print("") 387 | create_toc( 388 | args.output_dir, 389 | args.toc_file, 390 | toc_template=args.toc_template, 391 | homepage_template=args.homepage_template, 392 | ) 393 | else: 394 | print("No docs were created!") 395 | 396 | print("") 397 | 398 | 399 | if __name__ == "__main__": 400 | main() 401 | --------------------------------------------------------------------------------