Compare commits

...

148 Commits

Author SHA1 Message Date
Eric Lapouyade
f96b0b6413
Merge pull request #617 from waketzheng/project-section
feat: use project section
2025-09-24 13:48:54 +02:00
Waket Zheng
560b4b3311 Use poetry-dynamic-versioning instead of pdm 2025-09-24 12:25:32 +08:00
Waket Zheng
e77cbf8c7b Fix pip install error with --editable 2025-09-23 18:58:34 +08:00
Waket Zheng
7a6ddbcc54 Move docxcompose to optional dependency 2025-09-23 18:25:51 +08:00
Eric Lapouyade
d9bb19cdd4 Update setup.py 2025-07-15 17:15:05 +02:00
Eric Lapouyade
416729dddc Run black on template.py 2025-07-15 15:46:03 +02:00
Eric Lapouyade
cbbeae57bd Fix and improve get_undeclared_template_variables() method 2025-07-15 15:39:11 +02:00
Pablo Esteban
606d189787 - The get_undeclared_template_variables method now analyzes the original template, regardless of whether it has been rendered.
- Added optional context parameter to return only variables not present in the provided context.
- Added test tests/get_undeclared_variables.py:
    - Verifies behavior before rendering (all variables)
    - Verifies after rendering with incomplete context (only missing variables)
    - Verifies after rendering with complete context (empty set)
    - Verifies compatibility with custom Jinja2 environment
- All tests use asserts and are ready for CI integration.

Closes #585
2025-07-11 14:32:20 +02:00
Eric Lapouyade
399761f9c9 Update sphynx conf.py 2025-05-06 16:22:18 +02:00
Eric Lapouyade
4477729a57 move requirements.txt 2025-05-06 16:21:27 +02:00
Eric Lapouyade
b391b213eb update sphinx theme 2025-05-06 16:17:56 +02:00
Eric Lapouyade
e766040c5b Add .readthedocs.yaml 2025-05-06 16:04:07 +02:00
Eric Lapouyade
fb0e3583da v0.20.0 2025-05-06 15:51:54 +02:00
Eric Lapouyade
40861400b3 black 2025-05-02 16:42:32 +02:00
Eric Lapouyade
9392b06da4 add RichTextParagraph class 2025-05-02 16:29:22 +02:00
Eric Lapouyade
a48a6a96b2 Merge branch 'refs/heads/ST-Imrie-master' 2025-05-02 16:08:08 +02:00
Eric Lapouyade
685425095b
Merge pull request #592 from bm-rana/bold-rtl-fix
Add RTL support for bold/italic text
2025-05-02 15:58:19 +02:00
Eric Lapouyade
08a5d748f6
Merge pull request #593 from aperechnev/master
Possibility to skip missing pictures
2025-05-02 15:56:50 +02:00
ST-Imrie
ebc770a291 Updated RichText Jinja tags from rr/rp to r/q and updated test template 2025-05-01 11:39:48 +01:00
Alexander Pérechnev
97df786298 Added possibility to skip missing pictures instead of raising ValueError exception. 2025-04-04 17:38:56 +03:00
rana saab
994da90f66 fixed rtl italic formatting 2025-04-03 13:37:17 +03:00
ST-Imrie
aa7b7a77e3 Rich Text Paragraph Formatting Update 2025-02-28 17:53:26 +00:00
Eric Lapouyade
b97fa32f10 v0.19.1 2024-12-29 18:53:25 +01:00
Eric Lapouyade
def1205179
Merge pull request #575 from jhpyle/patch-1
encode XML as bytes in render_footnotes()
2024-12-29 18:43:33 +01:00
Jonathan Pyle
eed645c0c5
encode XML as bytes in render_footnotes()
`part._blob` should have the class `bytes`.
2024-11-29 15:23:46 -05:00
Eric Lapouyade
60caff83fa Flake8 : whitespace fix 2024-11-12 14:37:12 +01:00
Eric Lapouyade
65e00b189e Flake8 : whitespace fix 2024-11-12 14:31:55 +01:00
Eric Lapouyade
0f42e5a4c8 v0.19.0 2024-11-12 14:27:06 +01:00
Eric Lapouyade
bc92389ee1 Merge branch 'bartbroere-patch-1' 2024-11-12 14:08:49 +01:00
Bart Broere
0ef74a740e
Change to isinstance for type checking 2024-10-24 07:28:49 +02:00
Bart Broere
0d616add84
Apply suggestion since part.blob changes type in the loop
Co-authored-by: Chatnoir Miki <cmiki@amono.me>
2024-10-24 07:27:59 +02:00
Bart Broere
9124386b4c Fix flake8 2024-09-23 06:25:22 +00:00
Bart Broere
1cca257016 Add a test and use existing XML patching method 2024-09-23 06:24:41 +00:00
Bart Broere
a449f01f36 Fix flake8 2024-09-18 07:35:12 +00:00
Bart Broere
2812487300
Simplify the code 2024-09-17 21:19:19 +02:00
Bart Broere
2886a851e0
Change the XML library to lxml to avoid namespace renaming and other XML problems 2024-09-17 21:07:21 +02:00
Bart Broere
0061b556ec
[WIP] Support rendering variables in footnotes 2024-09-17 15:09:29 +02:00
Eric Lapouyade
0607e7175d
Merge pull request #556 from a-detiste/master 2024-08-01 01:55:58 +02:00
Alexandre Detiste
d99607003c remove leftover "six" reference 2024-07-30 23:12:40 +02:00
Eric Lapouyade
b9be3a5b50 Update CHANGES.rst 2024-07-21 16:48:36 +02:00
Eric Lapouyade
83ecb60bdd Merge remote-tracking branch 'origin/master' 2024-07-21 16:43:01 +02:00
Eric Lapouyade
a10c3c16e4 Remove python 2.x support 2024-07-21 16:42:41 +02:00
Eric Lapouyade
d472308ae5
Merge pull request #555 from jkpet/patch-1
Update index.rst
2024-07-21 16:22:06 +02:00
Eric Lapouyade
ce25ec8228
Merge pull request #554 from elapouya/dependabot/pip/setuptools-70.0.0
Bump setuptools from 69.5.1 to 70.0.0
2024-07-21 16:20:56 +02:00
Eric Lapouyade
ac38610947 Code styling 2024-07-21 16:17:47 +02:00
Eric Lapouyade
a50b52b317 PR #552 + black all files 2024-07-21 16:10:44 +02:00
Eric Lapouyade
f3ba468927
Merge pull request #552 from jeanmarcosdarosa/jeanmarcosdarosa-patch-hyperlink
Added hyperlink option in InlineImage
2024-07-21 16:01:35 +02:00
Eric Lapouyade
b7872d7723 Add poetry env 2024-07-21 15:37:37 +02:00
jkpet
0642badebc
Update index.rst
Updating 'sub-document' documentation to make explicit what was already implicit.
2024-07-19 17:03:07 +10:00
dependabot[bot]
664bb845fa
Bump setuptools from 69.5.1 to 70.0.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 69.5.1 to 70.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v69.5.1...v70.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 19:38:23 +00:00
Jean Marcos da Rosa
e0d5eb0e0a
Added hyperlink option in InlineImage 2024-07-09 10:33:26 -03:00
Eric Lapouyade
d322120d2a Fix CHANGES.rst 2024-05-01 16:56:11 +02:00
Eric Lapouyade
58de890f92 v0.17.0 -> requires python-docx 1.1.1+ 2024-05-01 16:50:16 +02:00
Eric Lapouyade
bee0706645
Merge pull request #527 from dralexxx/update_jinja2_version
regen pipfile.lock with newer jinja2 dependency version
2024-02-12 09:31:23 +01:00
dralex
24de22bfc6 regen pipfile.lock with newer jinja2 dependency version 2024-02-10 21:17:49 +01:00
Eric Lapouyade
d43c6debbf Changed README.rst 2023-05-08 11:44:44 +02:00
Eric Lapouyade
d539119fed PR #493 2023-05-08 11:15:41 +02:00
Eric Lapouyade
d042d4592c Merge branch 'quintagroup-fix/resolve_listing_function' 2023-05-08 11:12:28 +02:00
Adrian Vorobel
af2e09209b Fix: add whitespace to regexp, as they capture wrong xml tags (w:tc, w:tcBorders, etc.) 2023-05-05 12:44:04 +03:00
elapouya
d9801fd6e4 PR #482 2023-03-12 11:04:03 +01:00
Eric Lapouyade
6e51079b2a
Merge pull request #482 from dreizehnutters/master
fixed element properties for highlighted text
2023-03-12 10:41:57 +01:00
geb
9af9d9e69b
fixed element properties for highlighted text 2023-03-01 09:07:18 +01:00
elapouya
708581fa6e remove unused code in doc_properties.py 2023-01-07 14:09:28 +01:00
elapouya
e3b40ec959 Remove python 3.6 github style checking 2023-01-07 14:05:32 +01:00
elapouya
724623671c Merge branch 'Slarag-patch-doc-properties' 2023-01-07 14:02:44 +01:00
elapouya
ef09ce6277 0.16.5 2023-01-07 14:02:11 +01:00
Slarag
ce23f81cbe Fix #464 2022-11-18 14:18:32 +01:00
Eric Lapouyade
5d9bba118a
Merge pull request #455 from giancab25/documentation-edits-for-subdoc
Improved the readability of the Sub-documents section of documentation.
2022-09-11 11:19:24 +02:00
giancab25
006d5bc506 Improved the readability of the Sub-documents section of the documentation 2022-08-24 20:28:32 -04:00
Eric Lapouyade
4047845c3a Reorganize documentation 2022-08-05 11:10:11 +02:00
Eric Lapouyade
1a4a5f5723 Reorganize documentation 2022-08-05 11:04:07 +02:00
Eric Lapouyade
650b28d852 flake8 issue 2022-08-04 10:31:45 +02:00
Eric Lapouyade
890549d705 Merge remote-tracking branch 'origin/master' 2022-08-04 10:28:03 +02:00
Eric Lapouyade
52d9b28a31 Merge branch 'sailingnn-master' 2022-08-04 10:27:29 +02:00
Eric Lapouyade
4857a6ed72 Modify PR to be more generic 2022-08-04 10:27:02 +02:00
Eric Lapouyade
bd4b2be81d
Merge pull request #449 from krysros/patch-1
Typo in comment
2022-08-04 10:18:25 +02:00
Eric Lapouyade
ea987f9e38 Merge branch 'master' of https://github.com/sailingnn/python-docx-template into sailingnn-master 2022-08-04 09:46:44 +02:00
sailingnn
a9122c34d4
Update richtext_eastAsia.py 2022-08-04 09:05:06 +08:00
sailingnn
b75867397a
add eastAsia template
adding eastAsia template for test
2022-08-04 09:01:33 +08:00
sailingnn
badce481c6
add eastAsia test
add eastAsia test
2022-08-04 08:58:59 +08:00
Eric Lapouyade
85a7b92bf3
Merge pull request #451 from timgates42/bugfix_typos
docs: Fix a few typos
2022-07-25 12:10:21 +02:00
sailingnn
0660a0f37e
Update richtext.py
add support for eastAsia fonts in RichText
2022-07-21 09:01:30 +08:00
Tim Gates
2fd2140caf
docs: Fix a few typos
There are small typos in:
- docs/index.rst
- docxtpl/__main__.py
- docxtpl/template.py
- tests/embedded.py

