├── .gitignore ├── Makefile ├── README.rst ├── doc ├── Makefile ├── _static │ └── .gitignore ├── _themes │ └── armstrong │ │ ├── LICENSE │ │ ├── layout.html │ │ ├── rtd-themes.conf │ │ ├── static │ │ └── rtd.css_t │ │ ├── theme.conf │ │ └── theme.conf.orig ├── conf.py ├── content_types.rst ├── developer │ └── design_narratives.rst ├── index.rst └── relationship_types.rst ├── features ├── environment.py ├── open-package.feature ├── save-package.feature └── steps │ └── opc_steps.py ├── opc ├── __init__.py ├── constants.py ├── oxml.py ├── package.py ├── packuri.py ├── phys_pkg.py ├── pkgreader.py ├── pkgwriter.py └── spec.py ├── setup.py ├── tests ├── __init__.py ├── test_files │ ├── test.docx │ ├── test.pptx │ └── test.xlsx ├── test_oxml.py ├── test_package.py ├── test_packuri.py ├── test_phys_pkg.py ├── test_pkgreader.py ├── test_pkgwriter.py ├── unitdata.py └── unitutil.py ├── tox.ini └── util ├── gen_constants.py └── src_data └── part-types.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | dist 3 | doc/_build 4 | *.egg-info 5 | *.pyc 6 | _scratch 7 | Session.vim 8 | .tox 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python 2 | BEHAVE = behave 3 | SETUP = $(PYTHON) ./setup.py 4 | 5 | .PHONY: test sdist clean 6 | 7 | help: 8 | @echo "Please use \`make ' where is one or more of" 9 | @echo " accept run acceptance tests using behave" 10 | @echo " clean delete intermediate work product and start fresh" 11 | @echo " coverage run nosetests with coverage" 12 | @echo " readme update README.html from README.rst" 13 | @echo " register update metadata (README.rst) on PyPI" 14 | @echo " test run tests using setup.py" 15 | @echo " sdist generate a source distribution into dist/" 16 | @echo " upload upload distribution tarball to PyPI" 17 | 18 | accept: 19 | $(BEHAVE) --stop 20 | 21 | clean: 22 | find . -type f -name \*.pyc -exec rm {} \; 23 | rm -rf dist *.egg-info .coverage .DS_Store 24 | 25 | coverage: 26 | py.test --cov-report term-missing --cov=opc tests/ 27 | 28 | readme: 29 | rst2html README.rst >README.html 30 | open README.html 31 | 32 | register: 33 | $(SETUP) register 34 | 35 | sdist: 36 | $(SETUP) sdist 37 | 38 | test: 39 | $(SETUP) test 40 | 41 | upload: 42 | $(SETUP) sdist upload 43 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | python-opc 3 | ########## 4 | 5 | VERSION: 0.0.1d (first development release) 6 | 7 | 8 | STATUS (as of October 20 2013) 9 | ============================== 10 | 11 | First development release. Under active development. 12 | 13 | WARNING:`spike` branch is SUBJECT TO FULL REBASING at any time. You probably 14 | don't want to base a pull request on it without asking first. 15 | 16 | 17 | Vision 18 | ====== 19 | 20 | A robust, general-purpose library for manipulating Open Packaging Convention 21 | (OPC) packages, suitable as a foundation for a family of Open XML document 22 | libraries. Also to be suitable for general purpose manipulation of OPC 23 | packages, for example to access the XML and binary contents for indexing 24 | purposes and perhaps for manipulating package parts, for example to remove 25 | slide notes pages or to assemble presentations from individual slides in a 26 | library. 27 | 28 | 29 | Documentation 30 | ============= 31 | 32 | Documentation is hosted on Read The Docs (readthedocs.org) at 33 | https://python-opc.readthedocs.org/en/latest/. 34 | 35 | 36 | Reaching out 37 | ============ 38 | 39 | We'd love to hear from you if you like |po|, want a new feature, find a bug, 40 | need help using it, or just have a word of encouragement. 41 | 42 | The **mailing list** for |po| is (google groups ... ) 43 | 44 | The **issue tracker** is on github at `python-openxml/python-opc`_. 45 | 46 | Feature requests are best broached initially on the mailing list, they can be 47 | added to the issue tracker once we've clarified the best approach, 48 | particularly the appropriate API signature. 49 | 50 | .. _`python-openxml/python-opc`: 51 | https://github.com/python-openxml/python-opc 52 | 53 | 54 | Installation 55 | ============ 56 | 57 | |po| may be installed with ``pip`` if you have it available:: 58 | 59 | pip install python-opc 60 | 61 | It can also be installed using ``easy_install``:: 62 | 63 | easy_install python-opc 64 | 65 | If neither ``pip`` nor ``easy_install`` is available, it can be installed 66 | manually by downloading the distribution from PyPI, unpacking the tarball, 67 | and running ``setup.py``:: 68 | 69 | tar xvzf python-opc-0.0.1d1.tar.gz 70 | cd python-opc-0.0.1d1 71 | python setup.py install 72 | 73 | |po| depends on the ``lxml`` package. Both ``pip`` and ``easy_install`` will 74 | take care of satisfying that dependency for you, but if you use this last 75 | method you will need to install ``lxml`` yourself. 76 | 77 | 78 | Release History 79 | =============== 80 | 81 | June 23, 2013 - v0.0.1d1 82 | * Establish initial enviornment and development branches 83 | 84 | 85 | License 86 | ======= 87 | 88 | Licensed under the `MIT license`_. Short version: this code is copyrighted by 89 | me (Steve Canny), I give you permission to do what you want with it except 90 | remove my name from the credits. See the LICENSE file for specific terms. 91 | 92 | .. _MIT license: 93 | http://www.opensource.org/licenses/mit-license.php 94 | 95 | .. |po| replace:: ``python-opc`` 96 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | html: 20 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 21 | @echo 22 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " text to make text files" 38 | @echo " man to make manual pages" 39 | @echo " texinfo to make Texinfo files" 40 | @echo " info to make Texinfo files and run them through makeinfo" 41 | @echo " gettext to make PO message catalogs" 42 | @echo " changes to make an overview of all changed/added/deprecated items" 43 | @echo " linkcheck to check all external links for integrity" 44 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 45 | 46 | clean: 47 | -rm -rf $(BUILDDIR)/* 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-opc.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-opc.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-opc" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-opc" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-openxml/python-opc/e35d643ebc8c67b6c3388f38c57672a40f173010/doc/_static/.gitignore -------------------------------------------------------------------------------- /doc/_themes/armstrong/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Bay Citizen & Texas Tribune 2 | 3 | Original ReadTheDocs.org code 4 | Copyright (c) 2010 Charles Leifer, Eric Holscher, Bobby Grace 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /doc/_themes/armstrong/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | 3 | {% set script_files = script_files + [pathto("_static/searchtools.js", 1)] %} 4 | 5 | {% block htmltitle %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block footer %} 13 | 31 | 32 | 33 | {% if theme_analytics_code %} 34 | 35 | 46 | {% endif %} 47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /doc/_themes/armstrong/rtd-themes.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = rtd.css 4 | pygment_style = default 5 | show_sphinx = False 6 | 7 | [options] 8 | show_rtd = True 9 | 10 | white = #ffffff 11 | almost_white = #f8f8f8 12 | barely_white = #f2f2f2 13 | dirty_white = #eeeeee 14 | almost_dirty_white = #e6e6e6 15 | dirtier_white = #dddddd 16 | lighter_gray = #cccccc 17 | gray_a = #aaaaaa 18 | gray_9 = #999999 19 | light_gray = #888888 20 | gray_7 = #777777 21 | gray = #666666 22 | dark_gray = #444444 23 | gray_2 = #222222 24 | black = #111111 25 | light_color = #e8ecef 26 | light_medium_color = #DDEAF0 27 | medium_color = #8ca1af 28 | medium_color_link = #86989b 29 | medium_color_link_hover = #a6b8bb 30 | dark_color = #465158 31 | 32 | h1 = #000000 33 | h2 = #465158 34 | h3 = #6c818f 35 | 36 | link_color = #444444 37 | link_color_decoration = #CCCCCC 38 | 39 | medium_color_hover = #697983 40 | green_highlight = #8ecc4c 41 | 42 | 43 | positive_dark = #609060 44 | positive_medium = #70a070 45 | positive_light = #e9ffe9 46 | 47 | negative_dark = #900000 48 | negative_medium = #b04040 49 | negative_light = #ffe9e9 50 | negative_text = #c60f0f 51 | 52 | ruler = #abc 53 | 54 | viewcode_bg = #f4debf 55 | viewcode_border = #ac9 56 | 57 | highlight = #ffe080 58 | 59 | code_background = #eeeeee 60 | 61 | background = #465158 62 | background_link = #ffffff 63 | background_link_half = #ffffff 64 | background_text = #eeeeee 65 | background_text_link = #86989b 66 | -------------------------------------------------------------------------------- /doc/_themes/armstrong/static/rtd.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * rtd.css 3 | * ~~~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- sphinxdoc theme. Originally created by 6 | * Armin Ronacher for Werkzeug. 7 | * 8 | * Customized for ReadTheDocs by Eric Pierce & Eric Holscher 9 | * 10 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 11 | * :license: BSD, see LICENSE for details. 12 | * 13 | */ 14 | 15 | /* RTD colors 16 | * light blue: {{ theme_light_color }} 17 | * medium blue: {{ theme_medium_color }} 18 | * dark blue: {{ theme_dark_color }} 19 | * dark grey: {{ theme_grey_color }} 20 | * 21 | * medium blue hover: {{ theme_medium_color_hover }}; 22 | * green highlight: {{ theme_green_highlight }} 23 | * light blue (project bar): {{ theme_light_color }} 24 | */ 25 | 26 | @import url("basic.css"); 27 | 28 | /* PAGE LAYOUT -------------------------------------------------------------- */ 29 | 30 | body { 31 | font: 100%/1.5 "ff-meta-web-pro-1","ff-meta-web-pro-2",Arial,"Helvetica Neue",sans-serif; 32 | text-align: center; 33 | color: black; 34 | background-color: {{ theme_background }}; 35 | padding: 0; 36 | margin: 0; 37 | } 38 | 39 | div.document { 40 | text-align: left; 41 | background-color: {{ theme_light_color }}; 42 | } 43 | 44 | div.bodywrapper { 45 | background-color: {{ theme_white }}; 46 | border-left: 1px solid {{ theme_lighter_gray }}; 47 | border-bottom: 1px solid {{ theme_lighter_gray }}; 48 | margin: 0 0 0 16em; 49 | } 50 | 51 | div.body { 52 | margin: 0; 53 | padding: 0.5em 1.3em; 54 | max-width: 55em; 55 | min-width: 20em; 56 | } 57 | 58 | div.related { 59 | font-size: 1em; 60 | background-color: {{ theme_background }}; 61 | } 62 | 63 | div.documentwrapper { 64 | float: left; 65 | width: 100%; 66 | background-color: {{ theme_light_color }}; 67 | } 68 | 69 | 70 | /* HEADINGS --------------------------------------------------------------- */ 71 | 72 | h1 { 73 | margin: 0; 74 | padding: 0.7em 0 0.3em 0; 75 | font-size: 1.5em; 76 | line-height: 1.15; 77 | color: {{ theme_h1 }}; 78 | clear: both; 79 | } 80 | 81 | h2 { 82 | margin: 2em 0 0.2em 0; 83 | font-size: 1.35em; 84 | padding: 0; 85 | color: {{ theme_h2 }}; 86 | } 87 | 88 | h3 { 89 | margin: 1em 0 -0.3em 0; 90 | font-size: 1.2em; 91 | color: {{ theme_h3 }}; 92 | } 93 | 94 | div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { 95 | color: black; 96 | } 97 | 98 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 99 | display: none; 100 | margin: 0 0 0 0.3em; 101 | padding: 0 0.2em 0 0.2em; 102 | color: {{ theme_gray_a }} !important; 103 | } 104 | 105 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 106 | h5:hover a.anchor, h6:hover a.anchor { 107 | display: inline; 108 | } 109 | 110 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 111 | h5 a.anchor:hover, h6 a.anchor:hover { 112 | color: {{ theme_gray_7 }}; 113 | background-color: {{ theme_dirty_white }}; 114 | } 115 | 116 | 117 | /* LINKS ------------------------------------------------------------------ */ 118 | 119 | /* Normal links get a pseudo-underline */ 120 | a { 121 | color: {{ theme_link_color }}; 122 | text-decoration: none; 123 | border-bottom: 1px solid {{ theme_link_color_decoration }}; 124 | } 125 | 126 | /* Links in sidebar, TOC, index trees and tables have no underline */ 127 | .sphinxsidebar a, 128 | .toctree-wrapper a, 129 | .indextable a, 130 | #indices-and-tables a { 131 | color: {{ theme_dark_gray }}; 132 | text-decoration: none; 133 | border-bottom: none; 134 | } 135 | 136 | /* Most links get an underline-effect when hovered */ 137 | a:hover, 138 | div.toctree-wrapper a:hover, 139 | .indextable a:hover, 140 | #indices-and-tables a:hover { 141 | color: {{ theme_black }}; 142 | text-decoration: none; 143 | border-bottom: 1px solid {{ theme_black }}; 144 | } 145 | 146 | /* Footer links */ 147 | div.footer a { 148 | color: {{ theme_background_text_link }}; 149 | text-decoration: none; 150 | border: none; 151 | } 152 | div.footer a:hover { 153 | color: {{ theme_medium_color_link_hover }}; 154 | text-decoration: underline; 155 | border: none; 156 | } 157 | 158 | /* Permalink anchor (subtle grey with a red hover) */ 159 | div.body a.headerlink { 160 | color: {{ theme_lighter_gray }}; 161 | font-size: 1em; 162 | margin-left: 6px; 163 | padding: 0 4px 0 4px; 164 | text-decoration: none; 165 | border: none; 166 | } 167 | div.body a.headerlink:hover { 168 | color: {{ theme_negative_text }}; 169 | border: none; 170 | } 171 | 172 | 173 | /* NAVIGATION BAR --------------------------------------------------------- */ 174 | 175 | div.related ul { 176 | height: 2.5em; 177 | } 178 | 179 | div.related ul li { 180 | margin: 0; 181 | padding: 0.65em 0; 182 | float: left; 183 | display: block; 184 | color: {{ theme_background_link_half }}; /* For the >> separators */ 185 | font-size: 0.8em; 186 | } 187 | 188 | div.related ul li.right { 189 | float: right; 190 | margin-right: 5px; 191 | color: transparent; /* Hide the | separators */ 192 | } 193 | 194 | /* "Breadcrumb" links in nav bar */ 195 | div.related ul li a { 196 | order: none; 197 | background-color: inherit; 198 | font-weight: bold; 199 | margin: 6px 0 6px 4px; 200 | line-height: 1.75em; 201 | color: {{ theme_background_link }}; 202 | text-shadow: 0 1px rgba(0, 0, 0, 0.5); 203 | padding: 0.4em 0.8em; 204 | border: none; 205 | border-radius: 3px; 206 | } 207 | /* previous / next / modules / index links look more like buttons */ 208 | div.related ul li.right a { 209 | margin: 0.375em 0; 210 | background-color: {{ theme_medium_color_hover }}; 211 | text-shadow: 0 1px rgba(0, 0, 0, 0.5); 212 | border-radius: 3px; 213 | -webkit-border-radius: 3px; 214 | -moz-border-radius: 3px; 215 | } 216 | /* All navbar links light up as buttons when hovered */ 217 | div.related ul li a:hover { 218 | background-color: {{ theme_medium_color }}; 219 | color: {{ theme_white }}; 220 | text-decoration: none; 221 | border-radius: 3px; 222 | -webkit-border-radius: 3px; 223 | -moz-border-radius: 3px; 224 | } 225 | /* Take extra precautions for tt within links */ 226 | a tt, 227 | div.related ul li a tt { 228 | background: inherit !important; 229 | color: inherit !important; 230 | } 231 | 232 | 233 | /* SIDEBAR ---------------------------------------------------------------- */ 234 | 235 | div.sphinxsidebarwrapper { 236 | padding: 0; 237 | } 238 | 239 | div.sphinxsidebar { 240 | margin: 0; 241 | margin-left: -100%; 242 | float: left; 243 | top: 3em; 244 | left: 0; 245 | padding: 0 1em; 246 | width: 14em; 247 | font-size: 1em; 248 | text-align: left; 249 | background-color: {{ theme_light_color }}; 250 | } 251 | 252 | div.sphinxsidebar img { 253 | max-width: 12em; 254 | } 255 | 256 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 257 | margin: 1.2em 0 0.3em 0; 258 | font-size: 1em; 259 | padding: 0; 260 | color: {{ theme_gray_2 }}; 261 | font-family: "ff-meta-web-pro-1", "ff-meta-web-pro-2", "Arial", "Helvetica Neue", sans-serif; 262 | } 263 | 264 | div.sphinxsidebar h3 a { 265 | color: {{ theme_grey_color }}; 266 | } 267 | 268 | div.sphinxsidebar ul, 269 | div.sphinxsidebar p { 270 | margin-top: 0; 271 | padding-left: 0; 272 | line-height: 130%; 273 | background-color: {{ theme_light_color }}; 274 | } 275 | 276 | /* No bullets for nested lists, but a little extra indentation */ 277 | div.sphinxsidebar ul ul { 278 | list-style-type: none; 279 | margin-left: 1.5em; 280 | padding: 0; 281 | } 282 | 283 | /* A little top/bottom padding to prevent adjacent links' borders 284 | * from overlapping each other */ 285 | div.sphinxsidebar ul li { 286 | padding: 1px 0; 287 | } 288 | 289 | /* A little left-padding to make these align with the ULs */ 290 | div.sphinxsidebar p.topless { 291 | padding-left: 0 0 0 1em; 292 | } 293 | 294 | /* Make these into hidden one-liners */ 295 | div.sphinxsidebar ul li, 296 | div.sphinxsidebar p.topless { 297 | white-space: nowrap; 298 | overflow: hidden; 299 | } 300 | /* ...which become visible when hovered */ 301 | div.sphinxsidebar ul li:hover, 302 | div.sphinxsidebar p.topless:hover { 303 | overflow: visible; 304 | } 305 | 306 | /* Search text box and "Go" button */ 307 | #searchbox { 308 | margin-top: 2em; 309 | margin-bottom: 1em; 310 | background: {{ theme_dirtier_white }}; 311 | padding: 0.5em; 312 | border-radius: 6px; 313 | -moz-border-radius: 6px; 314 | -webkit-border-radius: 6px; 315 | } 316 | #searchbox h3 { 317 | margin-top: 0; 318 | } 319 | 320 | /* Make search box and button abut and have a border */ 321 | input, 322 | div.sphinxsidebar input { 323 | border: 1px solid {{ theme_gray_9 }}; 324 | float: left; 325 | } 326 | 327 | /* Search textbox */ 328 | input[type="text"] { 329 | margin: 0; 330 | padding: 0 3px; 331 | height: 20px; 332 | width: 144px; 333 | border-top-left-radius: 3px; 334 | border-bottom-left-radius: 3px; 335 | -moz-border-radius-topleft: 3px; 336 | -moz-border-radius-bottomleft: 3px; 337 | -webkit-border-top-left-radius: 3px; 338 | -webkit-border-bottom-left-radius: 3px; 339 | } 340 | /* Search button */ 341 | input[type="submit"] { 342 | margin: 0 0 0 -1px; /* -1px prevents a double-border with textbox */ 343 | height: 22px; 344 | color: {{ theme_dark_gray }}; 345 | background-color: {{ theme_light_color }}; 346 | padding: 1px 4px; 347 | font-weight: bold; 348 | border-top-right-radius: 3px; 349 | border-bottom-right-radius: 3px; 350 | -moz-border-radius-topright: 3px; 351 | -moz-border-radius-bottomright: 3px; 352 | -webkit-border-top-right-radius: 3px; 353 | -webkit-border-bottom-right-radius: 3px; 354 | } 355 | input[type="submit"]:hover { 356 | color: {{ theme_white }}; 357 | background-color: {{ theme_green_highlight }}; 358 | } 359 | 360 | div.sphinxsidebar p.searchtip { 361 | clear: both; 362 | padding: 0.5em 0 0 0; 363 | background: {{ theme_dirtier_white }}; 364 | color: {{ theme_gray }}; 365 | font-size: 0.9em; 366 | } 367 | 368 | /* Sidebar links are unusual */ 369 | div.sphinxsidebar li a, 370 | div.sphinxsidebar p a { 371 | background: {{ theme_light_color }}; /* In case links overlap main content */ 372 | border-radius: 3px; 373 | -moz-border-radius: 3px; 374 | -webkit-border-radius: 3px; 375 | border: 1px solid transparent; /* To prevent things jumping around on hover */ 376 | padding: 0 5px 0 5px; 377 | } 378 | div.sphinxsidebar li a:hover, 379 | div.sphinxsidebar p a:hover { 380 | color: {{ theme_black }}; 381 | text-decoration: none; 382 | border: 1px solid {{ theme_light_gray }}; 383 | } 384 | 385 | /* Tweak any link appearing in a heading */ 386 | div.sphinxsidebar h3 a { 387 | } 388 | 389 | 390 | 391 | 392 | /* OTHER STUFF ------------------------------------------------------------ */ 393 | 394 | cite, code, tt { 395 | font-family: 'Consolas', 'Deja Vu Sans Mono', 396 | 'Bitstream Vera Sans Mono', monospace; 397 | font-size: 0.95em; 398 | letter-spacing: 0.01em; 399 | } 400 | 401 | tt { 402 | background-color: {{ theme_code_background }}; 403 | color: {{ theme_dark_gray }}; 404 | } 405 | 406 | tt.descname, tt.descclassname, tt.xref { 407 | border: 0; 408 | } 409 | 410 | hr { 411 | border: 1px solid {{ theme_ruler }}; 412 | margin: 2em; 413 | } 414 | 415 | pre, #_fontwidthtest { 416 | font-family: 'Consolas', 'Deja Vu Sans Mono', 417 | 'Bitstream Vera Sans Mono', monospace; 418 | margin: 1em 2em; 419 | font-size: 0.95em; 420 | letter-spacing: 0.015em; 421 | line-height: 120%; 422 | padding: 0.5em; 423 | border: 1px solid {{ theme_lighter_gray }}; 424 | background-color: {{ theme_code_background }}; 425 | border-radius: 6px; 426 | -moz-border-radius: 6px; 427 | -webkit-border-radius: 6px; 428 | } 429 | 430 | pre a { 431 | color: inherit; 432 | text-decoration: underline; 433 | } 434 | 435 | td.linenos pre { 436 | padding: 0.5em 0; 437 | } 438 | 439 | div.quotebar { 440 | background-color: {{ theme_almost_white }}; 441 | max-width: 250px; 442 | float: right; 443 | padding: 2px 7px; 444 | border: 1px solid {{ theme_lighter_gray }}; 445 | } 446 | 447 | div.topic { 448 | background-color: {{ theme_almost_white }}; 449 | } 450 | 451 | table { 452 | border-collapse: collapse; 453 | margin: 0 -0.5em 0 -0.5em; 454 | } 455 | 456 | table td, table th { 457 | padding: 0.2em 0.5em 0.2em 0.5em; 458 | } 459 | 460 | 461 | /* ADMONITIONS AND WARNINGS ------------------------------------------------- */ 462 | 463 | /* Shared by admonitions, warnings and sidebars */ 464 | div.admonition, 465 | div.warning, 466 | div.sidebar { 467 | font-size: 0.9em; 468 | margin: 2em; 469 | padding: 0; 470 | /* 471 | border-radius: 6px; 472 | -moz-border-radius: 6px; 473 | -webkit-border-radius: 6px; 474 | */ 475 | } 476 | div.admonition p, 477 | div.warning p, 478 | div.sidebar p { 479 | margin: 0.5em 1em 0.5em 1em; 480 | padding: 0; 481 | } 482 | div.admonition pre, 483 | div.warning pre, 484 | div.sidebar pre { 485 | margin: 0.4em 1em 0.4em 1em; 486 | } 487 | div.admonition p.admonition-title, 488 | div.warning p.admonition-title, 489 | div.sidebar p.sidebar-title { 490 | margin: 0; 491 | padding: 0.1em 0 0.1em 0.5em; 492 | color: white; 493 | font-weight: bold; 494 | font-size: 1.1em; 495 | text-shadow: 0 1px rgba(0, 0, 0, 0.5); 496 | } 497 | div.admonition ul, div.admonition ol, 498 | div.warning ul, div.warning ol, 499 | div.sidebar ul, div.sidebar ol { 500 | margin: 0.1em 0.5em 0.5em 3em; 501 | padding: 0; 502 | } 503 | 504 | 505 | /* Admonitions and sidebars only */ 506 | div.admonition, div.sidebar { 507 | border: 1px solid {{ theme_positive_dark }}; 508 | background-color: {{ theme_positive_light }}; 509 | } 510 | div.admonition p.admonition-title, 511 | div.sidebar p.sidebar-title { 512 | background-color: {{ theme_positive_medium }}; 513 | border-bottom: 1px solid {{ theme_positive_dark }}; 514 | } 515 | 516 | 517 | /* Warnings only */ 518 | div.warning { 519 | border: 1px solid {{ theme_negative_dark }}; 520 | background-color: {{ theme_negative_light }}; 521 | } 522 | div.warning p.admonition-title { 523 | background-color: {{ theme_negative_medium }}; 524 | border-bottom: 1px solid {{ theme_negative_dark }}; 525 | } 526 | 527 | 528 | /* Sidebars only */ 529 | div.sidebar { 530 | max-width: 200px; 531 | } 532 | 533 | 534 | 535 | div.versioninfo { 536 | margin: 1em 0 0 0; 537 | border: 1px solid {{ theme_lighter_gray }}; 538 | background-color: {{ theme_light_medium_color }}; 539 | padding: 8px; 540 | line-height: 1.3em; 541 | font-size: 0.9em; 542 | } 543 | 544 | .viewcode-back { 545 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 546 | 'Verdana', sans-serif; 547 | } 548 | 549 | div.viewcode-block:target { 550 | background-color: {{ theme_viewcode_bg }}; 551 | border-top: 1px solid {{ theme_viewcode_border }}; 552 | border-bottom: 1px solid {{ theme_viewcode_border }}; 553 | } 554 | 555 | dl { 556 | margin: 1em 0 2.5em 0; 557 | } 558 | 559 | /* Highlight target when you click an internal link */ 560 | dt:target { 561 | background: {{ theme_highlight }}; 562 | } 563 | /* Don't highlight whole divs */ 564 | div.highlight { 565 | background: transparent; 566 | } 567 | /* But do highlight spans (so search results can be highlighted) */ 568 | span.highlight { 569 | background: {{ theme_highlight }}; 570 | } 571 | 572 | div.footer { 573 | background-color: {{ theme_background }}; 574 | color: {{ theme_background_text }}; 575 | padding: 0 2em 2em 2em; 576 | clear: both; 577 | font-size: 0.8em; 578 | text-align: center; 579 | } 580 | 581 | p { 582 | margin: 0.8em 0 0.5em 0; 583 | } 584 | 585 | .section p img { 586 | margin: 1em 2em; 587 | } 588 | 589 | 590 | /* MOBILE LAYOUT -------------------------------------------------------------- */ 591 | 592 | @media screen and (max-width: 600px) { 593 | 594 | h1, h2, h3, h4, h5 { 595 | position: relative; 596 | } 597 | 598 | ul { 599 | padding-left: 1.75em; 600 | } 601 | 602 | div.bodywrapper a.headerlink, #indices-and-tables h1 a { 603 | color: {{ theme_almost_dirty_white }}; 604 | font-size: 80%; 605 | float: right; 606 | line-height: 1.8; 607 | position: absolute; 608 | right: -0.7em; 609 | visibility: inherit; 610 | } 611 | 612 | div.bodywrapper h1 a.headerlink, #indices-and-tables h1 a { 613 | line-height: 1.5; 614 | } 615 | 616 | pre { 617 | font-size: 0.7em; 618 | overflow: auto; 619 | word-wrap: break-word; 620 | white-space: pre-wrap; 621 | } 622 | 623 | div.related ul { 624 | height: 2.5em; 625 | padding: 0; 626 | text-align: left; 627 | } 628 | 629 | div.related ul li { 630 | clear: both; 631 | color: {{ theme_dark_color }}; 632 | padding: 0.2em 0; 633 | } 634 | 635 | div.related ul li:last-child { 636 | border-bottom: 1px dotted {{ theme_medium_color }}; 637 | padding-bottom: 0.4em; 638 | margin-bottom: 1em; 639 | width: 100%; 640 | } 641 | 642 | div.related ul li a { 643 | color: {{ theme_dark_color }}; 644 | padding-right: 0; 645 | } 646 | 647 | div.related ul li a:hover { 648 | background: inherit; 649 | color: inherit; 650 | } 651 | 652 | div.related ul li.right { 653 | clear: none; 654 | padding: 0.65em 0; 655 | margin-bottom: 0.5em; 656 | } 657 | 658 | div.related ul li.right a { 659 | color: {{ theme_white }}; 660 | padding-right: 0.8em; 661 | } 662 | 663 | div.related ul li.right a:hover { 664 | background-color: {{ theme_medium_color }}; 665 | } 666 | 667 | div.body { 668 | clear: both; 669 | min-width: 0; 670 | word-wrap: break-word; 671 | } 672 | 673 | div.bodywrapper { 674 | margin: 0 0 0 0; 675 | } 676 | 677 | div.sphinxsidebar { 678 | float: none; 679 | margin: 0; 680 | width: auto; 681 | } 682 | 683 | div.sphinxsidebar input[type="text"] { 684 | height: 2em; 685 | line-height: 2em; 686 | width: 70%; 687 | } 688 | 689 | div.sphinxsidebar input[type="submit"] { 690 | height: 2em; 691 | margin-left: 0.5em; 692 | width: 20%; 693 | } 694 | 695 | div.sphinxsidebar p.searchtip { 696 | background: inherit; 697 | margin-bottom: 1em; 698 | } 699 | 700 | div.sphinxsidebar ul li, div.sphinxsidebar p.topless { 701 | white-space: normal; 702 | } 703 | 704 | .bodywrapper img { 705 | display: block; 706 | margin-left: auto; 707 | margin-right: auto; 708 | max-width: 100%; 709 | } 710 | 711 | div.documentwrapper { 712 | float: none; 713 | } 714 | 715 | div.admonition, div.warning, pre, blockquote { 716 | margin-left: 0em; 717 | margin-right: 0em; 718 | } 719 | 720 | .body p img { 721 | margin: 0; 722 | } 723 | 724 | #searchbox { 725 | background: transparent; 726 | } 727 | 728 | .related:not(:first-child) li { 729 | display: none; 730 | } 731 | 732 | .related:not(:first-child) li.right { 733 | display: block; 734 | } 735 | 736 | div.footer { 737 | padding: 1em; 738 | } 739 | 740 | .rtd_doc_footer .badge { 741 | float: none; 742 | margin: 1em auto; 743 | position: static; 744 | } 745 | 746 | .rtd_doc_footer .badge.revsys-inline { 747 | margin-right: auto; 748 | margin-bottom: 2em; 749 | } 750 | 751 | table.indextable { 752 | display: block; 753 | width: auto; 754 | } 755 | 756 | .indextable tr { 757 | display: block; 758 | } 759 | 760 | .indextable td { 761 | display: block; 762 | padding: 0; 763 | width: auto !important; 764 | } 765 | 766 | .indextable td dt { 767 | margin: 1em 0; 768 | } 769 | 770 | ul.search { 771 | margin-left: 0.25em; 772 | } 773 | 774 | ul.search li div.context { 775 | font-size: 90%; 776 | line-height: 1.1; 777 | margin-bottom: 1; 778 | margin-left: 0; 779 | } 780 | 781 | } 782 | -------------------------------------------------------------------------------- /doc/_themes/armstrong/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = rtd.css 4 | pygment_style = default 5 | show_sphinx = False 6 | 7 | [options] 8 | show_rtd = True 9 | 10 | white = #ffffff 11 | almost_white = #f8f8f8 12 | barely_white = #f2f2f2 13 | dirty_white = #eeeeee 14 | almost_dirty_white = #e6e6e6 15 | dirtier_white = #dddddd 16 | lighter_gray = #cccccc 17 | gray_a = #aaaaaa 18 | gray_9 = #999999 19 | light_gray = #888888 20 | gray_7 = #777777 21 | gray = #666666 22 | dark_gray = #444444 23 | gray_2 = #222222 24 | black = #111111 25 | light_color = #e8ecef 26 | light_medium_color = #DDEAF0 27 | medium_color = #8ca1af 28 | medium_color_link = #86989b 29 | medium_color_link_hover = #a6b8bb 30 | dark_color = #465158 31 | 32 | h1 = #000000 33 | h2 = #465158 34 | h3 = #6c818f 35 | 36 | link_color = #444444 37 | link_color_decoration = #CCCCCC 38 | 39 | medium_color_hover = #697983 40 | green_highlight = #8ecc4c 41 | 42 | 43 | positive_dark = #609060 44 | positive_medium = #70a070 45 | positive_light = #e9ffe9 46 | 47 | negative_dark = #900000 48 | negative_medium = #b04040 49 | negative_light = #ffe9e9 50 | negative_text = #c60f0f 51 | 52 | ruler = #abc 53 | 54 | viewcode_bg = #f4debf 55 | viewcode_border = #ac9 56 | 57 | highlight = #ffe080 58 | 59 | code_background = #eeeeee 60 | 61 | background = #465158 62 | background_link = #ffffff 63 | background_link_half = #ffffff 64 | background_text = #eeeeee 65 | background_text_link = #86989b 66 | -------------------------------------------------------------------------------- /doc/_themes/armstrong/theme.conf.orig: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = default 3 | stylesheet = rtd.css 4 | pygment_style = default 5 | show_sphinx = False 6 | 7 | [options] 8 | show_rtd = True 9 | 10 | white = #ffffff 11 | almost_white = #f8f8f8 12 | barely_white = #f2f2f2 13 | dirty_white = #eeeeee 14 | almost_dirty_white = #e6e6e6 15 | dirtier_white = #DAC6AF 16 | lighter_gray = #cccccc 17 | gray_a = #aaaaaa 18 | gray_9 = #999999 19 | light_gray = #888888 20 | gray_7 = #777777 21 | gray = #666666 22 | dark_gray = #444444 23 | gray_2 = #222222 24 | black = #111111 25 | light_color = #EDE4D8 26 | light_medium_color = #DDEAF0 27 | medium_color = #8ca1af 28 | medium_color_link = #634320 29 | medium_color_link_hover = #261a0c 30 | dark_color = rgba(160, 109, 52, 1.0) 31 | 32 | h1 = #1f3744 33 | h2 = #335C72 34 | h3 = #638fa6 35 | 36 | link_color = #335C72 37 | link_color_decoration = #99AEB9 38 | 39 | medium_color_hover = rgba(255, 255, 255, 0.25) 40 | medium_color = rgba(255, 255, 255, 0.5) 41 | green_highlight = #8ecc4c 42 | 43 | 44 | positive_dark = rgba(51, 77, 0, 1.0) 45 | positive_medium = rgba(102, 153, 0, 1.0) 46 | positive_light = rgba(102, 153, 0, 0.1) 47 | 48 | negative_dark = rgba(51, 13, 0, 1.0) 49 | negative_medium = rgba(204, 51, 0, 1.0) 50 | negative_light = rgba(204, 51, 0, 0.1) 51 | negative_text = #c60f0f 52 | 53 | ruler = #abc 54 | 55 | viewcode_bg = #f4debf 56 | viewcode_border = #ac9 57 | 58 | highlight = #ffe080 59 | 60 | code_background = rgba(0, 0, 0, 0.075) 61 | 62 | background = rgba(135, 57, 34, 1.0) 63 | background_link = rgba(212, 195, 172, 1.0) 64 | background_link_half = rgba(212, 195, 172, 0.5) 65 | background_text = rgba(212, 195, 172, 1.0) 66 | background_text_link = rgba(171, 138, 93, 1.0) 67 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-opc documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jun 29 17:34:36 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing 7 | # dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | sys.path.insert(0, os.path.abspath('..')) 21 | 22 | # -- General configuration --------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.intersphinx', 32 | 'sphinx.ext.todo', 33 | 'sphinx.ext.coverage', 34 | 'sphinx.ext.viewcode' 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = u'python-opc' 51 | copyright = u'2013, Steve Canny' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.1' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.1.0' 61 | 62 | # A string of reStructuredText that will be included at the end of every source 63 | # file that is read. This is the right place to add substitutions that should 64 | # be available in every file. 65 | rst_epilog = """ 66 | .. |OpcPackage| replace:: :class:`OpcPackage` 67 | 68 | .. |PackURI| replace:: :class:`PackURI` 69 | 70 | .. |Part| replace:: :class:`Part` 71 | 72 | .. |_Relationship| replace:: :class:`_Relationship` 73 | 74 | .. |RelationshipCollection| replace:: :class:`_RelationshipCollection` 75 | 76 | .. |po| replace:: ``python-opc`` 77 | 78 | .. |python-opc| replace:: ``python-opc`` 79 | """ 80 | 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | 108 | # -- Options for HTML output ------------------------------------------------ 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | html_theme = 'armstrong' 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | #html_theme_options = {} 118 | 119 | # Add any paths that contain custom themes here, relative to this directory. 120 | html_theme_path = ['_themes'] 121 | 122 | # The name for this set of Sphinx documents. If None, it defaults to 123 | # " v documentation". 124 | #html_title = None 125 | 126 | # A shorter title for the navigation bar. Default is the same as html_title. 127 | #html_short_title = None 128 | 129 | # The name of an image file (relative to this directory) to place at the top 130 | # of the sidebar. 131 | #html_logo = None 132 | 133 | # The name of an image file (within the static path) to use as favicon of the 134 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 135 | # pixels large. 136 | #html_favicon = None 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | html_static_path = ['_static'] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'python-opcdoc' 186 | 187 | 188 | # -- Options for LaTeX output ----------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, 203 | # target name, 204 | # title, 205 | # author, 206 | # documentclass [howto/manual]). 207 | latex_documents = [ 208 | ('index', 'python-opc.tex', u'python-opc Documentation', 209 | u'Steve Canny', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | #latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | #latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | #latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | #latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | #latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | #latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output ----------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'python-opc', u'python-opc Documentation', 239 | [u'Steve Canny'], 1) 240 | ] 241 | 242 | # If true, show URL addresses after external links. 243 | #man_show_urls = False 244 | 245 | 246 | # -- Options for Texinfo output --------------------------------------------- 247 | 248 | # Grouping the document tree into Texinfo files. List of tuples 249 | # (source start file, target name, title, author, 250 | # dir menu entry, description, category) 251 | texinfo_documents = [ 252 | ('index', 'python-opc', u'python-opc Documentation', 253 | u'Steve Canny', 'python-opc', 'One line description of project.', 254 | 'Miscellaneous'), 255 | ] 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #texinfo_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #texinfo_domain_indices = True 262 | 263 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 264 | #texinfo_show_urls = 'footnote' 265 | 266 | 267 | # Example configuration for intersphinx: refer to the Python standard library. 268 | intersphinx_mapping = {'http://docs.python.org/': None} 269 | -------------------------------------------------------------------------------- /doc/content_types.rst: -------------------------------------------------------------------------------- 1 | ########################### 2 | Content type constant names 3 | ########################### 4 | 5 | The following names are defined in the :mod:`opc.constants` module to allow 6 | content types to be referenced using an identifier rather than a literal 7 | value. 8 | 9 | The following import statement makes these available in a module:: 10 | 11 | from opc.constants import CONTENT_TYPE as CT 12 | 13 | A content type may then be referenced as a member of ``CT`` using dotted 14 | notation, for example:: 15 | 16 | part.content_type = CT.PML_SLIDE_LAYOUT 17 | 18 | The content type names are determined by transforming the trailing text of 19 | the content type string to upper snake case, replacing illegal Python 20 | identifier characters (dash and period) with an underscore, and prefixing one 21 | of these seven namespace abbreviations: 22 | 23 | * **DML** -- DrawingML 24 | * **OFC** -- Microsoft Office document 25 | * **OPC** -- Open Packaging Convention 26 | * **PML** -- PresentationML 27 | * **SML** -- SpreadsheetML 28 | * **WML** -- WordprocessingML 29 | * no prefix -- standard MIME types, such as those used for image formats like 30 | JPEG 31 | 32 | BMP 33 | image/bmp 34 | 35 | DML_CHART 36 | application/vnd.openxmlformats-officedocument.drawingml.chart+xml 37 | 38 | DML_CHARTSHAPES 39 | application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml 40 | 41 | DML_DIAGRAM_COLORS 42 | application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml 43 | 44 | DML_DIAGRAM_DATA 45 | application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml 46 | 47 | DML_DIAGRAM_LAYOUT 48 | application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml 49 | 50 | DML_DIAGRAM_STYLE 51 | application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml 52 | 53 | GIF 54 | image/gif 55 | 56 | JPEG 57 | image/jpeg 58 | 59 | MS_PHOTO 60 | image/vnd.ms-photo 61 | 62 | OFC_CUSTOM_PROPERTIES 63 | application/vnd.openxmlformats-officedocument.custom-properties+xml 64 | 65 | OFC_CUSTOM_XML_PROPERTIES 66 | application/vnd.openxmlformats-officedocument.customXmlProperties+xml 67 | 68 | OFC_DRAWING 69 | application/vnd.openxmlformats-officedocument.drawing+xml 70 | 71 | OFC_EXTENDED_PROPERTIES 72 | application/vnd.openxmlformats-officedocument.extended-properties+xml 73 | 74 | OFC_OLE_OBJECT 75 | application/vnd.openxmlformats-officedocument.oleObject 76 | 77 | OFC_PACKAGE 78 | application/vnd.openxmlformats-officedocument.package 79 | 80 | OFC_THEME 81 | application/vnd.openxmlformats-officedocument.theme+xml 82 | 83 | OFC_THEME_OVERRIDE 84 | application/vnd.openxmlformats-officedocument.themeOverride+xml 85 | 86 | OFC_VML_DRAWING 87 | application/vnd.openxmlformats-officedocument.vmlDrawing 88 | 89 | OPC_CORE_PROPERTIES 90 | application/vnd.openxmlformats-package.core-properties+xml 91 | 92 | OPC_DIGITAL_SIGNATURE_CERTIFICATE 93 | application/vnd.openxmlformats-package.digital-signature-certificate 94 | 95 | OPC_DIGITAL_SIGNATURE_ORIGIN 96 | application/vnd.openxmlformats-package.digital-signature-origin 97 | 98 | OPC_DIGITAL_SIGNATURE_XMLSIGNATURE 99 | application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml 100 | 101 | OPC_RELATIONSHIPS 102 | application/vnd.openxmlformats-package.relationships+xml 103 | 104 | PML_COMMENTS 105 | application/vnd.openxmlformats-officedocument.presentationml.comments+xml 106 | 107 | PML_COMMENT_AUTHORS 108 | application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml 109 | 110 | PML_HANDOUT_MASTER 111 | application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml 112 | 113 | PML_NOTES_MASTER 114 | application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml 115 | 116 | PML_NOTES_SLIDE 117 | application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml 118 | 119 | PML_PRESENTATION_MAIN 120 | application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml 121 | 122 | PML_PRES_PROPS 123 | application/vnd.openxmlformats-officedocument.presentationml.presProps+xml 124 | 125 | PML_PRINTER_SETTINGS 126 | application/vnd.openxmlformats-officedocument.presentationml.printerSettings 127 | 128 | PML_SLIDE 129 | application/vnd.openxmlformats-officedocument.presentationml.slide+xml 130 | 131 | PML_SLIDESHOW_MAIN 132 | application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml 133 | 134 | PML_SLIDE_LAYOUT 135 | application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml 136 | 137 | PML_SLIDE_MASTER 138 | application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml 139 | 140 | PML_SLIDE_UPDATE_INFO 141 | application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml 142 | 143 | PML_TABLE_STYLES 144 | application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml 145 | 146 | PML_TAGS 147 | application/vnd.openxmlformats-officedocument.presentationml.tags+xml 148 | 149 | PML_TEMPLATE_MAIN 150 | application/vnd.openxmlformats-officedocument.presentationml.template.main+xml 151 | 152 | PML_VIEW_PROPS 153 | application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml 154 | 155 | PNG 156 | image/png 157 | 158 | SML_CALC_CHAIN 159 | application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml 160 | 161 | SML_CHARTSHEET 162 | application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml 163 | 164 | SML_COMMENTS 165 | application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml 166 | 167 | SML_CONNECTIONS 168 | application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml 169 | 170 | SML_CUSTOM_PROPERTY 171 | application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty 172 | 173 | SML_DIALOGSHEET 174 | application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml 175 | 176 | SML_EXTERNAL_LINK 177 | application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml 178 | 179 | SML_PIVOT_CACHE_DEFINITION 180 | application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml 181 | 182 | SML_PIVOT_CACHE_RECORDS 183 | application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml 184 | 185 | SML_PIVOT_TABLE 186 | application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml 187 | 188 | SML_PRINTER_SETTINGS 189 | application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings 190 | 191 | SML_QUERY_TABLE 192 | application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml 193 | 194 | SML_REVISION_HEADERS 195 | application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml 196 | 197 | SML_REVISION_LOG 198 | application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml 199 | 200 | SML_SHARED_STRINGS 201 | application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml 202 | 203 | SML_SHEET 204 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 205 | 206 | SML_SHEET_METADATA 207 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml 208 | 209 | SML_STYLES 210 | application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml 211 | 212 | SML_TABLE 213 | application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml 214 | 215 | SML_TABLE_SINGLE_CELLS 216 | application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml 217 | 218 | SML_USER_NAMES 219 | application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml 220 | 221 | SML_VOLATILE_DEPENDENCIES 222 | application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml 223 | 224 | SML_WORKSHEET 225 | application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml 226 | 227 | TIFF 228 | image/tiff 229 | 230 | WML_COMMENTS 231 | application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml 232 | 233 | WML_DOCUMENT_GLOSSARY 234 | application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml 235 | 236 | WML_DOCUMENT_MAIN 237 | application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml 238 | 239 | WML_ENDNOTES 240 | application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml 241 | 242 | WML_FONT_TABLE 243 | application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml 244 | 245 | WML_FOOTER 246 | application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml 247 | 248 | WML_FOOTNOTES 249 | application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml 250 | 251 | WML_HEADER 252 | application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml 253 | 254 | WML_NUMBERING 255 | application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml 256 | 257 | WML_PRINTER_SETTINGS 258 | application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings 259 | 260 | WML_SETTINGS 261 | application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml 262 | 263 | WML_STYLES 264 | application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml 265 | 266 | WML_WEB_SETTINGS 267 | application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml 268 | 269 | XML 270 | application/xml 271 | 272 | X_EMF 273 | image/x-emf 274 | 275 | X_FONTDATA 276 | application/x-fontdata 277 | 278 | X_FONT_TTF 279 | application/x-font-ttf 280 | 281 | X_WMF 282 | image/x-wmf 283 | -------------------------------------------------------------------------------- /doc/developer/design_narratives.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Design Narratives 3 | ================= 4 | 5 | Narrative explorations into design issues, serving initially as an aid to 6 | reasoning and later as a memorandum of the considerations undertaken during 7 | the design process. 8 | 9 | 10 | Semi-random bits 11 | ---------------- 12 | 13 | *partname* is a marshaling/serialization concern. 14 | 15 | *partname* (pack URI) is the addressing scheme for accessing serialized parts 16 | within the package. It has no direct relevance to the unmarshaled graph except 17 | for use in re-marshaling unmanaged parts or to avoid renaming parts when the 18 | load partname will do just fine. 19 | 20 | What determines part to be constructed? Relationship type or content type? 21 | 22 | *Working hypothesis*: Content type should be used to determine the type of 23 | part to be constructed during unmarshaling. 24 | 25 | Content type is more granular than relationship type. For example, an image 26 | part can be any of several content types, e.g. jpg, gif, or png. Another 27 | example is RT.OFFICE_DOCUMENT. This can apply to any of CT.PRESENTATION, 28 | CT.DOCUMENT, or CT.SPREADSHEET and their variants. 29 | 30 | However, I can't think of any examples of where a particular content type 31 | may be the target of more than one possible relationship type. That seems 32 | like a logical possibility though. 33 | 34 | There are examples of where a relationship type (customXml for example) are 35 | used to refer to more than one part type (Additional Characteristics, 36 | Bibliography, and Custom XML parts in this case). In such a case I expect 37 | the unmarshaling and part selection would need to be delegated to the source 38 | part which presumably would contain enough information to resolve the 39 | ambiguity in its body XML. In that case, a BasePart could be constructed and 40 | let the source part create a specific subclass on |after_unmarshal|. 41 | 42 | When properties of a mutable type (e.g. list) are returned, what is returned 43 | should be a copy or perhaps an immutable variant (e.g. tuple) so that 44 | client-side changes don't need to be accounted for in testing. If the return 45 | value really needs to be mutable and a snapshot won't do, it's probably time to 46 | make it a custom collection so the types of mutation that are allowed can be 47 | specified and tested. 48 | 49 | In PackURI, the baseURI property does not include any trailing slash. This 50 | behavior is consistent with the values returned from ``posixpath.split()`` and 51 | is then in a form suitable for use in ``posixpath.join()``. 52 | 53 | 54 | Design Narrative -- Blob proxy 55 | ============================== 56 | 57 | Certain use cases would be better served if loading large binary parts such as 58 | images could be postponed or avoided. For example, if the use case is to 59 | retrieve full text from a presentation for indexing purposes, the resources 60 | and time consumed to load images into memory is wasted. It seems feasible to 61 | develop some sort of blob proxy to postpone the loading of these binary parts 62 | until such time as they are actually required, passing a proxy of some type to 63 | be used instead. If it were cleverly done, the client code wouldn't have to 64 | know, i.e. the proxy would be transparent. 65 | 66 | The main challenge I see is how to gain an entry point to close the zip archive 67 | after all loading has been completed. If it were reopened and closed each time 68 | a part was loaded that would be pretty expensive (an early verion of 69 | python-pptx did exactly that for other reasons). Maybe that could be done when 70 | the presentation is garbage collected or something. 71 | 72 | Another challenge is how to trigger the proxy to load itself. Maybe blob could 73 | be an object that has file semantics and the read method could lazy load. 74 | 75 | Another idea was to be able to open the package in read-only mode. If the file 76 | doesn't need to be saved, the actual binary objects don't actually need to be 77 | accessed. Maybe this would be more like read-text-only mode or something. 78 | I don't know how we'd guarantee that no one was interested in the image 79 | binaries, even if they promised not to save. 80 | 81 | I suppose there could be a "read binary parts" method somewhere that gets 82 | triggered the first time a binary part is accessed, as it would be during 83 | save(). That would address the zip close entry point challenge. 84 | 85 | It does all sound a bit complicated for the sake of saving a few milliseconds, 86 | unless someone (like Google :) was dealing with really large scale. 87 | 88 | 89 | Design Narrative -- Custom Part Class mapping 90 | ============================================= 91 | 92 | :: 93 | 94 | pkg.register_part_classes(part_class_mapping) 95 | 96 | part_class_mapping = { 97 | CT_SLIDE: _Slide, 98 | CT_PRESENTATION: _Presentation 99 | ... 100 | } 101 | 102 | 103 | Design Narrative -- Model-side relationships 104 | ============================================ 105 | 106 | Might it make sense to maintain XML of .rels stream throughout life-cycle? 107 | -------------------------------------------------------------------------- 108 | 109 | No. The primary rationale is that a partname is not a primary model-side 110 | entity; partnames are driven by the serialization concern, providing a method 111 | for addressing serialized parts. Partnames are not required to be up-to-date in 112 | the model until after the |before_marshal| call to the part returns. Even if 113 | all part names were kept up-to-date, it would be a leakage across concern 114 | boundaries to require a part to notify relationships of name changes; not to 115 | mention it would introduce additional complexity that has nothing to do with 116 | manipulation of the in-memory model. 117 | 118 | **always up-to-date principle** 119 | 120 | Model-side relationships are maintained as new parts are added or existing 121 | parts are deleted. Relationships for generic parts are maintained from load 122 | and delivered back for save without change. 123 | 124 | I'm not completely sure that the always-up-to-date principle need necessarily 125 | apply in every case. As long as the relationships are up-to-date before 126 | returning from the |before_marshal| call, I don't see a reason why that 127 | choice couldn't be at the designer's discretion. Because relationships don't 128 | have a compelling model-side runtime purpose, it might simplify the code to 129 | localize the pre-serialization concern to the |before_marshal| method. 130 | 131 | .. |before_marshal| replace:: :meth:`before_marshal` 132 | .. |after_unmarshal| replace:: :meth:`after_unmarshal` 133 | 134 | 135 | Members 136 | ------- 137 | 138 | **rId** 139 | 140 | The relationship identifier. Must be a unique xsd:ID string. It is usually 141 | of the form 'rId%d' % {sequential_int}, e.g. ``'rId9'``, but this need not 142 | be the case. In situations where a relationship is created (e.g. for a new 143 | part) or can be rewritten, e.g. if presentation->slide relationships were 144 | rewritten on |before_marshal|, this form is preferred. In all other cases 145 | the existing rId value should be preserved. When a relationship is what the 146 | spec terms as *explicit*, there is a reference to the relationship within 147 | the source part XML, the key of which is the rId value; changing the rId 148 | would break that mapping. 149 | 150 | The **sequence** of relationships in the collection is not significant. The 151 | relationship collection should be regarded as a mapping on rId, not as 152 | a sequence with the index indicated by the numeric suffix of rId. While 153 | PowerPoint observes the convention of using sequential rId values for 154 | the slide relationships of a presentation, for example, this should not be 155 | used to determine slide sequence, nor is it a requirement for package 156 | production (saving a .pptx file). 157 | 158 | **reltype** 159 | 160 | A clear purpose for reltype is still a mystery to me. 161 | 162 | **target_mode** 163 | 164 | **target_part** 165 | 166 | **target_ref** 167 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | python-opc 3 | ########## 4 | 5 | Welcome 6 | ======= 7 | 8 | |po| is a Python library for manipulating Open Packaging Convention (OPC) 9 | packages. An OPC package is the file format used by Microsoft Office 2007 and 10 | later for Word, Excel, and PowerPoint. 11 | 12 | **STATUS: as of Jul 28 2013 python-opc and this documentation for it are both 13 | work in progress.** 14 | 15 | 16 | Documentation 17 | ============= 18 | 19 | |OpcPackage| objects 20 | ==================== 21 | 22 | .. autoclass:: opc.OpcPackage 23 | :members: 24 | :member-order: bysource 25 | :undoc-members: 26 | 27 | 28 | |Part| objects 29 | ============== 30 | 31 | The |Part| class is the default type for package parts and also serves as the 32 | base class for custom part classes. 33 | 34 | .. autoclass:: opc.package.Part 35 | :members: 36 | :member-order: bysource 37 | :undoc-members: 38 | 39 | 40 | |_Relationship| objects 41 | ======================= 42 | 43 | The |_Relationship| class ... 44 | 45 | .. autoclass:: opc.package._Relationship 46 | :members: 47 | :member-order: bysource 48 | :undoc-members: 49 | 50 | 51 | Concepts 52 | ======== 53 | 54 | ISO/IEC 29500 Specification 55 | --------------------------- 56 | 57 | 58 | Package contents 59 | ---------------- 60 | 61 | Content types stream, package relationships, parts. 62 | 63 | 64 | Pack URIs 65 | --------- 66 | 67 | ... A partname is a special case of pack URI ... 68 | 69 | 70 | Parts 71 | ----- 72 | 73 | 74 | Relationships 75 | ------------- 76 | 77 | ... target mode ... relationship type ... rId ... targets 78 | 79 | 80 | Content types 81 | ------------- 82 | 83 | 84 | 85 | Contents 86 | ======== 87 | 88 | .. toctree:: 89 | content_types 90 | relationship_types 91 | developer/design_narratives 92 | :maxdepth: 2 93 | 94 | * :ref:`genindex` 95 | * :ref:`modindex` 96 | * :ref:`search` 97 | 98 | -------------------------------------------------------------------------------- /doc/relationship_types.rst: -------------------------------------------------------------------------------- 1 | ################################ 2 | Relationship type constant names 3 | ################################ 4 | 5 | 6 | The following names are defined in the :mod:`opc.constants` module to allow 7 | relationship types to be referenced using an identifier rather than a literal 8 | value. 9 | 10 | The following import statement makes these available in a module:: 11 | 12 | from opc.constants import RELATIONSHIP_TYPE as RT 13 | 14 | A relationship type may then be referenced as a member of ``RT`` using dotted 15 | notation, for example:: 16 | 17 | rel.reltype = RT.SLIDE_LAYOUT 18 | 19 | The relationship type names are determined by transforming the trailing text 20 | of the relationship type string to upper snake case and replacing illegal 21 | Python identifier characters (the occasional hyphen) with an underscore. 22 | 23 | AUDIO 24 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio 25 | 26 | A_F_CHUNK 27 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk 28 | 29 | CALC_CHAIN 30 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain 31 | 32 | CERTIFICATE 33 | \http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/certificate 34 | 35 | CHART 36 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart 37 | 38 | CHARTSHEET 39 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet 40 | 41 | CHART_USER_SHAPES 42 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes 43 | 44 | COMMENTS 45 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments 46 | 47 | COMMENT_AUTHORS 48 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors 49 | 50 | CONNECTIONS 51 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections 52 | 53 | CONTROL 54 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/control 55 | 56 | CORE_PROPERTIES 57 | \http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties 58 | 59 | CUSTOM_PROPERTIES 60 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties 61 | 62 | CUSTOM_PROPERTY 63 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty 64 | 65 | CUSTOM_XML 66 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml 67 | 68 | CUSTOM_XML_PROPS 69 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps 70 | 71 | DIAGRAM_COLORS 72 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors 73 | 74 | DIAGRAM_DATA 75 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData 76 | 77 | DIAGRAM_LAYOUT 78 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout 79 | 80 | DIAGRAM_QUICK_STYLE 81 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle 82 | 83 | DIALOGSHEET 84 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet 85 | 86 | DRAWING 87 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing 88 | 89 | ENDNOTES 90 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes 91 | 92 | EXTENDED_PROPERTIES 93 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties 94 | 95 | EXTERNAL_LINK 96 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink 97 | 98 | FONT 99 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/font 100 | 101 | FONT_TABLE 102 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable 103 | 104 | FOOTER 105 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer 106 | 107 | FOOTNOTES 108 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes 109 | 110 | GLOSSARY_DOCUMENT 111 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument 112 | 113 | HANDOUT_MASTER 114 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster 115 | 116 | HEADER 117 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/header 118 | 119 | HYPERLINK 120 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink 121 | 122 | IMAGE 123 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/image 124 | 125 | NOTES_MASTER 126 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster 127 | 128 | NOTES_SLIDE 129 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide 130 | 131 | NUMBERING 132 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering 133 | 134 | OFFICE_DOCUMENT 135 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument 136 | 137 | OLE_OBJECT 138 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject 139 | 140 | ORIGIN 141 | \http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin 142 | 143 | PACKAGE 144 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/package 145 | 146 | PIVOT_CACHE_DEFINITION 147 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition 148 | 149 | PIVOT_CACHE_RECORDS 150 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsheetml/pivotCacheRecords 151 | 152 | PIVOT_TABLE 153 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable 154 | 155 | PRES_PROPS 156 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps 157 | 158 | PRINTER_SETTINGS 159 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings 160 | 161 | QUERY_TABLE 162 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable 163 | 164 | REVISION_HEADERS 165 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders 166 | 167 | REVISION_LOG 168 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog 169 | 170 | SETTINGS 171 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings 172 | 173 | SHARED_STRINGS 174 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings 175 | 176 | SHEET_METADATA 177 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata 178 | 179 | SIGNATURE 180 | \http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/signature 181 | 182 | SLIDE 183 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide 184 | 185 | SLIDE_LAYOUT 186 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout 187 | 188 | SLIDE_MASTER 189 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster 190 | 191 | SLIDE_UPDATE_INFO 192 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo 193 | 194 | STYLES 195 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles 196 | 197 | TABLE 198 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/table 199 | 200 | TABLE_SINGLE_CELLS 201 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells 202 | 203 | TABLE_STYLES 204 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles 205 | 206 | TAGS 207 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags 208 | 209 | THEME 210 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme 211 | 212 | THEME_OVERRIDE 213 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride 214 | 215 | THUMBNAIL 216 | \http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail 217 | 218 | USERNAMES 219 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames 220 | 221 | VIDEO 222 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/video 223 | 224 | VIEW_PROPS 225 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps 226 | 227 | VML_DRAWING 228 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing 229 | 230 | VOLATILE_DEPENDENCIES 231 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatileDependencies 232 | 233 | WEB_SETTINGS 234 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings 235 | 236 | WORKSHEET_SOURCE 237 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource 238 | 239 | XML_MAPS 240 | \http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps 241 | 242 | -------------------------------------------------------------------------------- /features/environment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # environment.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Used by behave to set testing environment before and after running acceptance 12 | tests. 13 | """ 14 | 15 | import os 16 | 17 | scratch_dir = os.path.abspath( 18 | os.path.join(os.path.split(__file__)[0], '_scratch') 19 | ) 20 | 21 | 22 | def before_all(context): 23 | if not os.path.isdir(scratch_dir): 24 | os.mkdir(scratch_dir) 25 | -------------------------------------------------------------------------------- /features/open-package.feature: -------------------------------------------------------------------------------- 1 | Feature: Open an OPC package 2 | In order to access the methods and properties on an OPC package 3 | As an Open XML developer 4 | I need to open an arbitrary package 5 | 6 | Scenario: Open a PowerPoint file 7 | Given a python-opc working environment 8 | When I open a PowerPoint file 9 | Then the expected package rels are loaded 10 | And the expected parts are loaded 11 | -------------------------------------------------------------------------------- /features/save-package.feature: -------------------------------------------------------------------------------- 1 | Feature: Save an OPC package 2 | In order to satisfy myself that python-opc might work 3 | As a pptx developer 4 | I want to see it pass a basic round-trip sanity-check 5 | 6 | Scenario: Round-trip a .docx file 7 | Given a clean working directory 8 | When I open a Word file 9 | And I save the document package 10 | Then I see the docx file in the working directory 11 | 12 | Scenario: Round-trip a .pptx file 13 | Given a clean working directory 14 | When I open a PowerPoint file 15 | And I save the presentation package 16 | Then I see the pptx file in the working directory 17 | 18 | Scenario: Round-trip an .xlsx file 19 | Given a clean working directory 20 | When I open an Excel file 21 | And I save the spreadsheet package 22 | Then I see the xlsx file in the working directory 23 | -------------------------------------------------------------------------------- /features/steps/opc_steps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # opc_steps.py 4 | # 5 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under 8 | # the MIT License: http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Acceptance test steps for python-opc.""" 11 | 12 | import hashlib 13 | import os 14 | 15 | from behave import given, when, then 16 | 17 | from opc import OpcPackage 18 | from opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT 19 | 20 | 21 | def absjoin(*paths): 22 | return os.path.abspath(os.path.join(*paths)) 23 | 24 | thisdir = os.path.split(__file__)[0] 25 | scratch_dir = absjoin(thisdir, '../_scratch') 26 | test_file_dir = absjoin(thisdir, '../../tests/test_files') 27 | basic_docx_path = absjoin(test_file_dir, 'test.docx') 28 | basic_pptx_path = absjoin(test_file_dir, 'test.pptx') 29 | basic_xlsx_path = absjoin(test_file_dir, 'test.xlsx') 30 | saved_docx_path = absjoin(scratch_dir, 'test_out.docx') 31 | saved_pptx_path = absjoin(scratch_dir, 'test_out.pptx') 32 | saved_xlsx_path = absjoin(scratch_dir, 'test_out.xlsx') 33 | 34 | 35 | # given ==================================================== 36 | 37 | @given('a clean working directory') 38 | def step_given_clean_working_dir(context): 39 | files_to_clean_out = (saved_docx_path, saved_pptx_path, saved_xlsx_path) 40 | for path in files_to_clean_out: 41 | if os.path.isfile(path): 42 | os.remove(path) 43 | 44 | 45 | @given('a python-opc working environment') 46 | def step_given_python_opc_working_environment(context): 47 | pass 48 | 49 | 50 | # when ===================================================== 51 | 52 | @when('I open an Excel file') 53 | def step_when_open_basic_xlsx(context): 54 | context.pkg = OpcPackage.open(basic_xlsx_path) 55 | 56 | 57 | @when('I open a PowerPoint file') 58 | def step_when_open_basic_pptx(context): 59 | context.pkg = OpcPackage.open(basic_pptx_path) 60 | 61 | 62 | @when('I open a Word file') 63 | def step_when_open_basic_docx(context): 64 | context.pkg = OpcPackage.open(basic_docx_path) 65 | 66 | 67 | @when('I save the document package') 68 | def step_when_save_document_package(context): 69 | if os.path.isfile(saved_docx_path): 70 | os.remove(saved_docx_path) 71 | context.pkg.save(saved_docx_path) 72 | 73 | 74 | @when('I save the presentation package') 75 | def step_when_save_presentation_package(context): 76 | if os.path.isfile(saved_pptx_path): 77 | os.remove(saved_pptx_path) 78 | context.pkg.save(saved_pptx_path) 79 | 80 | 81 | @when('I save the spreadsheet package') 82 | def step_when_save_spreadsheet_package(context): 83 | if os.path.isfile(saved_xlsx_path): 84 | os.remove(saved_xlsx_path) 85 | context.pkg.save(saved_xlsx_path) 86 | 87 | 88 | # then ===================================================== 89 | 90 | @then('the expected package rels are loaded') 91 | def step_then_expected_pkg_rels_loaded(context): 92 | expected_rel_values = ( 93 | ('rId1', RT.OFFICE_DOCUMENT, False, '/ppt/presentation.xml'), 94 | ('rId2', RT.THUMBNAIL, False, '/docProps/thumbnail.jpeg'), 95 | ('rId3', RT.CORE_PROPERTIES, False, '/docProps/core.xml'), 96 | ('rId4', RT.EXTENDED_PROPERTIES, False, '/docProps/app.xml'), 97 | ) 98 | assert len(expected_rel_values) == len(context.pkg.rels) 99 | for rId, reltype, is_external, partname in expected_rel_values: 100 | rel = context.pkg.rels[rId] 101 | assert rel.rId == rId, "rId is '%s'" % rel.rId 102 | assert rel.reltype == reltype, "reltype is '%s'" % rel.reltype 103 | assert rel.is_external == is_external 104 | assert rel.target_part.partname == partname, ( 105 | "target partname is '%s'" % rel.target_part.partname) 106 | 107 | 108 | @then('the expected parts are loaded') 109 | def step_then_expected_parts_are_loaded(context): 110 | expected_part_values = { 111 | '/docProps/app.xml': ( 112 | CT.OFC_EXTENDED_PROPERTIES, 'e5a7552c35180b9796f2132d39bc0d208cf' 113 | '8761f', [] 114 | ), 115 | '/docProps/core.xml': ( 116 | CT.OPC_CORE_PROPERTIES, '08c8ff0912231db740fa1277d8fa4ef175a306e' 117 | '4', [] 118 | ), 119 | '/docProps/thumbnail.jpeg': ( 120 | CT.JPEG, '8a93420017d57f9c69f802639ee9791579b21af5', [] 121 | ), 122 | '/ppt/presentation.xml': ( 123 | CT.PML_PRESENTATION_MAIN, 124 | 'efa7bee0ac72464903a67a6744c1169035d52a54', 125 | [ 126 | ('rId1', RT.SLIDE_MASTER, False, 127 | '/ppt/slideMasters/slideMaster1.xml'), 128 | ('rId2', RT.SLIDE, False, '/ppt/slides/slide1.xml'), 129 | ('rId3', RT.PRINTER_SETTINGS, False, 130 | '/ppt/printerSettings/printerSettings1.bin'), 131 | ('rId4', RT.PRES_PROPS, False, '/ppt/presProps.xml'), 132 | ('rId5', RT.VIEW_PROPS, False, '/ppt/viewProps.xml'), 133 | ('rId6', RT.THEME, False, '/ppt/theme/theme1.xml'), 134 | ('rId7', RT.TABLE_STYLES, False, '/ppt/tableStyles.xml'), 135 | ] 136 | ), 137 | '/ppt/printerSettings/printerSettings1.bin': ( 138 | CT.PML_PRINTER_SETTINGS, 'b0feb4cc107c9b2d135b1940560cf8f045ffb7' 139 | '46', [] 140 | ), 141 | '/ppt/presProps.xml': ( 142 | CT.PML_PRES_PROPS, '7d4981fd742429e6b8cc99089575ac0ee7db5194', [] 143 | ), 144 | '/ppt/viewProps.xml': ( 145 | CT.PML_VIEW_PROPS, '172a42a6be09d04eab61ae3d49eff5580a4be451', [] 146 | ), 147 | '/ppt/theme/theme1.xml': ( 148 | CT.OFC_THEME, '9f362326d8dc050ab6eef7f17335094bd06da47e', [] 149 | ), 150 | '/ppt/tableStyles.xml': ( 151 | CT.PML_TABLE_STYLES, '49bfd13ed02199b004bf0a019a596f127758d926', 152 | [] 153 | ), 154 | '/ppt/slideMasters/slideMaster1.xml': ( 155 | CT.PML_SLIDE_MASTER, 'be6fe53e199ef10259227a447e4ac9530803ecce', 156 | [ 157 | ('rId1', RT.SLIDE_LAYOUT, False, 158 | '/ppt/slideLayouts/slideLayout1.xml'), 159 | ('rId2', RT.SLIDE_LAYOUT, False, 160 | '/ppt/slideLayouts/slideLayout2.xml'), 161 | ('rId3', RT.SLIDE_LAYOUT, False, 162 | '/ppt/slideLayouts/slideLayout3.xml'), 163 | ('rId4', RT.THEME, False, '/ppt/theme/theme1.xml'), 164 | ], 165 | ), 166 | '/ppt/slideLayouts/slideLayout1.xml': ( 167 | CT.PML_SLIDE_LAYOUT, 'bcbeb908e22346fecda6be389759ca9ed068693c', 168 | [ 169 | ('rId1', RT.SLIDE_MASTER, False, 170 | '/ppt/slideMasters/slideMaster1.xml'), 171 | ], 172 | ), 173 | '/ppt/slideLayouts/slideLayout2.xml': ( 174 | CT.PML_SLIDE_LAYOUT, '316d0fb0ce4c3560fa2ed4edc3becf2c4ce84b6b', 175 | [ 176 | ('rId1', RT.SLIDE_MASTER, False, 177 | '/ppt/slideMasters/slideMaster1.xml'), 178 | ], 179 | ), 180 | '/ppt/slideLayouts/slideLayout3.xml': ( 181 | CT.PML_SLIDE_LAYOUT, '5b704e54c995b7d1bd7d24ef996a573676cc15ca', 182 | [ 183 | ('rId1', RT.SLIDE_MASTER, False, 184 | '/ppt/slideMasters/slideMaster1.xml'), 185 | ], 186 | ), 187 | '/ppt/slides/slide1.xml': ( 188 | CT.PML_SLIDE, '1841b18f1191629c70b7176d8e210fa2ef079d85', 189 | [ 190 | ('rId1', RT.SLIDE_LAYOUT, False, 191 | '/ppt/slideLayouts/slideLayout1.xml'), 192 | ('rId2', RT.HYPERLINK, True, 193 | 'https://github.com/scanny/python-pptx'), 194 | ] 195 | ), 196 | } 197 | assert len(context.pkg.parts) == len(expected_part_values), ( 198 | "len(context.pkg.parts) is %d" % len(context.pkg.parts)) 199 | for part in context.pkg.parts: 200 | partname = part.partname 201 | content_type, sha1, exp_rel_vals = expected_part_values[partname] 202 | assert part.content_type == content_type, ( 203 | "content_type for %s is '%s'" % (partname, part.content_type)) 204 | blob_sha1 = hashlib.sha1(part.blob).hexdigest() 205 | assert blob_sha1 == sha1, ("SHA1 for %s is '%s'" % 206 | (partname, blob_sha1)) 207 | assert len(part.rels) == len(exp_rel_vals), ( 208 | "len(part.rels) for %s is %d" % (partname, len(part.rels))) 209 | for rId, reltype, is_external, target in exp_rel_vals: 210 | rel = part.rels[rId] 211 | assert rel.rId == rId, "rId is '%s'" % rel.rId 212 | assert rel.reltype == reltype, ("reltype for %s on %s is '%s'" % 213 | (rId, partname, rel.reltype)) 214 | assert rel.is_external == is_external 215 | if rel.is_external: 216 | assert rel.target_ref == target, ( 217 | "target_ref for %s on %s is '%s'" % 218 | (rId, partname, rel.target_ref)) 219 | else: 220 | assert rel.target_part.partname == target, ( 221 | "target partname for %s on %s is '%s'" % 222 | (rId, partname, rel.target_part.partname)) 223 | 224 | 225 | @then('I see the docx file in the working directory') 226 | def step_then_see_docx_file_in_working_dir(context): 227 | reason = "file '%s' not found" % saved_docx_path 228 | assert os.path.isfile(saved_docx_path), reason 229 | minimum = 20000 230 | filesize = os.path.getsize(saved_docx_path) 231 | assert filesize > minimum 232 | 233 | 234 | @then('I see the pptx file in the working directory') 235 | def step_then_see_pptx_file_in_working_dir(context): 236 | reason = "file '%s' not found" % saved_pptx_path 237 | assert os.path.isfile(saved_pptx_path), reason 238 | minimum = 20000 239 | filesize = os.path.getsize(saved_pptx_path) 240 | assert filesize > minimum 241 | 242 | 243 | @then('I see the xlsx file in the working directory') 244 | def step_then_see_xlsx_file_in_working_dir(context): 245 | reason = "file '%s' not found" % saved_xlsx_path 246 | assert os.path.isfile(saved_xlsx_path), reason 247 | minimum = 30000 248 | filesize = os.path.getsize(saved_xlsx_path) 249 | assert filesize > minimum 250 | -------------------------------------------------------------------------------- /opc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # __init__.py 4 | # 5 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | from opc.package import OpcPackage, Part, PartFactory # noqa 11 | 12 | __version__ = '0.0.1d1' 13 | -------------------------------------------------------------------------------- /opc/oxml.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # oxml.py 4 | # 5 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Classes that directly manipulate Open XML and provide direct object-oriented 12 | access to the XML elements. 13 | """ 14 | 15 | from lxml import etree, objectify 16 | 17 | from opc.constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM 18 | 19 | 20 | # configure objectified XML parser 21 | fallback_lookup = objectify.ObjectifyElementClassLookup() 22 | element_class_lookup = etree.ElementNamespaceClassLookup(fallback_lookup) 23 | oxml_parser = etree.XMLParser(remove_blank_text=True) 24 | oxml_parser.set_element_class_lookup(element_class_lookup) 25 | 26 | nsmap = { 27 | 'ct': NS.OPC_CONTENT_TYPES, 28 | 'pr': NS.OPC_RELATIONSHIPS, 29 | } 30 | 31 | 32 | # =========================================================================== 33 | # functions 34 | # =========================================================================== 35 | 36 | def oxml_fromstring(text): 37 | """``etree.fromstring()`` replacement that uses oxml parser""" 38 | return objectify.fromstring(text, oxml_parser) 39 | 40 | 41 | def oxml_tostring(elm, encoding=None, pretty_print=False, standalone=None): 42 | # if xsi parameter is not set to False, PowerPoint won't load without a 43 | # repair step; deannotate removes some original xsi:type tags in core.xml 44 | # if this parameter is left out (or set to True) 45 | objectify.deannotate(elm, xsi=False, cleanup_namespaces=True) 46 | return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, 47 | standalone=standalone) 48 | 49 | 50 | # =========================================================================== 51 | # Custom element classes 52 | # =========================================================================== 53 | 54 | class OxmlBaseElement(objectify.ObjectifiedElement): 55 | """ 56 | Base class for all custom element classes, to add standardized behavior 57 | to all classes in one place. 58 | """ 59 | @property 60 | def xml(self): 61 | """ 62 | Return XML string for this element, suitable for testing purposes. 63 | Pretty printed for readability and without an XML declaration at the 64 | top. 65 | """ 66 | return oxml_tostring(self, encoding='unicode', pretty_print=True) 67 | 68 | 69 | class CT_Default(OxmlBaseElement): 70 | """ 71 | ```` element, specifying the default content type to be applied 72 | to a part with the specified extension. 73 | """ 74 | @property 75 | def content_type(self): 76 | """ 77 | String held in the ``ContentType`` attribute of this ```` 78 | element. 79 | """ 80 | return self.get('ContentType') 81 | 82 | @property 83 | def extension(self): 84 | """ 85 | String held in the ``Extension`` attribute of this ```` 86 | element. 87 | """ 88 | return self.get('Extension') 89 | 90 | @staticmethod 91 | def new(ext, content_type): 92 | """ 93 | Return a new ```` element with attributes set to parameter 94 | values. 95 | """ 96 | xml = '' % nsmap['ct'] 97 | default = oxml_fromstring(xml) 98 | default.set('Extension', ext[1:]) 99 | default.set('ContentType', content_type) 100 | objectify.deannotate(default, cleanup_namespaces=True) 101 | return default 102 | 103 | 104 | class CT_Override(OxmlBaseElement): 105 | """ 106 | ```` element, specifying the content type to be applied for a 107 | part with the specified partname. 108 | """ 109 | @property 110 | def content_type(self): 111 | """ 112 | String held in the ``ContentType`` attribute of this ```` 113 | element. 114 | """ 115 | return self.get('ContentType') 116 | 117 | @staticmethod 118 | def new(partname, content_type): 119 | """ 120 | Return a new ```` element with attributes set to parameter 121 | values. 122 | """ 123 | xml = '' % nsmap['ct'] 124 | override = oxml_fromstring(xml) 125 | override.set('PartName', partname) 126 | override.set('ContentType', content_type) 127 | objectify.deannotate(override, cleanup_namespaces=True) 128 | return override 129 | 130 | @property 131 | def partname(self): 132 | """ 133 | String held in the ``PartName`` attribute of this ```` 134 | element. 135 | """ 136 | return self.get('PartName') 137 | 138 | 139 | class CT_Relationship(OxmlBaseElement): 140 | """ 141 | ```` element, representing a single relationship from a 142 | source to a target part. 143 | """ 144 | @staticmethod 145 | def new(rId, reltype, target, target_mode=RTM.INTERNAL): 146 | """ 147 | Return a new ```` element. 148 | """ 149 | xml = '' % nsmap['pr'] 150 | relationship = oxml_fromstring(xml) 151 | relationship.set('Id', rId) 152 | relationship.set('Type', reltype) 153 | relationship.set('Target', target) 154 | if target_mode == RTM.EXTERNAL: 155 | relationship.set('TargetMode', RTM.EXTERNAL) 156 | objectify.deannotate(relationship, cleanup_namespaces=True) 157 | return relationship 158 | 159 | @property 160 | def rId(self): 161 | """ 162 | String held in the ``Id`` attribute of this ```` 163 | element. 164 | """ 165 | return self.get('Id') 166 | 167 | @property 168 | def reltype(self): 169 | """ 170 | String held in the ``Type`` attribute of this ```` 171 | element. 172 | """ 173 | return self.get('Type') 174 | 175 | @property 176 | def target_ref(self): 177 | """ 178 | String held in the ``Target`` attribute of this ```` 179 | element. 180 | """ 181 | return self.get('Target') 182 | 183 | @property 184 | def target_mode(self): 185 | """ 186 | String held in the ``TargetMode`` attribute of this 187 | ```` element, either ``Internal`` or ``External``. 188 | Defaults to ``Internal``. 189 | """ 190 | return self.get('TargetMode', RTM.INTERNAL) 191 | 192 | 193 | class CT_Relationships(OxmlBaseElement): 194 | """ 195 | ```` element, the root element in a .rels file. 196 | """ 197 | def add_rel(self, rId, reltype, target, is_external=False): 198 | """ 199 | Add a child ```` element with attributes set according 200 | to parameter values. 201 | """ 202 | target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL 203 | relationship = CT_Relationship.new(rId, reltype, target, target_mode) 204 | self.append(relationship) 205 | 206 | @staticmethod 207 | def new(): 208 | """ 209 | Return a new ```` element. 210 | """ 211 | xml = '' % nsmap['pr'] 212 | relationships = oxml_fromstring(xml) 213 | objectify.deannotate(relationships, cleanup_namespaces=True) 214 | return relationships 215 | 216 | @property 217 | def xml(self): 218 | """ 219 | Return XML string for this element, suitable for saving in a .rels 220 | stream, not pretty printed and with an XML declaration at the top. 221 | """ 222 | return oxml_tostring(self, encoding='UTF-8', standalone=True) 223 | 224 | 225 | class CT_Types(OxmlBaseElement): 226 | """ 227 | ```` element, the container element for Default and Override 228 | elements in [Content_Types].xml. 229 | """ 230 | def add_default(self, ext, content_type): 231 | """ 232 | Add a child ```` element with attributes set to parameter 233 | values. 234 | """ 235 | default = CT_Default.new(ext, content_type) 236 | self.append(default) 237 | 238 | def add_override(self, partname, content_type): 239 | """ 240 | Add a child ```` element with attributes set to parameter 241 | values. 242 | """ 243 | override = CT_Override.new(partname, content_type) 244 | self.append(override) 245 | 246 | @property 247 | def defaults(self): 248 | try: 249 | return self.Default[:] 250 | except AttributeError: 251 | return [] 252 | 253 | @staticmethod 254 | def new(): 255 | """ 256 | Return a new ```` element. 257 | """ 258 | xml = '' % nsmap['ct'] 259 | types = oxml_fromstring(xml) 260 | objectify.deannotate(types, cleanup_namespaces=True) 261 | return types 262 | 263 | @property 264 | def overrides(self): 265 | try: 266 | return self.Override[:] 267 | except AttributeError: 268 | return [] 269 | 270 | 271 | ct_namespace = element_class_lookup.get_namespace(nsmap['ct']) 272 | ct_namespace['Default'] = CT_Default 273 | ct_namespace['Override'] = CT_Override 274 | ct_namespace['Types'] = CT_Types 275 | 276 | pr_namespace = element_class_lookup.get_namespace(nsmap['pr']) 277 | pr_namespace['Relationship'] = CT_Relationship 278 | pr_namespace['Relationships'] = CT_Relationships 279 | -------------------------------------------------------------------------------- /opc/package.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # package.py 4 | # 5 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides an API for manipulating Open Packaging Convention (OPC) packages. 12 | """ 13 | 14 | from opc.constants import RELATIONSHIP_TYPE as RT 15 | from opc.oxml import CT_Relationships 16 | from opc.packuri import PACKAGE_URI 17 | from opc.pkgreader import PackageReader 18 | from opc.pkgwriter import PackageWriter 19 | 20 | 21 | class OpcPackage(object): 22 | """ 23 | Main API class for |python-opc|. A new instance is constructed by calling 24 | the :meth:`open` class method with a path to a package file or file-like 25 | object containing one. 26 | """ 27 | def __init__(self): 28 | super(OpcPackage, self).__init__() 29 | self._rels = RelationshipCollection(PACKAGE_URI.baseURI) 30 | 31 | @property 32 | def main_document(self): 33 | """ 34 | Return a reference to the main document part for this package. 35 | Examples include a document part for a WordprocessingML package, a 36 | presentation part for a PresentationML package, or a workbook part 37 | for a SpreadsheetML package. 38 | """ 39 | rel = self._rels.get_rel_of_type(RT.OFFICE_DOCUMENT) 40 | return rel.target_part 41 | 42 | @staticmethod 43 | def open(pkg_file): 44 | """ 45 | Return an |OpcPackage| instance loaded with the contents of 46 | *pkg_file*. 47 | """ 48 | pkg = OpcPackage() 49 | pkg_reader = PackageReader.from_file(pkg_file) 50 | Unmarshaller.unmarshal(pkg_reader, pkg, PartFactory) 51 | return pkg 52 | 53 | @property 54 | def parts(self): 55 | """ 56 | Return an immutable sequence (tuple) containing a reference to each 57 | of the parts in this package. 58 | """ 59 | return tuple([p for p in self._walk_parts(self._rels)]) 60 | 61 | @property 62 | def rels(self): 63 | """ 64 | Return a reference to the |RelationshipCollection| holding the 65 | relationships for this package. 66 | """ 67 | return self._rels 68 | 69 | def save(self, pkg_file): 70 | """ 71 | Save this package to *pkg_file*, where *file* can be either a path to 72 | a file (a string) or a file-like object. 73 | """ 74 | for part in self.parts: 75 | part._before_marshal() 76 | PackageWriter.write(pkg_file, self._rels, self.parts) 77 | 78 | def _add_relationship(self, reltype, target, rId, external=False): 79 | """ 80 | Return newly added |_Relationship| instance of *reltype* between this 81 | package and part *target* with key *rId*. Target mode is set to 82 | ``RTM.EXTERNAL`` if *external* is |True|. 83 | """ 84 | return self._rels.add_relationship(reltype, target, rId, external) 85 | 86 | @staticmethod 87 | def _walk_parts(rels, visited_parts=None): 88 | """ 89 | Generate exactly one reference to each of the parts in the package by 90 | performing a depth-first traversal of the rels graph. 91 | """ 92 | if visited_parts is None: 93 | visited_parts = [] 94 | for rel in rels: 95 | if rel.is_external: 96 | continue 97 | part = rel.target_part 98 | if part in visited_parts: 99 | continue 100 | visited_parts.append(part) 101 | yield part 102 | for part in OpcPackage._walk_parts(part._rels, visited_parts): 103 | yield part 104 | 105 | 106 | class Part(object): 107 | """ 108 | Base class for package parts. Provides common properties and methods, but 109 | intended to be subclassed in client code to implement specific part 110 | behaviors. 111 | """ 112 | def __init__(self, partname, content_type, blob=None): 113 | super(Part, self).__init__() 114 | self._partname = partname 115 | self._content_type = content_type 116 | self._blob = blob 117 | self._rels = RelationshipCollection(partname.baseURI) 118 | 119 | @property 120 | def blob(self): 121 | """ 122 | Contents of this package part as a sequence of bytes. May be text or 123 | binary. 124 | """ 125 | return self._blob 126 | 127 | @property 128 | def content_type(self): 129 | """ 130 | Content type of this part. 131 | """ 132 | return self._content_type 133 | 134 | @property 135 | def partname(self): 136 | """ 137 | |PackURI| instance containing partname for this part. 138 | """ 139 | return self._partname 140 | 141 | @property 142 | def rels(self): 143 | """ 144 | |RelationshipCollection| instance containing rels for this part. 145 | """ 146 | return self._rels 147 | 148 | def _add_relationship(self, reltype, target, rId, external=False): 149 | """ 150 | Return newly added |_Relationship| instance of *reltype* between this 151 | part and *target* with key *rId*. Target mode is set to 152 | ``RTM.EXTERNAL`` if *external* is |True|. 153 | """ 154 | return self._rels.add_relationship(reltype, target, rId, external) 155 | 156 | def _after_unmarshal(self): 157 | """ 158 | Entry point for post-unmarshaling processing, for example to parse 159 | the part XML. May be overridden by subclasses without forwarding call 160 | to super. 161 | """ 162 | # don't place any code here, just catch call if not overridden by 163 | # subclass 164 | pass 165 | 166 | def _before_marshal(self): 167 | """ 168 | Entry point for pre-serialization processing, for example to finalize 169 | part naming if necessary. May be overridden by subclasses without 170 | forwarding call to super. 171 | """ 172 | # don't place any code here, just catch call if not overridden by 173 | # subclass 174 | pass 175 | 176 | 177 | class PartFactory(object): 178 | """ 179 | Provides a way for client code to specify a subclass of |Part| to be 180 | constructed by |Unmarshaller| based on its content type. 181 | """ 182 | part_type_for = {} 183 | 184 | def __new__(cls, partname, content_type, blob): 185 | if content_type in PartFactory.part_type_for: 186 | CustomPartClass = PartFactory.part_type_for[content_type] 187 | return CustomPartClass.load(partname, content_type, blob) 188 | return Part(partname, content_type, blob) 189 | 190 | 191 | class _Relationship(object): 192 | """ 193 | Value object for relationship to part. 194 | """ 195 | def __init__(self, rId, reltype, target, baseURI, external=False): 196 | super(_Relationship, self).__init__() 197 | self._rId = rId 198 | self._reltype = reltype 199 | self._target = target 200 | self._baseURI = baseURI 201 | self._is_external = bool(external) 202 | 203 | @property 204 | def is_external(self): 205 | return self._is_external 206 | 207 | @property 208 | def reltype(self): 209 | return self._reltype 210 | 211 | @property 212 | def rId(self): 213 | return self._rId 214 | 215 | @property 216 | def target_part(self): 217 | if self._is_external: 218 | raise ValueError("target_part property on _Relationship is undef" 219 | "ined when target mode is External") 220 | return self._target 221 | 222 | @property 223 | def target_ref(self): 224 | if self._is_external: 225 | return self._target 226 | else: 227 | return self._target.partname.relative_ref(self._baseURI) 228 | 229 | 230 | class RelationshipCollection(object): 231 | """ 232 | Collection object for |_Relationship| instances, having list semantics. 233 | """ 234 | def __init__(self, baseURI): 235 | super(RelationshipCollection, self).__init__() 236 | self._baseURI = baseURI 237 | self._rels = [] 238 | 239 | def __getitem__(self, key): 240 | """ 241 | Implements access by subscript, e.g. ``rels[9]``. It also implements 242 | dict-style lookup of a relationship by rId, e.g. ``rels['rId1']``. 243 | """ 244 | if isinstance(key, basestring): 245 | for rel in self._rels: 246 | if rel.rId == key: 247 | return rel 248 | raise KeyError("no rId '%s' in RelationshipCollection" % key) 249 | else: 250 | return self._rels.__getitem__(key) 251 | 252 | def __len__(self): 253 | """Implements len() built-in on this object""" 254 | return self._rels.__len__() 255 | 256 | def add_relationship(self, reltype, target, rId, external=False): 257 | """ 258 | Return a newly added |_Relationship| instance. 259 | """ 260 | rel = _Relationship(rId, reltype, target, self._baseURI, external) 261 | self._rels.append(rel) 262 | return rel 263 | 264 | def get_rel_of_type(self, reltype): 265 | """ 266 | Return single relationship of type *reltype* from the collection. 267 | Raises |KeyError| if no matching relationship is found. Raises 268 | |ValueError| if more than one matching relationship is found. 269 | """ 270 | matching = [rel for rel in self._rels if rel.reltype == reltype] 271 | if len(matching) == 0: 272 | tmpl = "no relationship of type '%s' in collection" 273 | raise KeyError(tmpl % reltype) 274 | if len(matching) > 1: 275 | tmpl = "multiple relationships of type '%s' in collection" 276 | raise ValueError(tmpl % reltype) 277 | return matching[0] 278 | 279 | @property 280 | def xml(self): 281 | """ 282 | Serialize this relationship collection into XML suitable for storage 283 | as a .rels file in an OPC package. 284 | """ 285 | rels_elm = CT_Relationships.new() 286 | for rel in self._rels: 287 | rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, 288 | rel.is_external) 289 | return rels_elm.xml 290 | 291 | 292 | class Unmarshaller(object): 293 | """ 294 | Hosts static methods for unmarshalling a package from a |PackageReader| 295 | instance. 296 | """ 297 | @staticmethod 298 | def unmarshal(pkg_reader, pkg, part_factory): 299 | """ 300 | Construct graph of parts and realized relationships based on the 301 | contents of *pkg_reader*, delegating construction of each part to 302 | *part_factory*. Package relationships are added to *pkg*. 303 | """ 304 | parts = Unmarshaller._unmarshal_parts(pkg_reader, part_factory) 305 | Unmarshaller._unmarshal_relationships(pkg_reader, pkg, parts) 306 | for part in parts.values(): 307 | part._after_unmarshal() 308 | 309 | @staticmethod 310 | def _unmarshal_parts(pkg_reader, part_factory): 311 | """ 312 | Return a dictionary of |Part| instances unmarshalled from 313 | *pkg_reader*, keyed by partname. Side-effect is that each part in 314 | *pkg_reader* is constructed using *part_factory*. 315 | """ 316 | parts = {} 317 | for partname, content_type, blob in pkg_reader.iter_sparts(): 318 | parts[partname] = part_factory(partname, content_type, blob) 319 | return parts 320 | 321 | @staticmethod 322 | def _unmarshal_relationships(pkg_reader, pkg, parts): 323 | """ 324 | Add a relationship to the source object corresponding to each of the 325 | relationships in *pkg_reader* with its target_part set to the actual 326 | target part in *parts*. 327 | """ 328 | for source_uri, srel in pkg_reader.iter_srels(): 329 | source = pkg if source_uri == '/' else parts[source_uri] 330 | target = (srel.target_ref if srel.is_external 331 | else parts[srel.target_partname]) 332 | source._add_relationship(srel.reltype, target, srel.rId, 333 | srel.is_external) 334 | -------------------------------------------------------------------------------- /opc/packuri.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # packuri.py 4 | # 5 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides the PackURI value type along with some useful known values such as 12 | PACKAGE_URI. 13 | """ 14 | 15 | import posixpath 16 | 17 | 18 | class PackURI(str): 19 | """ 20 | Provides access to pack URI components such as the baseURI and the 21 | filename slice. Behaves as |str| otherwise. 22 | """ 23 | def __new__(cls, pack_uri_str): 24 | if not pack_uri_str[0] == '/': 25 | tmpl = "PackURI must begin with slash, got '%s'" 26 | raise ValueError(tmpl % pack_uri_str) 27 | return str.__new__(cls, pack_uri_str) 28 | 29 | @staticmethod 30 | def from_rel_ref(baseURI, relative_ref): 31 | """ 32 | Return a |PackURI| instance containing the absolute pack URI formed by 33 | translating *relative_ref* onto *baseURI*. 34 | """ 35 | joined_uri = posixpath.join(baseURI, relative_ref) 36 | abs_uri = posixpath.abspath(joined_uri) 37 | return PackURI(abs_uri) 38 | 39 | @property 40 | def baseURI(self): 41 | """ 42 | The base URI of this pack URI, the directory portion, roughly 43 | speaking. E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. 44 | For the package pseudo-partname '/', baseURI is '/'. 45 | """ 46 | return posixpath.split(self)[0] 47 | 48 | @property 49 | def ext(self): 50 | """ 51 | The extension portion of this pack URI, e.g. ``'.xml'`` for 52 | ``'/ppt/slides/slide1.xml'``. Note that the period is included, 53 | consistent with the behavior of :meth:`posixpath.ext`. 54 | """ 55 | return posixpath.splitext(self)[1] 56 | 57 | @property 58 | def filename(self): 59 | """ 60 | The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for 61 | ``'/ppt/slides/slide1.xml'``. For the package pseudo-partname '/', 62 | filename is ''. 63 | """ 64 | return posixpath.split(self)[1] 65 | 66 | @property 67 | def membername(self): 68 | """ 69 | The pack URI with the leading slash stripped off, the form used as 70 | the Zip file membername for the package item. Returns '' for the 71 | package pseudo-partname '/'. 72 | """ 73 | return self[1:] 74 | 75 | def relative_ref(self, baseURI): 76 | """ 77 | Return string containing relative reference to package item from 78 | *baseURI*. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would 79 | return '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'. 80 | """ 81 | # workaround for posixpath bug in 2.6, doesn't generate correct 82 | # relative path when *start* (second) parameter is root ('/') 83 | if baseURI == '/': 84 | relpath = self[1:] 85 | else: 86 | relpath = posixpath.relpath(self, baseURI) 87 | return relpath 88 | 89 | @property 90 | def rels_uri(self): 91 | """ 92 | The pack URI of the .rels part corresponding to the current pack URI. 93 | Only produces sensible output if the pack URI is a partname or the 94 | package pseudo-partname '/'. 95 | """ 96 | rels_filename = '%s.rels' % self.filename 97 | rels_uri_str = posixpath.join(self.baseURI, '_rels', rels_filename) 98 | return PackURI(rels_uri_str) 99 | 100 | 101 | PACKAGE_URI = PackURI('/') 102 | CONTENT_TYPES_URI = PackURI('/[Content_Types].xml') 103 | -------------------------------------------------------------------------------- /opc/phys_pkg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # phys_pkg.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides a general interface to a *physical* OPC package, such as a zip file. 12 | """ 13 | 14 | from zipfile import ZIP_DEFLATED, ZipFile 15 | 16 | 17 | class PhysPkgReader(object): 18 | """ 19 | Factory for physical package reader objects. 20 | """ 21 | def __new__(cls, pkg_file): 22 | return ZipPkgReader(pkg_file) 23 | 24 | 25 | class PhysPkgWriter(object): 26 | """ 27 | Factory for physical package writer objects. 28 | """ 29 | def __new__(cls, pkg_file): 30 | return ZipPkgWriter(pkg_file) 31 | 32 | 33 | class ZipPkgReader(object): 34 | """ 35 | Implements |PhysPkgReader| interface for a zip file OPC package. 36 | """ 37 | _CONTENT_TYPES_MEMBERNAME = '[Content_Types].xml' 38 | 39 | def __init__(self, pkg_file): 40 | super(ZipPkgReader, self).__init__() 41 | self._zipf = ZipFile(pkg_file, 'r') 42 | 43 | def blob_for(self, pack_uri): 44 | """ 45 | Return blob corresponding to *pack_uri*. Raises |ValueError| if no 46 | matching member is present in zip archive. 47 | """ 48 | return self._zipf.read(pack_uri.membername) 49 | 50 | def close(self): 51 | """ 52 | Close the zip archive, releasing any resources it is using. 53 | """ 54 | self._zipf.close() 55 | 56 | @property 57 | def content_types_xml(self): 58 | """ 59 | Return the `[Content_Types].xml` blob from the zip package. 60 | """ 61 | return self._zipf.read(self._CONTENT_TYPES_MEMBERNAME) 62 | 63 | def rels_xml_for(self, source_uri): 64 | """ 65 | Return rels item XML for source with *source_uri* or None if no rels 66 | item is present. 67 | """ 68 | try: 69 | rels_xml = self._zipf.read(source_uri.rels_uri.membername) 70 | except KeyError: 71 | rels_xml = None 72 | return rels_xml 73 | 74 | 75 | class ZipPkgWriter(object): 76 | """ 77 | Implements |PhysPkgWriter| interface for a zip file OPC package. 78 | """ 79 | def __init__(self, pkg_file): 80 | super(ZipPkgWriter, self).__init__() 81 | self._zipf = ZipFile(pkg_file, 'w', compression=ZIP_DEFLATED) 82 | 83 | def close(self): 84 | """ 85 | Close the zip archive, flushing any pending physical writes and 86 | releasing any resources it's using. 87 | """ 88 | self._zipf.close() 89 | 90 | def write(self, pack_uri, blob): 91 | """ 92 | Write *blob* to this zip package with the membername corresponding to 93 | *pack_uri*. 94 | """ 95 | self._zipf.writestr(pack_uri.membername, blob) 96 | -------------------------------------------------------------------------------- /opc/pkgreader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pkgreader.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides a low-level, read-only API to a serialized Open Packaging Convention 12 | (OPC) package. 13 | """ 14 | 15 | from opc.constants import RELATIONSHIP_TARGET_MODE as RTM 16 | from opc.oxml import oxml_fromstring 17 | from opc.packuri import PACKAGE_URI, PackURI 18 | from opc.phys_pkg import PhysPkgReader 19 | 20 | 21 | class PackageReader(object): 22 | """ 23 | Provides access to the contents of a zip-format OPC package via its 24 | :attr:`serialized_parts` and :attr:`pkg_srels` attributes. 25 | """ 26 | def __init__(self, content_types, pkg_srels, sparts): 27 | super(PackageReader, self).__init__() 28 | self._pkg_srels = pkg_srels 29 | self._sparts = sparts 30 | 31 | @staticmethod 32 | def from_file(pkg_file): 33 | """ 34 | Return a |PackageReader| instance loaded with contents of *pkg_file*. 35 | """ 36 | phys_reader = PhysPkgReader(pkg_file) 37 | content_types = _ContentTypeMap.from_xml(phys_reader.content_types_xml) 38 | pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI) 39 | sparts = PackageReader._load_serialized_parts(phys_reader, pkg_srels, 40 | content_types) 41 | phys_reader.close() 42 | return PackageReader(content_types, pkg_srels, sparts) 43 | 44 | def iter_sparts(self): 45 | """ 46 | Generate a 3-tuple `(partname, content_type, blob)` for each of the 47 | serialized parts in the package. 48 | """ 49 | for spart in self._sparts: 50 | yield (spart.partname, spart.content_type, spart.blob) 51 | 52 | def iter_srels(self): 53 | """ 54 | Generate a 2-tuple `(source_uri, srel)` for each of the relationships 55 | in the package. 56 | """ 57 | for srel in self._pkg_srels: 58 | yield (PACKAGE_URI, srel) 59 | for spart in self._sparts: 60 | for srel in spart.srels: 61 | yield (spart.partname, srel) 62 | 63 | @staticmethod 64 | def _load_serialized_parts(phys_reader, pkg_srels, content_types): 65 | """ 66 | Return a list of |_SerializedPart| instances corresponding to the 67 | parts in *phys_reader* accessible by walking the relationship graph 68 | starting with *pkg_srels*. 69 | """ 70 | sparts = [] 71 | part_walker = PackageReader._walk_phys_parts(phys_reader, pkg_srels) 72 | for partname, blob, srels in part_walker: 73 | content_type = content_types[partname] 74 | spart = _SerializedPart(partname, content_type, blob, srels) 75 | sparts.append(spart) 76 | return tuple(sparts) 77 | 78 | @staticmethod 79 | def _srels_for(phys_reader, source_uri): 80 | """ 81 | Return |_SerializedRelationshipCollection| instance populated with 82 | relationships for source identified by *source_uri*. 83 | """ 84 | rels_xml = phys_reader.rels_xml_for(source_uri) 85 | return _SerializedRelationshipCollection.load_from_xml( 86 | source_uri.baseURI, rels_xml) 87 | 88 | @staticmethod 89 | def _walk_phys_parts(phys_reader, srels, visited_partnames=None): 90 | """ 91 | Generate a 3-tuple `(partname, blob, srels)` for each of the parts in 92 | *phys_reader* by walking the relationship graph rooted at srels. 93 | """ 94 | if visited_partnames is None: 95 | visited_partnames = [] 96 | for srel in srels: 97 | if srel.is_external: 98 | continue 99 | partname = srel.target_partname 100 | if partname in visited_partnames: 101 | continue 102 | visited_partnames.append(partname) 103 | part_srels = PackageReader._srels_for(phys_reader, partname) 104 | blob = phys_reader.blob_for(partname) 105 | yield (partname, blob, part_srels) 106 | for partname, blob, srels in PackageReader._walk_phys_parts( 107 | phys_reader, part_srels, visited_partnames): 108 | yield (partname, blob, srels) 109 | 110 | 111 | class _ContentTypeMap(object): 112 | """ 113 | Value type providing dictionary semantics for looking up content type by 114 | part name, e.g. ``content_type = cti['/ppt/presentation.xml']``. 115 | """ 116 | def __init__(self): 117 | super(_ContentTypeMap, self).__init__() 118 | self._overrides = dict() 119 | self._defaults = dict() 120 | 121 | def __getitem__(self, partname): 122 | """ 123 | Return content type for part identified by *partname*. 124 | """ 125 | if not isinstance(partname, PackURI): 126 | tmpl = "_ContentTypeMap key must be , got %s" 127 | raise KeyError(tmpl % type(partname)) 128 | if partname in self._overrides: 129 | return self._overrides[partname] 130 | if partname.ext in self._defaults: 131 | return self._defaults[partname.ext] 132 | tmpl = "no content type for partname '%s' in [Content_Types].xml" 133 | raise KeyError(tmpl % partname) 134 | 135 | @staticmethod 136 | def from_xml(content_types_xml): 137 | """ 138 | Return a new |_ContentTypeMap| instance populated with the contents 139 | of *content_types_xml*. 140 | """ 141 | types_elm = oxml_fromstring(content_types_xml) 142 | ctmap = _ContentTypeMap() 143 | ctmap._overrides = dict( 144 | (o.partname, o.content_type) for o in types_elm.overrides 145 | ) 146 | ctmap._defaults = dict( 147 | ('.%s' % d.extension, d.content_type) for d in types_elm.defaults 148 | ) 149 | return ctmap 150 | 151 | 152 | class _SerializedPart(object): 153 | """ 154 | Value object for an OPC package part. Provides access to the partname, 155 | content type, blob, and serialized relationships for the part. 156 | """ 157 | def __init__(self, partname, content_type, blob, srels): 158 | super(_SerializedPart, self).__init__() 159 | self._partname = partname 160 | self._content_type = content_type 161 | self._blob = blob 162 | self._srels = srels 163 | 164 | @property 165 | def partname(self): 166 | return self._partname 167 | 168 | @property 169 | def content_type(self): 170 | return self._content_type 171 | 172 | @property 173 | def blob(self): 174 | return self._blob 175 | 176 | @property 177 | def srels(self): 178 | return self._srels 179 | 180 | 181 | class _SerializedRelationship(object): 182 | """ 183 | Value object representing a serialized relationship in an OPC package. 184 | Serialized, in this case, means any target part is referred to via its 185 | partname rather than a direct link to an in-memory |Part| object. 186 | """ 187 | def __init__(self, baseURI, rel_elm): 188 | super(_SerializedRelationship, self).__init__() 189 | self._baseURI = baseURI 190 | self._rId = rel_elm.rId 191 | self._reltype = rel_elm.reltype 192 | self._target_mode = rel_elm.target_mode 193 | self._target_ref = rel_elm.target_ref 194 | 195 | @property 196 | def is_external(self): 197 | """ 198 | True if target_mode is ``RTM.EXTERNAL`` 199 | """ 200 | return self._target_mode == RTM.EXTERNAL 201 | 202 | @property 203 | def reltype(self): 204 | """Relationship type, like ``RT.OFFICE_DOCUMENT``""" 205 | return self._reltype 206 | 207 | @property 208 | def rId(self): 209 | """ 210 | Relationship id, like 'rId9', corresponds to the ``Id`` attribute on 211 | the ``CT_Relationship`` element. 212 | """ 213 | return self._rId 214 | 215 | @property 216 | def target_mode(self): 217 | """ 218 | String in ``TargetMode`` attribute of ``CT_Relationship`` element, 219 | one of ``RTM.INTERNAL`` or ``RTM.EXTERNAL``. 220 | """ 221 | return self._target_mode 222 | 223 | @property 224 | def target_ref(self): 225 | """ 226 | String in ``Target`` attribute of ``CT_Relationship`` element, a 227 | relative part reference for internal target mode or an arbitrary URI, 228 | e.g. an HTTP URL, for external target mode. 229 | """ 230 | return self._target_ref 231 | 232 | @property 233 | def target_partname(self): 234 | """ 235 | |PackURI| instance containing partname targeted by this relationship. 236 | Raises ``ValueError`` on reference if target_mode is ``'External'``. 237 | Use :attr:`target_mode` to check before referencing. 238 | """ 239 | if self.is_external: 240 | msg = ('target_partname attribute on Relationship is undefined w' 241 | 'here TargetMode == "External"') 242 | raise ValueError(msg) 243 | # lazy-load _target_partname attribute 244 | if not hasattr(self, '_target_partname'): 245 | self._target_partname = PackURI.from_rel_ref(self._baseURI, 246 | self.target_ref) 247 | return self._target_partname 248 | 249 | 250 | class _SerializedRelationshipCollection(object): 251 | """ 252 | Read-only sequence of |_SerializedRelationship| instances corresponding 253 | to the relationships item XML passed to constructor. 254 | """ 255 | def __init__(self): 256 | super(_SerializedRelationshipCollection, self).__init__() 257 | self._srels = [] 258 | 259 | def __iter__(self): 260 | """Support iteration, e.g. 'for x in srels:'""" 261 | return self._srels.__iter__() 262 | 263 | @staticmethod 264 | def load_from_xml(baseURI, rels_item_xml): 265 | """ 266 | Return |_SerializedRelationshipCollection| instance loaded with the 267 | relationships contained in *rels_item_xml*. Returns an empty 268 | collection if *rels_item_xml* is |None|. 269 | """ 270 | srels = _SerializedRelationshipCollection() 271 | if rels_item_xml is not None: 272 | rels_elm = oxml_fromstring(rels_item_xml) 273 | for rel_elm in rels_elm.Relationship: 274 | srels._srels.append(_SerializedRelationship(baseURI, rel_elm)) 275 | return srels 276 | -------------------------------------------------------------------------------- /opc/pkgwriter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pkgwriter.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides a low-level, write-only API to a serialized Open Packaging 12 | Convention (OPC) package, essentially an implementation of OpcPackage.save() 13 | """ 14 | 15 | from opc.constants import CONTENT_TYPE as CT 16 | from opc.oxml import CT_Types, oxml_tostring 17 | from opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI 18 | from opc.phys_pkg import PhysPkgWriter 19 | from opc.spec import default_content_types 20 | 21 | 22 | class PackageWriter(object): 23 | """ 24 | Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be 25 | either a path to a zip file (a string) or a file-like object. Its single 26 | API method, :meth:`write`, is static, so this class is not intended to 27 | be instantiated. 28 | """ 29 | @staticmethod 30 | def write(pkg_file, pkg_rels, parts): 31 | """ 32 | Write a physical package (.pptx file) to *pkg_file* containing 33 | *pkg_rels* and *parts* and a content types stream based on the 34 | content types of the parts. 35 | """ 36 | phys_writer = PhysPkgWriter(pkg_file) 37 | PackageWriter._write_content_types_stream(phys_writer, parts) 38 | PackageWriter._write_pkg_rels(phys_writer, pkg_rels) 39 | PackageWriter._write_parts(phys_writer, parts) 40 | phys_writer.close() 41 | 42 | @staticmethod 43 | def _write_content_types_stream(phys_writer, parts): 44 | """ 45 | Write ``[Content_Types].xml`` part to the physical package with an 46 | appropriate content type lookup target for each part in *parts*. 47 | """ 48 | phys_writer.write(CONTENT_TYPES_URI, _ContentTypesItem.xml_for(parts)) 49 | 50 | @staticmethod 51 | def _write_parts(phys_writer, parts): 52 | """ 53 | Write the blob of each part in *parts* to the package, along with a 54 | rels item for its relationships if and only if it has any. 55 | """ 56 | for part in parts: 57 | phys_writer.write(part.partname, part.blob) 58 | if len(part._rels): 59 | phys_writer.write(part.partname.rels_uri, part._rels.xml) 60 | 61 | @staticmethod 62 | def _write_pkg_rels(phys_writer, pkg_rels): 63 | """ 64 | Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the 65 | package. 66 | """ 67 | phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) 68 | 69 | 70 | class _ContentTypesItem(object): 71 | """ 72 | Service class that composes a content types item ([Content_Types].xml) 73 | based on a list of parts. Not meant to be instantiated, its single 74 | interface method is xml_for(), e.g. ``_ContentTypesItem.xml_for(parts)``. 75 | """ 76 | @staticmethod 77 | def xml_for(parts): 78 | """ 79 | Return content types XML mapping each part in *parts* to the 80 | appropriate content type and suitable for storage as 81 | ``[Content_Types].xml`` in an OPC package. 82 | """ 83 | defaults = dict((('.rels', CT.OPC_RELATIONSHIPS), ('.xml', CT.XML))) 84 | overrides = dict() 85 | for part in parts: 86 | _ContentTypesItem._add_content_type( 87 | defaults, overrides, part.partname, part.content_type 88 | ) 89 | return _ContentTypesItem._xml(defaults, overrides) 90 | 91 | @staticmethod 92 | def _add_content_type(defaults, overrides, partname, content_type): 93 | """ 94 | Add a content type for the part with *partname* and *content_type*, 95 | using a default or override as appropriate. 96 | """ 97 | ext = partname.ext 98 | if (ext, content_type) in default_content_types: 99 | defaults[ext] = content_type 100 | else: 101 | overrides[partname] = content_type 102 | 103 | @staticmethod 104 | def _xml(defaults, overrides): 105 | """ 106 | XML form of this content types item, suitable for storage as 107 | ``[Content_Types].xml`` in an OPC package. Although the sequence of 108 | elements is not strictly significant, as an aid to testing and 109 | readability Default elements are sorted by extension and Override 110 | elements are sorted by partname. 111 | """ 112 | _types_elm = CT_Types.new() 113 | for ext in sorted(defaults.keys()): 114 | _types_elm.add_default(ext, defaults[ext]) 115 | for partname in sorted(overrides.keys()): 116 | _types_elm.add_override(partname, overrides[partname]) 117 | return oxml_tostring(_types_elm, encoding='UTF-8', standalone=True) 118 | -------------------------------------------------------------------------------- /opc/spec.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # spec.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """ 11 | Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500. 12 | """ 13 | 14 | from opc.constants import CONTENT_TYPE as CT 15 | 16 | 17 | default_content_types = ( 18 | ('.bin', CT.PML_PRINTER_SETTINGS), 19 | ('.bin', CT.SML_PRINTER_SETTINGS), 20 | ('.bin', CT.WML_PRINTER_SETTINGS), 21 | ('.bmp', CT.BMP), 22 | ('.emf', CT.X_EMF), 23 | ('.fntdata', CT.X_FONTDATA), 24 | ('.gif', CT.GIF), 25 | ('.jpe', CT.JPEG), 26 | ('.jpeg', CT.JPEG), 27 | ('.jpg', CT.JPEG), 28 | ('.png', CT.PNG), 29 | ('.rels', CT.OPC_RELATIONSHIPS), 30 | ('.tif', CT.TIFF), 31 | ('.tiff', CT.TIFF), 32 | ('.wdp', CT.MS_PHOTO), 33 | ('.wmf', CT.X_WMF), 34 | ('.xlsx', CT.SML_SHEET), 35 | ('.xml', CT.XML), 36 | ) 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | 6 | from setuptools import setup 7 | 8 | # Read the version from opc.__version__ without importing the package 9 | # (and thus attempting to import packages it depends on that may not be 10 | # installed yet) 11 | thisdir = os.path.dirname(__file__) 12 | init_py_path = os.path.join(thisdir, 'opc', '__init__.py') 13 | version = re.search("__version__ = '([^']+)'", 14 | open(init_py_path).read()).group(1) 15 | 16 | 17 | NAME = 'python-opc' 18 | VERSION = version 19 | DESCRIPTION = ( 20 | 'Manipulate Open Packaging Convention (OPC) files, e.g. .docx, .pptx, an' 21 | 'd .xlsx files for Microsoft Office' 22 | ) 23 | KEYWORDS = 'opc open xml docx pptx xslx office' 24 | AUTHOR = 'Steve Canny' 25 | AUTHOR_EMAIL = 'python-opc@googlegroups.com' 26 | URL = 'https://github.com/python-openxml/python-opc' 27 | LICENSE = 'MIT' 28 | PACKAGES = ['opc'] 29 | 30 | INSTALL_REQUIRES = ['lxml'] 31 | TEST_SUITE = 'tests' 32 | TESTS_REQUIRE = ['behave', 'mock', 'pytest'] 33 | 34 | CLASSIFIERS = [ 35 | 'Development Status :: 2 - Pre-Alpha', 36 | 'Environment :: Console', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.2', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Topic :: Office/Business :: Office Suites', 48 | 'Topic :: Software Development :: Libraries' 49 | ] 50 | 51 | readme = os.path.join(os.path.dirname(__file__), 'README.rst') 52 | LONG_DESCRIPTION = open(readme).read() 53 | 54 | 55 | params = { 56 | 'name': NAME, 57 | 'version': VERSION, 58 | 'description': DESCRIPTION, 59 | 'keywords': KEYWORDS, 60 | 'long_description': LONG_DESCRIPTION, 61 | 'author': AUTHOR, 62 | 'author_email': AUTHOR_EMAIL, 63 | 'url': URL, 64 | 'license': LICENSE, 65 | 'packages': PACKAGES, 66 | 'install_requires': INSTALL_REQUIRES, 67 | 'tests_require': TESTS_REQUIRE, 68 | 'test_suite': TEST_SUITE, 69 | 'classifiers': CLASSIFIERS, 70 | } 71 | 72 | setup(**params) 73 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-openxml/python-opc/e35d643ebc8c67b6c3388f38c57672a40f173010/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_files/test.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-openxml/python-opc/e35d643ebc8c67b6c3388f38c57672a40f173010/tests/test_files/test.docx -------------------------------------------------------------------------------- /tests/test_files/test.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-openxml/python-opc/e35d643ebc8c67b6c3388f38c57672a40f173010/tests/test_files/test.pptx -------------------------------------------------------------------------------- /tests/test_files/test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-openxml/python-opc/e35d643ebc8c67b6c3388f38c57672a40f173010/tests/test_files/test.xlsx -------------------------------------------------------------------------------- /tests/test_oxml.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_oxml.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.oxml module.""" 11 | 12 | from opc.constants import RELATIONSHIP_TARGET_MODE as RTM 13 | from opc.oxml import ( 14 | CT_Default, CT_Override, CT_Relationship, CT_Relationships, CT_Types, 15 | oxml_tostring 16 | ) 17 | 18 | from .unitdata import ( 19 | a_Default, an_Override, a_Relationship, a_Relationships, a_Types 20 | ) 21 | 22 | 23 | class DescribeCT_Default(object): 24 | 25 | def it_provides_read_access_to_xml_values(self): 26 | default = a_Default().element 27 | assert default.extension == 'xml' 28 | assert default.content_type == 'application/xml' 29 | 30 | def it_can_construct_a_new_default_element(self): 31 | default = CT_Default.new('.xml', 'application/xml') 32 | expected_xml = a_Default().xml 33 | assert default.xml == expected_xml 34 | 35 | 36 | class DescribeCT_Override(object): 37 | 38 | def it_provides_read_access_to_xml_values(self): 39 | override = an_Override().element 40 | assert override.partname == '/part/name.xml' 41 | assert override.content_type == 'app/vnd.type' 42 | 43 | def it_can_construct_a_new_override_element(self): 44 | override = CT_Override.new('/part/name.xml', 'app/vnd.type') 45 | expected_xml = an_Override().xml 46 | assert override.xml == expected_xml 47 | 48 | 49 | class DescribeCT_Relationship(object): 50 | 51 | def it_provides_read_access_to_xml_values(self): 52 | rel = a_Relationship().element 53 | assert rel.rId == 'rId9' 54 | assert rel.reltype == 'ReLtYpE' 55 | assert rel.target_ref == 'docProps/core.xml' 56 | assert rel.target_mode == RTM.INTERNAL 57 | 58 | def it_can_construct_from_attribute_values(self): 59 | cases = ( 60 | ('rId9', 'ReLtYpE', 'foo/bar.xml', None), 61 | ('rId9', 'ReLtYpE', 'bar/foo.xml', RTM.INTERNAL), 62 | ('rId9', 'ReLtYpE', 'http://some/link', RTM.EXTERNAL), 63 | ) 64 | for rId, reltype, target, target_mode in cases: 65 | if target_mode is None: 66 | rel = CT_Relationship.new(rId, reltype, target) 67 | else: 68 | rel = CT_Relationship.new(rId, reltype, target, target_mode) 69 | builder = a_Relationship().with_target(target) 70 | if target_mode == RTM.EXTERNAL: 71 | builder = builder.with_target_mode(RTM.EXTERNAL) 72 | expected_rel_xml = builder.xml 73 | assert rel.xml == expected_rel_xml 74 | 75 | 76 | class DescribeCT_Relationships(object): 77 | 78 | def it_can_construct_a_new_relationships_element(self): 79 | rels = CT_Relationships.new() 80 | actual_xml = oxml_tostring(rels, encoding='unicode', 81 | pretty_print=True) 82 | expected_xml = ( 83 | '\n' 85 | ) 86 | assert actual_xml == expected_xml 87 | 88 | def it_can_build_rels_element_incrementally(self): 89 | # setup ------------------------ 90 | rels = CT_Relationships.new() 91 | # exercise --------------------- 92 | rels.add_rel('rId1', 'http://reltype1', 'docProps/core.xml') 93 | rels.add_rel('rId2', 'http://linktype', 'http://some/link', True) 94 | rels.add_rel('rId3', 'http://reltype2', '../slides/slide1.xml') 95 | # verify ----------------------- 96 | expected_rels_xml = a_Relationships().xml 97 | actual_xml = oxml_tostring(rels, encoding='unicode', 98 | pretty_print=True) 99 | assert actual_xml == expected_rels_xml 100 | 101 | def it_can_generate_rels_file_xml(self): 102 | expected_xml = ( 103 | '\n' 104 | ''.encode('utf-8') 106 | ) 107 | assert CT_Relationships.new().xml == expected_xml 108 | 109 | 110 | class DescribeCT_Types(object): 111 | 112 | def it_provides_access_to_default_child_elements(self): 113 | types = a_Types().element 114 | assert len(types.defaults) == 2 115 | for default in types.defaults: 116 | assert isinstance(default, CT_Default) 117 | 118 | def it_provides_access_to_override_child_elements(self): 119 | types = a_Types().element 120 | assert len(types.overrides) == 3 121 | for override in types.overrides: 122 | assert isinstance(override, CT_Override) 123 | 124 | def it_should_have_empty_list_on_no_matching_elements(self): 125 | types = a_Types().empty().element 126 | assert types.defaults == [] 127 | assert types.overrides == [] 128 | 129 | def it_can_construct_a_new_types_element(self): 130 | types = CT_Types.new() 131 | expected_xml = a_Types().empty().xml 132 | assert types.xml == expected_xml 133 | 134 | def it_can_build_types_element_incrementally(self): 135 | types = CT_Types.new() 136 | types.add_default('.xml', 'application/xml') 137 | types.add_default('.jpeg', 'image/jpeg') 138 | types.add_override('/docProps/core.xml', 'app/vnd.type1') 139 | types.add_override('/ppt/presentation.xml', 'app/vnd.type2') 140 | types.add_override('/docProps/thumbnail.jpeg', 'image/jpeg') 141 | expected_types_xml = a_Types().xml 142 | assert types.xml == expected_types_xml 143 | -------------------------------------------------------------------------------- /tests/test_package.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_package.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.package module.""" 11 | 12 | import pytest 13 | 14 | from mock import call, Mock, patch, PropertyMock 15 | 16 | from opc.constants import CONTENT_TYPE as CT 17 | from opc.oxml import CT_Relationships 18 | from opc.package import ( 19 | OpcPackage, Part, PartFactory, _Relationship, RelationshipCollection, 20 | Unmarshaller 21 | ) 22 | from opc.packuri import PACKAGE_URI, PackURI 23 | 24 | from .unitutil import class_mock, method_mock 25 | 26 | 27 | @pytest.fixture 28 | def RelationshipCollection_(request): 29 | return class_mock('opc.package.RelationshipCollection', request) 30 | 31 | 32 | class DescribeOpcPackage(object): 33 | 34 | @pytest.fixture 35 | def PackageReader_(self, request): 36 | return class_mock('opc.package.PackageReader', request) 37 | 38 | @pytest.fixture 39 | def PackageWriter_(self, request): 40 | return class_mock('opc.package.PackageWriter', request) 41 | 42 | @pytest.fixture 43 | def PartFactory_(self, request): 44 | return class_mock('opc.package.PartFactory', request) 45 | 46 | @pytest.fixture 47 | def parts(self, request): 48 | """ 49 | Return a mock patching property OpcPackage.parts, reversing the 50 | patch after each use. 51 | """ 52 | _patch = patch.object(OpcPackage, 'parts', new_callable=PropertyMock) 53 | request.addfinalizer(_patch.stop) 54 | return _patch.start() 55 | 56 | @pytest.fixture 57 | def Unmarshaller_(self, request): 58 | return class_mock('opc.package.Unmarshaller', request) 59 | 60 | def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, 61 | Unmarshaller_): 62 | # mockery ---------------------- 63 | pkg_file = Mock(name='pkg_file') 64 | pkg_reader = PackageReader_.from_file.return_value 65 | # exercise --------------------- 66 | pkg = OpcPackage.open(pkg_file) 67 | # verify ----------------------- 68 | PackageReader_.from_file.assert_called_once_with(pkg_file) 69 | Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, 70 | PartFactory_) 71 | assert isinstance(pkg, OpcPackage) 72 | 73 | def it_initializes_its_rels_collection_on_construction( 74 | self, RelationshipCollection_): 75 | pkg = OpcPackage() 76 | RelationshipCollection_.assert_called_once_with(PACKAGE_URI.baseURI) 77 | assert pkg.rels == RelationshipCollection_.return_value 78 | 79 | def it_can_add_a_relationship_to_a_part(self): 80 | # mockery ---------------------- 81 | reltype, target, rId = ( 82 | Mock(name='reltype'), Mock(name='target'), Mock(name='rId') 83 | ) 84 | pkg = OpcPackage() 85 | pkg._rels = Mock(name='_rels') 86 | # exercise --------------------- 87 | pkg._add_relationship(reltype, target, rId) 88 | # verify ----------------------- 89 | pkg._rels.add_relationship.assert_called_once_with(reltype, target, 90 | rId, False) 91 | 92 | def it_has_an_immutable_sequence_containing_its_parts(self): 93 | # mockery ---------------------- 94 | parts = [Mock(name='part1'), Mock(name='part2')] 95 | pkg = OpcPackage() 96 | # verify ----------------------- 97 | with patch.object(OpcPackage, '_walk_parts', return_value=parts): 98 | assert pkg.parts == (parts[0], parts[1]) 99 | 100 | def it_can_iterate_over_parts_by_walking_rels_graph(self): 101 | # +----------+ +--------+ 102 | # | pkg_rels |-----> | part_1 | 103 | # +----------+ +--------+ 104 | # | | ^ 105 | # v v | 106 | # external +--------+ 107 | # | part_2 | 108 | # +--------+ 109 | part1, part2 = (Mock(name='part1'), Mock(name='part2')) 110 | part1._rels = [Mock(name='rel1', is_external=False, target_part=part2)] 111 | part2._rels = [Mock(name='rel2', is_external=False, target_part=part1)] 112 | pkg_rels = [ 113 | Mock(name='rel3', is_external=False, target_part=part1), 114 | Mock(name='rel3', is_external=True), 115 | ] 116 | # exercise --------------------- 117 | generated_parts = [part for part in OpcPackage._walk_parts(pkg_rels)] 118 | # verify ----------------------- 119 | assert generated_parts == [part1, part2] 120 | 121 | def it_can_save_to_a_pkg_file(self, PackageWriter_, parts): 122 | # mockery ---------------------- 123 | pkg_file = Mock(name='pkg_file') 124 | pkg = OpcPackage() 125 | parts.return_value = parts = [Mock(name='part1'), Mock(name='part2')] 126 | # exercise --------------------- 127 | pkg.save(pkg_file) 128 | # verify ----------------------- 129 | for part in parts: 130 | part._before_marshal.assert_called_once_with() 131 | PackageWriter_.write.assert_called_once_with(pkg_file, pkg._rels, 132 | parts) 133 | 134 | 135 | class DescribePart(object): 136 | 137 | @pytest.fixture 138 | def part(self): 139 | partname = Mock(name='partname', baseURI='/') 140 | return Part(partname, None, None) 141 | 142 | def it_remembers_its_construction_state(self): 143 | partname, content_type, blob = ( 144 | Mock(name='partname'), Mock(name='content_type'), 145 | Mock(name='blob') 146 | ) 147 | part = Part(partname, content_type, blob) 148 | assert part.blob == blob 149 | assert part.content_type == content_type 150 | assert part.partname == partname 151 | 152 | def it_has_a_rels_collection_it_initializes_on_construction( 153 | self, RelationshipCollection_): 154 | partname = Mock(name='partname', baseURI='/') 155 | part = Part(partname, None, None) 156 | RelationshipCollection_.assert_called_once_with('/') 157 | assert part.rels == RelationshipCollection_.return_value 158 | 159 | def it_can_add_a_relationship_to_another_part(self, part): 160 | # mockery ---------------------- 161 | reltype, target, rId = ( 162 | Mock(name='reltype'), Mock(name='target'), Mock(name='rId') 163 | ) 164 | part._rels = Mock(name='_rels') 165 | # exercise --------------------- 166 | part._add_relationship(reltype, target, rId) 167 | # verify ----------------------- 168 | part._rels.add_relationship.assert_called_once_with(reltype, target, 169 | rId, False) 170 | 171 | def it_can_be_notified_after_unmarshalling_is_complete(self, part): 172 | part._after_unmarshal() 173 | 174 | def it_can_be_notified_before_marshalling_is_started(self, part): 175 | part._before_marshal() 176 | 177 | 178 | class DescribePartFactory(object): 179 | 180 | @pytest.fixture 181 | def Part_(self, request): 182 | return class_mock('opc.package.Part', request) 183 | 184 | def it_constructs_a_part_instance(self, Part_): 185 | # mockery ---------------------- 186 | partname, content_type, blob = ( 187 | Mock(name='partname'), Mock(name='content_type'), 188 | Mock(name='blob') 189 | ) 190 | # exercise --------------------- 191 | part = PartFactory(partname, content_type, blob) 192 | # verify ----------------------- 193 | Part_.assert_called_once_with(partname, content_type, blob) 194 | assert part == Part_.return_value 195 | 196 | def it_constructs_custom_part_type_for_registered_content_types(self): 197 | # mockery ---------------------- 198 | CustomPartClass = Mock(name='CustomPartClass') 199 | partname, blob = (Mock(name='partname'), Mock(name='blob')) 200 | # exercise --------------------- 201 | PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = CustomPartClass 202 | part = PartFactory(partname, CT.WML_DOCUMENT_MAIN, blob) 203 | # verify ----------------------- 204 | CustomPartClass.assert_called_once_with(partname, 205 | CT.WML_DOCUMENT_MAIN, blob) 206 | assert part is CustomPartClass.return_value 207 | 208 | 209 | class Describe_Relationship(object): 210 | 211 | def it_remembers_construction_values(self): 212 | # test data -------------------- 213 | rId = 'rId9' 214 | reltype = 'reltype' 215 | target = Mock(name='target_part') 216 | external = False 217 | # exercise --------------------- 218 | rel = _Relationship(rId, reltype, target, None, external) 219 | # verify ----------------------- 220 | assert rel.rId == rId 221 | assert rel.reltype == reltype 222 | assert rel.target_part == target 223 | assert rel.is_external == external 224 | 225 | def it_should_raise_on_target_part_access_on_external_rel(self): 226 | rel = _Relationship(None, None, None, None, external=True) 227 | with pytest.raises(ValueError): 228 | rel.target_part 229 | 230 | def it_should_have_target_ref_for_external_rel(self): 231 | rel = _Relationship(None, None, 'target', None, external=True) 232 | assert rel.target_ref == 'target' 233 | 234 | def it_should_have_relative_ref_for_internal_rel(self): 235 | """ 236 | Internal relationships (TargetMode == 'Internal' in the XML) should 237 | have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for 238 | the target_ref attribute. 239 | """ 240 | part = Mock(name='part', partname=PackURI('/ppt/media/image1.png')) 241 | baseURI = '/ppt/slides' 242 | rel = _Relationship(None, None, part, baseURI) # external=False 243 | assert rel.target_ref == '../media/image1.png' 244 | 245 | 246 | class DescribeRelationshipCollection(object): 247 | 248 | @pytest.fixture 249 | def _Relationship_(self, request): 250 | return class_mock('opc.package._Relationship', request) 251 | 252 | @pytest.fixture 253 | def rels(self): 254 | """ 255 | Populated RelationshipCollection instance that will exercise the 256 | rels.xml property. 257 | """ 258 | rels = RelationshipCollection('/baseURI') 259 | rels.add_relationship( 260 | reltype='http://rt-hyperlink', target='http://some/link', 261 | rId='rId1', external=True 262 | ) 263 | part = Mock(name='part') 264 | part.partname.relative_ref.return_value = '../media/image1.png' 265 | rels.add_relationship(reltype='http://rt-image', target=part, 266 | rId='rId2') 267 | return rels 268 | 269 | @pytest.fixture 270 | def rels_elm(self, request): 271 | """ 272 | Return a rels_elm mock that will be returned from 273 | CT_Relationships.new() 274 | """ 275 | # create rels_elm mock with a .xml property 276 | rels_elm = Mock(name='rels_elm') 277 | xml = PropertyMock(name='xml') 278 | type(rels_elm).xml = xml 279 | rels_elm.attach_mock(xml, 'xml') 280 | rels_elm.reset_mock() # to clear attach_mock call 281 | # patch CT_Relationships to return that rels_elm 282 | patch_ = patch.object(CT_Relationships, 'new', return_value=rels_elm) 283 | patch_.start() 284 | request.addfinalizer(patch_.stop) 285 | return rels_elm 286 | 287 | def it_has_a_len(self): 288 | rels = RelationshipCollection(None) 289 | assert len(rels) == 0 290 | 291 | def it_supports_indexed_access(self): 292 | rels = RelationshipCollection(None) 293 | try: 294 | rels[0] 295 | except TypeError: 296 | msg = 'RelationshipCollection does not support indexed access' 297 | pytest.fail(msg) 298 | except IndexError: 299 | pass 300 | 301 | def it_has_dict_style_lookup_of_rel_by_rId(self): 302 | rel = Mock(name='rel', rId='foobar') 303 | rels = RelationshipCollection(None) 304 | rels._rels.append(rel) 305 | assert rels['foobar'] == rel 306 | 307 | def it_should_raise_on_failed_lookup_by_rId(self): 308 | rel = Mock(name='rel', rId='foobar') 309 | rels = RelationshipCollection(None) 310 | rels._rels.append(rel) 311 | with pytest.raises(KeyError): 312 | rels['barfoo'] 313 | 314 | def it_can_add_a_relationship(self, _Relationship_): 315 | baseURI, rId, reltype, target, external = ( 316 | 'baseURI', 'rId9', 'reltype', 'target', False 317 | ) 318 | rels = RelationshipCollection(baseURI) 319 | rel = rels.add_relationship(reltype, target, rId, external) 320 | _Relationship_.assert_called_once_with(rId, reltype, target, baseURI, 321 | external) 322 | assert rels[0] == rel 323 | assert rel == _Relationship_.return_value 324 | 325 | def it_can_compose_rels_xml(self, rels, rels_elm): 326 | # exercise --------------------- 327 | rels.xml 328 | # trace ------------------------ 329 | print('Actual calls:\n%s' % rels_elm.mock_calls) 330 | # verify ----------------------- 331 | expected_rels_elm_calls = [ 332 | call.add_rel('rId1', 'http://rt-hyperlink', 'http://some/link', 333 | True), 334 | call.add_rel('rId2', 'http://rt-image', '../media/image1.png', 335 | False), 336 | call.xml() 337 | ] 338 | assert rels_elm.mock_calls == expected_rels_elm_calls 339 | 340 | 341 | class DescribeUnmarshaller(object): 342 | 343 | @pytest.fixture 344 | def _unmarshal_parts(self, request): 345 | return method_mock(Unmarshaller, '_unmarshal_parts', request) 346 | 347 | @pytest.fixture 348 | def _unmarshal_relationships(self, request): 349 | return method_mock(Unmarshaller, '_unmarshal_relationships', request) 350 | 351 | def it_can_unmarshal_from_a_pkg_reader(self, _unmarshal_parts, 352 | _unmarshal_relationships): 353 | # mockery ---------------------- 354 | pkg = Mock(name='pkg') 355 | pkg_reader = Mock(name='pkg_reader') 356 | part_factory = Mock(name='part_factory') 357 | parts = {1: Mock(name='part_1'), 2: Mock(name='part_2')} 358 | _unmarshal_parts.return_value = parts 359 | # exercise --------------------- 360 | Unmarshaller.unmarshal(pkg_reader, pkg, part_factory) 361 | # verify ----------------------- 362 | _unmarshal_parts.assert_called_once_with(pkg_reader, part_factory) 363 | _unmarshal_relationships.assert_called_once_with(pkg_reader, pkg, 364 | parts) 365 | for part in parts.values(): 366 | part._after_unmarshal.assert_called_once_with() 367 | 368 | def it_can_unmarshal_parts(self): 369 | # test data -------------------- 370 | part_properties = ( 371 | ('/part/name1.xml', 'app/vnd.contentType_A', ''), 372 | ('/part/name2.xml', 'app/vnd.contentType_B', ''), 373 | ('/part/name3.xml', 'app/vnd.contentType_C', ''), 374 | ) 375 | # mockery ---------------------- 376 | pkg_reader = Mock(name='pkg_reader') 377 | pkg_reader.iter_sparts.return_value = part_properties 378 | part_factory = Mock(name='part_factory') 379 | parts = [Mock(name='part1'), Mock(name='part2'), Mock(name='part3')] 380 | part_factory.side_effect = parts 381 | # exercise --------------------- 382 | retval = Unmarshaller._unmarshal_parts(pkg_reader, part_factory) 383 | # verify ----------------------- 384 | expected_calls = [call(*p) for p in part_properties] 385 | expected_parts = dict((p[0], parts[idx]) for (idx, p) in 386 | enumerate(part_properties)) 387 | assert part_factory.call_args_list == expected_calls 388 | assert retval == expected_parts 389 | 390 | def it_can_unmarshal_relationships(self): 391 | # test data -------------------- 392 | reltype = 'http://reltype' 393 | # mockery ---------------------- 394 | pkg_reader = Mock(name='pkg_reader') 395 | pkg_reader.iter_srels.return_value = ( 396 | ('/', Mock(name='srel1', rId='rId1', reltype=reltype, 397 | target_partname='partname1', is_external=False)), 398 | ('/', Mock(name='srel2', rId='rId2', reltype=reltype, 399 | target_ref='target_ref_1', is_external=True)), 400 | ('partname1', Mock(name='srel3', rId='rId3', reltype=reltype, 401 | target_partname='partname2', is_external=False)), 402 | ('partname2', Mock(name='srel4', rId='rId4', reltype=reltype, 403 | target_ref='target_ref_2', is_external=True)), 404 | ) 405 | pkg = Mock(name='pkg') 406 | parts = {} 407 | for num in range(1, 3): 408 | name = 'part%d' % num 409 | part = Mock(name=name) 410 | parts['partname%d' % num] = part 411 | pkg.attach_mock(part, name) 412 | # exercise --------------------- 413 | Unmarshaller._unmarshal_relationships(pkg_reader, pkg, parts) 414 | # verify ----------------------- 415 | expected_pkg_calls = [ 416 | call._add_relationship( 417 | reltype, parts['partname1'], 'rId1', False), 418 | call._add_relationship( 419 | reltype, 'target_ref_1', 'rId2', True), 420 | call.part1._add_relationship( 421 | reltype, parts['partname2'], 'rId3', False), 422 | call.part2._add_relationship( 423 | reltype, 'target_ref_2', 'rId4', True), 424 | ] 425 | assert pkg.mock_calls == expected_pkg_calls 426 | -------------------------------------------------------------------------------- /tests/test_packuri.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_packuri.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.packuri module.""" 11 | 12 | import pytest 13 | 14 | from opc.packuri import PackURI 15 | 16 | 17 | class DescribePackURI(object): 18 | 19 | def cases(self, expected_values): 20 | """ 21 | Return list of tuples zipped from uri_str cases and 22 | *expected_values*. Raise if lengths don't match. 23 | """ 24 | uri_str_cases = [ 25 | '/', 26 | '/ppt/presentation.xml', 27 | '/ppt/slides/slide1.xml', 28 | ] 29 | if len(expected_values) != len(uri_str_cases): 30 | msg = "len(expected_values) differs from len(uri_str_cases)" 31 | raise AssertionError(msg) 32 | pack_uris = [PackURI(uri_str) for uri_str in uri_str_cases] 33 | return zip(pack_uris, expected_values) 34 | 35 | def it_can_construct_from_relative_ref(self): 36 | baseURI = '/ppt/slides' 37 | relative_ref = '../slideLayouts/slideLayout1.xml' 38 | pack_uri = PackURI.from_rel_ref(baseURI, relative_ref) 39 | assert pack_uri == '/ppt/slideLayouts/slideLayout1.xml' 40 | 41 | def it_should_raise_on_construct_with_bad_pack_uri_str(self): 42 | with pytest.raises(ValueError): 43 | PackURI('foobar') 44 | 45 | def it_can_calculate_baseURI(self): 46 | expected_values = ('/', '/ppt', '/ppt/slides') 47 | for pack_uri, expected_baseURI in self.cases(expected_values): 48 | assert pack_uri.baseURI == expected_baseURI 49 | 50 | def it_can_calculate_extension(self): 51 | expected_values = ('', '.xml', '.xml') 52 | for pack_uri, expected_ext in self.cases(expected_values): 53 | assert pack_uri.ext == expected_ext 54 | 55 | def it_can_calculate_filename(self): 56 | expected_values = ('', 'presentation.xml', 'slide1.xml') 57 | for pack_uri, expected_filename in self.cases(expected_values): 58 | assert pack_uri.filename == expected_filename 59 | 60 | def it_can_calculate_membername(self): 61 | expected_values = ( 62 | '', 63 | 'ppt/presentation.xml', 64 | 'ppt/slides/slide1.xml', 65 | ) 66 | for pack_uri, expected_membername in self.cases(expected_values): 67 | assert pack_uri.membername == expected_membername 68 | 69 | def it_can_calculate_relative_ref_value(self): 70 | cases = ( 71 | ('/', '/ppt/presentation.xml', 'ppt/presentation.xml'), 72 | ('/ppt', '/ppt/slideMasters/slideMaster1.xml', 73 | 'slideMasters/slideMaster1.xml'), 74 | ('/ppt/slides', '/ppt/slideLayouts/slideLayout1.xml', 75 | '../slideLayouts/slideLayout1.xml'), 76 | ) 77 | for baseURI, uri_str, expected_relative_ref in cases: 78 | pack_uri = PackURI(uri_str) 79 | assert pack_uri.relative_ref(baseURI) == expected_relative_ref 80 | 81 | def it_can_calculate_rels_uri(self): 82 | expected_values = ( 83 | '/_rels/.rels', 84 | '/ppt/_rels/presentation.xml.rels', 85 | '/ppt/slides/_rels/slide1.xml.rels', 86 | ) 87 | for pack_uri, expected_rels_uri in self.cases(expected_values): 88 | assert pack_uri.rels_uri == expected_rels_uri 89 | -------------------------------------------------------------------------------- /tests/test_phys_pkg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_phys_pkg.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.phys_pkg module.""" 11 | 12 | try: 13 | from io import BytesIO # Python 3 14 | except ImportError: 15 | from StringIO import StringIO as BytesIO 16 | 17 | import hashlib 18 | 19 | from zipfile import ZIP_DEFLATED, ZipFile 20 | 21 | from opc.packuri import PACKAGE_URI, PackURI 22 | from opc.phys_pkg import ( 23 | PhysPkgReader, PhysPkgWriter, ZipPkgReader, ZipPkgWriter 24 | ) 25 | 26 | import pytest 27 | 28 | from mock import Mock 29 | 30 | from .unitutil import abspath, class_mock 31 | 32 | 33 | test_pptx_path = abspath('test_files/test.pptx') 34 | 35 | 36 | @pytest.fixture 37 | def ZipFile_(request): 38 | return class_mock('opc.phys_pkg.ZipFile', request) 39 | 40 | 41 | class DescribePhysPkgReader(object): 42 | 43 | @pytest.fixture 44 | def ZipPkgReader_(self, request): 45 | return class_mock('opc.phys_pkg.ZipPkgReader', request) 46 | 47 | def it_constructs_a_pkg_reader_instance(self, ZipPkgReader_): 48 | # mockery ---------------------- 49 | pkg_file = Mock(name='pkg_file') 50 | # exercise --------------------- 51 | phys_pkg_reader = PhysPkgReader(pkg_file) 52 | # verify ----------------------- 53 | ZipPkgReader_.assert_called_once_with(pkg_file) 54 | assert phys_pkg_reader == ZipPkgReader_.return_value 55 | 56 | 57 | class DescribePhysPkgWriter(object): 58 | 59 | @pytest.fixture 60 | def ZipPkgWriter_(self, request): 61 | return class_mock('opc.phys_pkg.ZipPkgWriter', request) 62 | 63 | def it_constructs_a_pkg_writer_instance(self, ZipPkgWriter_): 64 | # mockery ---------------------- 65 | pkg_file = Mock(name='pkg_file') 66 | # exercise --------------------- 67 | phys_pkg_writer = PhysPkgWriter(pkg_file) 68 | # verify ----------------------- 69 | ZipPkgWriter_.assert_called_once_with(pkg_file) 70 | assert phys_pkg_writer == ZipPkgWriter_.return_value 71 | 72 | 73 | class DescribeZipPkgReader(object): 74 | 75 | @pytest.fixture(scope='class') 76 | def phys_reader(self, request): 77 | phys_reader = ZipPkgReader(test_pptx_path) 78 | request.addfinalizer(phys_reader.close) 79 | return phys_reader 80 | 81 | def it_opens_pkg_file_zip_on_construction(self, ZipFile_): 82 | pkg_file = Mock(name='pkg_file') 83 | ZipPkgReader(pkg_file) 84 | ZipFile_.assert_called_once_with(pkg_file, 'r') 85 | 86 | def it_can_be_closed(self, ZipFile_): 87 | # mockery ---------------------- 88 | zipf = ZipFile_.return_value 89 | zip_pkg_reader = ZipPkgReader(None) 90 | # exercise --------------------- 91 | zip_pkg_reader.close() 92 | # verify ----------------------- 93 | zipf.close.assert_called_once_with() 94 | 95 | def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): 96 | pack_uri = PackURI('/ppt/presentation.xml') 97 | blob = phys_reader.blob_for(pack_uri) 98 | sha1 = hashlib.sha1(blob).hexdigest() 99 | assert sha1 == 'efa7bee0ac72464903a67a6744c1169035d52a54' 100 | 101 | def it_has_the_content_types_xml(self, phys_reader): 102 | sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() 103 | assert sha1 == '9604a4fb3bf9626f5ad59a4e82029b3a501f106a' 104 | 105 | def it_can_retrieve_rels_xml_for_source_uri(self, phys_reader): 106 | rels_xml = phys_reader.rels_xml_for(PACKAGE_URI) 107 | sha1 = hashlib.sha1(rels_xml).hexdigest() 108 | assert sha1 == 'e31451d4bbe7d24adbe21454b8e9fdae92f50de5' 109 | 110 | def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): 111 | partname = PackURI('/ppt/viewProps.xml') 112 | rels_xml = phys_reader.rels_xml_for(partname) 113 | assert rels_xml is None 114 | 115 | 116 | class DescribeZipPkgWriter(object): 117 | 118 | @pytest.fixture 119 | def pkg_file(self, request): 120 | pkg_file = BytesIO() 121 | request.addfinalizer(pkg_file.close) 122 | return pkg_file 123 | 124 | def it_opens_pkg_file_zip_on_construction(self, ZipFile_): 125 | pkg_file = Mock(name='pkg_file') 126 | ZipPkgWriter(pkg_file) 127 | ZipFile_.assert_called_once_with(pkg_file, 'w', 128 | compression=ZIP_DEFLATED) 129 | 130 | def it_can_be_closed(self, ZipFile_): 131 | # mockery ---------------------- 132 | zipf = ZipFile_.return_value 133 | zip_pkg_writer = ZipPkgWriter(None) 134 | # exercise --------------------- 135 | zip_pkg_writer.close() 136 | # verify ----------------------- 137 | zipf.close.assert_called_once_with() 138 | 139 | def it_can_write_a_blob(self, pkg_file): 140 | # setup ------------------------ 141 | pack_uri = PackURI('/part/name.xml') 142 | blob = ''.encode('utf-8') 143 | # exercise --------------------- 144 | pkg_writer = PhysPkgWriter(pkg_file) 145 | pkg_writer.write(pack_uri, blob) 146 | pkg_writer.close() 147 | # verify ----------------------- 148 | written_blob_sha1 = hashlib.sha1(blob).hexdigest() 149 | zipf = ZipFile(pkg_file, 'r') 150 | retrieved_blob = zipf.read(pack_uri.membername) 151 | zipf.close() 152 | retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() 153 | assert retrieved_blob_sha1 == written_blob_sha1 154 | -------------------------------------------------------------------------------- /tests/test_pkgreader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_pkgreader.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.pkgreader module.""" 11 | 12 | import pytest 13 | 14 | from mock import call, Mock, patch 15 | 16 | from opc.constants import RELATIONSHIP_TARGET_MODE as RTM 17 | from opc.packuri import PackURI 18 | from opc.phys_pkg import ZipPkgReader 19 | from opc.pkgreader import ( 20 | _ContentTypeMap, PackageReader, _SerializedPart, _SerializedRelationship, 21 | _SerializedRelationshipCollection 22 | ) 23 | 24 | from .unitutil import class_mock, initializer_mock, method_mock 25 | 26 | 27 | @pytest.fixture 28 | def oxml_fromstring(request): 29 | _patch = patch('opc.pkgreader.oxml_fromstring') 30 | request.addfinalizer(_patch.stop) 31 | return _patch.start() 32 | 33 | 34 | class DescribePackageReader(object): 35 | 36 | @pytest.fixture 37 | def from_xml(self, request): 38 | return method_mock(_ContentTypeMap, 'from_xml', request) 39 | 40 | @pytest.fixture 41 | def init(self, request): 42 | return initializer_mock(PackageReader, request) 43 | 44 | @pytest.fixture 45 | def _load_serialized_parts(self, request): 46 | return method_mock(PackageReader, '_load_serialized_parts', request) 47 | 48 | @pytest.fixture 49 | def PhysPkgReader_(self, request): 50 | _patch = patch('opc.pkgreader.PhysPkgReader', spec_set=ZipPkgReader) 51 | request.addfinalizer(_patch.stop) 52 | return _patch.start() 53 | 54 | @pytest.fixture 55 | def _SerializedPart_(self, request): 56 | return class_mock('opc.pkgreader._SerializedPart', request) 57 | 58 | @pytest.fixture 59 | def _SerializedRelationshipCollection_(self, request): 60 | return class_mock('opc.pkgreader._SerializedRelationshipCollection', 61 | request) 62 | 63 | @pytest.fixture 64 | def _srels_for(self, request): 65 | return method_mock(PackageReader, '_srels_for', request) 66 | 67 | @pytest.fixture 68 | def _walk_phys_parts(self, request): 69 | return method_mock(PackageReader, '_walk_phys_parts', request) 70 | 71 | def it_can_construct_from_pkg_file(self, init, PhysPkgReader_, from_xml, 72 | _srels_for, _load_serialized_parts): 73 | # mockery ---------------------- 74 | phys_reader = PhysPkgReader_.return_value 75 | content_types = from_xml.return_value 76 | pkg_srels = _srels_for.return_value 77 | sparts = _load_serialized_parts.return_value 78 | pkg_file = Mock(name='pkg_file') 79 | # exercise --------------------- 80 | pkg_reader = PackageReader.from_file(pkg_file) 81 | # verify ----------------------- 82 | PhysPkgReader_.assert_called_once_with(pkg_file) 83 | from_xml.assert_called_once_with(phys_reader.content_types_xml) 84 | _srels_for.assert_called_once_with(phys_reader, '/') 85 | _load_serialized_parts.assert_called_once_with(phys_reader, pkg_srels, 86 | content_types) 87 | phys_reader.close.assert_called_once_with() 88 | init.assert_called_once_with(content_types, pkg_srels, sparts) 89 | assert isinstance(pkg_reader, PackageReader) 90 | 91 | def it_can_iterate_over_the_serialized_parts(self): 92 | # mockery ---------------------- 93 | partname, content_type, blob = ('part/name.xml', 'app/vnd.type', 94 | '') 95 | spart = Mock(name='spart', partname=partname, 96 | content_type=content_type, blob=blob) 97 | pkg_reader = PackageReader(None, None, [spart]) 98 | iter_count = 0 99 | # exercise --------------------- 100 | for retval in pkg_reader.iter_sparts(): 101 | iter_count += 1 102 | # verify ----------------------- 103 | assert retval == (partname, content_type, blob) 104 | assert iter_count == 1 105 | 106 | def it_can_iterate_over_all_the_srels(self): 107 | # mockery ---------------------- 108 | pkg_srels = ['srel1', 'srel2'] 109 | sparts = [ 110 | Mock(name='spart1', partname='pn1', srels=['srel3', 'srel4']), 111 | Mock(name='spart2', partname='pn2', srels=['srel5', 'srel6']), 112 | ] 113 | pkg_reader = PackageReader(None, pkg_srels, sparts) 114 | # exercise --------------------- 115 | generated_tuples = [t for t in pkg_reader.iter_srels()] 116 | # verify ----------------------- 117 | expected_tuples = [ 118 | ('/', 'srel1'), 119 | ('/', 'srel2'), 120 | ('pn1', 'srel3'), 121 | ('pn1', 'srel4'), 122 | ('pn2', 'srel5'), 123 | ('pn2', 'srel6'), 124 | ] 125 | assert generated_tuples == expected_tuples 126 | 127 | def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): 128 | # test data -------------------- 129 | test_data = ( 130 | ('/part/name1.xml', 'app/vnd.type_1', '', 'srels_1'), 131 | ('/part/name2.xml', 'app/vnd.type_2', '', 'srels_2'), 132 | ) 133 | iter_vals = [(t[0], t[2], t[3]) for t in test_data] 134 | content_types = dict((t[0], t[1]) for t in test_data) 135 | # mockery ---------------------- 136 | phys_reader = Mock(name='phys_reader') 137 | pkg_srels = Mock(name='pkg_srels') 138 | _walk_phys_parts.return_value = iter_vals 139 | _SerializedPart_.side_effect = expected_sparts = ( 140 | Mock(name='spart_1'), Mock(name='spart_2') 141 | ) 142 | # exercise --------------------- 143 | retval = PackageReader._load_serialized_parts(phys_reader, pkg_srels, 144 | content_types) 145 | # verify ----------------------- 146 | expected_calls = [ 147 | call('/part/name1.xml', 'app/vnd.type_1', '', 'srels_1'), 148 | call('/part/name2.xml', 'app/vnd.type_2', '', 'srels_2'), 149 | ] 150 | assert _SerializedPart_.call_args_list == expected_calls 151 | assert retval == expected_sparts 152 | 153 | def it_can_walk_phys_pkg_parts(self, _srels_for): 154 | # test data -------------------- 155 | # +----------+ +--------+ 156 | # | pkg_rels |-----> | part_1 | 157 | # +----------+ +--------+ 158 | # | | ^ 159 | # v v | 160 | # external +--------+ +--------+ 161 | # | part_2 |---> | part_3 | 162 | # +--------+ +--------+ 163 | partname_1, partname_2, partname_3 = ( 164 | '/part/name1.xml', '/part/name2.xml', '/part/name3.xml' 165 | ) 166 | part_1_blob, part_2_blob, part_3_blob = ( 167 | '', '', '' 168 | ) 169 | srels = [ 170 | Mock(name='rId1', is_external=True), 171 | Mock(name='rId2', is_external=False, target_partname=partname_1), 172 | Mock(name='rId3', is_external=False, target_partname=partname_2), 173 | Mock(name='rId4', is_external=False, target_partname=partname_1), 174 | Mock(name='rId5', is_external=False, target_partname=partname_3), 175 | ] 176 | pkg_srels = srels[:2] 177 | part_1_srels = srels[2:3] 178 | part_2_srels = srels[3:5] 179 | part_3_srels = [] 180 | # mockery ---------------------- 181 | phys_reader = Mock(name='phys_reader') 182 | _srels_for.side_effect = [part_1_srels, part_2_srels, part_3_srels] 183 | phys_reader.blob_for.side_effect = [ 184 | part_1_blob, part_2_blob, part_3_blob 185 | ] 186 | # exercise --------------------- 187 | generated_tuples = [t for t in PackageReader._walk_phys_parts( 188 | phys_reader, pkg_srels)] 189 | # verify ----------------------- 190 | expected_tuples = [ 191 | (partname_1, part_1_blob, part_1_srels), 192 | (partname_2, part_2_blob, part_2_srels), 193 | (partname_3, part_3_blob, part_3_srels), 194 | ] 195 | assert generated_tuples == expected_tuples 196 | 197 | def it_can_retrieve_srels_for_a_source_uri( 198 | self, _SerializedRelationshipCollection_): 199 | # mockery ---------------------- 200 | phys_reader = Mock(name='phys_reader') 201 | source_uri = Mock(name='source_uri') 202 | rels_xml = phys_reader.rels_xml_for.return_value 203 | load_from_xml = _SerializedRelationshipCollection_.load_from_xml 204 | srels = load_from_xml.return_value 205 | # exercise --------------------- 206 | retval = PackageReader._srels_for(phys_reader, source_uri) 207 | # verify ----------------------- 208 | phys_reader.rels_xml_for.assert_called_once_with(source_uri) 209 | load_from_xml.assert_called_once_with(source_uri.baseURI, rels_xml) 210 | assert retval == srels 211 | 212 | 213 | class Describe_ContentTypeMap(object): 214 | 215 | def it_can_construct_from_types_xml(self, oxml_fromstring): 216 | # test data -------------------- 217 | content_types = ( 218 | 'app/vnd.type1', 'app/vnd.type2', 'app/vnd.type3', 219 | 'app/vnd.type4', 220 | ) 221 | content_types_xml = '' 222 | extensions = ('rels', 'xml') 223 | exts = tuple(['.%s' % extension for extension in extensions]) 224 | partnames = ('/part/name1.xml', '/part/name2.xml') 225 | # mockery ---------------------- 226 | overrides = ( 227 | Mock(name='override_elm_1', partname=partnames[0], 228 | content_type=content_types[0]), 229 | Mock(name='override_elm_2', partname=partnames[1], 230 | content_type=content_types[1]), 231 | ) 232 | defaults = ( 233 | Mock(name='default_elm_1', extension=extensions[0], 234 | content_type=content_types[2]), 235 | Mock(name='default_elm_2', extension=extensions[1], 236 | content_type=content_types[3]), 237 | ) 238 | types_elm = Mock( 239 | name='types_elm', overrides=overrides, defaults=defaults 240 | ) 241 | oxml_fromstring.return_value = types_elm 242 | # exercise --------------------- 243 | ct_map = _ContentTypeMap.from_xml(content_types_xml) 244 | # verify ----------------------- 245 | expected_overrides = { 246 | partnames[0]: content_types[0], partnames[1]: content_types[1] 247 | } 248 | expected_defaults = { 249 | exts[0]: content_types[2], exts[1]: content_types[3] 250 | } 251 | oxml_fromstring.assert_called_once_with(content_types_xml) 252 | assert ct_map._overrides == expected_overrides 253 | assert ct_map._defaults == expected_defaults 254 | 255 | def it_matches_overrides(self): 256 | # test data -------------------- 257 | partname = PackURI('/part/name1.xml') 258 | content_type = 'app/vnd.type1' 259 | # fixture ---------------------- 260 | ct_map = _ContentTypeMap() 261 | ct_map._overrides = {partname: content_type} 262 | # verify ----------------------- 263 | assert ct_map[partname] == content_type 264 | 265 | def it_falls_back_to_defaults(self): 266 | ct_map = _ContentTypeMap() 267 | ct_map._overrides = {PackURI('/part/name1.xml'): 'app/vnd.type1'} 268 | ct_map._defaults = {'.xml': 'application/xml'} 269 | assert ct_map[PackURI('/part/name2.xml')] == 'application/xml' 270 | 271 | def it_should_raise_on_partname_not_found(self): 272 | ct_map = _ContentTypeMap() 273 | with pytest.raises(KeyError): 274 | ct_map[PackURI('/!blat/rhumba.1x&')] 275 | 276 | def it_should_raise_on_key_not_instance_of_PackURI(self): 277 | ct_map = _ContentTypeMap() 278 | ct_map._overrides = {PackURI('/part/name1.xml'): 'app/vnd.type1'} 279 | with pytest.raises(KeyError): 280 | ct_map['/part/name1.xml'] 281 | 282 | 283 | class Describe_SerializedPart(object): 284 | 285 | def it_remembers_construction_values(self): 286 | # test data -------------------- 287 | partname = '/part/name.xml' 288 | content_type = 'app/vnd.type' 289 | blob = '' 290 | srels = 'srels proxy' 291 | # exercise --------------------- 292 | spart = _SerializedPart(partname, content_type, blob, srels) 293 | # verify ----------------------- 294 | assert spart.partname == partname 295 | assert spart.content_type == content_type 296 | assert spart.blob == blob 297 | assert spart.srels == srels 298 | 299 | 300 | class Describe_SerializedRelationship(object): 301 | 302 | def it_remembers_construction_values(self): 303 | # test data -------------------- 304 | rel_elm = Mock( 305 | name='rel_elm', rId='rId9', reltype='ReLtYpE', 306 | target_ref='docProps/core.xml', target_mode=RTM.INTERNAL 307 | ) 308 | # exercise --------------------- 309 | srel = _SerializedRelationship('/', rel_elm) 310 | # verify ----------------------- 311 | assert srel.rId == 'rId9' 312 | assert srel.reltype == 'ReLtYpE' 313 | assert srel.target_ref == 'docProps/core.xml' 314 | assert srel.target_mode == RTM.INTERNAL 315 | 316 | def it_knows_when_it_is_external(self): 317 | cases = (RTM.INTERNAL, RTM.EXTERNAL, 'FOOBAR') 318 | expected_values = (False, True, False) 319 | for target_mode, expected_value in zip(cases, expected_values): 320 | rel_elm = Mock(name='rel_elm', rId=None, reltype=None, 321 | target_ref=None, target_mode=target_mode) 322 | srel = _SerializedRelationship(None, rel_elm) 323 | assert srel.is_external is expected_value 324 | 325 | def it_can_calculate_its_target_partname(self): 326 | # test data -------------------- 327 | cases = ( 328 | ('/', 'docProps/core.xml', '/docProps/core.xml'), 329 | ('/ppt', 'viewProps.xml', '/ppt/viewProps.xml'), 330 | ('/ppt/slides', '../slideLayouts/slideLayout1.xml', 331 | '/ppt/slideLayouts/slideLayout1.xml'), 332 | ) 333 | for baseURI, target_ref, expected_partname in cases: 334 | # setup -------------------- 335 | rel_elm = Mock(name='rel_elm', rId=None, reltype=None, 336 | target_ref=target_ref, target_mode=RTM.INTERNAL) 337 | # exercise ----------------- 338 | srel = _SerializedRelationship(baseURI, rel_elm) 339 | # verify ------------------- 340 | assert srel.target_partname == expected_partname 341 | 342 | def it_raises_on_target_partname_when_external(self): 343 | rel_elm = Mock( 344 | name='rel_elm', rId='rId9', reltype='ReLtYpE', 345 | target_ref='docProps/core.xml', target_mode=RTM.EXTERNAL 346 | ) 347 | srel = _SerializedRelationship('/', rel_elm) 348 | with pytest.raises(ValueError): 349 | srel.target_partname 350 | 351 | 352 | class Describe_SerializedRelationshipCollection(object): 353 | 354 | @pytest.fixture 355 | def oxml_fromstring(self, request): 356 | _patch = patch('opc.pkgreader.oxml_fromstring') 357 | request.addfinalizer(_patch.stop) 358 | return _patch.start() 359 | 360 | @pytest.fixture 361 | def _SerializedRelationship_(self, request): 362 | return class_mock('opc.pkgreader._SerializedRelationship', request) 363 | 364 | def it_can_load_from_xml(self, oxml_fromstring, _SerializedRelationship_): 365 | # mockery ---------------------- 366 | baseURI, rels_item_xml, rel_elm_1, rel_elm_2 = ( 367 | Mock(name='baseURI'), Mock(name='rels_item_xml'), 368 | Mock(name='rel_elm_1'), Mock(name='rel_elm_2'), 369 | ) 370 | rels_elm = Mock(name='rels_elm', Relationship=[rel_elm_1, rel_elm_2]) 371 | oxml_fromstring.return_value = rels_elm 372 | # exercise --------------------- 373 | srels = _SerializedRelationshipCollection.load_from_xml( 374 | baseURI, rels_item_xml) 375 | # verify ----------------------- 376 | expected_calls = [ 377 | call(baseURI, rel_elm_1), 378 | call(baseURI, rel_elm_2), 379 | ] 380 | oxml_fromstring.assert_called_once_with(rels_item_xml) 381 | assert _SerializedRelationship_.call_args_list == expected_calls 382 | assert isinstance(srels, _SerializedRelationshipCollection) 383 | 384 | def it_should_be_iterable(self): 385 | srels = _SerializedRelationshipCollection() 386 | try: 387 | for x in srels: 388 | pass 389 | except TypeError: 390 | msg = "_SerializedRelationshipCollection object is not iterable" 391 | pytest.fail(msg) 392 | -------------------------------------------------------------------------------- /tests/test_pkgwriter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # test_pkgwriter.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test suite for opc.pkgwriter module.""" 11 | 12 | import pytest 13 | 14 | from mock import call, MagicMock, Mock, patch 15 | 16 | from opc.constants import CONTENT_TYPE as CT 17 | from opc.packuri import PackURI 18 | from opc.pkgwriter import _ContentTypesItem, PackageWriter 19 | 20 | from .unitutil import function_mock, method_mock 21 | 22 | 23 | class DescribePackageWriter(object): 24 | 25 | @pytest.fixture 26 | def PhysPkgWriter_(self, request): 27 | _patch = patch('opc.pkgwriter.PhysPkgWriter') 28 | request.addfinalizer(_patch.stop) 29 | return _patch.start() 30 | 31 | @pytest.fixture 32 | def xml_for(self, request): 33 | return method_mock(_ContentTypesItem, 'xml_for', request) 34 | 35 | @pytest.fixture 36 | def _write_methods(self, request): 37 | """Mock that patches all the _write_* methods of PackageWriter""" 38 | root_mock = Mock(name='PackageWriter') 39 | patch1 = patch.object(PackageWriter, '_write_content_types_stream') 40 | patch2 = patch.object(PackageWriter, '_write_pkg_rels') 41 | patch3 = patch.object(PackageWriter, '_write_parts') 42 | root_mock.attach_mock(patch1.start(), '_write_content_types_stream') 43 | root_mock.attach_mock(patch2.start(), '_write_pkg_rels') 44 | root_mock.attach_mock(patch3.start(), '_write_parts') 45 | 46 | def fin(): 47 | patch1.stop() 48 | patch2.stop() 49 | patch3.stop() 50 | 51 | request.addfinalizer(fin) 52 | return root_mock 53 | 54 | def it_can_write_a_package(self, PhysPkgWriter_, _write_methods): 55 | # mockery ---------------------- 56 | pkg_file = Mock(name='pkg_file') 57 | pkg_rels = Mock(name='pkg_rels') 58 | parts = Mock(name='parts') 59 | phys_writer = PhysPkgWriter_.return_value 60 | # exercise --------------------- 61 | PackageWriter.write(pkg_file, pkg_rels, parts) 62 | # verify ----------------------- 63 | expected_calls = [ 64 | call._write_content_types_stream(phys_writer, parts), 65 | call._write_pkg_rels(phys_writer, pkg_rels), 66 | call._write_parts(phys_writer, parts), 67 | ] 68 | PhysPkgWriter_.assert_called_once_with(pkg_file) 69 | assert _write_methods.mock_calls == expected_calls 70 | phys_writer.close.assert_called_once_with() 71 | 72 | def it_can_write_a_content_types_stream(self, xml_for): 73 | # mockery ---------------------- 74 | phys_writer = Mock(name='phys_writer') 75 | parts = Mock(name='parts') 76 | # exercise --------------------- 77 | PackageWriter._write_content_types_stream(phys_writer, parts) 78 | # verify ----------------------- 79 | xml_for.assert_called_once_with(parts) 80 | phys_writer.write.assert_called_once_with('/[Content_Types].xml', 81 | xml_for.return_value) 82 | 83 | def it_can_write_a_pkg_rels_item(self): 84 | # mockery ---------------------- 85 | phys_writer = Mock(name='phys_writer') 86 | pkg_rels = Mock(name='pkg_rels') 87 | # exercise --------------------- 88 | PackageWriter._write_pkg_rels(phys_writer, pkg_rels) 89 | # verify ----------------------- 90 | phys_writer.write.assert_called_once_with('/_rels/.rels', 91 | pkg_rels.xml) 92 | 93 | def it_can_write_a_list_of_parts(self): 94 | # mockery ---------------------- 95 | phys_writer = Mock(name='phys_writer') 96 | rels = MagicMock(name='rels') 97 | rels.__len__.return_value = 1 98 | part1 = Mock(name='part1', _rels=rels) 99 | part2 = Mock(name='part2', _rels=[]) 100 | # exercise --------------------- 101 | PackageWriter._write_parts(phys_writer, [part1, part2]) 102 | # verify ----------------------- 103 | expected_calls = [ 104 | call(part1.partname, part1.blob), 105 | call(part1.partname.rels_uri, part1._rels.xml), 106 | call(part2.partname, part2.blob), 107 | ] 108 | assert phys_writer.write.mock_calls == expected_calls 109 | 110 | 111 | class Describe_ContentTypesItem(object): 112 | 113 | @pytest.fixture 114 | def oxml_tostring(self, request): 115 | return function_mock('opc.pkgwriter.oxml_tostring', request) 116 | 117 | @pytest.fixture 118 | def parts(self): 119 | """list of parts that will exercise _ContentTypesItem.xml_for()""" 120 | return [ 121 | Mock(name='part_1', partname=PackURI('/docProps/core.xml'), 122 | content_type='app/vnd.core'), 123 | Mock(name='part_2', partname=PackURI('/docProps/thumbnail.jpeg'), 124 | content_type=CT.JPEG), 125 | Mock(name='part_3', partname=PackURI('/ppt/slides/slide2.xml'), 126 | content_type='app/vnd.ct_sld'), 127 | Mock(name='part_4', partname=PackURI('/ppt/slides/slide1.xml'), 128 | content_type='app/vnd.ct_sld'), 129 | Mock(name='part_5', partname=PackURI('/zebra/foo.bar'), 130 | content_type='app/vnd.foobar'), 131 | ] 132 | 133 | @pytest.fixture 134 | def types(self, request): 135 | """Mock returned by CT_Types.new() call""" 136 | types = Mock(name='types') 137 | _patch = patch('opc.pkgwriter.CT_Types') 138 | CT_Types = _patch.start() 139 | CT_Types.new.return_value = types 140 | request.addfinalizer(_patch.stop) 141 | return types 142 | 143 | def it_can_compose_content_types_xml(self, parts, types, oxml_tostring): 144 | # # exercise --------------------- 145 | _ContentTypesItem.xml_for(parts) 146 | # verify ----------------------- 147 | expected_types_calls = [ 148 | call.add_default('.jpeg', CT.JPEG), 149 | call.add_default('.rels', CT.OPC_RELATIONSHIPS), 150 | call.add_default('.xml', CT.XML), 151 | call.add_override('/docProps/core.xml', 'app/vnd.core'), 152 | call.add_override('/ppt/slides/slide1.xml', 'app/vnd.ct_sld'), 153 | call.add_override('/ppt/slides/slide2.xml', 'app/vnd.ct_sld'), 154 | call.add_override('/zebra/foo.bar', 'app/vnd.foobar'), 155 | ] 156 | assert types.mock_calls == expected_types_calls 157 | oxml_tostring.assert_called_once_with(types, encoding='UTF-8', 158 | standalone=True), 159 | -------------------------------------------------------------------------------- /tests/unitdata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # unitdata.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Test data builders for unit tests""" 11 | 12 | from opc.constants import NAMESPACE as NS 13 | from opc.oxml import oxml_fromstring 14 | 15 | 16 | class BaseBuilder(object): 17 | """ 18 | Provides common behavior for all data builders. 19 | """ 20 | @property 21 | def element(self): 22 | """Return element based on XML generated by builder""" 23 | return oxml_fromstring(self.xml) 24 | 25 | def with_indent(self, indent): 26 | """Add integer *indent* spaces at beginning of element XML""" 27 | self._indent = indent 28 | return self 29 | 30 | 31 | class CT_DefaultBuilder(BaseBuilder): 32 | """ 33 | Test data builder for CT_Default (Default) XML element that appears in 34 | `[Content_Types].xml`. 35 | """ 36 | def __init__(self): 37 | """Establish instance variables with default values""" 38 | self._content_type = 'application/xml' 39 | self._extension = 'xml' 40 | self._indent = 0 41 | self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES 42 | 43 | def with_content_type(self, content_type): 44 | """Set ContentType attribute to *content_type*""" 45 | self._content_type = content_type 46 | return self 47 | 48 | def with_extension(self, extension): 49 | """Set Extension attribute to *extension*""" 50 | self._extension = extension 51 | return self 52 | 53 | def without_namespace(self): 54 | """Don't include an 'xmlns=' attribute""" 55 | self._namespace = '' 56 | return self 57 | 58 | @property 59 | def xml(self): 60 | """Return Default element""" 61 | tmpl = '%s\n' 62 | indent = ' ' * self._indent 63 | return tmpl % (indent, self._namespace, self._extension, 64 | self._content_type) 65 | 66 | 67 | class CT_OverrideBuilder(BaseBuilder): 68 | """ 69 | Test data builder for CT_Override (Override) XML element that appears in 70 | `[Content_Types].xml`. 71 | """ 72 | def __init__(self): 73 | """Establish instance variables with default values""" 74 | self._content_type = 'app/vnd.type' 75 | self._indent = 0 76 | self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES 77 | self._partname = '/part/name.xml' 78 | 79 | def with_content_type(self, content_type): 80 | """Set ContentType attribute to *content_type*""" 81 | self._content_type = content_type 82 | return self 83 | 84 | def with_partname(self, partname): 85 | """Set PartName attribute to *partname*""" 86 | self._partname = partname 87 | return self 88 | 89 | def without_namespace(self): 90 | """Don't include an 'xmlns=' attribute""" 91 | self._namespace = '' 92 | return self 93 | 94 | @property 95 | def xml(self): 96 | """Return Override element""" 97 | tmpl = '%s\n' 98 | indent = ' ' * self._indent 99 | return tmpl % (indent, self._namespace, self._partname, 100 | self._content_type) 101 | 102 | 103 | class CT_RelationshipBuilder(BaseBuilder): 104 | """ 105 | Test data builder for CT_Relationship (Relationship) XML element that 106 | appears in .rels files 107 | """ 108 | def __init__(self): 109 | """Establish instance variables with default values""" 110 | self._rId = 'rId9' 111 | self._reltype = 'ReLtYpE' 112 | self._target = 'docProps/core.xml' 113 | self._target_mode = None 114 | self._indent = 0 115 | self._namespace = ' xmlns="%s"' % NS.OPC_RELATIONSHIPS 116 | 117 | def with_rId(self, rId): 118 | """Set Id attribute to *rId*""" 119 | self._rId = rId 120 | return self 121 | 122 | def with_reltype(self, reltype): 123 | """Set Type attribute to *reltype*""" 124 | self._reltype = reltype 125 | return self 126 | 127 | def with_target(self, target): 128 | """Set XXX attribute to *target*""" 129 | self._target = target 130 | return self 131 | 132 | def with_target_mode(self, target_mode): 133 | """Set TargetMode attribute to *target_mode*""" 134 | self._target_mode = None if target_mode == 'Internal' else target_mode 135 | return self 136 | 137 | def without_namespace(self): 138 | """Don't include an 'xmlns=' attribute""" 139 | self._namespace = '' 140 | return self 141 | 142 | @property 143 | def target_mode(self): 144 | if self._target_mode is None: 145 | return '' 146 | return ' TargetMode="%s"' % self._target_mode 147 | 148 | @property 149 | def xml(self): 150 | """Return Relationship element""" 151 | tmpl = '%s\n' 152 | indent = ' ' * self._indent 153 | return tmpl % (indent, self._namespace, self._rId, self._reltype, 154 | self._target, self.target_mode) 155 | 156 | 157 | class CT_RelationshipsBuilder(BaseBuilder): 158 | """ 159 | Test data builder for CT_Relationships (Relationships) XML element, the 160 | root element in .rels files. 161 | """ 162 | def __init__(self): 163 | """Establish instance variables with default values""" 164 | self._rels = ( 165 | ('rId1', 'http://reltype1', 'docProps/core.xml', 'Internal'), 166 | ('rId2', 'http://linktype', 'http://some/link', 'External'), 167 | ('rId3', 'http://reltype2', '../slides/slide1.xml', 'Internal'), 168 | ) 169 | 170 | @property 171 | def xml(self): 172 | """ 173 | Return XML string based on settings accumulated via method calls. 174 | """ 175 | xml = '\n' % NS.OPC_RELATIONSHIPS 176 | for rId, reltype, target, target_mode in self._rels: 177 | xml += (a_Relationship().with_rId(rId) 178 | .with_reltype(reltype) 179 | .with_target(target) 180 | .with_target_mode(target_mode) 181 | .with_indent(2) 182 | .without_namespace() 183 | .xml) 184 | xml += '\n' 185 | return xml 186 | 187 | 188 | class CT_TypesBuilder(BaseBuilder): 189 | """ 190 | Test data builder for CT_Types () XML element, the root element in 191 | [Content_Types].xml files 192 | """ 193 | def __init__(self): 194 | """Establish instance variables with default values""" 195 | self._defaults = ( 196 | ('xml', 'application/xml'), 197 | ('jpeg', 'image/jpeg'), 198 | ) 199 | self._empty = False 200 | self._overrides = ( 201 | ('/docProps/core.xml', 'app/vnd.type1'), 202 | ('/ppt/presentation.xml', 'app/vnd.type2'), 203 | ('/docProps/thumbnail.jpeg', 'image/jpeg'), 204 | ) 205 | 206 | def empty(self): 207 | self._empty = True 208 | return self 209 | 210 | @property 211 | def xml(self): 212 | """ 213 | Return XML string based on settings accumulated via method calls 214 | """ 215 | if self._empty: 216 | return '\n' % NS.OPC_CONTENT_TYPES 217 | 218 | xml = '\n' % NS.OPC_CONTENT_TYPES 219 | for extension, content_type in self._defaults: 220 | xml += (a_Default().with_extension(extension) 221 | .with_content_type(content_type) 222 | .with_indent(2) 223 | .without_namespace() 224 | .xml) 225 | for partname, content_type in self._overrides: 226 | xml += (an_Override().with_partname(partname) 227 | .with_content_type(content_type) 228 | .with_indent(2) 229 | .without_namespace() 230 | .xml) 231 | xml += '\n' 232 | return xml 233 | 234 | 235 | def a_Default(): 236 | """Return a CT_DefaultBuilder instance""" 237 | return CT_DefaultBuilder() 238 | 239 | 240 | def an_Override(): 241 | """Return a CT_OverrideBuilder instance""" 242 | return CT_OverrideBuilder() 243 | 244 | 245 | def a_Relationship(): 246 | """Return a CT_RelationshipBuilder instance""" 247 | return CT_RelationshipBuilder() 248 | 249 | 250 | def a_Relationships(): 251 | """Return a CT_RelationshipsBuilder instance""" 252 | return CT_RelationshipsBuilder() 253 | 254 | 255 | def a_Types(): 256 | """Return a CT_TypesBuilder instance""" 257 | return CT_TypesBuilder() 258 | -------------------------------------------------------------------------------- /tests/unitutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # unitutil.py 4 | # 5 | # Copyright (C) 2013 Steve Canny scanny@cisco.com 6 | # 7 | # This module is part of python-opc and is released under the MIT License: 8 | # http://www.opensource.org/licenses/mit-license.php 9 | 10 | """Utility functions for unit testing""" 11 | 12 | import os 13 | 14 | from mock import patch 15 | 16 | 17 | def abspath(relpath): 18 | thisdir = os.path.split(__file__)[0] 19 | return os.path.abspath(os.path.join(thisdir, relpath)) 20 | 21 | 22 | def class_mock(q_class_name, request): 23 | """ 24 | Return a mock patching the class with qualified name *q_class_name*. 25 | Patch is reversed after calling test returns. 26 | """ 27 | _patch = patch(q_class_name, autospec=True) 28 | request.addfinalizer(_patch.stop) 29 | return _patch.start() 30 | 31 | 32 | def function_mock(q_function_name, request): 33 | """ 34 | Return a mock patching the function with qualified name 35 | *q_function_name*. Patch is reversed after calling test returns. 36 | """ 37 | _patch = patch(q_function_name) 38 | request.addfinalizer(_patch.stop) 39 | return _patch.start() 40 | 41 | 42 | def initializer_mock(cls, request): 43 | """ 44 | Return a mock for the __init__ method on *cls* where the patch is 45 | reversed after pytest uses it. 46 | """ 47 | _patch = patch.object(cls, '__init__', return_value=None) 48 | request.addfinalizer(_patch.stop) 49 | return _patch.start() 50 | 51 | 52 | def method_mock(cls, method_name, request): 53 | """ 54 | Return a mock for method *method_name* on *cls* where the patch is 55 | reversed after pytest uses it. 56 | """ 57 | _patch = patch.object(cls, method_name) 58 | request.addfinalizer(_patch.stop) 59 | return _patch.start() 60 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # 2 | # tox.ini 3 | # 4 | # Copyright (C) 2012, 2013 Steve Canny scanny@cisco.com 5 | # 6 | # This module is part of python-opc and is released under the MIT License: 7 | # http://www.opensource.org/licenses/mit-license.php 8 | # 9 | # Configuration for tox and pytest 10 | 11 | [pytest] 12 | norecursedirs = doc *.egg-info features .git opc .tox 13 | python_classes = Test Describe 14 | python_functions = test_ it_ they_ 15 | 16 | [tox] 17 | envlist = py26, py27, py33 18 | 19 | [testenv] 20 | deps = 21 | behave 22 | lxml 23 | mock 24 | pytest 25 | 26 | commands = 27 | py.test -qx 28 | behave --format progress --stop --tags=-wip 29 | -------------------------------------------------------------------------------- /util/gen_constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # gen_constants.py 5 | # 6 | # Generate the constant definitions for opc/constants.py from an XML source 7 | # document. The constant names are calculated using the last part of the 8 | # content type or relationship type string. 9 | 10 | import os 11 | 12 | from lxml import objectify 13 | 14 | 15 | # calculate absolute path to xml file 16 | thisdir = os.path.split(__file__)[0] 17 | xml_relpath = 'src_data/part-types.xml' 18 | xml_path = os.path.join(thisdir, xml_relpath) 19 | 20 | 21 | def content_types_documentation_page(xml_path): 22 | """ 23 | Generate restructuredText (rst) documentation for content type constants. 24 | """ 25 | print '###########################' 26 | print 'Content type constant names' 27 | print '###########################' 28 | content_types = parse_content_types(xml_path) 29 | for name in sorted(content_types.keys()): 30 | print '\n%s' % name 31 | print ' %s' % content_types[name] 32 | 33 | 34 | def relationship_types_documentation_page(xml_path): 35 | """ 36 | Generate restructuredText (rst) documentation for relationship type 37 | constants. 38 | """ 39 | print '################################' 40 | print 'Relationship type constant names' 41 | print '################################' 42 | relationship_types = parse_relationship_types(xml_path) 43 | for name in sorted(relationship_types.keys()): 44 | print '\n%s' % name 45 | print ' \\%s' % relationship_types[name] 46 | 47 | 48 | def content_type_constant_names(xml_path): 49 | """ 50 | Calculate constant names for content types in source XML document 51 | """ 52 | print '\n\nclass CONTENT_TYPE(object):' 53 | content_types = parse_content_types(xml_path) 54 | for name in sorted(content_types.keys()): 55 | content_type = content_types[name] 56 | print ' %s = (' % name 57 | print ' \'%s\'' % content_type[:67] 58 | if len(content_type) > 67: 59 | print ' \'%s\'' % content_type[67:] 60 | print ' )' 61 | 62 | 63 | def relationship_type_constant_names(xml_path): 64 | """ 65 | Calculate constant names for relationship types in source XML document 66 | """ 67 | print '\n\nclass RELATIONSHIP_TYPE(object):' 68 | relationship_types = parse_relationship_types(xml_path) 69 | for name in sorted(relationship_types.keys()): 70 | relationship_type = relationship_types[name] 71 | print ' %s = (' % name 72 | print ' \'%s\'' % relationship_type[:67] 73 | if len(relationship_type) > 67: 74 | print ' \'%s\'' % relationship_type[67:] 75 | print ' )' 76 | 77 | 78 | def parse_content_types(xml_path): 79 | content_types = {} 80 | root = objectify.parse(xml_path).getroot() 81 | for part in root.iterchildren('*'): 82 | content_type = str(part.ContentType) 83 | if content_type.startswith('Any '): 84 | continue 85 | name = const_name(content_type) 86 | content_types[name] = content_type 87 | return content_types 88 | 89 | 90 | def parse_relationship_types(xml_path): 91 | relationship_types = {} 92 | root = objectify.parse(xml_path).getroot() 93 | for part in root.iterchildren('*'): 94 | relationship_type = str(part.SourceRelationship) 95 | if relationship_type == '': 96 | continue 97 | name = rel_const_name(relationship_type) 98 | if (name in relationship_types and 99 | relationship_type != relationship_types[name]): 100 | raise ValueError( 101 | '%s, %s, %s' % (name, relationship_type, 102 | relationship_types[name]) 103 | ) 104 | relationship_types[name] = relationship_type 105 | return relationship_types 106 | 107 | 108 | def const_name(content_type): 109 | prefix, camel_name = transform_prefix(content_type) 110 | return format_const_name(prefix, camel_name) 111 | 112 | 113 | def rel_const_name(relationship_type): 114 | camel_name = rel_type_camel_name(relationship_type) 115 | return format_rel_const_name(camel_name) 116 | 117 | 118 | def format_const_name(prefix, camel_name): 119 | camel_name = legalize_name(camel_name) 120 | snake_name = camel_to_snake(camel_name) 121 | tmpl = '%s_%s' if prefix else '%s%s' 122 | return tmpl % (prefix, snake_name.upper()) 123 | 124 | 125 | def format_rel_const_name(camel_name): 126 | camel_name = legalize_name(camel_name) 127 | snake_name = camel_to_snake(camel_name) 128 | return snake_name.upper() 129 | 130 | 131 | def legalize_name(name): 132 | """ 133 | Replace illegal variable name characters with underscore. 134 | """ 135 | legal_name = '' 136 | for char in name: 137 | if char in '.-': 138 | char = '_' 139 | legal_name += char 140 | return legal_name 141 | 142 | 143 | def camel_to_snake(camel_str): 144 | snake_str = '' 145 | for char in camel_str: 146 | if char.isupper(): 147 | snake_str += '_' 148 | snake_str += char.lower() 149 | return snake_str 150 | 151 | 152 | def transform_prefix(content_type): 153 | namespaces = ( 154 | ('application/vnd.openxmlformats-officedocument.drawingml.', 155 | 'DML'), 156 | ('application/vnd.openxmlformats-officedocument.presentationml.', 157 | 'PML'), 158 | ('application/vnd.openxmlformats-officedocument.spreadsheetml.', 159 | 'SML'), 160 | ('application/vnd.openxmlformats-officedocument.wordprocessingml.', 161 | 'WML'), 162 | ('application/vnd.openxmlformats-officedocument.', 163 | 'OFC'), 164 | ('application/vnd.openxmlformats-package.', 165 | 'OPC'), 166 | ('application/', ''), 167 | ('image/vnd.', ''), 168 | ('image/', ''), 169 | ) 170 | for prefix, new_prefix in namespaces: 171 | if content_type.startswith(prefix): 172 | start = len(prefix) 173 | camel_name = content_type[start:] 174 | if camel_name.endswith('+xml'): 175 | camel_name = camel_name[:-4] 176 | return (new_prefix, camel_name) 177 | return ('', content_type) 178 | 179 | 180 | def rel_type_camel_name(relationship_type): 181 | namespaces = ( 182 | ('http://schemas.openxmlformats.org/officeDocument/2006/relationship' 183 | 's/metadata/'), 184 | ('http://schemas.openxmlformats.org/officeDocument/2006/relationship' 185 | 's/spreadsheetml/'), 186 | ('http://schemas.openxmlformats.org/officeDocument/2006/relationship' 187 | 's/'), 188 | ('http://schemas.openxmlformats.org/package/2006/relationships/metad' 189 | 'ata/'), 190 | ('http://schemas.openxmlformats.org/package/2006/relationships/digit' 191 | 'al-signature/'), 192 | ('http://schemas.openxmlformats.org/package/2006/relationships/'), 193 | ) 194 | for namespace in namespaces: 195 | if relationship_type.startswith(namespace): 196 | start = len(namespace) 197 | camel_name = relationship_type[start:] 198 | return camel_name 199 | return relationship_type 200 | 201 | 202 | content_type_constant_names(xml_path) 203 | relationship_type_constant_names(xml_path) 204 | content_types_documentation_page(xml_path) 205 | relationship_types_documentation_page(xml_path) 206 | --------------------------------------------------------------------------------