Compare commits
1 Commits
master
...
revert-91-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
625e2c5968 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us fix the code
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To Reproduce
|
||||
Please, provide a fully standalone runnable test case : minimal python code + docx template + other files if needed (images etc...)
|
||||
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/help-request.md
vendored
20
.github/ISSUE_TEMPLATE/help-request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Help request
|
||||
about: Ask help to the community
|
||||
title: ''
|
||||
labels: help wanted
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe your problem
|
||||
A clear and concise description of what you want to do
|
||||
|
||||
## More details about your problem
|
||||
Steps to reproduce the behavior, expected behavior etc...
|
||||
|
||||
## Provide a test case
|
||||
If applicable, provide minimal python code + docx template + other files (images etc...) to reproduce the behavior
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Request for enhancement
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: Request for enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
25
.github/workflows/codestyle.yml
vendored
25
.github/workflows/codestyle.yml
vendored
@ -1,25 +0,0 @@
|
||||
# As per the example on https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
name: Python codestyling
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
pip install flake8
|
||||
# stop the build if there are code styling problems. The GitHub editor is 127 chars wide.
|
||||
flake8 . --count --max-line-length=127 --show-source --statistics
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,7 +21,6 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
tests/output/*
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "20"
|
||||
# rust: "1.70"
|
||||
# golang: "1.20"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
||||
# builder: "dirhtml"
|
||||
# Fail on all warnings to avoid broken references
|
||||
# fail_on_warning: true
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
# formats:
|
||||
# - pdf
|
||||
# - epub
|
||||
|
||||
# Optional but recommended, declare the Python requirements required
|
||||
# to build your documentation
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements.txt
|
||||
255
CHANGES.rst
255
CHANGES.rst
@ -1,225 +1,118 @@
|
||||
0.20.2 *(Unreleased)*
|
||||
-------------------
|
||||
- Move docxcompose to optional dependency (Thanks to Waket Zheng)
|
||||
|
||||
0.20.1 (2025-07-15)
|
||||
-------------------
|
||||
- Fix and improve get_undeclared_template_variables() method (Thanks to Pablo Esteban)
|
||||
|
||||
0.20.0 (2024-12-29)
|
||||
-------------------
|
||||
- Add RichTextParagraph (Thanks to ST-Imrie)
|
||||
- Add RTL support for bold/italic (Thanks to bm-rana)
|
||||
- Update documentation
|
||||
|
||||
0.19.1 (2024-12-29)
|
||||
-------------------
|
||||
- PR #575 : fix unicode in footnotes (Thanks to Jonathan Pyle)
|
||||
|
||||
0.19.0 (2024-11-12)
|
||||
-------------------
|
||||
- Support rendering variables in footnotes (Thanks to Bart Broere)
|
||||
|
||||
0.18.0 (2024-07-21)
|
||||
-------------------
|
||||
- IMPORTANT : Remove Python 2.x support
|
||||
- Add hyperlink option in InlineImage (Thanks to Jean Marcos da Rosa)
|
||||
- Update index.rst (Thanks to jkpet)
|
||||
- Add poetry env
|
||||
- Black all files
|
||||
|
||||
0.17.0 (2024-05-01)
|
||||
-------------------
|
||||
- Add support to python-docx 1.1.1
|
||||
|
||||
0.16.8 (2024-02-23)
|
||||
-------------------
|
||||
- PR #527 : upgrade Jinja2 in Pipfile.lock
|
||||
|
||||
0.16.7 (2023-05-08)
|
||||
-------------------
|
||||
- PR #493 - thanks to AdrianVorobel
|
||||
|
||||
0.16.6 (2023-03-12)
|
||||
-------------------
|
||||
- PR #482 - thanks to dreizehnutters
|
||||
|
||||
0.16.5 (2023-01-07)
|
||||
-------------------
|
||||
- PR #467 - thanks to Slarag
|
||||
- fix #465
|
||||
- fix #464
|
||||
|
||||
0.16.4 (2022-08-04)
|
||||
-------------------
|
||||
- Regional fonts for RichText
|
||||
- Reorganize documentation
|
||||
|
||||
0.16.3 (2022-07-14)
|
||||
-------------------
|
||||
- fix #448
|
||||
|
||||
0.16.2 (2022-07-14)
|
||||
-------------------
|
||||
- fix #444
|
||||
- fix #443
|
||||
|
||||
0.16.1 (2022-06-12)
|
||||
-------------------
|
||||
- PR #442
|
||||
|
||||
0.16.0 (2022-04-16)
|
||||
-------------------
|
||||
- add jinja2 comment support - Thanks to staffanm
|
||||
|
||||
0.15.2 (2022-01-12)
|
||||
-------------------
|
||||
- fix #408
|
||||
- Multi-rendering with same DocxTemplate object is now possible
|
||||
see tests/multi_rendering.py
|
||||
- fix #392
|
||||
- fix #398
|
||||
|
||||
0.14.1 (2021-10-01)
|
||||
-------------------
|
||||
- One can now use python -m docxtpl on command line
|
||||
to generate a docx from a template and a json file as a context
|
||||
Thanks to Lcrs123@github
|
||||
|
||||
0.12.0 (2021-08-15)
|
||||
-------------------
|
||||
- Code has be split into many files for better readability
|
||||
- Use docxcomposer to attach parts when a docx file is given to create a subdoc
|
||||
Images, styles etc... must now be taken in account in subdocs
|
||||
- Some internal XML IDs are now renumbered to avoid collision, thus images are not randomly disapearing anymore.
|
||||
- fix #372
|
||||
- fix #374
|
||||
- fix #375
|
||||
- fix #369
|
||||
- fix #368
|
||||
- fix #347
|
||||
- fix #181
|
||||
- fix #61
|
||||
|
||||
0.11.5 (2021-05-09)
|
||||
-------------------
|
||||
- PR #351
|
||||
- It is now possible to put InlineImage in header/footer
|
||||
- fix #323
|
||||
- fix #320
|
||||
- \\n, \\a, \\t and \\f are now accepted in simple context string. Thanks to chabErch@github
|
||||
|
||||
0.10.5 (2020-10-15)
|
||||
-------------------
|
||||
- Remove extension testing (#297)
|
||||
- Fix spaces missing in some cases (#116, #227)
|
||||
|
||||
0.9.2 (2020-04-26)
|
||||
-------------------
|
||||
- Fix #271
|
||||
- Code styling
|
||||
|
||||
0.8.1 (2020-04-14)
|
||||
-------------------
|
||||
- fix #266
|
||||
- docxtpl is now able to use latest python-docx (0.8.10). Thanks to Dutchy-@github.
|
||||
|
||||
0.7.0 (2020-04-09)
|
||||
-------------------
|
||||
- Add replace_zipname() method to replace Excel and PowerPoint embedded files
|
||||
|
||||
0.6.4 (2020-04-06)
|
||||
-------------------
|
||||
- Add the possibility to add RichText to a Richtext
|
||||
- Prevent lxml from attempting to parse None
|
||||
- PR #207 and #209
|
||||
- Handle spaces correctly when run are split by Jinja code (#205)
|
||||
- PR #203
|
||||
- DocxTemplate now accepts file-like objects (Thanks to edufresne)
|
||||
|
||||
0.5.20 (2019-05-23)
|
||||
-------------------
|
||||
- Fix #199
|
||||
- Add support for file-like objects for replace_media (#197)
|
||||
- Fix #176
|
||||
- Delegated autoescaping to Jinja2 Environment (#175)
|
||||
- Force to use python-docx 0.8.7 (#170)
|
||||
- Add getting undeclared variables in the template (#171)
|
||||
- Added `PAGE_BREAK` feature (#168)
|
||||
- Fixed issue #159: autoescaped values for both str and unicode.
|
||||
- Fix tables with gridSpan that have less cells after the tc forloop (#164)
|
||||
- Smart double quotes in jinja tags are now converted into simple double quotes
|
||||
- Smart quotes in jinja tags are now converted into simple quotes
|
||||
- Add custom jinja filter example in tests/
|
||||
- Reformat the code to be a little more PEP8 compliant
|
||||
- Add {% hm %} tag for table columns horizontal merging (Thanks to nickgashkov)
|
||||
- Split tests/tests_files dir into templates and output dirs
|
||||
- autoescape support for python 2.7
|
||||
- fix issue #154
|
||||
- Render can now autoescape context dict
|
||||
- Fix invalid xml parse because using {% vm %}
|
||||
- Cast to string non-string value given to RichText or Listing objects
|
||||
- Import html.escape instead of cgi.escape (deprecated)
|
||||
- Declare package as python2 and python3 compatible for wheel distrib
|
||||
- Add sub/superscript in RichText
|
||||
- Fix table vertical merge
|
||||
- An hyperlink can now be used in RichText
|
||||
|
||||
0.4.13 (2018-06-21)
|
||||
-------------------
|
||||
- Subdocument can now be based on an existing docx
|
||||
- Add font option in RichText
|
||||
- Better tabs and spaces management for MS Word 2016
|
||||
- Wheel distribution
|
||||
- Manage autoscaping on InlineImage, Richtext and Subdoc
|
||||
- Purge MANIFEST.in file
|
||||
- Accept variables starting with 'r' in {{}} when no space after {{
|
||||
- Remove debug traces
|
||||
- Add {% vm %} to merge cell vertically within a loop (Thanks to Arthaslixin)
|
||||
- use six.iteritems() instead of iteritems for python 3 compatibility
|
||||
- Fixed Bug #95 on replace_pic() method
|
||||
- Add replace_pic() method to replace pictures from its filename (Thanks to Riccardo Gusmeroli)
|
||||
0.4.1 (2017-09-10)
|
||||
------------------
|
||||
- Improve image attachment for InlineImage ojects
|
||||
|
||||
0.4.0 (2017-09-09)
|
||||
------------------
|
||||
- Add replace_media() method (useful for header/footer images)
|
||||
- Add replace_embedded() method (useful for embedding docx)
|
||||
|
||||
0.3.9 (2017-06-27)
|
||||
------------------
|
||||
- Fix exception in fix_table()
|
||||
|
||||
0.3.8 (2017-06-20)
|
||||
------------------
|
||||
- Fix bug when using more than one {{r }} or {%r %} in the same run
|
||||
|
||||
0.3.7 (2017-06-13)
|
||||
------------------
|
||||
- Fix git tag v0.3.6 was in fact for 0.3.5 package version
|
||||
so create a tag 0.3.7 for 0.3.7 package version
|
||||
|
||||
0.3.6 (2017-06-10)
|
||||
------------------
|
||||
- Better head/footer jinja2 handling (Thanks to hugokernel)
|
||||
|
||||
0.3.5 (2017-02-20)
|
||||
------------------
|
||||
- Fix bug where one is using '%' (modulo operator) inside a tag
|
||||
|
||||
0.3.4 (2017-02-14)
|
||||
------------------
|
||||
- Add Listing class to manage \n and \a (new paragraph) and escape text AND keep current styling
|
||||
|
||||
0.3.3 (2017-02-07)
|
||||
------------------
|
||||
- Add {%tc } tags for dynamic table columns (Thanks to majkls23)
|
||||
|
||||
0.3.2 (2017-01-16)
|
||||
------------------
|
||||
- Remove version limitation over sphinx package in setup.py
|
||||
|
||||
0.3.1 (2017-01-16)
|
||||
------------------
|
||||
- Add PNG & JPEG in tests/test_files/
|
||||
|
||||
0.3.0 (2017-01-15)
|
||||
------------------
|
||||
- You can now add images directly without using subdoc, it is much more faster.
|
||||
|
||||
0.2.5 (2017-01-14)
|
||||
------------------
|
||||
- Add dynamic colspan tag for tables
|
||||
|
||||
0.2.4 (2016-11-30)
|
||||
------------------
|
||||
- Fix /n in RichText class
|
||||
|
||||
0.2.3 (2016-08-09)
|
||||
------------------
|
||||
- Add Python 3 support for footer and header
|
||||
|
||||
0.2.2 (2016-06-11)
|
||||
------------------
|
||||
- Fix bug when using utf-8 chracters inside footer or header in .docx template
|
||||
It now detects header/footer encoding automatically
|
||||
|
||||
0.2.1 (2016-06-11)
|
||||
------------------
|
||||
- Fix bug where using subdocs is corrupting header and footer in generated docx
|
||||
Thanks to Denny Weinberg for his help.
|
||||
|
||||
0.2.0 (2016-03-17)
|
||||
------------------
|
||||
- Add Header and Footer support (Thanks to Denny Weinberg)
|
||||
|
||||
0.1.11 (2016-03-1)
|
||||
------------------
|
||||
- '>' and '<' can now be used inside jinja tags
|
||||
|
||||
0.1.10 (2016-02-11)
|
||||
-------------------
|
||||
- render() accepts optionnal jinja_env argument :
|
||||
useful to set custom filters and other things
|
||||
|
||||
0.1.9 (2016-01-18)
|
||||
------------------
|
||||
- better subdoc management : accept tables
|
||||
|
||||
0.1.8 (2015-11-05)
|
||||
------------------
|
||||
- better xml code cleaning around Jinja2 tags
|
||||
|
||||
0.1.7 (2015-09-09)
|
||||
------------------
|
||||
- python 3 support
|
||||
|
||||
0.1.6 (2015-05-11)
|
||||
------------------
|
||||
- remove debug code
|
||||
- add lxml dependency
|
||||
|
||||
0.1.5 (2015-05-11)
|
||||
------------------
|
||||
- fix template filter with quote
|
||||
|
||||
0.1.4 (2015-03-27)
|
||||
------------------
|
||||
- add RichText support
|
||||
|
||||
0.1.3 (2015-03-13)
|
||||
------------------
|
||||
- add subdoc support
|
||||
- add some exemples in tests/
|
||||
|
||||
0.1.2 (2015-03-12)
|
||||
------------------
|
||||
- First running version
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
graft docs
|
||||
prune docs/_build
|
||||
graft pyquery
|
||||
graft tests
|
||||
include *_fixt.py *.rst *.cfg *.ini
|
||||
global-exclude *.pyc
|
||||
global-exclude __pycache__
|
||||
13
Pipfile
13
Pipfile
@ -1,13 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
docxtpl = {editable = true, path = "."}
|
||||
flake8 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3"
|
||||
419
Pipfile.lock
generated
419
Pipfile.lock
generated
@ -1,419 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0368a5e08ceb2b4910a110742515b5ff1d04a3a3af2b91b49d922ef9aaab6915"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d",
|
||||
"sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.17.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f",
|
||||
"sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd",
|
||||
"sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea",
|
||||
"sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981",
|
||||
"sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b",
|
||||
"sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7",
|
||||
"sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8",
|
||||
"sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175",
|
||||
"sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d",
|
||||
"sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392",
|
||||
"sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad",
|
||||
"sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f",
|
||||
"sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f",
|
||||
"sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b",
|
||||
"sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875",
|
||||
"sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3",
|
||||
"sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800",
|
||||
"sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65",
|
||||
"sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2",
|
||||
"sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812",
|
||||
"sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50",
|
||||
"sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==24.10.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
|
||||
"sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.8"
|
||||
},
|
||||
"docxcompose": {
|
||||
"hashes": [
|
||||
"sha256:bcf2799a0b63c29eb77a3d799a2f28443ae0f69f8691ff3d753f706be515c3e9"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"docxtpl": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343",
|
||||
"sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
|
||||
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5",
|
||||
"sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b",
|
||||
"sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49",
|
||||
"sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c",
|
||||
"sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b",
|
||||
"sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba",
|
||||
"sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5",
|
||||
"sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7",
|
||||
"sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422",
|
||||
"sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88",
|
||||
"sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8",
|
||||
"sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57",
|
||||
"sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325",
|
||||
"sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a",
|
||||
"sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982",
|
||||
"sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8",
|
||||
"sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55",
|
||||
"sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2",
|
||||
"sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df",
|
||||
"sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84",
|
||||
"sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551",
|
||||
"sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a",
|
||||
"sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740",
|
||||
"sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e",
|
||||
"sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f",
|
||||
"sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60",
|
||||
"sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e",
|
||||
"sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6",
|
||||
"sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd",
|
||||
"sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd",
|
||||
"sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609",
|
||||
"sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20",
|
||||
"sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6",
|
||||
"sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e",
|
||||
"sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61",
|
||||
"sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4",
|
||||
"sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776",
|
||||
"sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779",
|
||||
"sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6",
|
||||
"sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252",
|
||||
"sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c",
|
||||
"sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92",
|
||||
"sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5",
|
||||
"sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e",
|
||||
"sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f",
|
||||
"sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54",
|
||||
"sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877",
|
||||
"sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e",
|
||||
"sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37",
|
||||
"sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590",
|
||||
"sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706",
|
||||
"sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142",
|
||||
"sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9",
|
||||
"sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c",
|
||||
"sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56",
|
||||
"sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5",
|
||||
"sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987",
|
||||
"sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729",
|
||||
"sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87",
|
||||
"sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7",
|
||||
"sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7",
|
||||
"sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf",
|
||||
"sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28",
|
||||
"sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056",
|
||||
"sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7",
|
||||
"sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e",
|
||||
"sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0",
|
||||
"sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872",
|
||||
"sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079",
|
||||
"sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4",
|
||||
"sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd",
|
||||
"sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9",
|
||||
"sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121",
|
||||
"sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7",
|
||||
"sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b",
|
||||
"sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d",
|
||||
"sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76",
|
||||
"sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530",
|
||||
"sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d",
|
||||
"sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7",
|
||||
"sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9",
|
||||
"sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd",
|
||||
"sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410",
|
||||
"sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40",
|
||||
"sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7",
|
||||
"sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b",
|
||||
"sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5",
|
||||
"sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5",
|
||||
"sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1",
|
||||
"sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997",
|
||||
"sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8",
|
||||
"sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc",
|
||||
"sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563",
|
||||
"sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c",
|
||||
"sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433",
|
||||
"sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6",
|
||||
"sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4",
|
||||
"sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4",
|
||||
"sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f",
|
||||
"sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1",
|
||||
"sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa",
|
||||
"sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f",
|
||||
"sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e",
|
||||
"sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063",
|
||||
"sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4",
|
||||
"sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5",
|
||||
"sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571",
|
||||
"sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf",
|
||||
"sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa",
|
||||
"sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d",
|
||||
"sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de",
|
||||
"sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd",
|
||||
"sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86",
|
||||
"sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82",
|
||||
"sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f",
|
||||
"sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140",
|
||||
"sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250",
|
||||
"sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172",
|
||||
"sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba",
|
||||
"sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751",
|
||||
"sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff",
|
||||
"sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c",
|
||||
"sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556",
|
||||
"sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44",
|
||||
"sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8",
|
||||
"sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7",
|
||||
"sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c",
|
||||
"sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e",
|
||||
"sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.4.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
|
||||
"sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
|
||||
"sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
|
||||
"sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
|
||||
"sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
|
||||
"sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
|
||||
"sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
|
||||
"sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
|
||||
"sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
|
||||
"sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
|
||||
"sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
|
||||
"sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
|
||||
"sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
|
||||
"sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
|
||||
"sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
|
||||
"sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
|
||||
"sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
|
||||
"sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
|
||||
"sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
|
||||
"sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
|
||||
"sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
|
||||
"sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
|
||||
"sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
|
||||
"sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
|
||||
"sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
|
||||
"sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
|
||||
"sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
|
||||
"sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
|
||||
"sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
|
||||
"sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
|
||||
"sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
|
||||
"sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
|
||||
"sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
|
||||
"sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
|
||||
"sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
|
||||
"sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
|
||||
"sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
|
||||
"sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
|
||||
"sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
|
||||
"sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
|
||||
"sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
|
||||
"sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
|
||||
"sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
|
||||
"sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
|
||||
"sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
|
||||
"sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
|
||||
"sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
|
||||
"sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
|
||||
"sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
|
||||
"sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
|
||||
"sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
|
||||
"sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
|
||||
"sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
|
||||
"sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
|
||||
"sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
|
||||
"sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
|
||||
"sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
|
||||
"sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
|
||||
"sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
|
||||
"sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
|
||||
"sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505",
|
||||
"sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
||||
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94",
|
||||
"sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.3.7"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9",
|
||||
"sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.13.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a",
|
||||
"sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.3.2"
|
||||
},
|
||||
"python-docx": {
|
||||
"hashes": [
|
||||
"sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe",
|
||||
"sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:2e308396e1d83de287ada2c2fd6e64286008fe6aca5008e0b6a8cb0e2c86eedd",
|
||||
"sha256:ea0e7655c05b74819f82e76e11a85b31779fee7c4969e82f72bab0664e8317e4"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==80.1.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.17.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
|
||||
"sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
|
||||
"sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
|
||||
"sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
|
||||
"sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
|
||||
"sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
|
||||
"sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
|
||||
"sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
|
||||
"sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
|
||||
"sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
|
||||
"sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
|
||||
"sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
|
||||
"sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
|
||||
"sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
|
||||
"sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
|
||||
"sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
|
||||
"sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
|
||||
"sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
|
||||
"sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
|
||||
"sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
|
||||
"sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
|
||||
"sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
|
||||
"sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
|
||||
"sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
|
||||
"sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
|
||||
"sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
|
||||
"sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
|
||||
"sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
|
||||
"sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
|
||||
"sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
|
||||
"sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
|
||||
"sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c",
|
||||
"sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==4.13.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
README.rst
16
README.rst
@ -14,25 +14,19 @@ This package uses 2 major packages :
|
||||
|
||||
python-docx-template has been created because python-docx is powerful for creating documents but not for modifying them.
|
||||
|
||||
The idea is to begin to create an example of the document you want to generate with microsoft word, it can be as complex as you want :
|
||||
The idea is to begin to create an exemple of the document you want to generate with microsoft word, it can be as complex as you want :
|
||||
pictures, index tables, footer, header, variables, anything you can do with word.
|
||||
Then, as you are still editing the document with microsoft word, you insert jinja2-like tags directly in the document.
|
||||
You save the document as a .docx file (xml format) : it will be your .docx template file.
|
||||
|
||||
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
|
||||
|
||||
Share
|
||||
-----
|
||||
|
||||
If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Please, `read the doc <http://docxtpl.readthedocs.org>`_
|
||||
|
||||
Other projects
|
||||
--------------
|
||||
|
||||
If you like python-docx-template, please have a look at some of my other projects :
|
||||
|
||||
- `django-listing <https://github.com/elapouya/django-listing>`_ : A listing/table library on steroid for Djano
|
||||
- `python-textops3 <https://github.com/elapouya/python-textops3>`_ : Chainable text operations
|
||||
- `django-robohash-svg <https://github.com/elapouya/django-robohash-svg>`_ : Create svg robots avatars
|
||||
|
||||
|
||||
157
docs/conf.py
157
docs/conf.py
@ -12,215 +12,215 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
'sphinx.ext.autodoc',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = ".rst"
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = "python-docx-template"
|
||||
copyright = "2015, Eric Lapouyade"
|
||||
project = u'python-docx-template'
|
||||
copyright = u'2015, Eric Lapouyade'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "0.20"
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "0.20.x"
|
||||
release = '0.1.5'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ["_build"]
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "sphinx_book_theme"
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "python-docx-templatedoc"
|
||||
htmlhelp_basename = 'python-docx-templatedoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template.tex",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"manual",
|
||||
),
|
||||
('index', 'python-docx-template.tex', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
@ -228,17 +228,12 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
["Eric Lapouyade"],
|
||||
1,
|
||||
)
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
[u'Eric Lapouyade'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
@ -247,25 +242,19 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"python-docx-template",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'python-docx-template', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
401
docs/index.rst
401
docs/index.rst
@ -8,14 +8,10 @@ Welcome to python-docx-template's documentation!
|
||||
|
||||
.. rubric:: Quickstart
|
||||
|
||||
To install using pip::
|
||||
To install::
|
||||
|
||||
pip install docxtpl
|
||||
|
||||
or using conda::
|
||||
|
||||
conda install docxtpl --channel conda-forge
|
||||
|
||||
Usage::
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
@ -42,6 +38,8 @@ You save the document as a .docx file (xml format) : it will be your .docx templ
|
||||
|
||||
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
|
||||
|
||||
Note : python-docx-template as been tested with MS Word 97, it may not work with other version.
|
||||
|
||||
Jinja2-like syntax
|
||||
------------------
|
||||
|
||||
@ -51,16 +49,10 @@ Nevertheless there are some restrictions and extensions to make it work inside a
|
||||
Restrictions
|
||||
++++++++++++
|
||||
|
||||
The usual jinja2 tags, are only to be used inside the same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
|
||||
If you want to manage paragraphs, table rows and a whole run with its style, you must use special tag syntax as explained in next chapter.
|
||||
The usual jinja2 tags, are only to be used inside a same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
|
||||
|
||||
**Note:**
|
||||
|
||||
a 'run' for Microsoft Word is a sequence of characters with the same style.
|
||||
For example, if you create a paragraph with all characters of the same style,
|
||||
MS Word will create internally only one 'run' in the paragraph. Now,
|
||||
if you put in bold a text in the middle of this paragraph,
|
||||
word will transform the previous 'run' into 3 different 'runs' (normal - bold - normal).
|
||||
Note : a 'run' for microsoft word is a sequence of characters with the same style. For example, if you create a paragraph with all characters the same style :
|
||||
word will create internally only one 'run' in the paragraph. Now, if you put in bold a text in the middle of this paragraph, word will transform the previous 'run' into 3 'runs' (normal - bold - normal).
|
||||
|
||||
Extensions
|
||||
++++++++++
|
||||
@ -68,56 +60,15 @@ Extensions
|
||||
Tags
|
||||
....
|
||||
|
||||
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used::
|
||||
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used ::
|
||||
|
||||
{%p jinja2_tag %} for paragraphs
|
||||
{%tr jinja2_tag %} for table rows
|
||||
{%tc jinja2_tag %} for table columns
|
||||
{%r jinja2_tag %} for runs
|
||||
|
||||
By using these tags, python-docx-template will take care to put the real jinja2 tags (without the `p`, `tr`, `tc` or `r`) at the right place into the document's xml source code.
|
||||
In addition, these tags also tell python-docx-template to **remove** the paragraph, table row, table column or run where the tags are located.
|
||||
|
||||
For example, if you have this kind of template::
|
||||
|
||||
{%p if display_paragraph %}
|
||||
One or many paragraphs
|
||||
{%p endif %}
|
||||
|
||||
The first and last paragraphs (those containing ``{%p ... %}`` tags) will never appear in generated docx, regardless of the ``display_paragraph`` value.
|
||||
|
||||
Here only::
|
||||
|
||||
One or many paragraphs
|
||||
|
||||
will appear in generated docx if ``display_paragraph`` is True, otherwise, no paragraph at all are displayed.
|
||||
|
||||
**IMPORTANT :** Always put space after a starting tag delimiter and a space before the ending one :
|
||||
|
||||
Avoid::
|
||||
|
||||
{%if something%}
|
||||
{%pif display_paragraph%}
|
||||
|
||||
Use instead::
|
||||
|
||||
{% if something %}
|
||||
{%p if display_paragraph %}
|
||||
|
||||
**IMPORTANT** : Do not use ``{%p``, ``{%tr``, ``{%tc`` or ``{%r`` twice in the same
|
||||
paragraph, row, column or run. Example :
|
||||
|
||||
Do not use this::
|
||||
|
||||
{%p if display_paragraph %}Here is my paragraph {%p endif %}
|
||||
|
||||
But use this instead in your docx template::
|
||||
|
||||
{%p if display_paragraph %}
|
||||
Here is my paragraph
|
||||
{%p endif %}
|
||||
|
||||
This syntax is possible because MS Word considers each line as a new paragraph (if you do not use SHIFT-RETURN).
|
||||
By using these tags, python-docx-template will take care to put the real jinja2 tags at the right place into the document's xml source code.
|
||||
In addition, these tags also tells python-docx-template to remove the paragraph, table row, table column or run where are located the begin and ending tags and only takes care about what is in between.
|
||||
|
||||
Display variables
|
||||
.................
|
||||
@ -126,100 +77,37 @@ As part of jinja2, one can used double braces::
|
||||
|
||||
{{ <var> }}
|
||||
|
||||
if ``<var>`` is a string, ``\n``, ``\a``, ``\t`` and ``\f`` will be translated respectively into newlines, new paragraphs, tabs and page breaks
|
||||
|
||||
But if ``<var>`` is a RichText_ object, you must specify that you are changing the actual 'run'::
|
||||
But if ``<var>`` is an RichText object, you must specify that you are changing the actual 'run' ::
|
||||
|
||||
{{r <var> }}
|
||||
|
||||
Note the ``r`` right after the opening braces.
|
||||
|
||||
**VERY IMPORTANT :** Variables must not contains characters like ``<``, ``>`` and ``&`` unless using Escaping_
|
||||
|
||||
**IMPORTANT :** Always put space after a starting var delimiter and a space before the ending one :
|
||||
|
||||
Avoid::
|
||||
|
||||
{{myvariable}}
|
||||
{{rmyrichtext}}
|
||||
|
||||
Use instead::
|
||||
|
||||
{{ myvariable }}
|
||||
{{r myrichtext }}
|
||||
|
||||
Comments
|
||||
........
|
||||
|
||||
You can add jinja-like comments in your template::
|
||||
|
||||
{#p this is a comment as a paragraph #}
|
||||
{#tr this is a comment as a table row #}
|
||||
{#tc this is a comment as a table cell #}
|
||||
|
||||
See tests/templates/comments_tpl.docx for an example.
|
||||
|
||||
Split and merge text
|
||||
....................
|
||||
|
||||
* You can merge a jinja2 tag with previous line by using ``{%-``
|
||||
* You can merge a jinja2 tag with next line by using ``-%}``
|
||||
|
||||
A text containing Jinja2 tags may be unreadable if too long::
|
||||
|
||||
My house is located {% if living_in_town %} in urban area {% else %} in countryside {% endif %} and I love it.
|
||||
|
||||
One can use *ENTER* or *SHIFT+ENTER* to split a text like below, then use ``{%-`` and ``-%}`` to tell docxtpl to merge the whole thing::
|
||||
|
||||
My house is located
|
||||
{%- if living_in_town -%}
|
||||
in urban area
|
||||
{%- else -%}
|
||||
in countryside
|
||||
{%- endif -%}
|
||||
and I love it.
|
||||
|
||||
**IMPORTANT :** Use an unbreakable space (*CTRL+SHIFT+SPACE*) when a space is wanted at line beginning or ending.
|
||||
|
||||
**IMPORTANT 2 :** ``{%- xxx -%}`` tags must be alone in a line : do not add some text before or after on the same line.
|
||||
|
||||
Escaping delimiters
|
||||
...................
|
||||
|
||||
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use::
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
|
||||
Tables
|
||||
......
|
||||
|
||||
Spanning
|
||||
~~~~~~~~
|
||||
|
||||
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
|
||||
|
||||
{% colspan <var> %}
|
||||
|
||||
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
|
||||
|
||||
You can also span horizontally within a for loop (see tests/horizontal_merge.py)::
|
||||
|
||||
{% hm %}
|
||||
|
||||
You can also merge cells vertically within a for loop (see tests/vertical_merge.py)::
|
||||
|
||||
{% vm %}
|
||||
Note the ``r`` right after the openning braces
|
||||
|
||||
Cell color
|
||||
~~~~~~~~~~
|
||||
..........
|
||||
|
||||
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell::
|
||||
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell ::
|
||||
|
||||
{% cellbg <var> %}
|
||||
|
||||
`<var>` must contain the color's hexadecimal code *without* the hash sign
|
||||
|
||||
.. _RichText:
|
||||
Column spanning
|
||||
...............
|
||||
|
||||
If you want to dynamically span a table cell over many column (this is useful when you have a table with a dynamic column count),
|
||||
you must put the following tag at the very beginning of the cell to span ::
|
||||
|
||||
{% colspan <var> %}
|
||||
|
||||
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
|
||||
|
||||
Escaping
|
||||
........
|
||||
|
||||
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use ::
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
|
||||
RichText
|
||||
--------
|
||||
@ -227,139 +115,61 @@ RichText
|
||||
When you use ``{{ <var> }}`` tag in your template, it will be replaced by the string contained within `var` variable.
|
||||
BUT it will keep the current style.
|
||||
If you want to add dynamically changeable style, you have to use both : the ``{{r <var> }}`` tag AND a ``RichText`` object within `var` variable.
|
||||
You can change color, bold, italic, size, font and so on, but the best way is to use Microsoft Word to define your own *character* style
|
||||
You can change color, bold, italic, size and so on, but the best way is to use Microsoft Word to define your own *caracter* style
|
||||
( Home tab -> modify style -> manage style button -> New style, select ‘Character style’ in the form ), see example in `tests/richtext.py`
|
||||
Instead of using ``RichText()``, one can use its shortcut : ``R()``
|
||||
|
||||
The ``RichText()`` or ``R()`` offers newline, new paragraph, and page break features : just use ``\n``, ``\a``, ``\t`` or ``\f`` in the
|
||||
text, they will be converted accordingly.
|
||||
|
||||
There is a specific case for font: if your font is not displayed correctly, it may be because it is defined
|
||||
only for a region. To know your region, it requires a little work by analyzing the document.xml inside the docx template (this is a zip file).
|
||||
To specify a region, you have to prefix your font name this that region and a column::
|
||||
|
||||
ch = RichText('测试TEST', font='eastAsia:微软雅黑')
|
||||
|
||||
**Important** : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
|
||||
*Important* : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
|
||||
you do not specify a style in ``RichText()``, the style will go back to a microsoft word default style.
|
||||
This will affect only character styles, not the paragraph styles (MSWord manages this 2 kind of styles).
|
||||
|
||||
**IMPORTANT** : Do not use 2 times ``{{r`` in the same run. Use RichText.add()
|
||||
method to concatenate several strings and styles at python side and only one
|
||||
``{{r`` at template side.
|
||||
|
||||
**Important** : ``RichText`` objects are rendered into xml *before* any filter is applied
|
||||
thus ``RichText`` are not compatible with Jinja2 filters. You cannot write in your template something like ``{{r <var>|lower }}``.
|
||||
Only solution is instead to do any filtering into your python code when creating the ``RichText`` object.
|
||||
|
||||
Hyperlink with RichText
|
||||
+++++++++++++++++++++++
|
||||
|
||||
You can add an hyperlink to a text by using a Richtext with this syntax::
|
||||
|
||||
tpl=DocxTemplate('your_template.docx')
|
||||
rt = RichText('You can add an hyperlink, here to ')
|
||||
rt.add('google',url_id=tpl.build_url_id('http://google.com'))
|
||||
|
||||
Put ``rt`` in your context, then use ``{{r rt}}`` in your template
|
||||
|
||||
RichTextParagraph
|
||||
-----------------
|
||||
|
||||
If you want to change paragraph properties, you can use ``RichTextParagraph()`` or ``RP()`` object.
|
||||
It must be added to the template by using ``{{p <var> }}``.
|
||||
Have a look to the example here ``tests/richtextparagraph.py``.
|
||||
|
||||
|
||||
Inline image
|
||||
------------
|
||||
|
||||
You can dynamically add one or many images into your document (tested with JPEG and PNG files).
|
||||
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage::
|
||||
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage ::
|
||||
|
||||
myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))
|
||||
myimage = InlineImage(tpl,'test_files/python_logo.png',width=Mm(20))
|
||||
|
||||
You just have to specify the template object, the image file path and optionally width and/or height.
|
||||
You just have to specify the template object, the image file path and optionnally width and/or height.
|
||||
For height and width you have to use millimeters (Mm), inches (Inches) or points(Pt) class.
|
||||
Please see tests/inline_image.py for an example.
|
||||
|
||||
Sub-documents
|
||||
-------------
|
||||
|
||||
> Need to install with the subdoc extra: `pip install "docxtpl[subdoc]"`
|
||||
A template variable can contain a complex and built from scratch with python-docx word document.
|
||||
To do so, get first a sub-document object from template object and use it as a python-docx document object, see example in `tests/subdoc.py`.
|
||||
|
||||
A template variable can contain a complex subdoc object and be built from scratch using python-docx document methods.
|
||||
To do so, first, get the sub-document object from your template object, then use it by treating it as a python-docx document object.
|
||||
See example in `tests/subdoc.py`.
|
||||
Escaping, newline, new paragraph, Listing
|
||||
-----------------------------------------
|
||||
|
||||
Since docxtpl V0.12.0, it is now possible to merge an existing .docx as a subdoc, just specify its path when
|
||||
calling method `new_subdoc()` ::
|
||||
|
||||
tpl = DocxTemplate('templates/merge_docx_master_tpl.docx')
|
||||
sd = tpl.new_subdoc('templates/merge_docx_subdoc.docx')
|
||||
|
||||
context = {
|
||||
'mysubdoc': sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('output/merge_docx.docx')
|
||||
|
||||
In the above example, the content of 'templates/merge_docx_subdoc.docx' will be inserted into the parent document in place of the declared
|
||||
variable `{{p mysubdoc }}`.
|
||||
|
||||
See `tests/merge_docx.py` for full code.
|
||||
|
||||
.. _Escaping:
|
||||
|
||||
Escaping
|
||||
--------
|
||||
|
||||
By default, no escaping is done : read carefully this chapter if you want to avoid crashes during docx generation.
|
||||
|
||||
When you use a ``{{ <var> }}``, under the hood, you are modifying an **XML** word document, this means you cannot use all chars,
|
||||
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 4 ways :
|
||||
When you use a ``{{ <var> }}``, you are modifying an **XML** word document, this means you cannot use all chars,
|
||||
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 3 ways :
|
||||
|
||||
* ``context = { 'var':R('my text') }`` and ``{{r <var> }}`` in the template (note the ``r``),
|
||||
* ``context = { 'var':'my text'}`` and ``{{ <var>|e }}`` in your word template
|
||||
* ``context = { 'var':escape('my text')}`` and ``{{ <var> }}`` in the template.
|
||||
* enable autoescaping when calling render method: ``tpl.render(context, autoescape=True)`` (default is autoescape=False)
|
||||
|
||||
The ``RichText()`` or ``R()`` offers newline and new paragraph feature : just use ``\n`` or ``\a`` in the
|
||||
text, they will be converted accordingly.
|
||||
|
||||
See tests/escape.py example for more informations.
|
||||
|
||||
Another solution, if you want to include a listing into your document, that is to escape the text and manage ``\n``, ``\a``, and ``\f``
|
||||
Another solution, if you want to include a listing into your document, that is to escape the text and manage \n and \a,
|
||||
you can use the ``Listing`` class :
|
||||
|
||||
in your python code::
|
||||
in your python code ::
|
||||
|
||||
context = { 'mylisting':Listing('the listing\nwith\nsome\nlines \a and some paragraph \a and special chars : <>&') }
|
||||
|
||||
in your docx template just use ``{{ mylisting }}``
|
||||
|
||||
With ``Listing()``, you will keep the current character styling (except after a ``\a`` as you start a new paragraph).
|
||||
|
||||
Replace docx pictures
|
||||
---------------------
|
||||
|
||||
It is not possible to dynamically add images in header/footer, but you can change them.
|
||||
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
|
||||
You can do that for all medias at the same time.
|
||||
Note: the aspect ratio will be the same as the replaced image
|
||||
Note2 : Specify the filename that has been used to insert the image in the docx template (only its basename, not the full path)
|
||||
|
||||
Syntax to replace dummy_header_pic.jpg::
|
||||
|
||||
tpl.replace_pic('dummy_header_pic.jpg','header_pic_i_want.jpg')
|
||||
|
||||
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
|
||||
Replace docx medias
|
||||
-------------------
|
||||
|
||||
It is not possible to dynamically add other medias than images in header/footer, but you can change them.
|
||||
The idea is to put a dummy media in your template, render the template as usual, then replace the dummy media with another one.
|
||||
It is not possible to dynamically add images in header/footer, but you can change them.
|
||||
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
|
||||
You can do that for all medias at the same time.
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx.
|
||||
@ -370,9 +180,8 @@ Syntax to replace dummy_header_pic.jpg::
|
||||
tpl.replace_media('dummy_header_pic.jpg','header_pic_i_want.jpg')
|
||||
|
||||
|
||||
WARNING : unlike replace_pic() method, dummy_header_pic.jpg MUST exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
dummy_header_pic.jpg must exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
file as the one inserted manually in the docx template.
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
Replace embedded objects
|
||||
------------------------
|
||||
@ -381,134 +190,32 @@ It works like medias replacement, except it is for embedded objects like embedde
|
||||
|
||||
Syntax to replace embedded_dummy.docx::
|
||||
|
||||
tpl.replace_embedded('embedded_dummy.docx','embedded_docx_i_want.docx')
|
||||
tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx')
|
||||
|
||||
|
||||
WARNING : unlike replace_pic() method, embedded_dummy.docx MUST exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
embdded_dummy.docx must exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
file as the one inserted manually in the docx template.
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
Note that `replace_embedded()` may not work on other documents than embedded docx.
|
||||
Instead, you should use zipname replacement::
|
||||
|
||||
tpl.replace_zipname(
|
||||
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
|
||||
'my_excel_file.xlsx')
|
||||
|
||||
The zipname is the one you can find when you open docx with WinZip, 7zip (Windows) or unzip -l (Linux).
|
||||
The zipname starts with "word/embeddings/". Note that the file to be replaced is renamed by MSWord, so you have to guess a little bit...
|
||||
|
||||
This works for embedded MSWord file like Excel or PowerPoint file, but won't work for others like PDF, Python or even Text files :
|
||||
For these ones, MSWord generate an oleObjectNNN.bin file which is no use to be replaced as it is encoded.
|
||||
|
||||
Get Defined Variables
|
||||
---------------------
|
||||
|
||||
In order to get the missing variables after rendering use ::
|
||||
|
||||
tpl=DocxTemplate('your_template.docx')
|
||||
tpl.render(context_dict)
|
||||
set_of_variables = tpl.get_undeclared_template_variables(context=context_dict)
|
||||
|
||||
**IMPORTANT** : If `context` is not passed, you will get a set with all keys you need, e.g. to be prompted to a user or written in a file for manual processing.
|
||||
|
||||
Multiple rendering
|
||||
------------------
|
||||
|
||||
Since v0.15.0, it is possible to create ``DocxTemplate`` object once and call
|
||||
``render(context)`` several times. Note that if you want to use replacement
|
||||
methods like ``replace_media()``, ``replace_embedded()`` and/or ``replace_zipname()``
|
||||
during multiple rendering, you will have to call ``reset_replacements()``
|
||||
at rendering loop start.
|
||||
|
||||
|
||||
|
||||
Microsoft Word 2016 special cases
|
||||
---------------------------------
|
||||
|
||||
MS Word 2016 will ignore ``\t`` tabulations. This is special to that version.
|
||||
Libreoffice or Wordpad do not have this problem. The same thing occurs for line
|
||||
beginning with a jinja2 tag providing spaces : They will be ignored.
|
||||
To solve these problem, the solution is to use Richtext::
|
||||
|
||||
tpl.render({
|
||||
'test_space_r' : RichText(' '),
|
||||
'test_tabs_r': RichText(5*'\t'),
|
||||
})
|
||||
|
||||
And in your template, use the {{r notation::
|
||||
|
||||
{{r test_space_r}} Spaces will be preserved
|
||||
{{r test_tabs_r}} Tabs will be displayed
|
||||
|
||||
|
||||
Jinja custom filters
|
||||
--------------------
|
||||
|
||||
``render()`` accepts ``jinja_env`` optional argument : you may pass a jinja environment object.
|
||||
``render()`` accepts ``jinja_env`` optionnal argument : you may pass a jinja environment object.
|
||||
By this way you will be able to add some custom jinja filters::
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import jinja2
|
||||
|
||||
def multiply_by(value, by):
|
||||
return value * by
|
||||
|
||||
doc = DocxTemplate("my_word_template.docx")
|
||||
context = { 'price_dollars' : 5.00 }
|
||||
context = { 'company_name' : "World company" }
|
||||
jinja_env = jinja2.Environment()
|
||||
jinja_env.filters['multiply_by'] = multiply_by
|
||||
jinja_env.filters['myfilter'] = myfilterfunc
|
||||
doc.render(context,jinja_env)
|
||||
doc.save("generated_doc.docx")
|
||||
|
||||
Then in your template, you will be able to use::
|
||||
|
||||
Euros price : {{ price_dollars|multiply_by(0.88) }}
|
||||
|
||||
|
||||
Command-line execution
|
||||
----------------------
|
||||
|
||||
One can use `docxtpl` module directly on command line to generate a docx from a template and a json file as a context::
|
||||
|
||||
usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename
|
||||
|
||||
Make docx file from existing template docx and json data.
|
||||
|
||||
positional arguments:
|
||||
template_path The path to the template docx file.
|
||||
json_path The path to the json file with the data.
|
||||
output_filename The filename to save the generated docx.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-o, --overwrite If output file already exists, overwrites without asking
|
||||
for confirmation
|
||||
-q, --quiet Do not display unnecessary messages
|
||||
|
||||
|
||||
See tests/module_execute.py for an example.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The best way to see how it works is to read examples, they are located in `tests/` directory.
|
||||
Docx test templates are in `tests/templates/`. To generate final docx files::
|
||||
|
||||
cd tests/
|
||||
python runtests.py
|
||||
|
||||
Generated files are located in `tests/output` directory.
|
||||
|
||||
If you are not sure about your python environment, python-docx-template provides Pipfiles
|
||||
for that::
|
||||
|
||||
pip install pipenv (if not already done)
|
||||
cd python-docx-template (where Pipfiles are)
|
||||
pipenv install --python 3.6 -d
|
||||
pipenv shell
|
||||
cd tests/
|
||||
python runtests.py
|
||||
The best way to see how it works is to read examples, they are located in `tests/` directory. Templates and generated .docx files are in `tests/test_files/`.
|
||||
|
||||
Share
|
||||
-----
|
||||
|
||||
@ -1,18 +1,377 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
__version__ = "0.20.1"
|
||||
__version__ = '0.4.1'
|
||||
|
||||
# flake8: noqa
|
||||
from .inline_image import InlineImage
|
||||
from .listing import Listing
|
||||
from .richtext import RichText, R, RichTextParagraph, RP
|
||||
from .template import DocxTemplate
|
||||
try:
|
||||
from .subdoc import Subdoc
|
||||
except ImportError:
|
||||
pass
|
||||
from lxml import etree
|
||||
from docx import Document
|
||||
from docx.opc.oxml import serialize_part_xml, parse_xml
|
||||
from jinja2 import Template
|
||||
from cgi import escape
|
||||
import re
|
||||
import six
|
||||
import binascii
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
NEWLINE = '</w:t><w:br/><w:t xml:space="preserve">'
|
||||
NEWPARAGRAPH = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">'
|
||||
|
||||
class DocxTemplate(object):
|
||||
""" Class for managing docx files as they were jinja2 templates """
|
||||
|
||||
HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
||||
FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
||||
|
||||
def __init__(self, docx):
|
||||
self.docx = Document(docx)
|
||||
self.crc_to_new_media = {}
|
||||
self.crc_to_new_embedded = {}
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.docx, name)
|
||||
|
||||
def xml_to_string(self, xml, encoding='unicode'):
|
||||
# Be careful : pretty_print MUST be set to False, otherwise patch_xml() won't work properly
|
||||
return etree.tostring(xml, encoding='unicode', pretty_print=False)
|
||||
|
||||
def get_docx(self):
|
||||
return self.docx
|
||||
|
||||
def get_xml(self):
|
||||
return self.xml_to_string(self.docx._element.body)
|
||||
|
||||
def write_xml(self,filename):
|
||||
with open(filename,'w') as fh:
|
||||
fh.write(self.get_xml())
|
||||
|
||||
def patch_xml(self,src_xml):
|
||||
# strip all xml tags inside {% %} and {{ }} that MS word can insert into xml source
|
||||
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})','',src_xml,flags=re.DOTALL)
|
||||
def striptags(m):
|
||||
return re.sub('</w:t>.*?(<w:t>|<w:t [^>]*>)','',m.group(0),flags=re.DOTALL)
|
||||
src_xml = re.sub(r'{%(?:(?!%}).)*|{{(?:(?!}}).)*',striptags,src_xml,flags=re.DOTALL)
|
||||
|
||||
# manage table cell colspan
|
||||
def colspan(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
|
||||
cell_xml = re.sub(r'<w:gridSpan[^/]*/>','', cell_xml, count=1)
|
||||
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2), cell_xml)
|
||||
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)',colspan,src_xml,flags=re.DOTALL)
|
||||
|
||||
# manage table cell background color
|
||||
def cellbg(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
|
||||
cell_xml = re.sub(r'<w:shd[^/]*/>','', cell_xml, count=1)
|
||||
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2), cell_xml)
|
||||
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)',cellbg,src_xml,flags=re.DOTALL)
|
||||
|
||||
# avoid {{r and {%r tags to strip MS xml tags too far
|
||||
src_xml = re.sub(r'({{r.*?}}|{%r.*?%})',r'</w:t></w:r><w:r><w:t>\1</w:t></w:r><w:r><w:t>',src_xml,flags=re.DOTALL)
|
||||
|
||||
for y in ['tr', 'tc', 'p', 'r']:
|
||||
# replace into xml code the row/paragraph/run containing {%y xxx %} or {{y xxx}} template tag
|
||||
# by {% xxx %} or {{ xx }} without any surronding <w:y> tags :
|
||||
# This is mandatory to have jinja2 generating correct xml code
|
||||
pat = r'<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>' % {'y':y}
|
||||
src_xml = re.sub(pat, r'\1 \2',src_xml,flags=re.DOTALL)
|
||||
|
||||
def clean_tags(m):
|
||||
return m.group(0).replace(r"‘","'").replace('<','<').replace('>','>')
|
||||
src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})',clean_tags,src_xml)
|
||||
|
||||
return src_xml
|
||||
|
||||
def render_xml(self,src_xml,context,jinja_env=None):
|
||||
if jinja_env:
|
||||
template = jinja_env.from_string(src_xml)
|
||||
else:
|
||||
template = Template(src_xml)
|
||||
dst_xml = template.render(context)
|
||||
dst_xml = dst_xml.replace('{_{','{{').replace('}_}','}}').replace('{_%','{%').replace('%_}','%}')
|
||||
return dst_xml
|
||||
|
||||
def build_xml(self,context,jinja_env=None):
|
||||
xml = self.get_xml()
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml(xml, context, jinja_env)
|
||||
return xml
|
||||
|
||||
def map_tree(self, tree):
|
||||
root = self.docx._element
|
||||
body = root.body
|
||||
root.replace(body, tree)
|
||||
|
||||
def get_headers_footers_xml(self, uri):
|
||||
for relKey, val in self.docx._part._rels.items():
|
||||
if val.reltype == uri:
|
||||
yield relKey, self.xml_to_string(parse_xml(val._target._blob))
|
||||
|
||||
def get_headers_footers_encoding(self,xml):
|
||||
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"',xml,re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return 'utf-8'
|
||||
|
||||
def build_headers_footers_xml(self,context, uri,jinja_env=None):
|
||||
for relKey, xml in self.get_headers_footers_xml(uri):
|
||||
encoding = self.get_headers_footers_encoding(xml)
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml(xml, context, jinja_env)
|
||||
yield relKey, xml.encode(encoding)
|
||||
|
||||
def map_headers_footers_xml(self, relKey, xml):
|
||||
self.docx._part._rels[relKey]._target._blob = xml
|
||||
|
||||
def render(self,context,jinja_env=None):
|
||||
# Body
|
||||
xml_src = self.build_xml(context,jinja_env)
|
||||
|
||||
# fix tables if needed
|
||||
tree = self.fix_tables(xml_src)
|
||||
|
||||
self.map_tree(tree)
|
||||
|
||||
# Headers
|
||||
for relKey, xml in self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env):
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# Footers
|
||||
for relKey, xml in self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env):
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# using of TC tag in for cycle can cause that count of columns does not correspond to
|
||||
# real count of columns in row. This function is able to fix it.
|
||||
def fix_tables(self, xml):
|
||||
tree = etree.fromstring(xml)
|
||||
# get namespace
|
||||
ns = '{' + tree.nsmap['w'] + '}'
|
||||
# walk trough xml and find table
|
||||
for t in tree.iter(ns+'tbl'):
|
||||
tblGrid = t.find(ns+'tblGrid')
|
||||
columns = tblGrid.findall(ns+'gridCol')
|
||||
to_add = 0
|
||||
# walk trough all rows and try to find if there is higher cell count
|
||||
for r in t.iter(ns+'tr'):
|
||||
cells = r.findall(ns+'tc')
|
||||
if (len(columns) + to_add) < len(cells):
|
||||
to_add = len(cells) - len(columns)
|
||||
# is neccessary to add columns?
|
||||
if to_add > 0:
|
||||
# at first, calculate width of table according to columns
|
||||
# (we want to preserve it)
|
||||
width = 0.0
|
||||
new_average = None
|
||||
for c in columns:
|
||||
if not c.get(ns+'w') == None:
|
||||
width += float(c.get(ns+'w'))
|
||||
# try to keep proportion of table
|
||||
if width > 0:
|
||||
old_average = width / len(columns)
|
||||
new_average = width / (len(columns) + to_add)
|
||||
# scale the old columns
|
||||
for c in columns:
|
||||
c.set(ns+'w', str(int(float(c.get(ns+'w')) * new_average/old_average)))
|
||||
# add new columns
|
||||
for i in range(to_add):
|
||||
etree.SubElement(tblGrid, ns+'gridCol', {ns+'w': str(int(new_average))})
|
||||
return tree
|
||||
|
||||
def new_subdoc(self):
|
||||
return Subdoc(self)
|
||||
|
||||
@staticmethod
|
||||
def get_file_crc(filename):
|
||||
with open(filename, 'rb') as fh:
|
||||
buf = fh.read()
|
||||
crc = (binascii.crc32(buf) & 0xFFFFFFFF)
|
||||
return crc
|
||||
|
||||
def replace_media(self,src_file,dst_file):
|
||||
"""Replace one media by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
|
||||
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
Note2 : it is important to have the source media file as it is required to calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, 'rb') as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_media[crc] = fh.read()
|
||||
|
||||
def replace_embedded(self,src_file,dst_file):
|
||||
"""Replace one embdded object by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
|
||||
|
||||
Note2 : it is important to have the source file as it is required to calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, 'rb') as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_embedded[crc] = fh.read()
|
||||
|
||||
def post_processing(self,docx_filename):
|
||||
if self.crc_to_new_media or self.crc_to_new_embedded:
|
||||
backup_filename = '%s_docxtpl_before_replace_medias' % docx_filename
|
||||
os.rename(docx_filename,backup_filename)
|
||||
|
||||
with zipfile.ZipFile(backup_filename) as zin:
|
||||
with zipfile.ZipFile(docx_filename, 'w') as zout:
|
||||
for item in zin.infolist():
|
||||
buf = zin.read(item.filename)
|
||||
if item.filename.startswith('word/media/') and item.CRC in self.crc_to_new_media:
|
||||
zout.writestr(item, self.crc_to_new_media[item.CRC])
|
||||
elif item.filename.startswith('word/embeddings/') and item.CRC in self.crc_to_new_embedded:
|
||||
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
|
||||
else:
|
||||
zout.writestr(item, buf)
|
||||
|
||||
os.remove(backup_filename)
|
||||
|
||||
def save(self,filename,*args,**kwargs):
|
||||
self.docx.save(filename,*args,**kwargs)
|
||||
self.post_processing(filename)
|
||||
|
||||
|
||||
class Subdoc(object):
|
||||
""" Class for subdocument to insert into master document """
|
||||
def __init__(self, tpl):
|
||||
self.tpl = tpl
|
||||
self.docx = tpl.get_docx()
|
||||
self.subdocx = Document()
|
||||
self.subdocx._part = self.docx._part
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.subdocx, name)
|
||||
|
||||
def _get_xml(self):
|
||||
if self.subdocx._element.body.sectPr is not None:
|
||||
self.subdocx._element.body.remove(self.subdocx._element.body.sectPr)
|
||||
xml = re.sub(r'</?w:body[^>]*>','',etree.tostring(self.subdocx._element.body, encoding='unicode', pretty_print=False))
|
||||
return xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __str__(self):
|
||||
return self._get_xml()
|
||||
|
||||
|
||||
class RichText(object):
|
||||
""" class to generate Rich Text when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class, but this only for texts INSIDE an existing paragraph.
|
||||
"""
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ''
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(self, text, style=None,
|
||||
color=None,
|
||||
highlight=None,
|
||||
size=None,
|
||||
bold=False,
|
||||
italic=False,
|
||||
underline=False,
|
||||
strike=False):
|
||||
|
||||
|
||||
if not isinstance(text, six.text_type):
|
||||
text = text.decode('utf-8',errors='ignore')
|
||||
text = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
|
||||
|
||||
prop = u''
|
||||
|
||||
if style:
|
||||
prop += u'<w:rStyle w:val="%s"/>' % style
|
||||
if color:
|
||||
if color[0] == '#':
|
||||
color = color[1:]
|
||||
prop += u'<w:color w:val="%s"/>' % color
|
||||
if highlight:
|
||||
if highlight[0] == '#':
|
||||
highlight = highlight[1:]
|
||||
prop += u'<w:highlight w:val="%s"/>' % highlight
|
||||
if size:
|
||||
prop += u'<w:sz w:val="%s"/>' % size
|
||||
prop += u'<w:szCs w:val="%s"/>' % size
|
||||
if bold:
|
||||
prop += u'<w:b/>'
|
||||
if italic:
|
||||
prop += u'<w:i/>'
|
||||
if underline:
|
||||
if underline not in ['single','double']:
|
||||
underline = 'single'
|
||||
prop += u'<w:u w:val="%s"/>' % underline
|
||||
if strike:
|
||||
prop += u'<w:strike/>'
|
||||
|
||||
self.xml += u'<w:r>'
|
||||
if prop:
|
||||
self.xml += u'<w:rPr>%s</w:rPr>' % prop
|
||||
self.xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
R = RichText
|
||||
|
||||
class Listing(object):
|
||||
r"""class to manage \n and \a without to use RichText, by this way you keep the current template styling
|
||||
|
||||
use {{ mylisting }} in your template and context={ mylisting:Listing(the_listing_with_newlines) }
|
||||
"""
|
||||
def __init__(self, text):
|
||||
self.xml = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
class InlineImage(object):
|
||||
"""Class to generate an inline image
|
||||
|
||||
This is much faster than using Subdoc class.
|
||||
"""
|
||||
tpl = None
|
||||
image_descriptor = None
|
||||
width = None
|
||||
height = None
|
||||
|
||||
def __init__(self, tpl, image_descriptor, width=None, height=None):
|
||||
self.tpl, self.image_descriptor = tpl, image_descriptor
|
||||
self.width, self.height = width, height
|
||||
|
||||
def _insert_image(self):
|
||||
pic = self.tpl.docx._part.new_pic_inline(
|
||||
self.image_descriptor,
|
||||
self.width,
|
||||
self.height
|
||||
).xml
|
||||
return '</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>' \
|
||||
'<w:t xml:space="preserve">' % pic
|
||||
|
||||
def __unicode__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __str__(self):
|
||||
return self._insert_image()
|
||||
@ -1,175 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
from .template import DocxTemplate, TemplateError
|
||||
|
||||
TEMPLATE_ARG = "template_path"
|
||||
JSON_ARG = "json_path"
|
||||
OUTPUT_ARG = "output_filename"
|
||||
OVERWRITE_ARG = "overwrite"
|
||||
QUIET_ARG = "quiet"
|
||||
|
||||
|
||||
def make_arg_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format(
|
||||
TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG
|
||||
),
|
||||
description="Make docx file from existing template docx and json data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
TEMPLATE_ARG, type=str, help="The path to the template docx file."
|
||||
)
|
||||
parser.add_argument(
|
||||
JSON_ARG, type=str, help="The path to the json file with the data."
|
||||
)
|
||||
parser.add_argument(
|
||||
OUTPUT_ARG, type=str, help="The filename to save the generated docx."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-" + OVERWRITE_ARG[0],
|
||||
"--" + OVERWRITE_ARG,
|
||||
action="store_true",
|
||||
help="If output file already exists, overwrites without asking for confirmation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-" + QUIET_ARG[0],
|
||||
"--" + QUIET_ARG,
|
||||
action="store_true",
|
||||
help="Do not display unnecessary messages",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def get_args(parser):
|
||||
try:
|
||||
parsed_args = vars(parser.parse_args())
|
||||
return parsed_args
|
||||
# Argument errors raise a SystemExit with code 2. Normal usage of the
|
||||
# --help or -h flag raises a SystemExit with code 0.
|
||||
except SystemExit as e:
|
||||
if e.code == 0:
|
||||
raise SystemExit
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Correct usage is:\n{parser.usage}".format(parser=parser)
|
||||
)
|
||||
|
||||
|
||||
def is_argument_valid(arg_name, arg_value, overwrite):
|
||||
# Basic checks for the arguments
|
||||
if arg_name == TEMPLATE_ARG:
|
||||
return os.path.isfile(arg_value) and arg_value.endswith(".docx")
|
||||
elif arg_name == JSON_ARG:
|
||||
return os.path.isfile(arg_value) and arg_value.endswith(".json")
|
||||
elif arg_name == OUTPUT_ARG:
|
||||
return arg_value.endswith(".docx") and check_exists_ask_overwrite(
|
||||
arg_value, overwrite
|
||||
)
|
||||
elif arg_name in [OVERWRITE_ARG, QUIET_ARG]:
|
||||
return arg_value in [True, False]
|
||||
|
||||
|
||||
def check_exists_ask_overwrite(arg_value, overwrite):
|
||||
# If output file does not exist or command was run with overwrite option,
|
||||
# returns True, else asks for overwrite confirmation. If overwrite is
|
||||
# confirmed returns True, else raises OSError.
|
||||
if os.path.exists(arg_value) and not overwrite:
|
||||
try:
|
||||
msg = (
|
||||
"File %s already exists, would you like to overwrite the existing file? "
|
||||
"(y/n)" % arg_value
|
||||
)
|
||||
if input(msg).lower() == "y":
|
||||
return True
|
||||
else:
|
||||
raise OSError
|
||||
except OSError:
|
||||
raise RuntimeError(
|
||||
"File %s already exists, please choose a different name." % arg_value
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def validate_all_args(parsed_args):
|
||||
overwrite = parsed_args[OVERWRITE_ARG]
|
||||
# Raises AssertionError if any of the arguments is not validated
|
||||
try:
|
||||
for arg_name, arg_value in parsed_args.items():
|
||||
if not is_argument_valid(arg_name, arg_value, overwrite):
|
||||
raise AssertionError
|
||||
except AssertionError:
|
||||
raise RuntimeError(
|
||||
'The specified {arg_name} "{arg_value}" is not valid.'.format(
|
||||
arg_name=arg_name, arg_value=arg_value
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_json_data(json_path):
|
||||
with open(json_path) as file:
|
||||
try:
|
||||
json_data = json.load(file)
|
||||
return json_data
|
||||
except json.JSONDecodeError as e:
|
||||
print(
|
||||
"There was an error on line {e.lineno}, column {e.colno} while trying "
|
||||
"to parse file {json_path}".format(e=e, json_path=json_path)
|
||||
)
|
||||
raise RuntimeError("Failed to get json data.")
|
||||
|
||||
|
||||
def make_docxtemplate(template_path):
|
||||
try:
|
||||
return DocxTemplate(template_path)
|
||||
except TemplateError:
|
||||
raise RuntimeError("Could not create docx template.")
|
||||
|
||||
|
||||
def render_docx(doc, json_data):
|
||||
try:
|
||||
doc.render(json_data)
|
||||
return doc
|
||||
except TemplateError:
|
||||
raise RuntimeError("An error ocurred while trying to render the docx")
|
||||
|
||||
|
||||
def save_file(doc, parsed_args):
|
||||
try:
|
||||
output_path = parsed_args[OUTPUT_ARG]
|
||||
doc.save(output_path)
|
||||
if not parsed_args[QUIET_ARG]:
|
||||
print(
|
||||
"Document successfully generated and saved at {output_path}".format(
|
||||
output_path=output_path
|
||||
)
|
||||
)
|
||||
except OSError as e:
|
||||
print("{e.strerror}. Could not save file {e.filename}.".format(e=e))
|
||||
raise RuntimeError("Failed to save file.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = make_arg_parser()
|
||||
# Everything is in a try-except block that catches a RuntimeError that is
|
||||
# raised if any of the individual functions called cause an error
|
||||
# themselves, terminating the main function.
|
||||
parsed_args = get_args(parser)
|
||||
try:
|
||||
validate_all_args(parsed_args)
|
||||
json_data = get_json_data(os.path.abspath(parsed_args[JSON_ARG]))
|
||||
doc = make_docxtemplate(os.path.abspath(parsed_args[TEMPLATE_ARG]))
|
||||
doc = render_docx(doc, json_data)
|
||||
save_file(doc, parsed_args)
|
||||
except RuntimeError as e:
|
||||
print("Error: " + e.__str__())
|
||||
return
|
||||
finally:
|
||||
if not parsed_args[QUIET_ARG]:
|
||||
print("Exiting program!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
from docx.oxml import OxmlElement, parse_xml
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
|
||||
class InlineImage(object):
|
||||
"""Class to generate an inline image
|
||||
|
||||
This is much faster than using Subdoc class.
|
||||
"""
|
||||
|
||||
tpl = None
|
||||
image_descriptor = None
|
||||
width = None
|
||||
height = None
|
||||
anchor = None
|
||||
|
||||
def __init__(self, tpl, image_descriptor, width=None, height=None, anchor=None):
|
||||
self.tpl, self.image_descriptor = tpl, image_descriptor
|
||||
self.width, self.height = width, height
|
||||
self.anchor = anchor
|
||||
|
||||
def _add_hyperlink(self, run, url, part):
|
||||
# Create a relationship for the hyperlink
|
||||
r_id = part.relate_to(
|
||||
url,
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
||||
is_external=True,
|
||||
)
|
||||
|
||||
# Find the <wp:docPr> and <pic:cNvPr> element
|
||||
docPr = run.xpath(".//wp:docPr")[0]
|
||||
cNvPr = run.xpath(".//pic:cNvPr")[0]
|
||||
|
||||
# Create the <a:hlinkClick> element
|
||||
hlinkClick1 = OxmlElement("a:hlinkClick")
|
||||
hlinkClick1.set(qn("r:id"), r_id)
|
||||
hlinkClick2 = OxmlElement("a:hlinkClick")
|
||||
hlinkClick2.set(qn("r:id"), r_id)
|
||||
|
||||
# Insert the <a:hlinkClick> element right after the <wp:docPr> element
|
||||
docPr.append(hlinkClick1)
|
||||
cNvPr.append(hlinkClick2)
|
||||
|
||||
return run
|
||||
|
||||
def _insert_image(self):
|
||||
pic = self.tpl.current_rendering_part.new_pic_inline(
|
||||
self.image_descriptor,
|
||||
self.width,
|
||||
self.height,
|
||||
).xml
|
||||
if self.anchor:
|
||||
run = parse_xml(pic)
|
||||
if run.xpath(".//a:blip"):
|
||||
hyperlink = self._add_hyperlink(
|
||||
run, self.anchor, self.tpl.current_rendering_part
|
||||
)
|
||||
pic = hyperlink.xml
|
||||
|
||||
return (
|
||||
"</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>"
|
||||
'<w:t xml:space="preserve">' % pic
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __str__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __html__(self):
|
||||
return self._insert_image()
|
||||
@ -1,35 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
try:
|
||||
from html import escape
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape
|
||||
|
||||
|
||||
class Listing(object):
|
||||
r"""class to manage \n and \a without to use RichText,
|
||||
by this way you keep the current template styling
|
||||
|
||||
use {{ mylisting }} in your template and
|
||||
context={ mylisting:Listing(the_listing_with_newlines) }
|
||||
"""
|
||||
|
||||
def __init__(self, text):
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (str, bytes)):
|
||||
text = str(text)
|
||||
self.xml = escape(text)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
@ -1,180 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
try:
|
||||
from html import escape
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape
|
||||
|
||||
|
||||
class RichText(object):
|
||||
"""class to generate Rich Text when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class,
|
||||
but this only for texts INSIDE an existing paragraph.
|
||||
"""
|
||||
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ""
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(
|
||||
self,
|
||||
text,
|
||||
style=None,
|
||||
color=None,
|
||||
highlight=None,
|
||||
size=None,
|
||||
subscript=None,
|
||||
superscript=None,
|
||||
bold=False,
|
||||
italic=False,
|
||||
underline=False,
|
||||
strike=False,
|
||||
font=None,
|
||||
url_id=None,
|
||||
rtl=False,
|
||||
lang=None,
|
||||
):
|
||||
|
||||
# If a RichText is added
|
||||
if isinstance(text, RichText):
|
||||
self.xml += text.xml
|
||||
return
|
||||
|
||||
# # If nothing to add : just return
|
||||
# if text is None or text == "":
|
||||
# return
|
||||
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (str, bytes)):
|
||||
text = str(text)
|
||||
if not isinstance(text, str):
|
||||
text = text.decode("utf-8", errors="ignore")
|
||||
text = escape(text)
|
||||
|
||||
prop = ""
|
||||
|
||||
if style:
|
||||
prop += '<w:rStyle w:val="%s"/>' % style
|
||||
if color:
|
||||
if color[0] == "#":
|
||||
color = color[1:]
|
||||
prop += '<w:color w:val="%s"/>' % color
|
||||
if highlight:
|
||||
if highlight[0] == "#":
|
||||
highlight = highlight[1:]
|
||||
prop += '<w:shd w:fill="%s"/>' % highlight
|
||||
if size:
|
||||
prop += '<w:sz w:val="%s"/>' % size
|
||||
prop += '<w:szCs w:val="%s"/>' % size
|
||||
if subscript:
|
||||
prop += '<w:vertAlign w:val="subscript"/>'
|
||||
if superscript:
|
||||
prop += '<w:vertAlign w:val="superscript"/>'
|
||||
if bold:
|
||||
prop += "<w:b/>"
|
||||
if rtl:
|
||||
prop += "<w:bCs/>"
|
||||
if italic:
|
||||
prop += "<w:i/>"
|
||||
if rtl:
|
||||
prop += "<w:iCs/>"
|
||||
if underline:
|
||||
if underline not in [
|
||||
"single",
|
||||
"double",
|
||||
"thick",
|
||||
"dotted",
|
||||
"dash",
|
||||
"dotDash",
|
||||
"dotDotDash",
|
||||
"wave",
|
||||
]:
|
||||
underline = "single"
|
||||
prop += '<w:u w:val="%s"/>' % underline
|
||||
if strike:
|
||||
prop += "<w:strike/>"
|
||||
if font:
|
||||
regional_font = ""
|
||||
if ":" in font:
|
||||
region, font = font.split(":", 1)
|
||||
regional_font = ' w:{region}="{font}"'.format(font=font, region=region)
|
||||
prop += '<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"{regional_font}/>'.format(
|
||||
font=font, regional_font=regional_font
|
||||
)
|
||||
if rtl:
|
||||
prop += '<w:rtl w:val="true"/>'
|
||||
if lang:
|
||||
prop += '<w:lang w:val="%s"/>' % lang
|
||||
xml = "<w:r>"
|
||||
if prop:
|
||||
xml += "<w:rPr>%s</w:rPr>" % prop
|
||||
xml += '<w:t xml:space="preserve">%s</w:t></w:r>' % text
|
||||
if url_id:
|
||||
xml = '<w:hyperlink r:id="%s" w:tgtFrame="_blank">%s</w:hyperlink>' % (
|
||||
url_id,
|
||||
xml,
|
||||
)
|
||||
self.xml += xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
class RichTextParagraph(object):
|
||||
"""class to generate Rich Text Paragraphs when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class,
|
||||
but this only for texts OUTSIDE an existing paragraph.
|
||||
"""
|
||||
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ""
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(
|
||||
self,
|
||||
text,
|
||||
parastyle=None,
|
||||
):
|
||||
|
||||
# If a RichText is added
|
||||
if not isinstance(text, RichText):
|
||||
text = RichText(text)
|
||||
|
||||
prop = ""
|
||||
if parastyle:
|
||||
prop += '<w:pStyle w:val="%s"/>' % parastyle
|
||||
|
||||
xml = "<w:p>"
|
||||
if prop:
|
||||
xml += "<w:pPr>%s</w:pPr>" % prop
|
||||
xml += text.xml
|
||||
xml += "</w:p>"
|
||||
self.xml += xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
R = RichText
|
||||
RP = RichTextParagraph
|
||||
@ -1,103 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docx import Document
|
||||
from docx.oxml import CT_SectPr
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as RT
|
||||
from docxcompose.properties import CustomProperties
|
||||
from docxcompose.utils import xpath
|
||||
from docxcompose.composer import Composer
|
||||
from docxcompose.utils import NS
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
|
||||
class SubdocComposer(Composer):
|
||||
def attach_parts(self, doc, remove_property_fields=True):
|
||||
"""Attach docx parts instead of appending the whole document
|
||||
thus subdoc insertion can be delegated to jinja2"""
|
||||
self.reset_reference_mapping()
|
||||
|
||||
# Remove custom property fields but keep the values
|
||||
if remove_property_fields:
|
||||
cprops = CustomProperties(doc)
|
||||
for name in cprops.keys():
|
||||
cprops.dissolve_fields(name)
|
||||
|
||||
self._create_style_id_mapping(doc)
|
||||
|
||||
for element in doc.element.body:
|
||||
if isinstance(element, CT_SectPr):
|
||||
continue
|
||||
self.add_referenced_parts(doc.part, self.doc.part, element)
|
||||
self.add_styles(doc, element)
|
||||
self.add_numberings(doc, element)
|
||||
self.restart_first_numbering(doc, element)
|
||||
self.add_images(doc, element)
|
||||
self.add_diagrams(doc, element)
|
||||
self.add_shapes(doc, element)
|
||||
self.add_footnotes(doc, element)
|
||||
self.remove_header_and_footer_references(doc, element)
|
||||
|
||||
self.add_styles_from_other_parts(doc)
|
||||
self.renumber_bookmarks()
|
||||
self.renumber_docpr_ids()
|
||||
self.renumber_nvpicpr_ids()
|
||||
self.fix_section_types(doc)
|
||||
|
||||
def add_diagrams(self, doc, element):
|
||||
# While waiting docxcompose 1.3.3
|
||||
dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]")
|
||||
for dgm_rel in dgm_rels:
|
||||
for item, rt_type in (
|
||||
("dm", RT.DIAGRAM_DATA),
|
||||
("lo", RT.DIAGRAM_LAYOUT),
|
||||
("qs", RT.DIAGRAM_QUICK_STYLE),
|
||||
("cs", RT.DIAGRAM_COLORS),
|
||||
):
|
||||
dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item))
|
||||
dm_part = doc.part.rels[dm_rid].target_part
|
||||
new_rid = self.doc.part.relate_to(dm_part, rt_type)
|
||||
dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid)
|
||||
|
||||
|
||||
class Subdoc(object):
|
||||
"""Class for subdocument to insert into master document"""
|
||||
|
||||
def __init__(self, tpl, docpath=None):
|
||||
self.tpl = tpl
|
||||
self.docx = tpl.get_docx()
|
||||
self.subdocx = Document(docpath)
|
||||
if docpath:
|
||||
compose = SubdocComposer(self.docx)
|
||||
compose.attach_parts(self.subdocx)
|
||||
else:
|
||||
self.subdocx._part = self.docx._part
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.subdocx, name)
|
||||
|
||||
def _get_xml(self):
|
||||
if self.subdocx.element.body.sectPr is not None:
|
||||
self.subdocx.element.body.remove(self.subdocx.element.body.sectPr)
|
||||
xml = re.sub(
|
||||
r"</?w:body[^>]*>",
|
||||
"",
|
||||
etree.tostring(
|
||||
self.subdocx.element.body, encoding="unicode", pretty_print=False
|
||||
),
|
||||
)
|
||||
return xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __str__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __html__(self):
|
||||
return self._get_xml()
|
||||
@ -1,926 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from os import PathLike
|
||||
from typing import TYPE_CHECKING, Any, Optional, IO, Union, Dict, Set
|
||||
import functools
|
||||
import io
|
||||
from lxml import etree
|
||||
from docx import Document
|
||||
from docx.opc.oxml import parse_xml
|
||||
from docx.opc.part import XmlPart
|
||||
import docx.oxml.ns
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
|
||||
from jinja2 import Environment, Template, meta
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
try:
|
||||
from html import escape # noqa: F401
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape # noqa: F401
|
||||
import re
|
||||
import binascii
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .subdoc import Subdoc
|
||||
|
||||
|
||||
class DocxTemplate(object):
|
||||
"""Class for managing docx files as they were jinja2 templates"""
|
||||
|
||||
HEADER_URI = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
||||
)
|
||||
FOOTER_URI = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
||||
)
|
||||
|
||||
def __init__(self, template_file: Union[IO[bytes], str, PathLike]) -> None:
|
||||
self.template_file = template_file
|
||||
self.reset_replacements()
|
||||
self.docx = None
|
||||
self.is_rendered = False
|
||||
self.is_saved = False
|
||||
self.allow_missing_pics = False
|
||||
|
||||
def init_docx(self, reload: bool = True):
|
||||
if not self.docx or (self.is_rendered and reload):
|
||||
self.docx = Document(self.template_file)
|
||||
self.is_rendered = False
|
||||
|
||||
def render_init(self):
|
||||
self.init_docx()
|
||||
self.pic_map = {}
|
||||
self.current_rendering_part = None
|
||||
self.docx_ids_index = 1000
|
||||
self.is_saved = False
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.docx, name)
|
||||
|
||||
def xml_to_string(self, xml, encoding="unicode"):
|
||||
# Be careful : pretty_print MUST be set to False, otherwise patch_xml()
|
||||
# won't work properly
|
||||
return etree.tostring(xml, encoding="unicode", pretty_print=False)
|
||||
|
||||
def get_docx(self):
|
||||
self.init_docx()
|
||||
return self.docx
|
||||
|
||||
def get_xml(self):
|
||||
return self.xml_to_string(self.docx._element.body)
|
||||
|
||||
def write_xml(self, filename):
|
||||
with open(filename, "w") as fh:
|
||||
fh.write(self.get_xml())
|
||||
|
||||
def patch_xml(self, src_xml):
|
||||
"""Make a lots of cleaning to have a raw xml understandable by jinja2 :
|
||||
strip all unnecessary xml tags, manage table cell background color and colspan,
|
||||
unescape html entities, etc..."""
|
||||
|
||||
# replace {<something>{ by {{ ( works with {{ }} {% and %} {# and #})
|
||||
src_xml = re.sub(
|
||||
r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})",
|
||||
"",
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
|
||||
# same thing with {% ... %} and {# #}
|
||||
# "jinja2 stuff" could a variable, a 'if' etc... anything jinja2 will understand
|
||||
def striptags(m):
|
||||
return re.sub(
|
||||
"</w:t>.*?(<w:t>|<w:t [^>]*>)", "", m.group(0), flags=re.DOTALL
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*",
|
||||
striptags,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# manage table cell colspan
|
||||
def colspan(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(
|
||||
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
|
||||
"",
|
||||
cell_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
cell_xml = re.sub(r"<w:gridSpan[^/]*/>", "", cell_xml, count=1)
|
||||
return re.sub(
|
||||
r"(<w:tcPr[^>]*>)",
|
||||
r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2),
|
||||
cell_xml,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)",
|
||||
colspan,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# manage table cell background color
|
||||
def cellbg(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(
|
||||
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
|
||||
"",
|
||||
cell_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
cell_xml = re.sub(r"<w:shd[^/]*/>", "", cell_xml, count=1)
|
||||
return re.sub(
|
||||
r"(<w:tcPr[^>]*>)",
|
||||
r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2),
|
||||
cell_xml,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)",
|
||||
cellbg,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# ensure space preservation
|
||||
src_xml = re.sub(
|
||||
r"<w:t>((?:(?!<w:t>).)*)({{.*?}}|{%.*?%})",
|
||||
r'<w:t xml:space="preserve">\1\2',
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
src_xml = re.sub(
|
||||
r"({{r\s.*?}}|{%r\s.*?%})",
|
||||
r'</w:t></w:r><w:r><w:t xml:space="preserve">\1</w:t></w:r><w:r><w:t xml:space="preserve">',
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# {%- will merge with previous paragraph text
|
||||
src_xml = re.sub(r"</w:t>(?:(?!</w:t>).)*?{%-", "{%", src_xml, flags=re.DOTALL)
|
||||
# -%} will merge with next paragraph text
|
||||
src_xml = re.sub(
|
||||
r"-%}(?:(?!<w:t[ >]|{%|{{).)*?<w:t[^>]*?>", "%}", src_xml, flags=re.DOTALL
|
||||
)
|
||||
|
||||
for y in ["tr", "tc", "p", "r"]:
|
||||
# replace into xml code the row/paragraph/run containing
|
||||
# {%y xxx %} or {{y xxx}} template tag
|
||||
# by {% xxx %} or {{ xx }} without any surrounding <w:y> tags :
|
||||
# This is mandatory to have jinja2 generating correct xml code
|
||||
pat = (
|
||||
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>"
|
||||
% {"y": y}
|
||||
)
|
||||
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
|
||||
|
||||
for y in ["tr", "tc", "p"]:
|
||||
# same thing, but for {#y xxx #} (but not where y == 'r', since that
|
||||
# makes less sense to use comments in that context
|
||||
pat = (
|
||||
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({#)%(y)s ([^}#]*(?:#})).*?</w:%(y)s>"
|
||||
% {"y": y}
|
||||
)
|
||||
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
|
||||
|
||||
# add vMerge
|
||||
# use {% vm %} to make this table cell and its copies
|
||||
# be vertically merged within a {% for %}
|
||||
def v_merge_tc(m):
|
||||
def v_merge(m1):
|
||||
return (
|
||||
'<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>'
|
||||
+ m1.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
|
||||
+ "{% if loop.first %}"
|
||||
+ m1.group(2) # Everything before ``{% vm %}``.
|
||||
+ m1.group(3) # Everything after ``{% vm %}``.
|
||||
+ "{% endif %}"
|
||||
+ m1.group(4) # ``</w:t>``.
|
||||
)
|
||||
|
||||
return re.sub(
|
||||
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(</w:t>)",
|
||||
v_merge,
|
||||
m.group(),
|
||||
# Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% vm %}`` inside.
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?</w:tc[ >]",
|
||||
v_merge_tc,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Use ``{% hm %}`` to make table cell become horizontally merged within
|
||||
# a ``{% for %}``.
|
||||
def h_merge_tc(m):
|
||||
xml_to_patch = (
|
||||
m.group()
|
||||
) # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% hm %}`` inside.
|
||||
|
||||
def with_gridspan(m1):
|
||||
return (
|
||||
m1.group(1) # ``w:gridSpan w:val="``.
|
||||
+ "{{ "
|
||||
+ m1.group(2)
|
||||
+ " * loop.length }}" # Content of ``w:val``, multiplied by loop length.
|
||||
+ m1.group(3) # Closing quotation mark.
|
||||
)
|
||||
|
||||
def without_gridspan(m2):
|
||||
return (
|
||||
'<w:gridSpan w:val="{{ loop.length }}"/>'
|
||||
+ m2.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
|
||||
+ m2.group(2) # Everything before ``{% hm %}``.
|
||||
+ m2.group(3) # Everything after ``{% hm %}``.
|
||||
+ m2.group(4) # ``</w:t>``.
|
||||
)
|
||||
|
||||
if re.search(r"w:gridSpan", xml_to_patch):
|
||||
# Simple case, there's already ``gridSpan``, multiply its value.
|
||||
|
||||
xml = re.sub(
|
||||
r'(w:gridSpan w:val=")(\d+)(")',
|
||||
with_gridspan,
|
||||
xml_to_patch,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
xml = re.sub(
|
||||
r"{%\s*hm\s*%}",
|
||||
"",
|
||||
xml, # Patched xml.
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
else:
|
||||
# There're no ``gridSpan``, add one.
|
||||
xml = re.sub(
|
||||
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(</w:t>)",
|
||||
without_gridspan,
|
||||
xml_to_patch,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Discard every other cell generated in loop.
|
||||
return "{% if loop.first %}" + xml + "{% endif %}"
|
||||
|
||||
src_xml = re.sub(
|
||||
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?</w:tc[ >]",
|
||||
h_merge_tc,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
def clean_tags(m):
|
||||
return (
|
||||
m.group(0)
|
||||
.replace(r"‘", "'")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("“", '"')
|
||||
.replace("”", '"')
|
||||
.replace("‘", "'")
|
||||
.replace("’", "'")
|
||||
)
|
||||
|
||||
src_xml = re.sub(r"(?<=\{[\{%])(.*?)(?=[\}%]})", clean_tags, src_xml)
|
||||
|
||||
return src_xml
|
||||
|
||||
def render_xml_part(self, src_xml, part, context, jinja_env=None):
|
||||
src_xml = re.sub(r"<w:p([ >])", r"\n<w:p\1", src_xml)
|
||||
try:
|
||||
self.current_rendering_part = part
|
||||
if jinja_env:
|
||||
template = jinja_env.from_string(src_xml)
|
||||
else:
|
||||
template = Template(src_xml)
|
||||
dst_xml = template.render(context)
|
||||
except TemplateError as exc:
|
||||
if hasattr(exc, "lineno") and exc.lineno is not None:
|
||||
line_number = max(exc.lineno - 4, 0)
|
||||
exc.docx_context = map(
|
||||
lambda x: re.sub(r"<[^>]+>", "", x),
|
||||
src_xml.splitlines()[line_number: (line_number + 7)], # fmt: skip
|
||||
)
|
||||
|
||||
raise exc
|
||||
dst_xml = re.sub(r"\n<w:p([ >])", r"<w:p\1", dst_xml)
|
||||
dst_xml = (
|
||||
dst_xml.replace("{_{", "{{")
|
||||
.replace("}_}", "}}")
|
||||
.replace("{_%", "{%")
|
||||
.replace("%_}", "%}")
|
||||
)
|
||||
dst_xml = self.resolve_listing(dst_xml)
|
||||
return dst_xml
|
||||
|
||||
def render_properties(
|
||||
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
|
||||
) -> None:
|
||||
# List of string attributes of docx.opc.coreprops.CoreProperties which are strings.
|
||||
# It seems that some attributes cannot be written as strings. Those are commented out.
|
||||
properties = [
|
||||
"author",
|
||||
# 'category',
|
||||
"comments",
|
||||
# 'content_status',
|
||||
"identifier",
|
||||
# 'keywords',
|
||||
"language",
|
||||
# 'last_modified_by',
|
||||
"subject",
|
||||
"title",
|
||||
# 'version',
|
||||
]
|
||||
if jinja_env is None:
|
||||
jinja_env = Environment()
|
||||
|
||||
for prop in properties:
|
||||
initial = getattr(self.docx.core_properties, prop)
|
||||
template = jinja_env.from_string(initial)
|
||||
rendered = template.render(context)
|
||||
setattr(self.docx.core_properties, prop, rendered)
|
||||
|
||||
def render_footnotes(
|
||||
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
|
||||
) -> None:
|
||||
if jinja_env is None:
|
||||
jinja_env = Environment()
|
||||
|
||||
for section in self.docx.sections:
|
||||
for part in section.part.package.parts:
|
||||
if part.content_type == (
|
||||
"application/vnd.openxmlformats-officedocument"
|
||||
".wordprocessingml.footnotes+xml"
|
||||
):
|
||||
xml = self.patch_xml(
|
||||
part.blob.decode("utf-8")
|
||||
if isinstance(part.blob, bytes)
|
||||
else part.blob
|
||||
)
|
||||
xml = self.render_xml_part(xml, part, context, jinja_env)
|
||||
part._blob = xml.encode("utf-8")
|
||||
|
||||
def resolve_listing(self, xml):
|
||||
|
||||
def resolve_text(run_properties, paragraph_properties, m):
|
||||
xml = m.group(0).replace(
|
||||
"\t",
|
||||
"</w:t></w:r>"
|
||||
"<w:r>%s<w:tab/></w:r>"
|
||||
'<w:r>%s<w:t xml:space="preserve">' % (run_properties, run_properties),
|
||||
)
|
||||
xml = xml.replace(
|
||||
"\a",
|
||||
"</w:t></w:r></w:p>"
|
||||
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
|
||||
% (paragraph_properties, run_properties),
|
||||
)
|
||||
xml = xml.replace("\n", '</w:t><w:br/><w:t xml:space="preserve">')
|
||||
xml = xml.replace(
|
||||
"\f",
|
||||
"</w:t></w:r></w:p>"
|
||||
'<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
|
||||
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
|
||||
% (paragraph_properties, run_properties),
|
||||
)
|
||||
return xml
|
||||
|
||||
def resolve_run(paragraph_properties, m):
|
||||
run_properties = re.search(r"<w:rPr>.*?</w:rPr>", m.group(0))
|
||||
run_properties = run_properties.group(0) if run_properties else ""
|
||||
return re.sub(
|
||||
r"<w:t(?: [^>]*)?>.*?</w:t>",
|
||||
lambda x: resolve_text(run_properties, paragraph_properties, x),
|
||||
m.group(0),
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
def resolve_paragraph(m):
|
||||
paragraph_properties = re.search(r"<w:pPr>.*?</w:pPr>", m.group(0))
|
||||
paragraph_properties = (
|
||||
paragraph_properties.group(0) if paragraph_properties else ""
|
||||
)
|
||||
return re.sub(
|
||||
r"<w:r(?: [^>]*)?>.*?</w:r>",
|
||||
lambda x: resolve_run(paragraph_properties, x),
|
||||
m.group(0),
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
xml = re.sub(
|
||||
r"<w:p(?: [^>]*)?>.*?</w:p>", resolve_paragraph, xml, flags=re.DOTALL
|
||||
)
|
||||
|
||||
return xml
|
||||
|
||||
def build_xml(self, context, jinja_env=None):
|
||||
xml = self.get_xml()
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml_part(xml, self.docx._part, context, jinja_env)
|
||||
return xml
|
||||
|
||||
def map_tree(self, tree):
|
||||
root = self.docx._element
|
||||
body = root.body
|
||||
root.replace(body, tree)
|
||||
|
||||
def get_headers_footers(self, uri):
|
||||
for relKey, val in self.docx._part.rels.items():
|
||||
if (val.reltype == uri) and (val.target_part.blob):
|
||||
yield relKey, val.target_part
|
||||
|
||||
def get_part_xml(self, part):
|
||||
return self.xml_to_string(parse_xml(part.blob))
|
||||
|
||||
def get_headers_footers_encoding(self, xml):
|
||||
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"', xml, re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return "utf-8"
|
||||
|
||||
def build_headers_footers_xml(self, context, uri, jinja_env=None):
|
||||
for relKey, part in self.get_headers_footers(uri):
|
||||
xml = self.get_part_xml(part)
|
||||
encoding = self.get_headers_footers_encoding(xml)
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml_part(xml, part, context, jinja_env)
|
||||
yield relKey, xml.encode(encoding)
|
||||
|
||||
def map_headers_footers_xml(self, relKey, xml):
|
||||
part = self.docx._part.rels[relKey].target_part
|
||||
new_part = XmlPart.load(part.partname, part.content_type, xml, part.package)
|
||||
for rId, rel in part.rels.items():
|
||||
new_part.load_rel(rel.reltype, rel._target, rel.rId, rel.is_external)
|
||||
self.docx._part.rels[relKey]._target = new_part
|
||||
|
||||
def render(
|
||||
self,
|
||||
context: Dict[str, Any],
|
||||
jinja_env: Optional[Environment] = None,
|
||||
autoescape: bool = False,
|
||||
) -> None:
|
||||
# init template working attributes
|
||||
self.render_init()
|
||||
|
||||
if autoescape:
|
||||
if not jinja_env:
|
||||
jinja_env = Environment(autoescape=autoescape)
|
||||
else:
|
||||
jinja_env.autoescape = autoescape
|
||||
|
||||
# Body
|
||||
xml_src = self.build_xml(context, jinja_env)
|
||||
|
||||
# fix tables if needed
|
||||
tree = self.fix_tables(xml_src)
|
||||
|
||||
# fix docPr ID's
|
||||
self.fix_docpr_ids(tree)
|
||||
|
||||
# Replace body xml tree
|
||||
self.map_tree(tree)
|
||||
|
||||
# Headers
|
||||
headers = self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env)
|
||||
for relKey, xml in headers:
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# Footers
|
||||
footers = self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env)
|
||||
for relKey, xml in footers:
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
self.render_properties(context, jinja_env)
|
||||
|
||||
self.render_footnotes(context, jinja_env)
|
||||
|
||||
# set rendered flag
|
||||
self.is_rendered = True
|
||||
|
||||
# using of TC tag in for cycle can cause that count of columns does not
|
||||
# correspond to real count of columns in row. This function is able to fix it.
|
||||
def fix_tables(self, xml):
|
||||
parser = etree.XMLParser(recover=True)
|
||||
tree = etree.fromstring(xml, parser=parser)
|
||||
# get namespace
|
||||
ns = "{" + tree.nsmap["w"] + "}"
|
||||
# walk trough xml and find table
|
||||
for t in tree.iter(ns + "tbl"):
|
||||
tblGrid = t.find(ns + "tblGrid")
|
||||
columns = tblGrid.findall(ns + "gridCol")
|
||||
to_add = 0
|
||||
# walk trough all rows and try to find if there is higher cell count
|
||||
for r in t.iter(ns + "tr"):
|
||||
cells = r.findall(ns + "tc")
|
||||
if (len(columns) + to_add) < len(cells):
|
||||
to_add = len(cells) - len(columns)
|
||||
# is necessary to add columns?
|
||||
if to_add > 0:
|
||||
# at first, calculate width of table according to columns
|
||||
# (we want to preserve it)
|
||||
width = 0.0
|
||||
new_average = None
|
||||
for c in columns:
|
||||
if not c.get(ns + "w") is None:
|
||||
width += float(c.get(ns + "w"))
|
||||
# try to keep proportion of table
|
||||
if width > 0:
|
||||
old_average = width / len(columns)
|
||||
new_average = width / (len(columns) + to_add)
|
||||
# scale the old columns
|
||||
for c in columns:
|
||||
c.set(
|
||||
ns + "w",
|
||||
str(
|
||||
int(float(c.get(ns + "w")) * new_average / old_average)
|
||||
),
|
||||
)
|
||||
# add new columns
|
||||
for i in range(to_add):
|
||||
etree.SubElement(
|
||||
tblGrid, ns + "gridCol", {ns + "w": str(int(new_average))}
|
||||
)
|
||||
|
||||
# Refetch columns after columns addition.
|
||||
columns = tblGrid.findall(ns + "gridCol")
|
||||
columns_len = len(columns)
|
||||
|
||||
cells_len_max = 0
|
||||
|
||||
def get_cell_len(total, cell):
|
||||
tc_pr = cell.find(ns + "tcPr")
|
||||
grid_span = None if tc_pr is None else tc_pr.find(ns + "gridSpan")
|
||||
|
||||
if grid_span is not None:
|
||||
return total + int(grid_span.get(ns + "val"))
|
||||
|
||||
return total + 1
|
||||
|
||||
# Calculate max of table cells to compare with `gridCol`.
|
||||
for r in t.iter(ns + "tr"):
|
||||
cells = r.findall(ns + "tc")
|
||||
cells_len = functools.reduce(get_cell_len, cells, 0)
|
||||
cells_len_max = max(cells_len_max, cells_len)
|
||||
|
||||
to_remove = columns_len - cells_len_max
|
||||
|
||||
# If after the loop, there're less columns, than
|
||||
# originally was, remove extra `gridCol` declarations.
|
||||
if to_remove > 0:
|
||||
# Have to keep track of the removed width to scale the
|
||||
# table back to its original width.
|
||||
removed_width = 0.0
|
||||
|
||||
for c in columns[-to_remove:]:
|
||||
removed_width += float(c.get(ns + "w"))
|
||||
|
||||
tblGrid.remove(c)
|
||||
|
||||
columns_left = tblGrid.findall(ns + "gridCol")
|
||||
|
||||
# Distribute `removed_width` across all columns that has
|
||||
# left after extras removal.
|
||||
extra_space = 0
|
||||
if len(columns_left) > 0:
|
||||
extra_space = removed_width / len(columns_left)
|
||||
extra_space = int(extra_space)
|
||||
|
||||
for c in columns_left:
|
||||
c.set(ns + "w", str(int(float(c.get(ns + "w")) + extra_space)))
|
||||
|
||||
return tree
|
||||
|
||||
def fix_docpr_ids(self, tree):
|
||||
# some Ids may have some collisions : so renumbering all of them :
|
||||
for elt in tree.xpath("//wp:docPr", namespaces=docx.oxml.ns.nsmap):
|
||||
self.docx_ids_index += 1
|
||||
elt.attrib["id"] = str(self.docx_ids_index)
|
||||
|
||||
def new_subdoc(self, docpath=None) -> Subdoc:
|
||||
from .subdoc import Subdoc
|
||||
|
||||
self.init_docx()
|
||||
return Subdoc(self, docpath)
|
||||
|
||||
@staticmethod
|
||||
def get_file_crc(file_obj):
|
||||
if hasattr(file_obj, "read"):
|
||||
buf = file_obj.read()
|
||||
else:
|
||||
with open(file_obj, "rb") as fh:
|
||||
buf = fh.read()
|
||||
|
||||
crc = binascii.crc32(buf) & 0xFFFFFFFF
|
||||
return crc
|
||||
|
||||
def replace_media(self, src_file, dst_file):
|
||||
"""Replace one media by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in
|
||||
docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer,
|
||||
then specify it with its replacement in this function using the file path
|
||||
or file-like objects.
|
||||
|
||||
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
|
||||
-- or --
|
||||
tpl.replace_media(io.BytesIO(image_stream), io.BytesIO(new_image_stream))
|
||||
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
|
||||
Note2: it is important to have the source media file as it is required
|
||||
to calculate its CRC to find them in the docx
|
||||
"""
|
||||
|
||||
crc = self.get_file_crc(src_file)
|
||||
if hasattr(dst_file, "read"):
|
||||
self.crc_to_new_media[crc] = dst_file.read()
|
||||
else:
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.crc_to_new_media[crc] = fh.read()
|
||||
|
||||
def replace_pic(self, embedded_file, dst_file):
|
||||
"""Replace embedded picture with original-name given by embedded_file.
|
||||
(give only the file basename, not the full path)
|
||||
The new picture is given by dst_file (either a filename or a file-like
|
||||
object)
|
||||
|
||||
Notes:
|
||||
1) embedded_file and dst_file must have the same extension/format
|
||||
in case dst_file is a file-like object, no check is done on
|
||||
format compatibility
|
||||
2) the aspect ratio will be the same as the replaced image
|
||||
3) There is no need to keep the original file (this is not the case
|
||||
for replace_embedded and replace_media)
|
||||
"""
|
||||
|
||||
if hasattr(dst_file, "read"):
|
||||
# NOTE: file extension not checked
|
||||
self.pics_to_replace[embedded_file] = dst_file.read()
|
||||
else:
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.pics_to_replace[embedded_file] = fh.read()
|
||||
|
||||
def replace_embedded(self, src_file, dst_file):
|
||||
"""Replace one embedded object by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images
|
||||
in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer,
|
||||
then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
|
||||
|
||||
Note2 : it is important to have the source file as it is required to
|
||||
calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, "rb") as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_embedded[crc] = fh.read()
|
||||
|
||||
def replace_zipname(self, zipname, dst_file):
|
||||
"""Replace one file in the docx file
|
||||
|
||||
First note that a MSWord .docx file is in fact a zip file.
|
||||
|
||||
This method can be used to replace document embedded in the docx template.
|
||||
|
||||
Some embedded document may have been modified by MSWord while saving
|
||||
the template : thus replace_embedded() cannot be used as CRC is not the
|
||||
same as the original file.
|
||||
|
||||
This method works for embedded MSWord file like Excel or PowerPoint file,
|
||||
but won't work for others like PDF, Python or even Text files :
|
||||
For these ones, MSWord generate an oleObjectNNN.bin file which is no
|
||||
use to be replaced as it is encoded.
|
||||
|
||||
Syntax:
|
||||
|
||||
tpl.replace_zipname(
|
||||
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
|
||||
'my_excel_file.xlsx')
|
||||
|
||||
The zipname is the one you can find when you open docx with WinZip,
|
||||
7zip (Windows) or unzip -l (Linux). The zipname starts with
|
||||
"word/embeddings/". Note that the file is renamed by MSWord,
|
||||
so you have to guess a little bit...
|
||||
"""
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.zipname_to_replace[zipname] = fh.read()
|
||||
|
||||
def reset_replacements(self):
|
||||
"""Reset replacement dictionaries
|
||||
|
||||
This will reset data for image/embedded/zipname replacement
|
||||
|
||||
This is useful when calling several times render() with different
|
||||
image/embedded/zipname replacements without re-instantiating
|
||||
DocxTemplate object.
|
||||
In this case, the right sequence for each rendering will be :
|
||||
- reset_replacements(...)
|
||||
- replace_zipname(...), replace_media(...) and/or replace_embedded(...),
|
||||
- render(...)
|
||||
|
||||
If you instantiate DocxTemplate object before each render(),
|
||||
this method is useless.
|
||||
"""
|
||||
self.crc_to_new_media = {}
|
||||
self.crc_to_new_embedded = {}
|
||||
self.zipname_to_replace = {}
|
||||
self.pics_to_replace = {}
|
||||
|
||||
def post_processing(self, docx_file):
|
||||
if self.crc_to_new_media or self.crc_to_new_embedded or self.zipname_to_replace:
|
||||
|
||||
if hasattr(docx_file, "read"):
|
||||
tmp_file = io.BytesIO()
|
||||
DocxTemplate(docx_file).save(tmp_file)
|
||||
tmp_file.seek(0)
|
||||
docx_file.seek(0)
|
||||
docx_file.truncate()
|
||||
docx_file.seek(0)
|
||||
|
||||
else:
|
||||
tmp_file = "%s_docxtpl_before_replace_medias" % docx_file
|
||||
os.rename(docx_file, tmp_file)
|
||||
|
||||
with zipfile.ZipFile(tmp_file) as zin:
|
||||
with zipfile.ZipFile(docx_file, "w") as zout:
|
||||
for item in zin.infolist():
|
||||
buf = zin.read(item.filename)
|
||||
if item.filename in self.zipname_to_replace:
|
||||
zout.writestr(item, self.zipname_to_replace[item.filename])
|
||||
elif (
|
||||
item.filename.startswith("word/media/")
|
||||
and item.CRC in self.crc_to_new_media
|
||||
):
|
||||
zout.writestr(item, self.crc_to_new_media[item.CRC])
|
||||
elif (
|
||||
item.filename.startswith("word/embeddings/")
|
||||
and item.CRC in self.crc_to_new_embedded
|
||||
):
|
||||
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
|
||||
else:
|
||||
zout.writestr(item, buf)
|
||||
|
||||
if not hasattr(tmp_file, "read"):
|
||||
os.remove(tmp_file)
|
||||
if hasattr(docx_file, "read"):
|
||||
docx_file.seek(0)
|
||||
|
||||
def pre_processing(self):
|
||||
|
||||
if self.pics_to_replace:
|
||||
self._replace_pics()
|
||||
|
||||
def _replace_pics(self):
|
||||
"""Replaces pictures xml tags in the docx template with pictures provided by the user"""
|
||||
|
||||
replaced_pics = {key: False for key in self.pics_to_replace}
|
||||
|
||||
# Main document
|
||||
part = self.docx.part
|
||||
self._replace_docx_part_pics(part, replaced_pics)
|
||||
|
||||
# Header/Footer
|
||||
for relid, rel in part.rels.items():
|
||||
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
|
||||
self._replace_docx_part_pics(rel.target_part, replaced_pics)
|
||||
|
||||
if not self.allow_missing_pics:
|
||||
# make sure all template images defined by user were replaced
|
||||
for img_id, replaced in replaced_pics.items():
|
||||
if not replaced:
|
||||
raise ValueError(
|
||||
"Picture %s not found in the docx template" % img_id
|
||||
)
|
||||
|
||||
def get_pic_map(self):
|
||||
return self.pic_map
|
||||
|
||||
def _replace_docx_part_pics(self, doc_part, replaced_pics):
|
||||
|
||||
et = etree.fromstring(doc_part.blob)
|
||||
|
||||
part_map = {}
|
||||
|
||||
gds = et.xpath("//a:graphic/a:graphicData", namespaces=docx.oxml.ns.nsmap)
|
||||
for gd in gds:
|
||||
rel = None
|
||||
# Either IMAGE, CHART, SMART_ART, ...
|
||||
try:
|
||||
if gd.attrib["uri"] == docx.oxml.ns.nsmap["pic"]:
|
||||
# Either PICTURE or LINKED_PICTURE image
|
||||
blip = gd.xpath(
|
||||
"pic:pic/pic:blipFill/a:blip", namespaces=docx.oxml.ns.nsmap
|
||||
)[0]
|
||||
dest = blip.xpath("@r:embed", namespaces=docx.oxml.ns.nsmap)
|
||||
if len(dest) > 0:
|
||||
rel = dest[0]
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
non_visual_properties = "pic:pic/pic:nvPicPr/pic:cNvPr/"
|
||||
filename = gd.xpath(
|
||||
"%s@name" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)[0]
|
||||
titles = gd.xpath(
|
||||
"%s@title" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)
|
||||
if titles:
|
||||
title = titles[0]
|
||||
else:
|
||||
title = ""
|
||||
descriptions = gd.xpath(
|
||||
"%s@descr" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)
|
||||
if descriptions:
|
||||
description = descriptions[0]
|
||||
else:
|
||||
description = ""
|
||||
|
||||
part_map[filename] = (
|
||||
doc_part.rels[rel].target_ref,
|
||||
doc_part.rels[rel].target_part,
|
||||
)
|
||||
|
||||
# replace data
|
||||
for img_id, img_data in self.pics_to_replace.items():
|
||||
if img_id == filename or img_id == title or img_id == description:
|
||||
part_map[filename][1]._blob = img_data
|
||||
replaced_pics[img_id] = True
|
||||
break
|
||||
|
||||
# FIXME: figure out what exceptions are thrown here
|
||||
# and catch more specific exceptions
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self.pic_map.update(part_map)
|
||||
|
||||
def build_url_id(self, url):
|
||||
self.init_docx()
|
||||
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True)
|
||||
|
||||
def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> None:
|
||||
# case where save() is called without doing rendering
|
||||
# ( user wants only to replace image/embedded/zipname )
|
||||
if not self.is_saved and not self.is_rendered:
|
||||
self.docx = Document(self.template_file)
|
||||
self.pre_processing()
|
||||
self.docx.save(filename, *args, **kwargs)
|
||||
self.post_processing(filename)
|
||||
self.is_saved = True
|
||||
|
||||
def get_undeclared_template_variables(
|
||||
self,
|
||||
jinja_env: Optional[Environment] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
) -> Set[str]:
|
||||
# Create a temporary document to analyze the template without affecting the current state
|
||||
temp_doc = Document(self.template_file)
|
||||
|
||||
# Get XML from the temporary document
|
||||
xml = self.xml_to_string(temp_doc._element.body)
|
||||
xml = self.patch_xml(xml)
|
||||
|
||||
# Add headers and footers
|
||||
for uri in [self.HEADER_URI, self.FOOTER_URI]:
|
||||
for relKey, val in temp_doc._part.rels.items():
|
||||
if (val.reltype == uri) and (val.target_part.blob):
|
||||
_xml = self.xml_to_string(parse_xml(val.target_part.blob))
|
||||
xml += self.patch_xml(_xml)
|
||||
|
||||
if jinja_env:
|
||||
env = jinja_env
|
||||
else:
|
||||
env = Environment()
|
||||
|
||||
parse_content = env.parse(xml)
|
||||
all_variables = meta.find_undeclared_variables(parse_content)
|
||||
|
||||
# If context is provided, return only variables that are not in the context
|
||||
if context is not None:
|
||||
provided_variables = set(context.keys())
|
||||
return all_variables - provided_variables
|
||||
|
||||
# If no context provided, return all variables (original behavior)
|
||||
return all_variables
|
||||
1032
poetry.lock
generated
1032
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,70 +0,0 @@
|
||||
[project]
|
||||
name = "docxtpl"
|
||||
dynamic = ["version"]
|
||||
description = "Python docx template engine"
|
||||
authors = [{name="Eric Lapouyade", email="elapouya@proton.me"}]
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
license = {text="LGPL-2.1-only"}
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
keywords = ["jinja2"]
|
||||
dependencies = [
|
||||
"python-docx",
|
||||
"jinja2",
|
||||
"lxml",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
subdoc = ["docxcompose"]
|
||||
docs = ["Sphinx", "sphinxcontrib-napoleon"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"mypy >=1.18.2; python_version >= '3.9'",
|
||||
"lxml-stubs >=0.5.1; python_version >= '3.9'",
|
||||
"flake8 >=7.3.0; python_version >= '3.9'"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/elapouya/python-docx-template"
|
||||
repository = "https://github.com/elapouya/python-docx-template.git"
|
||||
document = "https://docxtpl.readthedocs.org"
|
||||
|
||||
[tool.poetry]
|
||||
version = "0.0.0"
|
||||
|
||||
[tool.poetry.requires-plugins]
|
||||
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
[tool.poetry-dynamic-versioning.from-file]
|
||||
source = "docxtpl/__init__.py"
|
||||
pattern = '__version__ = "(.+)"'
|
||||
|
||||
[tool.mypy]
|
||||
pretty = true
|
||||
python_version = "3.9"
|
||||
check_untyped_defs = true
|
||||
warn_unused_ignores = true
|
||||
exclude = ["docs", "build", "setup.py"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["docxcompose.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core", "poetry-dynamic-versioning >=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
@ -1,5 +0,0 @@
|
||||
python-docx
|
||||
docxcompose
|
||||
jinja2
|
||||
lxml
|
||||
sphinx-book-theme
|
||||
67
setup.py
67
setup.py
@ -1,23 +1,17 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# To register onto Pypi :
|
||||
# python setup.py sdist bdist_wheel upload
|
||||
|
||||
|
||||
def read(*names):
|
||||
values = dict()
|
||||
for name in names:
|
||||
filename = name + ".rst"
|
||||
filename = name + '.rst'
|
||||
if os.path.isfile(filename):
|
||||
fd = open(filename)
|
||||
value = fd.read()
|
||||
fd.close()
|
||||
else:
|
||||
value = ""
|
||||
value = ''
|
||||
values[name] = value
|
||||
return values
|
||||
|
||||
@ -28,50 +22,35 @@ long_description = """
|
||||
News
|
||||
====
|
||||
%(CHANGES)s
|
||||
""" % read(
|
||||
"README", "CHANGES"
|
||||
)
|
||||
|
||||
""" % read('README', 'CHANGES')
|
||||
|
||||
def get_version(pkg):
|
||||
path = os.path.join(os.path.dirname(__file__), pkg, "__init__.py")
|
||||
if sys.version_info >= (3, 0):
|
||||
fh = open(path, encoding="utf-8") # required to read utf-8 file on windows
|
||||
else:
|
||||
fh = open(path) # encoding parameter does not exist in python 2
|
||||
with fh:
|
||||
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]', fh.read(), re.M)
|
||||
path = os.path.join(os.path.dirname(__file__),pkg,'__init__.py')
|
||||
with open(path) as fh:
|
||||
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]',fh.read(),re.M)
|
||||
if m:
|
||||
return m.group(1)
|
||||
raise RuntimeError("Unable to find __version__ string in %s." % path)
|
||||
|
||||
|
||||
setup(
|
||||
name="docxtpl",
|
||||
version=get_version("docxtpl"),
|
||||
description="Python docx template engine",
|
||||
setup(name='docxtpl',
|
||||
version=get_version('docxtpl'),
|
||||
description='Python docx template engine',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/x-rst",
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
],
|
||||
keywords="jinja2",
|
||||
url="https://github.com/elapouya/python-docx-template",
|
||||
author="Eric Lapouyade",
|
||||
license="LGPL-2.1-only",
|
||||
license_files=[],
|
||||
packages=["docxtpl"],
|
||||
install_requires=["python-docx>=1.1.1", "jinja2", "lxml"],
|
||||
extras_require={"docs": ["Sphinx", "sphinxcontrib-napoleon"], "subdoc": ["docxcompose"]},
|
||||
eager_resources=["docs"],
|
||||
zip_safe=False,
|
||||
)
|
||||
keywords='jinja2',
|
||||
url='https://github.com/elapouya/python-docx-template',
|
||||
author='Eric Lapouyade',
|
||||
author_email='elapouya@gmail.com',
|
||||
license='LGPL 2.1',
|
||||
packages=['docxtpl'],
|
||||
install_requires=['six', 'python-docx', 'jinja2', 'lxml'],
|
||||
extras_require={'docs': ['Sphinx', 'sphinxcontrib-napoleon']},
|
||||
eager_resources=['docs'],
|
||||
zip_safe=False)
|
||||
|
||||
@ -1,42 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/cellbg_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/cellbg_tpl.docx')
|
||||
|
||||
context = {
|
||||
"alerts": [
|
||||
{
|
||||
"date": "2015-03-10",
|
||||
"desc": RichText("Very critical alert", color="FF0000", bold=True),
|
||||
"type": "CRITICAL",
|
||||
"bg": "FF0000",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-11",
|
||||
"desc": RichText("Just a warning"),
|
||||
"type": "WARNING",
|
||||
"bg": "FFDD00",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-12",
|
||||
"desc": RichText("Information"),
|
||||
"type": "INFO",
|
||||
"bg": "8888FF",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-13",
|
||||
"desc": RichText("Debug trace"),
|
||||
"type": "DEBUG",
|
||||
"bg": "FF00FF",
|
||||
},
|
||||
'alerts' : [
|
||||
{'date' : '2015-03-10', 'desc' : RichText('Very critical alert',color='FF0000', bold=True), 'type' : 'CRITICAL', 'bg': 'FF0000' },
|
||||
{'date' : '2015-03-11', 'desc' : RichText('Just a warning'), 'type' : 'WARNING', 'bg': 'FFDD00' },
|
||||
{'date' : '2015-03-12', 'desc' : RichText('Information'), 'type' : 'INFO', 'bg': '8888FF' },
|
||||
{'date' : '2015-03-13', 'desc' : RichText('Debug trace'), 'type' : 'DEBUG', 'bg': 'FF00FF' },
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/cellbg.docx")
|
||||
tpl.save('test_files/cellbg.docx')
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/comments_tpl.docx")
|
||||
|
||||
tpl.render({})
|
||||
tpl.save("output/comments.docx")
|
||||
@ -1,35 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: sandeeprah, Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import jinja2
|
||||
|
||||
jinja_env = jinja2.Environment()
|
||||
|
||||
|
||||
# to create new filters, first create functions that accept the value to filter
|
||||
# as first argument, and filter parameters as next arguments
|
||||
def my_filterA(value, my_string_arg):
|
||||
return_value = value + " " + my_string_arg
|
||||
return return_value
|
||||
|
||||
|
||||
def my_filterB(value, my_float_arg):
|
||||
return_value = value + my_float_arg
|
||||
return return_value
|
||||
|
||||
|
||||
# Then, declare them to jinja like this :
|
||||
jinja_env.filters["my_filterA"] = my_filterA
|
||||
jinja_env.filters["my_filterB"] = my_filterB
|
||||
|
||||
|
||||
context = {"base_value_string": " Hello", "base_value_float": 1.5}
|
||||
|
||||
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
|
||||
tpl.render(context, jinja_env)
|
||||
tpl.save("output/custom_jinja_filters.docx")
|
||||
@ -1,10 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
doctemplate = r"templates/doc_properties_tpl.docx"
|
||||
|
||||
tpl = DocxTemplate(doctemplate)
|
||||
|
||||
context = {"test": "HelloWorld"}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/doc_properties.docx")
|
||||
@ -1,15 +1,15 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/dynamic_table_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/dynamic_table_tpl.docx')
|
||||
|
||||
context = {
|
||||
"col_labels": ["fruit", "vegetable", "stone", "thing"],
|
||||
"tbl_contents": [
|
||||
{"label": "yellow", "cols": ["banana", "capsicum", "pyrite", "taxi"]},
|
||||
{"label": "red", "cols": ["apple", "tomato", "cinnabar", "doubledecker"]},
|
||||
{"label": "green", "cols": ["guava", "cucumber", "aventurine", "card"]},
|
||||
],
|
||||
'col_labels' : ['fruit', 'vegetable', 'stone', 'thing'],
|
||||
'tbl_contents': [
|
||||
{'label': 'yellow', 'cols': ['banana', 'capsicum', 'pyrite', 'taxi']},
|
||||
{'label': 'red', 'cols': ['apple', 'tomato', 'cinnabar', 'doubledecker']},
|
||||
{'label': 'green', 'cols': ['guava', 'cucumber', 'aventurine', 'card']},
|
||||
]
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/dynamic_table.docx")
|
||||
tpl.save('test_files/dynamic_table.docx')
|
||||
|
||||
@ -1,45 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-09-09
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
# rendering the "dynamic embedded docx":
|
||||
embedded_docx_tpl = DocxTemplate("templates/embedded_embedded_docx_tpl.docx")
|
||||
embedded_docx_tpl=DocxTemplate('test_files/embedded_embedded_docx_tpl.docx')
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
'name' : 'John Doe',
|
||||
}
|
||||
embedded_docx_tpl.render(context)
|
||||
embedded_docx_tpl.save("output/embedded_embedded_docx.docx")
|
||||
embedded_docx_tpl.save('test_files/embedded_embedded_docx.docx')
|
||||
|
||||
|
||||
# rendering the main document :
|
||||
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
|
||||
# rendring the main document :
|
||||
tpl=DocxTemplate('test_files/embedded_main_tpl.docx')
|
||||
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
'name' : 'John Doe',
|
||||
}
|
||||
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
|
||||
)
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx"
|
||||
)
|
||||
|
||||
# The zipname is the one you can find when you open docx with WinZip, 7zip (Windows)
|
||||
# or unzip -l (Linux). The zipname starts with "word/embeddings/".
|
||||
# Note that the file is renamed by MSWord, so you have to guess a little bit...
|
||||
tpl.replace_zipname(
|
||||
"word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx", "templates/real_Excel.xlsx"
|
||||
)
|
||||
tpl.replace_zipname(
|
||||
"word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx",
|
||||
"templates/real_PowerPoint.pptx",
|
||||
)
|
||||
|
||||
tpl.replace_embedded('test_files/embedded_dummy.docx','test_files/embedded_static_docx.docx')
|
||||
tpl.replace_embedded('test_files/embedded_dummy2.docx','test_files/embedded_embedded_docx.docx')
|
||||
tpl.render(context)
|
||||
tpl.save("output/embedded.docx")
|
||||
tpl.save('test_files/embedded.docx')
|
||||
@ -1,54 +1,12 @@
|
||||
from docxtpl import DocxTemplate, R, Listing
|
||||
from docxtpl import *
|
||||
|
||||
tpl = DocxTemplate("templates/escape_tpl.docx")
|
||||
tpl = DocxTemplate("test_files/escape_tpl.docx")
|
||||
|
||||
context = {
|
||||
"myvar": R(
|
||||
'"less than" must be escaped : <, this can be done with RichText() or R()'
|
||||
),
|
||||
"myescvar": 'It can be escaped with a "|e" jinja filter in the template too : < ',
|
||||
"nlnp": R(
|
||||
"Here is a multiple\nlines\nstring\aand some\aother\aparagraphs",
|
||||
color="#ff00ff",
|
||||
),
|
||||
"mylisting": Listing("the listing\nwith\nsome\nlines\nand special chars : <>& ..."),
|
||||
"page_break": R("\f"),
|
||||
"new_listing": """
|
||||
This is a new listing
|
||||
Now, does not require Listing() Object
|
||||
Here is a \t tab\a
|
||||
Here is a new paragraph\a
|
||||
Here is a page break : \f
|
||||
That's it
|
||||
""",
|
||||
"some_html": (
|
||||
"HTTP/1.1 200 OK\n"
|
||||
"Server: Apache-Coyote/1.1\n"
|
||||
"Cache-Control: no-store\n"
|
||||
"Expires: Thu, 01 Jan 1970 00:00:00 GMT\n"
|
||||
"Pragma: no-cache\n"
|
||||
"Content-Type: text/html;charset=UTF-8\n"
|
||||
"Content-Language: zh-CN\n"
|
||||
"Date: Thu, 22 Oct 2020 10:59:40 GMT\n"
|
||||
"Content-Length: 9866\n"
|
||||
"\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
" <title>Struts Problem Report</title>\n"
|
||||
" <style>\n"
|
||||
" \tpre {\n"
|
||||
"\t \tmargin: 0;\n"
|
||||
"\t padding: 0;\n"
|
||||
"\t } "
|
||||
"\n"
|
||||
" </style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"...\n"
|
||||
"</body>\n"
|
||||
"</html>"
|
||||
),
|
||||
}
|
||||
context = {'myvar': R('"less than" must be escaped : <, this can be done with RichText() or R()'),
|
||||
'myescvar':'It can be escaped with a "|e" jinja filter in the template too : < ',
|
||||
'nlnp' : R('Here is a multiple\nlines\nstring\aand some\aother\aparagraphs\aNOTE: the current character styling is removed'),
|
||||
'mylisting': Listing('the listing\nwith\nsome\nlines\nand special chars : <>&'),
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/escape.docx")
|
||||
tpl.save("test_files/escape.docx")
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
"""
|
||||
@author: Max Podolskii
|
||||
"""
|
||||
|
||||
import os
|
||||
from unicodedata import name
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
XML_RESERVED = """<"&'>"""
|
||||
|
||||
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
|
||||
|
||||
context = {
|
||||
"nested_dict": {name(str(c)): c for c in XML_RESERVED},
|
||||
"autoescape": 'Escaped "str & ing"!',
|
||||
"autoescape_unicode": "This is an escaped <unicode> example \u4f60 & \u6211",
|
||||
"iteritems": lambda x: x.items(),
|
||||
}
|
||||
|
||||
tpl.render(context, autoescape=True)
|
||||
|
||||
OUTPUT = "output"
|
||||
if not os.path.exists(OUTPUT):
|
||||
os.makedirs(OUTPUT)
|
||||
tpl.save(OUTPUT + "/escape_auto.docx")
|
||||
@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2024-09-23
|
||||
|
||||
@author: Bart Broere
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/footnotes.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/footnotes_tpl.docx")
|
||||
|
||||
context = {"a_jinja_variable": "A Jinja variable!"}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
@ -1,180 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test for get_undeclared_template_variables method
|
||||
|
||||
This test demonstrates the correct behavior of get_undeclared_template_variables:
|
||||
1. Before rendering - finds all template variables
|
||||
2. After rendering with incomplete context - finds missing variables
|
||||
3. After rendering with complete context - returns empty set
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
def test_before_render():
|
||||
"""Test that get_undeclared_template_variables finds all variables before rendering"""
|
||||
print("=== Test 1: Before render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
undeclared = tpl.get_undeclared_template_variables()
|
||||
print(f"Variables found: {undeclared}")
|
||||
|
||||
# Should find all variables
|
||||
expected_vars = {
|
||||
"name",
|
||||
"age",
|
||||
"email",
|
||||
"is_student",
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"skills",
|
||||
"projects",
|
||||
"company_name",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
"author",
|
||||
}
|
||||
|
||||
if undeclared == expected_vars:
|
||||
print("PASS: Found all expected variables before render")
|
||||
else:
|
||||
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_vars
|
||||
|
||||
|
||||
def test_after_incomplete_render():
|
||||
"""Test that get_undeclared_template_variables finds missing variables after incomplete render"""
|
||||
print("\n=== Test 2: After incomplete render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
|
||||
# Provide only some variables (missing several)
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
"age": 25,
|
||||
"email": "john@example.com",
|
||||
"is_student": True,
|
||||
"skills": ["Python", "Django"],
|
||||
"company_name": "Test Corp",
|
||||
"author": "Test Author",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
undeclared = tpl.get_undeclared_template_variables(context=context)
|
||||
print(f"Missing variables: {undeclared}")
|
||||
|
||||
# Should find missing variables
|
||||
expected_missing = {
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"projects",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
}
|
||||
|
||||
if undeclared == expected_missing:
|
||||
print("PASS: Found missing variables after incomplete render")
|
||||
else:
|
||||
print(f"FAIL: Expected missing {expected_missing}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_missing
|
||||
|
||||
|
||||
def test_after_complete_render():
|
||||
"""Test that get_undeclared_template_variables returns empty set after complete render"""
|
||||
print("\n=== Test 3: After complete render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
|
||||
# Provide all variables
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
"age": 25,
|
||||
"email": "john@example.com",
|
||||
"is_student": True,
|
||||
"has_degree": True,
|
||||
"degree_field": "Computer Science",
|
||||
"skills": ["Python", "Django", "JavaScript"],
|
||||
"projects": [
|
||||
{"name": "Project A", "year": 2023, "description": "A great project"},
|
||||
{"name": "Project B", "year": 2024, "description": "Another great project"},
|
||||
],
|
||||
"company_name": "Test Corp",
|
||||
"page_number": 1,
|
||||
"generation_date": "2024-01-15",
|
||||
"author": "Test Author",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
undeclared = tpl.get_undeclared_template_variables(context=context)
|
||||
print(f"Undeclared variables: {undeclared}")
|
||||
|
||||
# Should return empty set
|
||||
if undeclared == set():
|
||||
print("PASS: No undeclared variables after complete render")
|
||||
else:
|
||||
print(f"FAIL: Expected empty set, got {undeclared}")
|
||||
|
||||
return undeclared == set()
|
||||
|
||||
|
||||
def test_with_custom_jinja_env():
|
||||
"""Test that get_undeclared_template_variables works with custom Jinja environment"""
|
||||
print("\n=== Test 4: With custom Jinja environment ===")
|
||||
from jinja2 import Environment
|
||||
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
custom_env = Environment()
|
||||
|
||||
undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env)
|
||||
print(f"Variables found with custom env: {undeclared}")
|
||||
|
||||
# Should find all variables
|
||||
expected_vars = {
|
||||
"name",
|
||||
"age",
|
||||
"email",
|
||||
"is_student",
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"skills",
|
||||
"projects",
|
||||
"company_name",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
"author",
|
||||
}
|
||||
|
||||
if undeclared == expected_vars:
|
||||
print("PASS: Custom Jinja environment works correctly")
|
||||
else:
|
||||
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_vars
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing get_undeclared_template_variables method...")
|
||||
print("=" * 50)
|
||||
|
||||
# Run all tests
|
||||
test1_passed = test_before_render()
|
||||
test2_passed = test_after_incomplete_render()
|
||||
test3_passed = test_after_complete_render()
|
||||
test4_passed = test_with_custom_jinja_env()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("SUMMARY:")
|
||||
print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}")
|
||||
print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}")
|
||||
print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}")
|
||||
print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}")
|
||||
|
||||
all_passed = test1_passed and test2_passed and test3_passed and test4_passed
|
||||
|
||||
if all_passed:
|
||||
print("ALL TESTS PASSED!")
|
||||
else:
|
||||
print("SOME TESTS FAILED!")
|
||||
|
||||
print("\nNote: This test demonstrates that get_undeclared_template_variables")
|
||||
print("now correctly analyzes the original template, not the rendered document.")
|
||||
@ -1,25 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/header_footer_tpl.docx')
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph(
|
||||
"This is a sub-document to check it does not break header and footer"
|
||||
)
|
||||
p = sd.add_paragraph('This is a sub-document to check it does not break header and footer')
|
||||
|
||||
context = {
|
||||
"title": "Header and footer test",
|
||||
"company_name": "The World Wide company",
|
||||
"date": "2016-03-17",
|
||||
"mysubdoc": sd,
|
||||
'title' : 'Header and footer test',
|
||||
'company_name' : 'The World Wide company',
|
||||
'date' : '2016-03-17',
|
||||
'mysubdoc' : sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer.docx")
|
||||
tpl.save('test_files/header_footer.docx')
|
||||
@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_entities_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/header_footer_entities_tpl.docx')
|
||||
|
||||
context = {
|
||||
"title": "Header and footer test",
|
||||
'title' : 'Header and footer test',
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer_entities.docx")
|
||||
tpl.save('test_files/header_footer_entities.docx')
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-09-03
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/header_footer_image.docx"
|
||||
DEST_FILE = 'test_files/header_footer_image.docx'
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/header_footer_image_tpl.docx')
|
||||
|
||||
context = {
|
||||
"mycompany": "The World Wide company",
|
||||
'mycompany' : 'The World Wide company',
|
||||
}
|
||||
tpl.replace_media("templates/dummy_pic_for_header.png", "templates/python.png")
|
||||
tpl.replace_media('test_files/dummy_pic_for_header.png','test_files/python.png')
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2019-05-22
|
||||
|
||||
@author: Eric Dufresne
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import io
|
||||
|
||||
DEST_FILE = "output/header_footer_image_file_obj.docx"
|
||||
DEST_FILE2 = "output/header_footer_image_file_obj2.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
"mycompany": "The World Wide company",
|
||||
}
|
||||
|
||||
dummy_pic = io.BytesIO(open("templates/dummy_pic_for_header.png", "rb").read())
|
||||
new_image = io.BytesIO(open("templates/python.png", "rb").read())
|
||||
tpl.replace_media(dummy_pic, new_image)
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
dummy_pic.seek(0)
|
||||
new_image.seek(0)
|
||||
tpl.replace_media(dummy_pic, new_image)
|
||||
tpl.render(context)
|
||||
|
||||
file_obj = io.BytesIO()
|
||||
tpl.save(file_obj)
|
||||
file_obj.seek(0)
|
||||
with open(DEST_FILE2, "wb") as f:
|
||||
f.write(file_obj.read())
|
||||
@ -1,24 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-04-06
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
|
||||
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
|
||||
from docx.shared import Mm
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
"inline_image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
|
||||
"images": [
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
],
|
||||
}
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer_inline_image.docx")
|
||||
@ -1,28 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2016-07-19
|
||||
|
||||
@author: AhnSeongHyun
|
||||
|
||||
Edited : 2016-07-19 by Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx")
|
||||
tpl=DocxTemplate('test_files/header_footer_tpl_utf8.docx')
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph(
|
||||
"This is a sub-document to check it does not break header and footer with utf-8 "
|
||||
"characters inside the template .docx"
|
||||
)
|
||||
p = sd.add_paragraph(u'This is a sub-document to check it does not break header and footer with utf-8 characters inside the template .docx')
|
||||
|
||||
context = {
|
||||
"title": "헤더와 푸터",
|
||||
"company_name": "세계적 회사",
|
||||
"date": "2016-03-17",
|
||||
"mysubdoc": sd,
|
||||
'title' : u'헤더와 푸터',
|
||||
'company_name' : u'세계적 회사',
|
||||
'date' : u'2016-03-17',
|
||||
'mysubdoc' : sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer_utf8.docx")
|
||||
tpl.save('test_files/header_footer_utf8.docx')
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
|
||||
tpl.render({})
|
||||
tpl.save("output/horizontal_merge.docx")
|
||||
@ -1,51 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-01-14
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
|
||||
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
|
||||
from docx.shared import Mm
|
||||
import jinja2
|
||||
from docx.shared import Mm, Inches, Pt
|
||||
|
||||
tpl = DocxTemplate("templates/inline_image_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/inline_image_tpl.docx')
|
||||
|
||||
context = {
|
||||
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
|
||||
"myimageratio": InlineImage(
|
||||
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
|
||||
),
|
||||
"frameworks": [
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
|
||||
"desc": "The web framework for perfectionists with deadlines",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/zope.png", height=Mm(10)),
|
||||
"desc": "Zope is a leading Open Source Application Server "
|
||||
"and Content Management Framework",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/pyramid.png", height=Mm(10)),
|
||||
"desc": "Pyramid is a lightweight Python web framework aimed at taking "
|
||||
"small web apps into big web apps.",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/bottle.png", height=Mm(10)),
|
||||
"desc": "Bottle is a fast, simple and lightweight WSGI micro web-framework "
|
||||
"for Python",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/tornado.png", height=Mm(10)),
|
||||
"desc": "Tornado is a Python web framework and asynchronous networking "
|
||||
"library.",
|
||||
},
|
||||
],
|
||||
'myimage' : InlineImage(tpl,'test_files/python_logo.png',width=Mm(20)),
|
||||
'myimageratio': InlineImage(tpl, 'test_files/python_jpeg.jpg', width=Mm(30), height=Mm(60)),
|
||||
|
||||
'frameworks' : [{'image' : InlineImage(tpl,'test_files/django.png',height=Mm(10)),
|
||||
'desc' : 'The web framework for perfectionists with deadlines'},
|
||||
|
||||
{'image' : InlineImage(tpl,'test_files/zope.png',height=Mm(10)),
|
||||
'desc' : 'Zope is a leading Open Source Application Server and Content Management Framework'},
|
||||
|
||||
{'image': InlineImage(tpl, 'test_files/pyramid.png', height=Mm(10)),
|
||||
'desc': 'Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.'},
|
||||
|
||||
{'image' : InlineImage(tpl,'test_files/bottle.png',height=Mm(10)),
|
||||
'desc' : 'Bottle is a fast, simple and lightweight WSGI micro web-framework for Python'},
|
||||
|
||||
{'image': InlineImage(tpl, 'test_files/tornado.png', height=Mm(10)),
|
||||
'desc': 'Tornado is a Python web framework and asynchronous networking library.'},
|
||||
]
|
||||
}
|
||||
# testing that it works also when autoescape has been forced to True
|
||||
jinja_env = jinja2.Environment(autoescape=True)
|
||||
tpl.render(context, jinja_env)
|
||||
tpl.save("output/inline_image.docx")
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/inline_image.docx')
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
|
||||
tpl.render({})
|
||||
tpl.save("output/less_cells_after_loop.docx")
|
||||
@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
tpl = DocxTemplate("templates/merge_docx_master_tpl.docx")
|
||||
sd = tpl.new_subdoc("templates/merge_docx_subdoc.docx")
|
||||
|
||||
context = {
|
||||
"mysubdoc": sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/merge_docx.docx")
|
||||
@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
|
||||
|
||||
context = {
|
||||
"living_in_town": True,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/merge_paragraph.docx")
|
||||
@ -1,25 +0,0 @@
|
||||
import os
|
||||
|
||||
TEMPLATE_PATH = "templates/module_execute_tpl.docx"
|
||||
JSON_PATH = "templates/module_execute.json"
|
||||
OUTPUT_FILENAME = "output/module_execute.docx"
|
||||
OVERWRITE = "-o"
|
||||
QUIET = "-q"
|
||||
|
||||
|
||||
if os.path.exists(OUTPUT_FILENAME):
|
||||
os.unlink(OUTPUT_FILENAME)
|
||||
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
cmd = "python -m docxtpl %s %s %s %s %s" % (
|
||||
TEMPLATE_PATH,
|
||||
JSON_PATH,
|
||||
OUTPUT_FILENAME,
|
||||
OVERWRITE,
|
||||
QUIET,
|
||||
)
|
||||
print('Executing "%s" ...' % cmd)
|
||||
os.system(cmd)
|
||||
|
||||
if os.path.exists(OUTPUT_FILENAME):
|
||||
print(" --> File %s has been generated." % OUTPUT_FILENAME)
|
||||
@ -1,40 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-12-20
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/multi_rendering_tpl.docx")
|
||||
|
||||
documents_data = [
|
||||
{
|
||||
"dest_file": "multi_render1.docx",
|
||||
"context": {
|
||||
"title": "Title ONE",
|
||||
"body": "This is the body for first document",
|
||||
},
|
||||
},
|
||||
{
|
||||
"dest_file": "multi_render2.docx",
|
||||
"context": {
|
||||
"title": "Title TWO",
|
||||
"body": "This is the body for second document",
|
||||
},
|
||||
},
|
||||
{
|
||||
"dest_file": "multi_render3.docx",
|
||||
"context": {
|
||||
"title": "Title THREE",
|
||||
"body": "This is the body for third document",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for document_data in documents_data:
|
||||
dest_file = document_data["dest_file"]
|
||||
context = document_data["context"]
|
||||
tpl.render(context)
|
||||
tpl.save("output/%s" % dest_file)
|
||||
@ -1,45 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2016-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/nested_for_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/nested_for_tpl.docx')
|
||||
|
||||
context = {
|
||||
"dishes": [
|
||||
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
|
||||
{
|
||||
"name": "Hamburger",
|
||||
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
|
||||
},
|
||||
{
|
||||
"name": "Apple pie",
|
||||
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
|
||||
},
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Saint-Exupery",
|
||||
"books": [
|
||||
{"title": "Le petit prince"},
|
||||
{"title": "L'aviateur"},
|
||||
{"title": "Vol de nuit"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Barjavel",
|
||||
"books": [
|
||||
{"title": "Ravage"},
|
||||
{"title": "La nuit des temps"},
|
||||
{"title": "Le grand secret"},
|
||||
],
|
||||
},
|
||||
'dishes' : [
|
||||
{'name' : 'Pizza', 'ingredients' : ['bread','tomato', 'ham', 'cheese']},
|
||||
{'name' : 'Hamburger', 'ingredients' : ['bread','chopped steak', 'cheese', 'sauce']},
|
||||
{'name' : 'Apple pie', 'ingredients' : ['flour','apples', 'suggar', 'quince jelly']},
|
||||
],
|
||||
'authors' : [
|
||||
{'name' : 'Saint-Exupery', 'books' : [
|
||||
{'title' : 'Le petit prince'},
|
||||
{'title' : "L'aviateur"},
|
||||
{'title' : 'Vol de nuit'},
|
||||
]},
|
||||
{'name' : 'Barjavel', 'books' : [
|
||||
{'title' : 'Ravage'},
|
||||
{'title' : "La nuit des temps"},
|
||||
{'title' : 'Le grand secret'},
|
||||
]},
|
||||
]
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/nested_for.docx")
|
||||
tpl.save('test_files/nested_for.docx')
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/order_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/order_tpl.docx')
|
||||
|
||||
context = {
|
||||
"customer_name": "Eric",
|
||||
"items": [
|
||||
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
|
||||
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
|
||||
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
|
||||
'customer_name' : 'Eric',
|
||||
'items' : [
|
||||
{'desc' : 'Python interpreters', 'qty' : 2, 'price' : 'FREE' },
|
||||
{'desc' : 'Django projects', 'qty' : 5403, 'price' : 'FREE' },
|
||||
{'desc' : 'Guido', 'qty' : 1, 'price' : '100,000,000.00' },
|
||||
],
|
||||
"in_europe": True,
|
||||
"is_paid": False,
|
||||
"company_name": "The World Wide company",
|
||||
"total_price": "100,000,000.00",
|
||||
'in_europe' : True,
|
||||
'is_paid': False,
|
||||
'company_name' : 'The World Wide company',
|
||||
'total_price' : '100,000,000.00'
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/order.docx")
|
||||
tpl.save('test_files/order.docx')
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
# With old docxtpl version, "... for spicy ..." was replaced by "... forspicy..."
|
||||
# This test is for checking that is some cases the spaces are not lost anymore
|
||||
|
||||
tpl = DocxTemplate("templates/preserve_spaces_tpl.docx")
|
||||
|
||||
tags = ["tag_1", "tag_2"]
|
||||
replacement = ["looking", "too"]
|
||||
|
||||
context = dict(zip(tags, replacement))
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/preserve_spaces.docx")
|
||||
@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2017-09-03
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/replace_picture.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
|
||||
|
||||
context = {}
|
||||
|
||||
tpl.replace_pic("python_logo.png", "templates/python.png")
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
@ -1,64 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/richtext_tpl.docx')
|
||||
|
||||
rt = RichText()
|
||||
rt.add("a rich text", style="myrichtextstyle")
|
||||
rt.add(" with ")
|
||||
rt.add("some italic", italic=True)
|
||||
rt.add(" and ")
|
||||
rt.add("some violet", color="#ff00ff")
|
||||
rt.add(" and ")
|
||||
rt.add("some striked", strike=True)
|
||||
rt.add(" and ")
|
||||
rt.add("some Highlighted", highlight="#ffff00")
|
||||
rt.add(" and ")
|
||||
rt.add("some small", size=14)
|
||||
rt.add(" or ")
|
||||
rt.add("big", size=60)
|
||||
rt.add(" text.")
|
||||
rt.add("\nYou can add an hyperlink, here to ")
|
||||
rt.add("google", url_id=tpl.build_url_id("http://google.com"))
|
||||
rt.add("\nEt voilà ! ")
|
||||
rt.add("\n1st line")
|
||||
rt.add("\n2nd line")
|
||||
rt.add("\n3rd line")
|
||||
rt.add("\aA new paragraph : <cool>\a")
|
||||
rt.add("--- A page break here (see next page) ---\f")
|
||||
|
||||
for ul in [
|
||||
"single",
|
||||
"double",
|
||||
"thick",
|
||||
"dotted",
|
||||
"dash",
|
||||
"dotDash",
|
||||
"dotDotDash",
|
||||
"wave",
|
||||
]:
|
||||
rt.add("\nUnderline : " + ul + " \n", underline=ul)
|
||||
rt.add("\nFonts :\n", underline=True)
|
||||
rt.add("Arial\n", font="Arial")
|
||||
rt.add("Courier New\n", font="Courier New")
|
||||
rt.add("Times New Roman\n", font="Times New Roman")
|
||||
rt.add("\n\nHere some")
|
||||
rt.add("superscript", superscript=True)
|
||||
rt.add(" and some")
|
||||
rt.add("subscript", subscript=True)
|
||||
|
||||
rt_embedded = RichText("an example of ")
|
||||
rt_embedded.add(rt)
|
||||
rt = RichText('an exemple of ')
|
||||
rt.add('a rich text', style='myrichtextstyle')
|
||||
rt.add(' with ')
|
||||
rt.add('some italic', italic=True)
|
||||
rt.add(' and ')
|
||||
rt.add('some violet', color='#ff00ff')
|
||||
rt.add(' and ')
|
||||
rt.add('some striked', strike=True)
|
||||
rt.add(' and ')
|
||||
rt.add('some small', size=14)
|
||||
rt.add(' or ')
|
||||
rt.add('big', size=60)
|
||||
rt.add(' text.')
|
||||
rt.add(' Et voilà ! ')
|
||||
rt.add('\n1st line')
|
||||
rt.add('\n2nd line')
|
||||
rt.add('\n3rd line')
|
||||
rt.add('\n\n<cool>')
|
||||
|
||||
context = {
|
||||
"example": rt_embedded,
|
||||
'example' : rt,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext.docx")
|
||||
tpl.save('test_files/richtext.docx')
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_and_if_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/richtext_and_if_tpl.docx')
|
||||
|
||||
|
||||
context = {"foobar": RichText("Foobar!", color="ff0000")}
|
||||
context = {
|
||||
'foobar': RichText('Foobar!', color='ff0000')
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext_and_if.docx")
|
||||
tpl.save('test_files/richtext_and_if.docx')
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2022-08-03
|
||||
@author: Dongfang Song
|
||||
"""
|
||||
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_eastAsia_tpl.docx")
|
||||
rt = RichText("测试TEST", font="eastAsia:Microsoft YaHei")
|
||||
ch = RichText("测试TEST", font="eastAsia:微软雅黑")
|
||||
sun = RichText("测试TEST", font="eastAsia:SimSun")
|
||||
context = {
|
||||
"example": rt,
|
||||
"Chinese": ch,
|
||||
"simsun": sun,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext_eastAsia.docx")
|
||||
@ -1,63 +0,0 @@
|
||||
"""
|
||||
Created : 2025-02-28
|
||||
|
||||
@author: Hannah Imrie
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, RichText, RichTextParagraph
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_paragraph_tpl.docx")
|
||||
|
||||
rtp = RichTextParagraph()
|
||||
rt = RichText()
|
||||
|
||||
rtp.add(
|
||||
"The rich text paragraph function allows paragraph styles to be added to text",
|
||||
parastyle="myrichparastyle",
|
||||
)
|
||||
rtp.add("Any built in paragraph style can be used", parastyle="IntenseQuote")
|
||||
rtp.add(
|
||||
"or you can add your own, unlocking all style options", parastyle="createdStyle"
|
||||
)
|
||||
rtp.add(
|
||||
"To use, just create a style in your template word doc with the formatting you want "
|
||||
"and call it in the code.",
|
||||
parastyle="normal",
|
||||
)
|
||||
|
||||
rtp.add("This allows for the use of")
|
||||
rtp.add("custom bullet\apoints", parastyle="SquareBullet")
|
||||
rtp.add("Numbered Bullet Points", parastyle="BasicNumbered")
|
||||
rtp.add("and Alpha Bullet Points.", parastyle="alphaBracketNumbering")
|
||||
rtp.add("You can", parastyle="normal")
|
||||
rtp.add("set the", parastyle="centerAlign")
|
||||
rtp.add("text alignment", parastyle="rightAlign")
|
||||
rtp.add(
|
||||
"as well as the spacing between lines of text. Like this for example, "
|
||||
"this text has very tight spacing between the lines.\aIt also has no space between "
|
||||
"paragraphs of the same style.",
|
||||
parastyle="TightLineSpacing",
|
||||
)
|
||||
rtp.add(
|
||||
"Unlike this one, which has extra large spacing between lines for when you want to "
|
||||
"space things out a bit or just write a little less.",
|
||||
parastyle="WideLineSpacing",
|
||||
)
|
||||
rtp.add(
|
||||
"You can also set the background colour of a line.", parastyle="LineShadingGreen"
|
||||
)
|
||||
|
||||
rt.add("This works with ")
|
||||
rt.add("Rich ", bold=True)
|
||||
rt.add("Text ", italic=True)
|
||||
rt.add("Strings", underline="single")
|
||||
rt.add(" too.")
|
||||
|
||||
rtp.add(rt, parastyle="SquareBullet")
|
||||
|
||||
context = {
|
||||
"example": rtp,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext_paragraph.docx")
|
||||
@ -1,17 +1,13 @@
|
||||
import subprocess
|
||||
import glob
|
||||
import os
|
||||
import six
|
||||
|
||||
tests = sorted(glob.glob("[A-Za-z]*.py"))
|
||||
excludes = ["runtests.py"]
|
||||
|
||||
output_dir = os.path.join(os.path.dirname(__file__), "output")
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
tests = sorted(glob.glob('[A-Za-z]*.py'))
|
||||
excludes = ['runtests.py']
|
||||
|
||||
for test in tests:
|
||||
if test not in excludes:
|
||||
print("%s ..." % test)
|
||||
subprocess.call(["python", "./%s" % test])
|
||||
six.print_('%s ...' % test)
|
||||
subprocess.call(['python','./%s' % test])
|
||||
|
||||
print("Done.")
|
||||
six.print_('Done.')
|
||||
|
||||
@ -1,36 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
from docx.shared import Inches
|
||||
|
||||
tpl = DocxTemplate("templates/subdoc_tpl.docx")
|
||||
tpl=DocxTemplate('test_files/subdoc_tpl.docx')
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph("This is a sub-document inserted into a bigger one")
|
||||
p = sd.add_paragraph("It has been ")
|
||||
p.add_run("dynamically").style = "dynamic"
|
||||
p.add_run(" generated with python by using ")
|
||||
p.add_run("python-docx").italic = True
|
||||
p.add_run(" library")
|
||||
p = sd.add_paragraph('This is a sub-document inserted into a bigger one')
|
||||
p = sd.add_paragraph('It has been ')
|
||||
p.add_run('dynamically').style = 'dynamic'
|
||||
p.add_run(' generated with python by using ')
|
||||
p.add_run('python-docx').italic = True
|
||||
p.add_run(' library')
|
||||
|
||||
sd.add_heading("Heading, level 1", level=1)
|
||||
sd.add_paragraph("This is an Intense quote", style="IntenseQuote")
|
||||
sd.add_heading('Heading, level 1', level=1)
|
||||
sd.add_paragraph('This is an Intense quote', style='IntenseQuote')
|
||||
|
||||
sd.add_paragraph("A picture :")
|
||||
sd.add_picture("templates/python_logo.png", width=Inches(1.25))
|
||||
sd.add_paragraph('A picture :')
|
||||
sd.add_picture('test_files/python_logo.png', width=Inches(1.25))
|
||||
|
||||
sd.add_paragraph("A Table :")
|
||||
sd.add_paragraph('A Table :')
|
||||
table = sd.add_table(rows=1, cols=3)
|
||||
hdr_cells = table.rows[0].cells
|
||||
hdr_cells[0].text = "Qty"
|
||||
hdr_cells[1].text = "Id"
|
||||
hdr_cells[2].text = "Desc"
|
||||
recordset = ((1, 101, "Spam"), (2, 42, "Eggs"), (3, 631, "Spam,spam, eggs, and ham"))
|
||||
hdr_cells[0].text = 'Qty'
|
||||
hdr_cells[1].text = 'Id'
|
||||
hdr_cells[2].text = 'Desc'
|
||||
recordset=( (1,101,'Spam'),
|
||||
(2,42,'Eggs'),
|
||||
(3,631,'Spam,spam, eggs, and ham') )
|
||||
for item in recordset:
|
||||
row_cells = table.add_row().cells
|
||||
row_cells[0].text = str(item[0])
|
||||
@ -38,8 +40,8 @@ for item in recordset:
|
||||
row_cells[2].text = item[2]
|
||||
|
||||
context = {
|
||||
"mysubdoc": sd,
|
||||
'mysubdoc' : sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/subdoc.docx")
|
||||
tpl.save('test_files/subdoc.docx')
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
print("=" * 80)
|
||||
print("Generating template error for testing (so it is safe to ignore) :")
|
||||
print("." * 80)
|
||||
try:
|
||||
tpl = DocxTemplate("templates/template_error_tpl.docx")
|
||||
tpl.render({"test_variable": "test variable value"})
|
||||
except TemplateError as the_error:
|
||||
print(str(the_error))
|
||||
if hasattr(the_error, "docx_context"):
|
||||
print("Context:")
|
||||
for line in the_error.docx_context:
|
||||
print(line)
|
||||
tpl.save("output/template_error.docx")
|
||||
print("." * 80)
|
||||
print(" End of TemplateError Test ")
|
||||
print("=" * 80)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
{"json_dict_var" : {"json_dict_var":"successfully inserted"},
|
||||
"json_array_var": ["json","array","var","successfully", "inserted"],
|
||||
"json_string_var":"json_string_var successfully inserted",
|
||||
"json_int_var":123,
|
||||
"json_float_var":1.234,
|
||||
"json_true_var":true,
|
||||
"json_false_var":false,
|
||||
"json_none_var":null}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
tests/test_files/cellbg.docx
Normal file
BIN
tests/test_files/cellbg.docx
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
tests/test_files/dynamic_table.docx
Normal file
BIN
tests/test_files/dynamic_table.docx
Normal file
Binary file not shown.
BIN
tests/test_files/escape.docx
Normal file
BIN
tests/test_files/escape.docx
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user