Fixes:
- Should read `surrounding` rather than `surronding`.
- Should read `rendering` rather than `rendring`.
- Should read `providing` rather than `provinding`.
- Should read `optionally` rather than `optionnally`.
- Should read `optional` rather than `optionnal`.
- Should read `necessary` rather than `neccessary`.
- Should read `existing` rather than `exsting`.
- Should read `embedded` rather than `embdded`.
- Should read `dictionaries` rather than `dictionnaries`.
- Should read `cleaning` rather than `cleanning`.
- Should read `catches` rather than `cacthes`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-07-21 08:16:56 +10:00
Krystian Rosiński
d4415c34a4
Typo in comment 2022-07-17 13:54:19 +02:00
Eric Lapouyade
a712baff03 fix #448 2022-07-14 18:16:20 +02:00
Eric Lapouyade
7816ea6270 update changelog 2022-07-14 15:25:24 +02:00
Eric Lapouyade
5f21feb72d fix #444 2022-07-14 15:06:21 +02:00
elapouya
7290528b3d flake8 fix 2022-06-12 16:28:02 +02:00
elapouya
a0f6589d34 PR #442 2022-06-12 16:17:27 +02:00
elapouya
0ab1144a8d Merge branch 'karolzlot-type_hints1' 2022-06-12 16:15:46 +02:00
elapouya
6da5022b41 Merge branch 'type_hints1' of https://github.com/karolzlot/python-docx-template into karolzlot-type_hints1
PR #442
2022-06-12 16:02:44 +02:00
Karol Zlot
bda05f2101 return None 2022-06-12 10:44:24 +02:00
Karol Zlot
25a6e42850 Reverse name changes 2022-06-12 10:43:03 +02:00
elapouya
624bc15a10 fix #443 2022-06-12 10:30:44 +02:00
Karol Zlot
43ceb38465 Add type hints to get_undeclared_template_variables 2022-06-06 23:49:48 +02:00
Karol Zlot
2681c4801d Add first type hints 2022-06-06 23:44:07 +02:00
elapouya
c1f994e632 Changed escape.py test, see #346 2022-05-21 15:06:24 +02:00
Eric Lapouyade
9b4e623c3c Add documentation for jinja-like comments in templates 2022-04-16 12:18:07 +02:00
Eric Lapouyade
6bfd2eb85a PR #427 2022-04-16 11:02:40 +02:00
Eric Lapouyade
d283f3f88a PR #427 2022-04-16 10:58:42 +02:00
Eric Lapouyade
b4e533ae66 PR #427 2022-04-16 10:55:42 +02:00
Staffan Malmgren
4e073ea451 Added support for jinja comments for paragraphs and table rows+cells 2022-03-28 23:43:34 +02:00
Eric Lapouyade
8a3c2051ad fix codestyle.yml 2022-03-12 18:43:11 +01:00
Eric Lapouyade
9a7ac8f4e9 fix codestyle.yml 2022-03-12 18:40:52 +01:00
Eric Lapouyade
00b9a97222 fix codestyle.yml 2022-03-12 18:39:59 +01:00
Eric Lapouyade
605844aa17 fix codestyle.ymle 2022-03-12 18:38:25 +01:00
Eric Lapouyade
b95a23395f fix #422 2022-03-12 18:31:03 +01:00
Eric Lapouyade
23038b98de fix #408 2022-01-12 15:25:07 +01:00
Eric Lapouyade
19733a6b5b
Merge pull request #402 from 42sol-eu/master
Add documentation for get_undeclared_template_variables() method
2021-12-30 12:07:43 +01:00
Andreas **Felix** Häberle
d2037d2a54
added interface function to check variables in document 2021-12-22 18:49:35 +01:00
Eric Lapouyade
e574b6c650 remove f-string for python < 3.6 compability 2021-12-20 16:31:29 +01:00
Eric Lapouyade
589262664f fix #392 2021-12-20 12:07:12 +01:00
Eric Lapouyade
d22aeb41db Fix #398 2021-12-19 12:18:57 +01:00
Eric Lapouyade
4a9acdcf79
Merge pull request #386 from Lcrs123/make_module_executable
Improvements for template used in test module_execute.py
2021-10-06 15:10:06 +02:00
Lucas
234ec65e9d improvements for test template 2021-10-06 09:13:39 -03:00
Eric Lapouyade
e3e1ab58d1 Fix typo 2021-10-01 16:09:11 +02:00
Eric Lapouyade
727d07fa3e Changed code to be python < 3.6 compatible 2021-10-01 12:34:32 +02:00
Eric Lapouyade
0238472033 Changed code to be python < 3.6 compatible 2021-10-01 12:29:56 +02:00
Eric Lapouyade
5e8aeae272 Changed code to be python < 3.6 compatible 2021-10-01 12:25:45 +02:00
Eric Lapouyade
5dfe448a9a Changed code to be python < 3.6 compatible 2021-10-01 12:20:46 +02:00
Eric Lapouyade
541d5ce7cb Changed code to be python < 3.6 compatible 2021-10-01 12:11:43 +02:00
Eric Lapouyade
b0a5de05aa Codestyling 2021-10-01 08:31:19 +02:00
Eric Lapouyade
d900f9296a Merge branch 'Lcrs123-make_module_executable' 2021-09-30 17:08:23 +02:00
Eric Lapouyade
1f3ef9b236 Add quiet option, update doc, modify test 2021-09-30 17:08:05 +02:00
Lucas
e4737ddfbb Added test for main function, template and json for running test 2021-09-30 09:41:58 -03:00
Lucas
7b2d7e1946 Added argument for overwrite without confirmation and constants for easier referencing of arguments 2021-09-30 09:39:40 -03:00
Lucas
2dee2ddd1c Small change in error message for main() 2021-09-29 13:04:47 -03:00
Lucas
5f3fe03108 Small change in error catching in validate function 2021-09-29 11:04:42 -03:00
Lucas
b6f9c992f5 Added function for rendering docx instead of directly calling method on DocxTemplate instance. Added exception chaining to a few functions there were missing it. 2021-09-29 10:35:26 -03:00
Lucas
a78462ee0f Reformat, added comments and small fixes in error catching 2021-09-29 09:46:16 -03:00
Lucas
8004fcf6bc Added basic functionality to make the module executable 2021-09-28 15:25:09 -03:00
Lucas
a45cc806d8 Added basic functionality to make the module executable 2021-09-28 15:12:29 -03:00
Eric Lapouyade
21a0a46a43
Merge pull request #376 from DeanWunder/patch-1
Update index.rst, fixed a misspell.
2021-08-16 01:20:41 +02:00
DeanWunder
9708802400
Update index.rst, fixed a misspell. 2021-08-16 08:39:08 +10:00
Eric Lapouyade
61c8924663 V0.12.0 2021-08-15 15:50:45 +02:00
Eric Lapouyade
b7d782ab20 Should solve #372 2021-08-15 02:58:50 +02:00
Eric Lapouyade
6722d63537 code styling 2021-07-31 14:16:23 +02:00
Eric Lapouyade
bf2240f4b2 code styling 2021-07-31 14:12:22 +02:00
Eric Lapouyade
ab61d39847 code styling 2021-07-31 13:53:44 +02:00
Eric Lapouyade
f780714a52 Added requirements.txt 2021-07-31 13:44:28 +02:00
Eric Lapouyade
30712bbc67 Working on better subdocs 2021-07-31 12:49:47 +02:00
Eric Lapouyade
0a1cb24b12 Update issue templates 2021-07-16 11:18:49 +02:00
Eric Lapouyade
7a65b2a310 Update issue templates 2021-07-16 11:07:42 +02:00
Eric Lapouyade
a435b1c46a Update issue templates 2021-07-16 11:05:16 +02:00
Eric Lapouyade
ef622f28e5 Update issue templates 2021-07-16 11:02:03 +02:00
Eric Lapouyade
44fb098904 Update issue templates 2021-07-16 11:00:01 +02:00
Eric Lapouyade
ba9a4629e9 PR #351 2021-05-09 12:36:28 +02:00
Eric Lapouyade
60151d6677 Merge branch 'alanoe-ae/support_img_title_and_desc_in_replace_pic' 2021-05-09 12:32:12 +02:00
75 changed files with 4183 additions and 1449 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
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 Normal file
View File

@ -0,0 +1,20 @@
---
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.

View File

@ -0,0 +1,20 @@
---
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.

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -22,4 +22,4 @@ jobs:
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
flake8 . --count --max-line-length=127 --show-source --statistics

35
.readthedocs.yaml Normal file
View File

@ -0,0 +1,35 @@
# 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

View File

@ -1,17 +1,112 @@
0.11.4 (2021-04-06)
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
0.11.2 (2020-11-09)
-------------------
- fix #323
0.11.1 (2020-10-27)
-------------------
- fix #320
0.11.0 (2020-10-19)
-------------------
- \\n, \\a, \\t and \\f are now accepted in simple context string. Thanks to chabErch@github
0.10.5 (2020-10-15)

10
Pipfile
View File

@ -1,13 +1,13 @@
[[source]]
url = "https://pypi.python.org/simple"
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[dev-packages]
"e1839a8" = {path = ".", editable = true}
[packages]
[dev-packages]
docxtpl = {editable = true, path = "."}
flake8 = "*"
[requires]
python_version = "3.6"
python_version = "3"

514
Pipfile.lock generated
View File

@ -1,181 +1,419 @@
{
"_meta": {
"hash": {
"sha256": "23d67e77e436d7d9001f9d16dcfd0a12cc15bc444b55aace59c1d999efbb2680"
"sha256": "0368a5e08ceb2b4910a110742515b5ff1d04a3a3af2b91b49d922ef9aaab6915"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"flake8": {
"hashes": [
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
],
"index": "pypi",
"version": "==3.8.4"
},
"importlib-metadata": {
"hashes": [
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
],
"markers": "python_version < '3.8'",
"version": "==2.0.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pyflakes": {
"hashes": [
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"zipp": {
"hashes": [
"sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
"sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
],
"markers": "python_version >= '3.6'",
"version": "==3.4.0"
}
},
"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": "."
},
"e1839a8": {
"editable": true,
"path": "."
"flake8": {
"hashes": [
"sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343",
"sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"
],
"index": "pypi",
"version": "==7.2.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.2"
"markers": "python_version >= '3.7'",
"version": "==3.1.6"
},
"lxml": {
"hashes": [
"sha256:098fb713b31050463751dcc694878e1d39f316b86366fb9fe3fbbe5396ac9fab",
"sha256:0e89f5d422988c65e6936e4ec0fe54d6f73f3128c80eb7ecc3b87f595523607b",
"sha256:189ad47203e846a7a4951c17694d845b6ade7917c47c64b29b86526eefc3adf5",
"sha256:1d87936cb5801c557f3e981c9c193861264c01209cb3ad0964a16310ca1b3301",
"sha256:211b3bcf5da70c2d4b84d09232534ad1d78320762e2c59dedc73bf01cb1fc45b",
"sha256:2358809cc64394617f2719147a58ae26dac9e21bae772b45cfb80baa26bfca5d",
"sha256:23c83112b4dada0b75789d73f949dbb4e8f29a0a3511647024a398ebd023347b",
"sha256:24e811118aab6abe3ce23ff0d7d38932329c513f9cef849d3ee88b0f848f2aa9",
"sha256:2d5896ddf5389560257bbe89317ca7bcb4e54a02b53a3e572e1ce4226512b51b",
"sha256:2d6571c48328be4304aee031d2d5046cbc8aed5740c654575613c5a4f5a11311",
"sha256:2e311a10f3e85250910a615fe194839a04a0f6bc4e8e5bb5cac221344e3a7891",
"sha256:302160eb6e9764168e01d8c9ec6becddeb87776e81d3fcb0d97954dd51d48e0a",
"sha256:3a7a380bfecc551cfd67d6e8ad9faa91289173bdf12e9cfafbd2bdec0d7b1ec1",
"sha256:3d9b2b72eb0dbbdb0e276403873ecfae870599c83ba22cadff2db58541e72856",
"sha256:475325e037fdf068e0c2140b818518cf6bc4aa72435c407a798b2db9f8e90810",
"sha256:4b7572145054330c8e324a72d808c8c8fbe12be33368db28c39a255ad5f7fb51",
"sha256:4fff34721b628cce9eb4538cf9a73d02e0f3da4f35a515773cce6f5fe413b360",
"sha256:56eff8c6fb7bc4bcca395fdff494c52712b7a57486e4fbde34c31bb9da4c6cc4",
"sha256:573b2f5496c7e9f4985de70b9bbb4719ffd293d5565513e04ac20e42e6e5583f",
"sha256:7ecaef52fd9b9535ae5f01a1dd2651f6608e4ec9dc136fc4dfe7ebe3c3ddb230",
"sha256:803a80d72d1f693aa448566be46ffd70882d1ad8fc689a2e22afe63035eb998a",
"sha256:8862d1c2c020cb7a03b421a9a7b4fe046a208db30994fc8ff68c627a7915987f",
"sha256:9b06690224258db5cd39a84e993882a6874676f5de582da57f3df3a82ead9174",
"sha256:a71400b90b3599eb7bf241f947932e18a066907bf84617d80817998cee81e4bf",
"sha256:bb252f802f91f59767dcc559744e91efa9df532240a502befd874b54571417bd",
"sha256:be1ebf9cc25ab5399501c9046a7dcdaa9e911802ed0e12b7d620cd4bbf0518b3",
"sha256:be7c65e34d1b50ab7093b90427cbc488260e4b3a38ef2435d65b62e9fa3d798a",
"sha256:c0dac835c1a22621ffa5e5f999d57359c790c52bbd1c687fe514ae6924f65ef5",
"sha256:c152b2e93b639d1f36ec5a8ca24cde4a8eefb2b6b83668fcd8e83a67badcb367",
"sha256:d182eada8ea0de61a45a526aa0ae4bcd222f9673424e65315c35820291ff299c",
"sha256:d18331ea905a41ae71596502bd4c9a2998902328bbabd29e3d0f5f8569fabad1",
"sha256:d20d32cbb31d731def4b1502294ca2ee99f9249b63bc80e03e67e8f8e126dea8",
"sha256:d4ad7fd3269281cb471ad6c7bafca372e69789540d16e3755dd717e9e5c9d82f",
"sha256:d6f8c23f65a4bfe4300b85f1f40f6c32569822d08901db3b6454ab785d9117cc",
"sha256:d84d741c6e35c9f3e7406cb7c4c2e08474c2a6441d59322a00dcae65aac6315d",
"sha256:e65c221b2115a91035b55a593b6eb94aa1206fa3ab374f47c6dc10d364583ff9",
"sha256:f98b6f256be6cec8dd308a8563976ddaff0bdc18b730720f6f4bee927ffe926f"
"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 >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.6.1"
"markers": "python_version >= '3.6'",
"version": "==5.4.0"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
"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 >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
"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:bc76ecac6b2d00ce6442a69d03a6f35c71cd72293cd8405a7472dfe317920024"
"sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe",
"sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"
],
"version": "==0.8.10"
"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:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
"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"
}
}
}

View File

@ -21,10 +21,6 @@ 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.
Share
-----
If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template
Documentation
-------------
@ -34,8 +30,9 @@ Please, `read the doc <http://docxtpl.readthedocs.org>`_
Other projects
--------------
Have a look at some of my 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

View File

@ -26,33 +26,33 @@
# 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'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'python-docx-template'
copyright = u'2015, Eric Lapouyade'
project = "python-docx-template"
copyright = "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.9'
version = "0.20"
# The full version, including alpha/beta/rc tags.
release = '0.9.x'
release = "0.20.x"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -66,7 +66,7 @@ release = '0.9.x'
# 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.
@ -84,7 +84,7 @@ exclude_patterns = ['_build']
# 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 = []
@ -97,7 +97,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = "sphinx_book_theme"
# 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
@ -126,7 +126,7 @@ html_theme = 'default'
# 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
@ -175,7 +175,7 @@ html_static_path = ['_static']
# 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 ---------------------------------------------
@ -193,8 +193,13 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'python-docx-template.tex', u'python-docx-template Documentation',
u'Eric Lapouyade', 'manual'),
(
"index",
"python-docx-template.tex",
"python-docx-template Documentation",
"Eric Lapouyade",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
@ -223,8 +228,13 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'python-docx-template', u'python-docx-template Documentation',
[u'Eric Lapouyade'], 1)
(
"index",
"python-docx-template",
"python-docx-template Documentation",
["Eric Lapouyade"],
1,
)
]
# If true, show URL addresses after external links.
@ -237,9 +247,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'python-docx-template', u'python-docx-template Documentation',
u'Eric Lapouyade', 'python-docx-template', 'One line description of project.',
'Miscellaneous'),
(
"index",
"python-docx-template",
"python-docx-template Documentation",
"Eric Lapouyade",
"python-docx-template",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.

View File

@ -42,8 +42,6 @@ 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
------------------
@ -64,51 +62,102 @@ 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).
**Important:**
Always put space after a jinja2 starting var/tag delimiter and a space before the ending one :
Avoid::
{{myvariable}}
{%if something%}
Use instead::
{{ myvariable }}
{% if something %}
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 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 begin and ending tags are located and only takes care about what is in between.
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 ::
Do not use this::
{%p if display_paragraph %}Here is my paragraph {%p endif %}
But use this instead in your docx template ::
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 and
``{%p`` tags are not in the same paragraph in the second case.
This syntax is possible because MS Word considers each line as a new paragraph (if you do not use SHIFT-RETURN).
Display variables
.................
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'::
{{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
....................
@ -134,33 +183,35 @@ One can use *ENTER* or *SHIFT+ENTER* to split a text like below, then use ``{%-`
**IMPORTANT 2 :** ``{%- xxx -%}`` tags must be alone in a line : do not add some text before or after on the same line.
Escaping delimiters
...................
Display variables
.................
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use::
As part of jinja2, one can used double braces::
{_%, %_}, {_{ or }_}
{{ <var> }}
Tables
......
if ``<var>`` is a string, ``\n``, ``\a``, ``\t`` and ``\f`` will be translated respectively into newlines, new paragraphs, tabs and page breaks
Spanning
~~~~~~~~
But if ``<var>`` is a RichText_ object, you must specify that you are changing the actual 'run'::
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
{{r <var> }}
{% colspan <var> %}
Note the ``r`` right after the openning braces.
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
**IMPORTANT** : Do not use the ``r`` variable in your template because ``{{r}}`` could be interpreted as a ``{{r``
without variable specified. Nevertheless, you can use a bigger variable name starting
with 'r'. For example ``{{render_color}}`` will be interpreted as ``{{ render_color }}`` not as ``{{r ender_color}}``.
You can also span horizontally within a for loop (see tests/horizontal_merge.py)::
**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.
{% hm %}
You can also merge cells vertically within a for loop (see tests/vertical_merge.py)::
{% vm %}
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::
@ -168,24 +219,6 @@ There is a special case when you want to change the background color of a table
`<var>` must contain the color's hexadecimal code *without* the hash sign
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:
RichText
@ -194,14 +227,31 @@ 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 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, font and so on, but the best way is to use Microsoft Word to define your own *character* 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
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
+++++++++++++++++++++++
@ -213,6 +263,14 @@ You can add an hyperlink to a text by using a Richtext with this syntax::
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
------------
@ -221,20 +279,45 @@ just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of
myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))
You just have to specify the template object, the image file path and optionnally width and/or height.
You just have to specify the template object, the image file path and optionally 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
-------------
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`.
> Need to install with the subdoc extra: `pip install "docxtpl[subdoc]"`
Escaping, newline, new paragraph, Listing
-----------------------------------------
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`.
When you use a ``{{ <var> }}``, you are modifying an **XML** word document, this means you cannot use all chars,
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 :
* ``context = { 'var':R('my text') }`` and ``{{r <var> }}`` in the template (note the ``r``),
@ -242,12 +325,9 @@ especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. T
* ``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, new paragraph, and page break features : just use ``\n``, ``\a``, ``\t`` or ``\f`` 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``, ``\a``, and ``\f``
you can use the ``Listing`` class :
in your python code::
@ -255,6 +335,7 @@ 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
@ -300,10 +381,10 @@ It works like medias replacement, except it is for embedded objects like embedde
Syntax to replace embedded_dummy.docx::
tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx')
tpl.replace_embedded('embedded_dummy.docx','embedded_docx_i_want.docx')
WARNING : unlike replace_pic() method, embdded_dummy.docx MUST exist in the template directory when rendering and saving the generated docx. It must be the same
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
file as the one inserted manually in the docx template.
The replacement occurs in headers, footers and the whole document's body.
@ -320,6 +401,26 @@ The zipname starts with "word/embeddings/". Note that the file to be replaced is
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
@ -327,7 +428,7 @@ 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 provinding spaces : They will be ignored.
beginning with a jinja2 tag providing spaces : They will be ignored.
To solve these problem, the solution is to use Richtext::
tpl.render({
@ -340,25 +441,11 @@ And in your template, use the {{r notation::
{{r test_space_r}} Spaces will be preserved
{{r test_tabs_r}} Tabs will be displayed
Tables
------
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
{% colspan <number of column to span> %}
or 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 %}
Jinja custom filters
--------------------
``render()`` accepts ``jinja_env`` optionnal argument : you may pass a jinja environment object.
``render()`` accepts ``jinja_env`` optional 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
@ -378,6 +465,30 @@ 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
--------

View File

@ -4,849 +4,15 @@ Created : 2015-03-12
@author: Eric Lapouyade
"""
__version__ = '0.11.4'
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
__version__ = "0.20.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 html import escape
from .subdoc import Subdoc
except ImportError:
# cgi.escape is deprecated in python 3.7
from cgi import escape
import re
import six
import binascii
import os
import zipfile
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 = {}
self.zipname_to_replace = {}
self.pics_to_replace = {}
self.pic_map = {}
self.current_rendering_part = None
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):
""" Make a lots of cleanning 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 %} )
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})', '',
src_xml, flags=re.DOTALL)
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
# same thing with {% ... %}
# "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 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)
# 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"&#8216;", "'")
.replace('&lt;', '<')
.replace('&gt;', '>')
.replace(u'', u'"')
.replace(u'', u'"')
.replace(u"", u"'")
.replace(u"", u"'"))
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 = src_xml.replace(r'<w:p>', '\n<w:p>')
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)])
raise exc
dst_xml = dst_xml.replace('\n<w:p>', '<w:p>')
dst_xml = (dst_xml
.replace('{_{', '{{')
.replace('}_}', '}}')
.replace('{_%', '{%')
.replace('%_}', '%}'))
dst_xml = self.resolve_listing(dst_xml)
return dst_xml
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, jinja_env=None, autoescape=False):
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)
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)
# 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 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') 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 new_subdoc(self, docpath=None):
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 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 six.iteritems(part.rels):
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
self._replace_docx_part_pics(rel.target_part, replaced_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 six.iteritems(self.pics_to_replace):
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):
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK,
is_external=True)
def save(self, filename, *args, **kwargs):
self.pre_processing()
self.docx.save(filename, *args, **kwargs)
self.post_processing(filename)
def get_undeclared_template_variables(self, jinja_env=None):
xml = self.get_xml()
xml = self.patch_xml(xml)
for uri in [self.HEADER_URI, self.FOOTER_URI]:
for relKey, part in self.get_headers_footers(uri):
_xml = self.get_part_xml(part)
xml += self.patch_xml(_xml)
if jinja_env:
env = jinja_env
else:
env = Environment()
parse_content = env.parse(xml)
return meta.find_undeclared_variables(parse_content)
undeclared_template_variables = property(get_undeclared_template_variables)
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)
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()
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):
# If a RichText is added
if isinstance(text, RichText):
self.xml += text.xml
return
# If not a string : cast to string (ex: int, dict etc...)
if not isinstance(text, (six.text_type, six.binary_type)):
text = six.text_type(text)
if not isinstance(text, six.text_type):
text = text.decode('utf-8', errors='ignore')
text = escape(text)
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 subscript:
prop += u'<w:vertAlign w:val="subscript"/>'
if superscript:
prop += u'<w:vertAlign w:val="superscript"/>'
if bold:
prop += u'<w:b/>'
if italic:
prop += u'<w:i/>'
if underline:
if underline not in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']:
underline = 'single'
prop += u'<w:u w:val="%s"/>' % underline
if strike:
prop += u'<w:strike/>'
if font:
prop += (u'<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"/>'
.format(font=font))
xml = u'<w:r>'
if prop:
xml += u'<w:rPr>%s</w:rPr>' % prop
xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
if url_id:
xml = (u'<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
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):
# If not a string : cast to string (ex: int, dict etc...)
if not isinstance(text, (six.text_type, six.binary_type)):
text = six.text_type(text)
self.xml = escape(text)
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
def __html__(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.current_rendering_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()
def __html__(self):
return self._insert_image()
pass

175
docxtpl/__main__.py Normal file
View File

@ -0,0 +1,175 @@
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()

78
docxtpl/inline_image.py Normal file
View File

@ -0,0 +1,78 @@
# -*- 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()

35
docxtpl/listing.py Normal file
View File

@ -0,0 +1,35 @@
# -*- 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

180
docxtpl/richtext.py Normal file
View File

@ -0,0 +1,180 @@
# -*- 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

103
docxtpl/subdoc.py Normal file
View File

@ -0,0 +1,103 @@
# -*- 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()

926
docxtpl/template.py Normal file
View File

@ -0,0 +1,926 @@
# -*- 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"&#8216;", "'")
.replace("&lt;", "<")
.replace("&gt;", ">")
.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 Normal file

File diff suppressed because it is too large Load Diff

70
pyproject.toml Normal file
View File

@ -0,0 +1,70 @@
[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"

View File

@ -1,4 +1,5 @@
six
python-docx
docxcompose
jinja2
lxml
sphinx-book-theme

View File

@ -1,2 +0,0 @@
[bdist_wheel]
universal = 1

View File

@ -1,8 +1,9 @@
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
@ -10,13 +11,13 @@ import sys
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
@ -27,13 +28,15 @@ 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')
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
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:
@ -43,27 +46,32 @@ def get_version(pkg):
raise RuntimeError("Unable to find __version__ string in %s." % path)
setup(name='docxtpl',
version=get_version('docxtpl'),
description='Python docx template engine',
long_description=long_description,
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.4",
],
keywords='jinja2',
url='https://github.com/elapouya/python-docx-template',
author='Eric Lapouyade',
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)
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 :: 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",
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,
)

View File

@ -1,42 +1,42 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/cellbg_tpl.docx')
tpl = DocxTemplate("templates/cellbg_tpl.docx")
context = {
'alerts': [
"alerts": [
{
'date': '2015-03-10',
'desc': RichText('Very critical alert', color='FF0000', bold=True),
'type': 'CRITICAL',
'bg': 'FF0000',
"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-11",
"desc": RichText("Just a warning"),
"type": "WARNING",
"bg": "FFDD00",
},
{
'date': '2015-03-12',
'desc': RichText('Information'),
'type': 'INFO',
'bg': '8888FF',
"date": "2015-03-12",
"desc": RichText("Information"),
"type": "INFO",
"bg": "8888FF",
},
{
'date': '2015-03-13',
'desc': RichText('Debug trace'),
'type': 'DEBUG',
'bg': 'FF00FF',
"date": "2015-03-13",
"desc": RichText("Debug trace"),
"type": "DEBUG",
"bg": "FF00FF",
},
],
}
tpl.render(context)
tpl.save('output/cellbg.docx')
tpl.save("output/cellbg.docx")

6
tests/comments.py Normal file
View File

@ -0,0 +1,6 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/comments_tpl.docx")
tpl.render({})
tpl.save("output/comments.docx")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: sandeeprah, Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
import jinja2
@ -14,7 +14,7 @@ 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_value = value + " " + my_string_arg
return return_value
@ -24,12 +24,12 @@ def my_filterB(value, my_float_arg):
# Then, declare them to jinja like this :
jinja_env.filters['my_filterA'] = my_filterA
jinja_env.filters['my_filterB'] = my_filterB
jinja_env.filters["my_filterA"] = my_filterA
jinja_env.filters["my_filterB"] = my_filterB
context = {'base_value_string': ' Hello', 'base_value_float': 1.5}
context = {"base_value_string": " Hello", "base_value_float": 1.5}
tpl = DocxTemplate('templates/custom_jinja_filters_tpl.docx')
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
tpl.render(context, jinja_env)
tpl.save('output/custom_jinja_filters.docx')
tpl.save("output/custom_jinja_filters.docx")

10
tests/doc_properties.py Normal file
View File

@ -0,0 +1,10 @@
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")

View File

@ -1,15 +1,15 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/dynamic_table_tpl.docx')
tpl = DocxTemplate("templates/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("output/dynamic_table.docx")

View File

@ -1,45 +1,45 @@
# -*- 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("templates/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("output/embedded_embedded_docx.docx")
# rendring the main document :
tpl = DocxTemplate('templates/embedded_main_tpl.docx')
# rendering the main document :
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
context = {
'name': 'John Doe',
"name": "John Doe",
}
tpl.replace_embedded(
'templates/embedded_dummy.docx', 'templates/embedded_static_docx.docx'
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
)
tpl.replace_embedded(
'templates/embedded_dummy2.docx', 'output/embedded_embedded_docx.docx'
"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'
"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',
"word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx",
"templates/real_PowerPoint.pptx",
)
tpl.render(context)
tpl.save('output/embedded.docx')
tpl.save("output/embedded.docx")

View File

@ -1,19 +1,19 @@
from docxtpl import DocxTemplate, R, Listing
tpl = DocxTemplate('templates/escape_tpl.docx')
tpl = DocxTemplate("templates/escape_tpl.docx")
context = {
'myvar': R(
"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 : <>& ...'
"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",
),
'page_break': R('\f'),
'new_listing': """
"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
@ -21,34 +21,34 @@ 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>'
"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>"
),
}
tpl.render(context)
tpl.save('output/escape.docx')
tpl.save("output/escape.docx")

View File

@ -5,25 +5,23 @@
import os
from unicodedata import name
from six import iteritems, text_type
from docxtpl import DocxTemplate
XML_RESERVED = """<"&'>"""
tpl = DocxTemplate('templates/escape_tpl_auto.docx')
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
context = {
'nested_dict': {name(text_type(c)): c for c in XML_RESERVED},
'autoescape': 'Escaped "str & ing"!',
'autoescape_unicode': u'This is an escaped <unicode> example \u4f60 & \u6211',
'iteritems': iteritems,
"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'
OUTPUT = "output"
if not os.path.exists(OUTPUT):
os.makedirs(OUTPUT)
tpl.save(OUTPUT + '/escape_auto.docx')
tpl.save(OUTPUT + "/escape_auto.docx")

17
tests/footnotes.py Normal file
View File

@ -0,0 +1,17 @@
# -*- 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)

View File

@ -0,0 +1,180 @@
#!/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.")

View File

@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/header_footer_tpl.docx')
tpl = DocxTemplate("templates/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'
"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("output/header_footer.docx")

View File

@ -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("templates/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("output/header_footer_entities.docx")

View File

@ -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 = "output/header_footer_image.docx"
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
tpl = DocxTemplate("templates/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("templates/dummy_pic_for_header.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -1,29 +1,29 @@
# -*- 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'
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')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
context = {
'mycompany': 'The World Wide company',
"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())
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')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
dummy_pic.seek(0)
new_image.seek(0)
tpl.replace_media(dummy_pic, new_image)
@ -32,5 +32,5 @@ tpl.render(context)
file_obj = io.BytesIO()
tpl.save(file_obj)
file_obj.seek(0)
with open(DEST_FILE2, 'wb') as f:
with open(DEST_FILE2, "wb") as f:
f.write(file_obj.read())

View File

@ -1,19 +1,24 @@
# -*- 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')
tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx")
context = {
'inline_image': InlineImage(tpl, 'templates/django.png', height=Mm(10))
"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')
tpl.save("output/header_footer_inline_image.docx")

View File

@ -1,27 +1,28 @@
# -*- 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("templates/header_footer_tpl_utf8.docx")
sd = tpl.new_subdoc()
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'
"This is a sub-document to check it does not break header and footer with utf-8 "
"characters inside the template .docx"
)
context = {
'title': u'헤더와 푸터',
'company_name': u'세계적 회사',
'date': u'2016-03-17',
'mysubdoc': sd,
"title": "헤더와 푸터",
"company_name": "세계적 회사",
"date": "2016-03-17",
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('output/header_footer_utf8.docx')
tpl.save("output/header_footer_utf8.docx")

View File

@ -2,6 +2,6 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/horizontal_merge_tpl.docx')
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
tpl.render({})
tpl.save('output/horizontal_merge.docx')
tpl.save("output/horizontal_merge.docx")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-01-14
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, InlineImage
@ -11,37 +11,41 @@ from docxtpl import DocxTemplate, InlineImage
from docx.shared import Mm
import jinja2
tpl = DocxTemplate('templates/inline_image_tpl.docx')
tpl = DocxTemplate("templates/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)
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
"myimageratio": InlineImage(
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
),
'frameworks': [
"frameworks": [
{
'image': InlineImage(tpl, 'templates/django.png', height=Mm(10)),
'desc': 'The web framework for perfectionists with deadlines',
"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/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/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/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.',
"image": InlineImage(tpl, "templates/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.save("output/inline_image.docx")

View File

@ -1,5 +1,5 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/less_cells_after_loop_tpl.docx')
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
tpl.render({})
tpl.save('output/less_cells_after_loop.docx')
tpl.save("output/less_cells_after_loop.docx")

19
tests/merge_docx.py Normal file
View File

@ -0,0 +1,19 @@
# -*- 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")

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/merge_paragraph_tpl.docx')
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
context = {
'living_in_town': True,
"living_in_town": True,
}
tpl.render(context)
tpl.save('output/merge_paragraph.docx')
tpl.save("output/merge_paragraph.docx")

25
tests/module_execute.py Normal file
View File

@ -0,0 +1,25 @@
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)

40
tests/multi_rendering.py Normal file
View File

@ -0,0 +1,40 @@
# -*- 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)

View File

@ -1,45 +1,45 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2016-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/nested_for_tpl.docx')
tpl = DocxTemplate("templates/nested_for_tpl.docx")
context = {
'dishes': [
{'name': 'Pizza', 'ingredients': ['bread', 'tomato', 'ham', 'cheese']},
"dishes": [
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
{
'name': 'Hamburger',
'ingredients': ['bread', 'chopped steak', 'cheese', 'sauce'],
"name": "Hamburger",
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
},
{
'name': 'Apple pie',
'ingredients': ['flour', 'apples', 'suggar', 'quince jelly'],
"name": "Apple pie",
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
},
],
'authors': [
"authors": [
{
'name': 'Saint-Exupery',
'books': [
{'title': 'Le petit prince'},
{'title': "L'aviateur"},
{'title': 'Vol de nuit'},
"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'},
"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("output/nested_for.docx")

View File

@ -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("templates/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("output/order.docx")

View File

@ -3,12 +3,12 @@ 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')
tpl = DocxTemplate("templates/preserve_spaces_tpl.docx")
tags = ['tag_1', 'tag_2']
replacement = ['looking', 'too']
tags = ["tag_1", "tag_2"]
replacement = ["looking", "too"]
context = dict(zip(tags, replacement))
tpl.render(context)
tpl.save('output/preserve_spaces.docx')
tpl.save("output/preserve_spaces.docx")

View File

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-03
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
DEST_FILE = 'output/replace_picture.docx'
DEST_FILE = "output/replace_picture.docx"
tpl = DocxTemplate('templates/replace_picture_tpl.docx')
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
context = {}
tpl.replace_pic('python_logo.png', 'templates/python.png')
tpl.replace_pic("python_logo.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -1,52 +1,64 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/richtext_tpl.docx')
tpl = DocxTemplate("templates/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 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('\n\n<cool>')
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)
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 = RichText("an example of ")
rt_embedded.add(rt)
context = {
'example': rt_embedded,
"example": rt_embedded,
}
tpl.render(context)
tpl.save('output/richtext.docx')
tpl.save("output/richtext.docx")

View File

@ -1,16 +1,16 @@
# -*- 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("templates/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("output/richtext_and_if.docx")

View File

@ -0,0 +1,21 @@
# -*- 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")

View File

@ -0,0 +1,63 @@
"""
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")

View File

@ -1,18 +1,17 @@
import subprocess
import glob
import six
import os
tests = sorted(glob.glob('[A-Za-z]*.py'))
excludes = ['runtests.py']
tests = sorted(glob.glob("[A-Za-z]*.py"))
excludes = ["runtests.py"]
output_dir = os.path.join(os.path.dirname(__file__), 'output')
output_dir = os.path.join(os.path.dirname(__file__), "output")
if not os.path.exists(output_dir):
os.mkdir(output_dir)
for test in tests:
if test not in excludes:
six.print_('%s ...' % test)
subprocess.call(['python', './%s' % test])
print("%s ..." % test)
subprocess.call(["python", "./%s" % test])
six.print_('Done.')
print("Done.")

View File

@ -1,36 +1,36 @@
# -*- 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("templates/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("templates/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 +38,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("output/subdoc.docx")

View File

@ -1,20 +1,19 @@
from docxtpl import DocxTemplate
from jinja2.exceptions import TemplateError
import six
six.print_('=' * 80)
six.print_("Generating template error for testing (so it is safe to ignore) :")
six.print_('.' * 80)
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'})
tpl = DocxTemplate("templates/template_error_tpl.docx")
tpl.render({"test_variable": "test variable value"})
except TemplateError as the_error:
six.print_(six.text_type(the_error))
if hasattr(the_error, 'docx_context'):
six.print_("Context:")
print(str(the_error))
if hasattr(the_error, "docx_context"):
print("Context:")
for line in the_error.docx_context:
six.print_(line)
tpl.save('output/template_error.docx')
six.print_('.' * 80)
six.print_(" End of TemplateError Test ")
six.print_('=' * 80)
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.

Binary file not shown.

View File

@ -0,0 +1,8 @@
{"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.

View File

@ -1,23 +1,23 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-10-15
@author: Arthaslixin
'''
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/vertical_merge_tpl.docx')
tpl = DocxTemplate("templates/vertical_merge_tpl.docx")
context = {
'items': [
{'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'},
{'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'},
{'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'},
"items": [
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
],
'total_price': '100,000,000.00',
'category': 'Book',
"total_price": "100,000,000.00",
"category": "Book",
}
tpl.render(context)
tpl.save('output/vertical_merge.docx')
tpl.save("output/vertical_merge.docx")

View File

@ -1,5 +1,5 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate('templates/vertical_merge_nested_tpl.docx')
tpl = DocxTemplate("templates/vertical_merge_nested_tpl.docx")
tpl.render({})
tpl.save('output/vertical_merge_nested.docx')
tpl.save("output/vertical_merge_nested.docx")

View File

@ -1,12 +1,12 @@
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate('templates/word2016_tpl.docx')
tpl = DocxTemplate("templates/word2016_tpl.docx")
tpl.render(
{
'test_space': ' ',
'test_tabs': 5 * '\t',
'test_space_r': RichText(' '),
'test_tabs_r': RichText(5 * '\t'),
"test_space": " ",
"test_tabs": 5 * "\t",
"test_space_r": RichText(" "),
"test_tabs_r": RichText(5 * "\t"),
}
)
tpl.save('output/word2016.docx')
tpl.save("output/word2016.docx")