diff --git a/.gitignore b/.gitignore index e24445137..5aabfd8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +/build/ .coverage /dist/ /docs/.build/ -/*.egg-info +/src/*.egg-info *.pyc .pytest_cache/ _scratch/ diff --git a/HISTORY.rst b/HISTORY.rst index 2b33a4d5d..e6c5bd333 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,26 @@ Release History --------------- +1.0.0 (2023-10-01) ++++++++++++++++++++ + +- Remove Python 2 support. Supported versions are 3.7+ +* Fix #85: Paragraph.text includes hyperlink text +* Add #1113: Hyperlink.address +* Add Hyperlink.contains_page_break +* Add Hyperlink.runs +* Add Hyperlink.text +* Add Paragraph.contains_page_break +* Add Paragraph.hyperlinks +* Add Paragraph.iter_inner_content() +* Add Paragraph.rendered_page_breaks +* Add RenderedPageBreak.following_paragraph_fragment +* Add RenderedPageBreak.preceding_paragraph_fragment +* Add Run.contains_page_break +* Add Run.iter_inner_content() +* Add Section.iter_inner_content() + + 0.8.11 (2021-05-15) +++++++++++++++++++ diff --git a/MANIFEST.in b/MANIFEST.in index 90c2f9fdc..c75168672 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include HISTORY.rst LICENSE README.rst tox.ini -graft docx/templates +graft src/docx/templates graft features graft tests graft docs diff --git a/Makefile b/Makefile index f335818fe..0478b2bce 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,34 @@ BEHAVE = behave MAKE = make PYTHON = python -SETUP = $(PYTHON) ./setup.py +BUILD = $(PYTHON) -m build +TWINE = $(PYTHON) -m twine -.PHONY: accept clean coverage docs readme register sdist test upload +.PHONY: accept build clean cleandocs coverage docs install opendocs sdist test +.PHONY: test-upload wheel help: @echo "Please use \`make ' where is one or more of" - @echo " accept run acceptance tests using behave" - @echo " clean delete intermediate work product and start fresh" - @echo " cleandocs delete intermediate documentation files" - @echo " coverage run nosetests with coverage" - @echo " docs generate documentation" - @echo " opendocs open browser to local version of documentation" - @echo " register update metadata (README.rst) on PyPI" - @echo " sdist generate a source distribution into dist/" - @echo " upload upload distribution tarball to PyPI" + @echo " accept run acceptance tests using behave" + @echo " build generate both sdist and wheel suitable for upload to PyPI" + @echo " clean delete intermediate work product and start fresh" + @echo " cleandocs delete intermediate documentation files" + @echo " coverage run pytest with coverage" + @echo " docs generate documentation" + @echo " opendocs open browser to local version of documentation" + @echo " register update metadata (README.rst) on PyPI" + @echo " sdist generate a source distribution into dist/" + @echo " test run unit tests using pytest" + @echo " test-upload upload distribution to TestPyPI" + @echo " upload upload distribution tarball to PyPI" + @echo " wheel generate a binary distribution into dist/" accept: $(BEHAVE) --stop +build: + $(BUILD) + clean: find . -type f -name \*.pyc -exec rm {} \; rm -rf dist *.egg-info .coverage .DS_Store @@ -33,14 +42,23 @@ coverage: docs: $(MAKE) -C docs html +install: + pip install -Ue . + opendocs: open docs/.build/html/index.html -register: - $(SETUP) register - sdist: - $(SETUP) sdist + $(BUILD) --sdist . + +test: + pytest -x + +test-upload: sdist wheel + $(TWINE) upload --repository testpypi dist/* + +upload: clean sdist wheel + $(TWINE) upload dist/* -upload: - $(SETUP) sdist upload +wheel: + $(BUILD) --wheel . diff --git a/README.md b/README.md new file mode 100644 index 000000000..c35cf0200 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# python-docx + +*python-docx* is a Python library for reading, creating, and updating Microsoft Word 2007+ (.docx) files. + +## Installation + +``` +pip install python-docx +``` + +## Example + +```python +>>> from docx import Document + +>>> document = Document() +>>> document.add_paragraph("It was a dark and stormy night.") + +>>> document.save("dark-and-stormy.docx") + +>>> document = Document("dark-and-stormy.docx") +>>> document.paragraphs[0].text +'It was a dark and stormy night.' +``` + +More information is available in the [python-docx documentation](https://python-docx.readthedocs.org/en/latest/) diff --git a/README.rst b/README.rst deleted file mode 100644 index 82d1f0bd7..000000000 --- a/README.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. image:: https://travis-ci.org/python-openxml/python-docx.svg?branch=master - :target: https://travis-ci.org/python-openxml/python-docx - -*python-docx* is a Python library for creating and updating Microsoft Word -(.docx) files. - -More information is available in the `python-docx documentation`_. - -.. _`python-docx documentation`: - https://python-docx.readthedocs.org/en/latest/ diff --git a/docs/api/document.rst b/docs/api/document.rst index 8ab9ecfe4..42ec0211f 100644 --- a/docs/api/document.rst +++ b/docs/api/document.rst @@ -50,68 +50,68 @@ if that behavior is desired. .. attribute:: author - *string* -- An entity primarily responsible for making the content of the + `string` -- An entity primarily responsible for making the content of the resource. .. attribute:: category - *string* -- A categorization of the content of this package. Example + `string` -- A categorization of the content of this package. Example values might include: Resume, Letter, Financial Forecast, Proposal, or Technical Presentation. .. attribute:: comments - *string* -- An account of the content of the resource. + `string` -- An account of the content of the resource. .. attribute:: content_status - *string* -- completion status of the document, e.g. 'draft' + `string` -- completion status of the document, e.g. 'draft' .. attribute:: created - *datetime* -- time of intial creation of the document + `datetime` -- time of intial creation of the document .. attribute:: identifier - *string* -- An unambiguous reference to the resource within a given + `string` -- An unambiguous reference to the resource within a given context, e.g. ISBN. .. attribute:: keywords - *string* -- descriptive words or short phrases likely to be used as + `string` -- descriptive words or short phrases likely to be used as search terms for this document .. attribute:: language - *string* -- language the document is written in + `string` -- language the document is written in .. attribute:: last_modified_by - *string* -- name or other identifier (such as email address) of person + `string` -- name or other identifier (such as email address) of person who last modified the document .. attribute:: last_printed - *datetime* -- time the document was last printed + `datetime` -- time the document was last printed .. attribute:: modified - *datetime* -- time the document was last modified + `datetime` -- time the document was last modified .. attribute:: revision - *int* -- number of this revision, incremented by Word each time the + `int` -- number of this revision, incremented by Word each time the document is saved. Note however |docx| does not automatically increment the revision number when it saves a document. .. attribute:: subject - *string* -- The topic of the content of the resource. + `string` -- The topic of the content of the resource. .. attribute:: title - *string* -- The name given to the resource. + `string` -- The name given to the resource. .. attribute:: version - *string* -- free-form version string + `string` -- free-form version string diff --git a/docs/api/shared.rst b/docs/api/shared.rst index 215e5338c..161b8bac4 100644 --- a/docs/api/shared.rst +++ b/docs/api/shared.rst @@ -52,7 +52,7 @@ allowing values to be expressed in the units most appropriate to the context. :members: :undoc-members: - *r*, *g*, and *b* are each an integer in the range 0-255 inclusive. Using + `r`, `g`, and `b` are each an integer in the range 0-255 inclusive. Using the hexidecimal integer notation, e.g. `0x42` may enhance readability where hex RGB values are in use:: diff --git a/docs/api/style.rst b/docs/api/style.rst index 9e05ab351..afee95c00 100644 --- a/docs/api/style.rst +++ b/docs/api/style.rst @@ -35,10 +35,10 @@ in the appropriate style. part, style_id -|_CharacterStyle| objects +|CharacterStyle| objects ------------------------- -.. autoclass:: _CharacterStyle() +.. autoclass:: CharacterStyle() :show-inheritance: :members: :inherited-members: @@ -46,10 +46,10 @@ in the appropriate style. element, part, style_id, type -|_ParagraphStyle| objects +|ParagraphStyle| objects ------------------------- -.. autoclass:: _ParagraphStyle() +.. autoclass:: ParagraphStyle() :show-inheritance: :members: :inherited-members: diff --git a/docs/api/text.rst b/docs/api/text.rst index cc9b4892f..f76e3ba33 100644 --- a/docs/api/text.rst +++ b/docs/api/text.rst @@ -19,6 +19,13 @@ Text-related objects :members: +|Hyperlink| objects +------------------- + +.. autoclass:: docx.text.hyperlink.Hyperlink() + :members: + + |Run| objects ------------- @@ -33,6 +40,13 @@ Text-related objects :members: +|RenderedPageBreak| objects +--------------------------- + +.. autoclass:: docx.text.pagebreak.RenderedPageBreak() + :members: + + |TabStop| objects ----------------- diff --git a/docs/conf.py b/docs/conf.py index 1a988453a..06b428064 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) from docx import __version__ # noqa @@ -31,28 +31,28 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode' + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-docx' -copyright = u'2013, Steve Canny' +project = "python-docx" +copyright = "2013, Steve Canny" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -79,7 +79,9 @@ .. |_Cell| replace:: :class:`._Cell` -.. |_CharacterStyle| replace:: :class:`._CharacterStyle` +.. |_CharacterStyle| replace:: :class:`.CharacterStyle` + +.. |CharacterStyle| replace:: :class:`.CharacterStyle` .. |Cm| replace:: :class:`.Cm` @@ -115,6 +117,8 @@ .. |HeaderPart| replace:: :class:`.HeaderPart` +.. |Hyperlink| replace:: :class:`.Hyperlink` + .. |ImageParts| replace:: :class:`.ImageParts` .. |Inches| replace:: :class:`.Inches` @@ -145,7 +149,9 @@ .. |ParagraphFormat| replace:: :class:`.ParagraphFormat` -.. |_ParagraphStyle| replace:: :class:`._ParagraphStyle` +.. |_ParagraphStyle| replace:: :class:`.ParagraphStyle` + +.. |ParagraphStyle| replace:: :class:`.ParagraphStyle` .. |Part| replace:: :class:`.Part` @@ -155,6 +161,8 @@ .. |Relationships| replace:: :class:`._Relationships` +.. |RenderedPageBreak| replace:: :class:`.RenderedPageBreak` + .. |RGBColor| replace:: :class:`.RGBColor` .. |_Row| replace:: :class:`._Row` @@ -193,7 +201,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['.build'] +exclude_patterns = [".build"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -211,7 +219,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -221,7 +229,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'armstrong' +html_theme = "armstrong" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -229,7 +237,7 @@ # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -250,7 +258,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -263,8 +271,7 @@ # Custom sidebar templates, maps document names to template names. # html_sidebars = {} html_sidebars = { - '**': ['localtoc.html', 'relations.html', 'sidebarlinks.html', - 'searchbox.html'] + "**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"] } # Additional templates that should be rendered to pages, maps page names to @@ -298,7 +305,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-docxdoc' +htmlhelp_basename = "python-docxdoc" # -- Options for LaTeX output ----------------------------------------------- @@ -306,10 +313,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -321,8 +326,7 @@ # author, # documentclass [howto/manual]). latex_documents = [ - ('index', 'python-docx.tex', u'python-docx Documentation', - u'Steve Canny', 'manual'), + ("index", "python-docx.tex", "python-docx Documentation", "Steve Canny", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -350,10 +354,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'python-docx', u'python-docx Documentation', - [u'Steve Canny'], 1) -] +man_pages = [("index", "python-docx", "python-docx Documentation", ["Steve Canny"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -365,9 +366,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-docx', u'python-docx Documentation', - u'Steve Canny', 'python-docx', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-docx", + "python-docx Documentation", + "Steve Canny", + "python-docx", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. @@ -381,4 +388,4 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/3/': None} +intersphinx_mapping = {"http://docs.python.org/3/": None} diff --git a/docs/dev/analysis/features/header.rst b/docs/dev/analysis/features/header.rst index e259a2d53..1fe75f316 100644 --- a/docs/dev/analysis/features/header.rst +++ b/docs/dev/analysis/features/header.rst @@ -10,15 +10,15 @@ a section title or page number. Such a header is also known as a running head. A page footer is analogous in every way to a page header except that it appears at the bottom of a page. It should not be confused with a footnote, which is not uniform -between pages. For brevity's sake, the term *header* is often used here to refer to what +between pages. For brevity's sake, the term `header` is often used here to refer to what may be either a header or footer object, trusting the reader to understand its applicability to both object types. In book-printed documents, where pages are printed on both sides, when opened, the front -or *recto* side of each page appears to the right of the bound edge and the back or -*verso* side of each page appears on the left. The first printed page receives the +or `recto` side of each page appears to the right of the bound edge and the back or +`verso` side of each page appears on the left. The first printed page receives the page-number "1", and is always a recto page. Because pages are numbered consecutively, -each recto page receives an *odd* page number and each verso page receives an *even* +each recto page receives an `odd` page number and each verso page receives an `even` page number. The header appearing on a recto page often differs from that on a verso page. Supporting diff --git a/docs/dev/analysis/features/sections.rst b/docs/dev/analysis/features/sections.rst index f57c0b4bf..7f9dce91f 100644 --- a/docs/dev/analysis/features/sections.rst +++ b/docs/dev/analysis/features/sections.rst @@ -2,7 +2,7 @@ Sections ======== -Word supports the notion of a *section*, having distinct page layout settings. +Word supports the notion of a `section`, having distinct page layout settings. This is how, for example, a document can contain some pages in portrait layout and others in landscape. Section breaks are implemented completely differently from line, page, and column breaks. The former adds a ```` diff --git a/docs/dev/analysis/features/shapes/index.rst b/docs/dev/analysis/features/shapes/index.rst index 37b4c49f4..19e42de0e 100644 --- a/docs/dev/analysis/features/shapes/index.rst +++ b/docs/dev/analysis/features/shapes/index.rst @@ -2,8 +2,8 @@ Shapes (in general) =================== -A graphical object that appears in a Word document is known as a *shape*. -A shape can be *inline* or *floating*. An inline shape appears on a text +A graphical object that appears in a Word document is known as a `shape`. +A shape can be `inline` or `floating`. An inline shape appears on a text baseline as though it were a character glyph and affects the line height. A floating shape appears at an arbitrary location on the document and text may wrap around it. Several types of shape can be placed, including a picture, a diff --git a/docs/dev/analysis/features/styles/character-style.rst b/docs/dev/analysis/features/styles/character-style.rst index d06046daa..1779872fa 100644 --- a/docs/dev/analysis/features/styles/character-style.rst +++ b/docs/dev/analysis/features/styles/character-style.rst @@ -62,7 +62,7 @@ A baseline regular run:: -Adding *Emphasis* character style:: +Adding `Emphasis` character style:: diff --git a/docs/dev/analysis/features/styles/index.rst b/docs/dev/analysis/features/styles/index.rst index 3cbfcbb27..ddcec1c1b 100644 --- a/docs/dev/analysis/features/styles/index.rst +++ b/docs/dev/analysis/features/styles/index.rst @@ -11,7 +11,7 @@ Styles character-style latent-styles -Word supports the definition of *styles* to allow a group of formatting +Word supports the definition of `styles` to allow a group of formatting properties to be easily and consistently applied to a paragraph, run, table, or numbering scheme, all at once. The mechanism is similar to how Cascading Style Sheets (CSS) works with HTML. diff --git a/docs/dev/analysis/features/styles/latent-styles.rst b/docs/dev/analysis/features/styles/latent-styles.rst index 1423e5303..497b0b9f9 100644 --- a/docs/dev/analysis/features/styles/latent-styles.rst +++ b/docs/dev/analysis/features/styles/latent-styles.rst @@ -132,7 +132,7 @@ The `w:latentStyles` element used in the default Word 2011 template:: Latent style behavior --------------------- -* A style has two categories of attribute, *behavioral* and *formatting*. +* A style has two categories of attribute, `behavioral` and `formatting`. Behavioral attributes specify where and when the style should appear in the user interface. Behavioral attributes can be specified for latent styles using the ```` element and its ```` child @@ -157,14 +157,14 @@ Latent style behavior value is 0 if not specified. * **semiHidden**. The `semiHidden` attribute causes the style to be excluded - from the recommended list. The notion of *semi* in this context is that + from the recommended list. The notion of `semi` in this context is that while the style is hidden from the recommended list, it still appears in the "All Styles" list. This attribute is removed on first application of the style if an `unhideWhenUsed` attribute set |True| is also present. * **unhideWhenUsed**. The `unhideWhenUsed` attribute causes any `semiHidden` attribute to be removed when the style is first applied to content. Word - does *not* remove the `semiHidden` attribute just because there exists an + does `not` remove the `semiHidden` attribute just because there exists an object in the document having that style. The `unhideWhenUsed` attribute is not removed along with the `semiHidden` attribute when the style is applied. diff --git a/docs/dev/analysis/features/styles/style.rst b/docs/dev/analysis/features/styles/style.rst index e8e3ebf5b..a00ede05d 100644 --- a/docs/dev/analysis/features/styles/style.rst +++ b/docs/dev/analysis/features/styles/style.rst @@ -16,7 +16,7 @@ There are six behavior properties: hidden Style operates to assign formatting properties, but does not appear in - the UI under any circumstances. Used for *internal* styles assigned by an + the UI under any circumstances. Used for `internal` styles assigned by an application that should not be under the control of an end-user. priority @@ -98,10 +98,10 @@ semi-hidden ----------- The `w:semiHidden` element specifies visibility of the style in the so-called -*main* user interface. For Word, this means the style gallery and the +`main` user interface. For Word, this means the style gallery and the recommended, styles-in-use, and in-current-document lists. The all-styles list and current-style dropdown in the styles pane would then be considered -part of an *advanced* user interface. +part of an `advanced` user interface. Behavior ~~~~~~~~ diff --git a/docs/dev/analysis/features/table/table-props.rst b/docs/dev/analysis/features/table/table-props.rst index 8485c7bc8..73e97449e 100644 --- a/docs/dev/analysis/features/table/table-props.rst +++ b/docs/dev/analysis/features/table/table-props.rst @@ -23,7 +23,7 @@ a table:: Autofit ------- -Word has two algorithms for laying out a table, *fixed-width* or *autofit*. +Word has two algorithms for laying out a table, *fixed-width* or `autofit`. The default is autofit. Word will adjust column widths in an autofit table based on cell contents. A fixed-width table retains its column widths regardless of the contents. Either algorithm will adjust column widths diff --git a/docs/dev/analysis/features/text/font.rst b/docs/dev/analysis/features/text/font.rst index 1682b5c76..626065006 100644 --- a/docs/dev/analysis/features/text/font.rst +++ b/docs/dev/analysis/features/text/font.rst @@ -138,16 +138,16 @@ The semantics of the three values are as follows: +-------+---------------------------------------------------------------+ | value | meaning | +=======+===============================================================+ -| True | The effective value of the property is unconditionally *on*. | +| True | The effective value of the property is unconditionally `on`. | | | Contrary settings in the style hierarchy have no effect. | +-------+---------------------------------------------------------------+ -| False | The effective value of the property is unconditionally *off*. | +| False | The effective value of the property is unconditionally `off`. | | | Contrary settings in the style hierarchy have no effect. | +-------+---------------------------------------------------------------+ | None | The element is not present. The effective value is | | | inherited from the style hierarchy. If no value for this | | | property is present in the style hierarchy, the effective | -| | value is *off*. | +| | value is `off`. | +-------+---------------------------------------------------------------+ @@ -155,7 +155,7 @@ Toggle properties ----------------- Certain of the boolean run properties are *toggle properties*. A toggle -property is one that behaves like a *toggle* at certain places in the style +property is one that behaves like a `toggle` at certain places in the style hierarchy. Toggle here means that setting the property on has the effect of reversing the prior setting rather than unconditionally setting the property on. diff --git a/docs/dev/analysis/features/text/hyperlink.rst b/docs/dev/analysis/features/text/hyperlink.rst new file mode 100644 index 000000000..4dff91d20 --- /dev/null +++ b/docs/dev/analysis/features/text/hyperlink.rst @@ -0,0 +1,352 @@ + +Hyperlink +========= + +Word allows hyperlinks to be placed in a document wherever paragraphs can appear. + +The target (URL) of a hyperlink may be external, such as a web site, or internal, to +another location in the document. + +The visible text of a hyperlink is held in one or more runs. Technically a hyperlink can +have zero runs, but this occurs only in contrived cases (otherwise there would be +nothing to click on). As usual, each run can have its own distinct text formatting +(font), so for example one word in the hyperlink can be bold, etc. By default, Word +applies the built-in `Hyperlink` character style to a newly inserted hyperlink. + +Note that rendered page-breaks can occur in the middle of a hyperlink. + +A |Hyperlink| is a child of |Paragraph|, a peer of |Run|. + + +Candidate protocol +------------------ + +An external hyperlink has an address and an optional anchor. An internal hyperlink has +only an anchor. An anchor is also known as a *URI fragment* and follows a hash mark +("#"). + +Note that the anchor and URL are stored in two distinct attributes, so you need to +concatenate `.address` and `.anchor` if you want the whole thing. + +.. highlight:: python + +**Access hyperlinks in a paragraph**:: + + >>> hyperlinks = paragraph.hyperlinks + [] + +**Access hyperlinks in a paragraph in document order with runs**:: + + >>> list(paragraph.iter_inner_content()) + [ + + + + ] + +**Access hyperlink address**:: + + >>> hyperlink.address + 'https://google.com/' + +**Access hyperlinks runs**:: + + >>> hyperlink.runs + [ + + + + ] + +**Determine whether a hyperlink contains a rendered page-break**:: + + >>> hyperlink.contains_page_break + False + +**Access visible text of a hyperlink**:: + + >>> hyperlink.text + 'an excellent Wikipedia article on ferrets' + +**Add an external hyperlink**:: + + >>> hyperlink = paragraph.add_hyperlink( + 'About', address='http://us.com', anchor='about' + ) + >>> hyperlink + + >>> hyperlink.text + 'About' + >>> hyperlink.address + 'http://us.com' + >>> hyperlink.anchor + 'about' + +**Add an internal hyperlink (to a bookmark)**:: + + >>> hyperlink = paragraph.add_hyperlink('Section 1', anchor='Section_1') + >>> hyperlink.text + 'Section 1' + >>> hyperlink.anchor + 'Section_1' + >>> hyperlink.address + None + +**Modify hyperlink properties**:: + + >>> hyperlink.text = 'Froogle' + >>> hyperlink.text + 'Froogle' + >>> hyperlink.address = 'mailto:info@froogle.com?subject=sup dawg?' + >>> hyperlink.address + 'mailto:info@froogle.com?subject=sup%20dawg%3F' + >>> hyperlink.anchor = None + >>> hyperlink.anchor + None + +**Add additional runs to a hyperlink**:: + + >>> hyperlink.text = 'A ' + >>> # .insert_run inserts a new run at idx, defaults to idx=-1 + >>> hyperlink.insert_run(' link').bold = True + >>> hyperlink.insert_run('formatted', idx=1).bold = True + >>> hyperlink.text + 'A formatted link' + >>> [r for r in hyperlink.iter_runs()] + [, + , + ] + +**Iterate over the run-level items a paragraph contains**:: + + >>> paragraph = document.add_paragraph('A paragraph having a link to: ') + >>> paragraph.add_hyperlink(text='github', address='http://github.com') + >>> [item for item in paragraph.iter_run_level_items()]: + [, ] + +**Paragraph.text now includes text contained in a hyperlink**:: + + >>> paragraph.text + 'A paragraph having a link to: github' + + +Word Behaviors +-------------- + +* What are the semantics of the w:history attribute on w:hyperlink? I'm + suspecting this indicates whether the link should show up blue (unvisited) + or purple (visited). I'm inclined to think we need that as a read/write + property on hyperlink. We should see what the MS API does on this count. + +* We probably need to enforce some character-set restrictions on w:anchor. + Word doesn't seem to like spaces or hyphens, for example. The simple type + ST_String doesn't look like it takes care of this. + +* We'll need to test URL escaping of special characters like spaces and + question marks in Hyperlink.address. + +* What does Word do when loading a document containing an internal hyperlink + having an anchor value that doesn't match an existing bookmark? We'll want + to know because we're sure to get support inquiries from folks who don't + match those up and wonder why they get a repair error or whatever. + + +Specimen XML +------------ + +.. highlight:: xml + + +External links +~~~~~~~~~~~~~~ + +The address (URL) of an external hyperlink is stored in the document.xml.rels +file, keyed by the w:hyperlink@r:id attribute:: + + + + This is an external link to + + + + + + + Google + + + + +... mapping to relationship in document.xml.rels:: + + + + + +A hyperlink can contain multiple runs of text (and a whole lot of other +stuff, including nested hyperlinks, at least as far as the schema indicates):: + + + + + + + + A hyperlink containing an + + + + + + + italicized + + + + + + word + + + + + +Internal links +~~~~~~~~~~~~~~ + +An internal link provides "jump to another document location" behavior in the +Word UI. An internal link is distinguished by the absence of an r:id +attribute. In this case, the w:anchor attribute is required. The value of the +anchor attribute is the name of a bookmark in the document. + +Example:: + + + + See + + + + + + + Section 4 + + + + for more details. + + + +... referring to this bookmark elsewhere in the document:: + + + + + Section 4 + + + + + +Schema excerpt +-------------- + +.. highlight:: xml + +:: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dev/analysis/features/text/index.rst b/docs/dev/analysis/features/text/index.rst index 2fff03924..b1e2fa7f8 100644 --- a/docs/dev/analysis/features/text/index.rst +++ b/docs/dev/analysis/features/text/index.rst @@ -5,6 +5,7 @@ Text .. toctree:: :titlesonly: + hyperlink tab-stops font-highlight-color paragraph-format diff --git a/docs/dev/analysis/features/text/paragraph-format.rst b/docs/dev/analysis/features/text/paragraph-format.rst index febc9300a..6e5398a13 100644 --- a/docs/dev/analysis/features/text/paragraph-format.rst +++ b/docs/dev/analysis/features/text/paragraph-format.rst @@ -10,7 +10,7 @@ spacing, space before and after, and widow/orphan control. Alignment (justification) ------------------------- -In Word, each paragraph has an *alignment* attribute that specifies how to +In Word, each paragraph has an `alignment` attribute that specifies how to justify the lines of the paragraph when the paragraph is laid out on the page. Common values are left, right, centered, and justified. @@ -45,7 +45,7 @@ Paragraph spacing Spacing between subsequent paragraphs is controlled by the paragraph spacing attributes. Spacing can be applied either before the paragraph, after it, or -both. The concept is similar to that of *padding* or *margin* in CSS. +both. The concept is similar to that of `padding` or `margin` in CSS. WordprocessingML supports paragraph spacing specified as either a length value or as a multiple of the line height; however only a length value is supported via the Word UI. Inter-paragraph spacing "overlaps", such that the diff --git a/docs/user/hdrftr.rst b/docs/user/hdrftr.rst index 040da7ad3..ae378536b 100644 --- a/docs/user/hdrftr.rst +++ b/docs/user/hdrftr.rst @@ -12,7 +12,7 @@ header is also known as a *running head*. A *page footer* is analogous in every way to a page header except that it appears at the bottom of a page. It should not be confused with a footnote, which is not uniform -between pages. For brevity's sake, the term *header* is often used here to refer to what +between pages. For brevity's sake, the term `header` is often used here to refer to what may be either a header or footer object, trusting the reader to understand its applicability to both object types. @@ -20,7 +20,7 @@ applicability to both object types. Accessing the header for a section ---------------------------------- -Headers and footers are linked to a *section*; this allows each section to have +Headers and footers are linked to a `section`; this allows each section to have a distinct header and/or footer. For example, a landscape section might have a wider header than a portrait section. @@ -33,7 +33,7 @@ for that section:: >>> header -A |_Header| object is *always* present on ``Section.header``, even when no header is +A |_Header| object is `always` present on ``Section.header``, even when no header is defined for that section. The presence of an actual header definition is indicated by ``_Header.is_linked_to_previous``:: @@ -129,7 +129,7 @@ Here they are in a nutshell: ``True`` in both those cases. 4. The content of a ``_Header`` object is its own content if it has a header definition. - If not, its content is that of the first prior section that *does* have a header + If not, its content is that of the first prior section that `does` have a header definition. If no sections have a header definition, a new one is added on the first section and all other sections inherit that one. This adding of a header definition happens the first time header content is accessed, perhaps by referencing @@ -159,7 +159,7 @@ definition does nothing. Inherited content is automatically located ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Editing the content of a header edits the content of the *source* header, taking into +Editing the content of a header edits the content of the `source` header, taking into account any "inheritance". So for example, if the section 2 header inherits from section 1 and you edit the section 2 header, you actually change the contents of the section 1 header. A new header definition is not added for section 2 unless you first explicitly diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 604fe7a2d..0d6982ee0 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -189,7 +189,7 @@ or over a network and don't want to get the filesystem involved. Image size ~~~~~~~~~~ -By default, the added image appears at *native* size. This is often bigger than +By default, the added image appears at `native` size. This is often bigger than you want. Native size is calculated as ``pixels / dpi``. So a 300x300 pixel image having 300 dpi resolution appears in a one inch square. The problem is most images don't contain a dpi property and it defaults to 72 dpi. This would @@ -250,7 +250,7 @@ a little about what goes on inside a paragraph. The short version is this: height, tabs, and so forth. #. Character-level formatting, such as bold and italic, are applied at the - *run* level. All content within a paragraph must be within a run, but there + `run` level. All content within a paragraph must be within a run, but there can be more than one. So a paragraph with a bold word in the middle would need three runs, a normal one, a bold one containing the word, and another normal one for the text after. @@ -310,7 +310,7 @@ settings. In general you can think of a character style as specifying a font, including its typeface, size, color, bold, italic, etc. Like paragraph styles, a character style must already be defined in the -document you open with the ``Document()`` call (*see* +document you open with the ``Document()`` call (`see` :ref:`understanding_styles`). A character style can be specified when adding a new run:: diff --git a/docs/user/sections.rst b/docs/user/sections.rst index 3fe98acd0..895021874 100644 --- a/docs/user/sections.rst +++ b/docs/user/sections.rst @@ -3,15 +3,14 @@ Working with Sections ===================== -Word supports the notion of a *section*, a division of a document having the -same page layout settings, such as margins and page orientation. This is how, -for example, a document can contain some pages in portrait layout and others in -landscape. - -Most Word documents have only the single section that comes by default and -further, most of those have no reason to change the default margins or other -page layout. But when you *do* need to change the page layout, you'll need -to understand sections to get it done. +Word supports the notion of a `section`, a division of a document having the same page +layout settings, such as margins and page orientation. This is how, for example, a +document can contain some pages in portrait layout and others in landscape. Each section +also defines the headers and footers that apply to the pages of that section. + +Most Word documents have only the single section that comes by default and further, most +of those have no reason to change the default margins or other page layout. But when you +`do` need to change the page layout, you'll need to understand sections to get it done. Accessing sections diff --git a/docs/user/shapes.rst b/docs/user/shapes.rst index ec5d22797..5dcefbf61 100644 --- a/docs/user/shapes.rst +++ b/docs/user/shapes.rst @@ -2,11 +2,11 @@ Understanding pictures and other shapes ======================================= -Conceptually, Word documents have two *layers*, a *text layer* and a *drawing +Conceptually, Word documents have two `layers`, a *text layer* and a *drawing layer*. In the text layer, text objects are flowed from left to right and from top to bottom, starting a new page when the prior one is filled. In the drawing -layer, drawing objects, called *shapes*, are placed at arbitrary positions. -These are sometimes referred to as *floating* shapes. +layer, drawing objects, called `shapes`, are placed at arbitrary positions. +These are sometimes referred to as `floating` shapes. A picture is a shape that can appear in either the text or drawing layer. When it appears in the text layer it is called an *inline shape*, or more diff --git a/docs/user/styles-understanding.rst b/docs/user/styles-understanding.rst index e49fdea83..114b7ad6a 100644 --- a/docs/user/styles-understanding.rst +++ b/docs/user/styles-understanding.rst @@ -125,7 +125,7 @@ access purposes. A style's :attr:`style_id` is used internally to key a content object such as a paragraph to its style. However this value is generated automatically by Word and is not guaranteed to be stable across saves. In general, the style -id is formed simply by removing spaces from the *localized* style name, +id is formed simply by removing spaces from the `localized` style name, however there are exceptions. Users of |docx| should generally avoid using the style id unless they are confident with the internals involved. @@ -155,13 +155,13 @@ Style Behavior -------------- In addition to collecting a set of formatting properties, a style has five -properties that specify its *behavior*. This behavior is relatively simple, +properties that specify its `behavior`. This behavior is relatively simple, basically amounting to when and where the style appears in the Word or LibreOffice UI. The key notion to understanding style behavior is the recommended list. In the style pane in Word, the user can select which list of styles they want to -see. One of these is named *Recommended* and is known as the *recommended +see. One of these is named `Recommended` and is known as the *recommended list*. All five behavior properties affect some aspect of the style’s appearance in this list and in the style gallery. diff --git a/docs/user/text.rst b/docs/user/text.rst index 1b28feaab..f2e54f3b4 100644 --- a/docs/user/text.rst +++ b/docs/user/text.rst @@ -22,7 +22,7 @@ A table is also a block-level object. An inline object is a portion of the content that occurs inside a block-level item. An example would be a word that appears in bold or a sentence in -all-caps. The most common inline object is a *run*. All content within +all-caps. The most common inline object is a `run`. All content within a block container is inside of an inline object. Typically, a paragraph contains one or more runs, each of which contain some part of the paragraph's text. @@ -55,7 +55,7 @@ The formatting properties of a paragraph are accessed using the Horizontal alignment (justification) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Also known as *justification*, the horizontal alignment of a paragraph can be +Also known as `justification`, the horizontal alignment of a paragraph can be set to left, centered, right, or fully justified (aligned on both the left and right sides) using values from the enumeration :ref:`WdParagraphAlignment`:: @@ -180,7 +180,7 @@ Paragraph spacing The :attr:`~.ParagraphFormat.space_before` and :attr:`~.ParagraphFormat.space_after` properties control the spacing between subsequent paragraphs, controlling the spacing before and after a paragraph, -respectively. Inter-paragraph spacing is *collapsed* during page layout, +respectively. Inter-paragraph spacing is `collapsed` during page layout, meaning the spacing between two paragraphs is the maximum of the `space_after` for the first paragraph and the `space_before` of the second paragraph. Paragraph spacing is specified as a |Length| value, often using diff --git a/docx/compat.py b/docx/compat.py deleted file mode 100644 index 1e3012d95..000000000 --- a/docx/compat.py +++ /dev/null @@ -1,39 +0,0 @@ -# encoding: utf-8 - -""" -Provides Python 2/3 compatibility objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys - -# =========================================================================== -# Python 3 versions -# =========================================================================== - -if sys.version_info >= (3, 0): - - from collections.abc import Sequence - from io import BytesIO - - def is_string(obj): - """Return True if *obj* is a string, False otherwise.""" - return isinstance(obj, str) - - Unicode = str - -# =========================================================================== -# Python 2 versions -# =========================================================================== - -else: - - from collections import Sequence # noqa - from StringIO import StringIO as BytesIO # noqa - - def is_string(obj): - """Return True if *obj* is a string, False otherwise.""" - return isinstance(obj, basestring) # noqa - - Unicode = unicode # noqa diff --git a/docx/dml/color.py b/docx/dml/color.py deleted file mode 100644 index 2f2f25cb2..000000000 --- a/docx/dml/color.py +++ /dev/null @@ -1,116 +0,0 @@ -# encoding: utf-8 - -""" -DrawingML objects related to color, ColorFormat being the most prominent. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..enum.dml import MSO_COLOR_TYPE -from ..oxml.simpletypes import ST_HexColorAuto -from ..shared import ElementProxy - - -class ColorFormat(ElementProxy): - """ - Provides access to color settings such as RGB color, theme color, and - luminance adjustments. - """ - - __slots__ = () - - def __init__(self, rPr_parent): - super(ColorFormat, self).__init__(rPr_parent) - - @property - def rgb(self): - """ - An |RGBColor| value or |None| if no RGB color is specified. - - When :attr:`type` is `MSO_COLOR_TYPE.RGB`, the value of this property - will always be an |RGBColor| value. It may also be an |RGBColor| - value if :attr:`type` is `MSO_COLOR_TYPE.THEME`, as Word writes the - current value of a theme color when one is assigned. In that case, - the RGB value should be interpreted as no more than a good guess - however, as the theme color takes precedence at rendering time. Its - value is |None| whenever :attr:`type` is either |None| or - `MSO_COLOR_TYPE.AUTO`. - - Assigning an |RGBColor| value causes :attr:`type` to become - `MSO_COLOR_TYPE.RGB` and any theme color is removed. Assigning |None| - causes any color to be removed such that the effective color is - inherited from the style hierarchy. - """ - color = self._color - if color is None: - return None - if color.val == ST_HexColorAuto.AUTO: - return None - return color.val - - @rgb.setter - def rgb(self, value): - if value is None and self._color is None: - return - rPr = self._element.get_or_add_rPr() - rPr._remove_color() - if value is not None: - rPr.get_or_add_color().val = value - - @property - def theme_color(self): - """ - A member of :ref:`MsoThemeColorIndex` or |None| if no theme color is - specified. When :attr:`type` is `MSO_COLOR_TYPE.THEME`, the value of - this property will always be a member of :ref:`MsoThemeColorIndex`. - When :attr:`type` has any other value, the value of this property is - |None|. - - Assigning a member of :ref:`MsoThemeColorIndex` causes :attr:`type` - to become `MSO_COLOR_TYPE.THEME`. Any existing RGB value is retained - but ignored by Word. Assigning |None| causes any color specification - to be removed such that the effective color is inherited from the - style hierarchy. - """ - color = self._color - if color is None or color.themeColor is None: - return None - return color.themeColor - - @theme_color.setter - def theme_color(self, value): - if value is None: - if self._color is not None: - self._element.rPr._remove_color() - return - self._element.get_or_add_rPr().get_or_add_color().themeColor = value - - @property - def type(self): - """ - Read-only. A member of :ref:`MsoColorType`, one of RGB, THEME, or - AUTO, corresponding to the way this color is defined. Its value is - |None| if no color is applied at this level, which causes the - effective color to be inherited from the style hierarchy. - """ - color = self._color - if color is None: - return None - if color.themeColor is not None: - return MSO_COLOR_TYPE.THEME - if color.val == ST_HexColorAuto.AUTO: - return MSO_COLOR_TYPE.AUTO - return MSO_COLOR_TYPE.RGB - - @property - def _color(self): - """ - Return `w:rPr/w:color` or |None| if not present. Helper to factor out - repetitive element access. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.color diff --git a/docx/document.py b/docx/document.py deleted file mode 100644 index 6493c458b..000000000 --- a/docx/document.py +++ /dev/null @@ -1,205 +0,0 @@ -# encoding: utf-8 - -"""|Document| and closely related objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from docx.blkcntnr import BlockItemContainer -from docx.enum.section import WD_SECTION -from docx.enum.text import WD_BREAK -from docx.section import Section, Sections -from docx.shared import ElementProxy, Emu - - -class Document(ElementProxy): - """WordprocessingML (WML) document. - - Not intended to be constructed directly. Use :func:`docx.Document` to open or create - a document. - """ - - __slots__ = ('_part', '__body') - - def __init__(self, element, part): - super(Document, self).__init__(element) - self._part = part - self.__body = None - - def add_heading(self, text="", level=1): - """Return a heading paragraph newly added to the end of the document. - - The heading paragraph will contain *text* and have its paragraph style - determined by *level*. If *level* is 0, the style is set to `Title`. If *level* - is 1 (or omitted), `Heading 1` is used. Otherwise the style is set to `Heading - {level}`. Raises |ValueError| if *level* is outside the range 0-9. - """ - if not 0 <= level <= 9: - raise ValueError("level must be in range 0-9, got %d" % level) - style = "Title" if level == 0 else "Heading %d" % level - return self.add_paragraph(text, style) - - def add_page_break(self): - """Return newly |Paragraph| object containing only a page break.""" - paragraph = self.add_paragraph() - paragraph.add_run().add_break(WD_BREAK.PAGE) - return paragraph - - def add_paragraph(self, text='', style=None): - """ - Return a paragraph newly added to the end of the document, populated - with *text* and having paragraph style *style*. *text* can contain - tab (``\\t``) characters, which are converted to the appropriate XML - form for a tab. *text* can also include newline (``\\n``) or carriage - return (``\\r``) characters, each of which is converted to a line - break. - """ - return self._body.add_paragraph(text, style) - - def add_picture(self, image_path_or_stream, width=None, height=None): - """ - Return a new picture shape added in its own paragraph at the end of - the document. The picture contains the image at - *image_path_or_stream*, scaled based on *width* and *height*. If - neither width nor height is specified, the picture appears at its - native size. If only one is specified, it is used to compute - a scaling factor that is then applied to the unspecified dimension, - preserving the aspect ratio of the image. The native size of the - picture is calculated using the dots-per-inch (dpi) value specified - in the image file, defaulting to 72 dpi if no value is specified, as - is often the case. - """ - run = self.add_paragraph().add_run() - return run.add_picture(image_path_or_stream, width, height) - - def add_section(self, start_type=WD_SECTION.NEW_PAGE): - """ - Return a |Section| object representing a new section added at the end - of the document. The optional *start_type* argument must be a member - of the :ref:`WdSectionStart` enumeration, and defaults to - ``WD_SECTION.NEW_PAGE`` if not provided. - """ - new_sectPr = self._element.body.add_section_break() - new_sectPr.start_type = start_type - return Section(new_sectPr, self._part) - - def add_table(self, rows, cols, style=None): - """ - Add a table having row and column counts of *rows* and *cols* - respectively and table style of *style*. *style* may be a paragraph - style object or a paragraph style name. If *style* is |None|, the - table inherits the default table style of the document. - """ - table = self._body.add_table(rows, cols, self._block_width) - table.style = style - return table - - @property - def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this document. - """ - return self._part.core_properties - - @property - def inline_shapes(self): - """ - An |InlineShapes| object providing access to the inline shapes in - this document. An inline shape is a graphical object, such as - a picture, contained in a run of text and behaving like a character - glyph, being flowed like other text in a paragraph. - """ - return self._part.inline_shapes - - @property - def paragraphs(self): - """ - A list of |Paragraph| instances corresponding to the paragraphs in - the document, in document order. Note that paragraphs within revision - marks such as ```` or ```` do not appear in this list. - """ - return self._body.paragraphs - - @property - def part(self): - """ - The |DocumentPart| object of this document. - """ - return self._part - - def save(self, path_or_stream): - """ - Save this document to *path_or_stream*, which can be either a path to - a filesystem location (a string) or a file-like object. - """ - self._part.save(path_or_stream) - - @property - def sections(self): - """|Sections| object providing access to each section in this document.""" - return Sections(self._element, self._part) - - @property - def settings(self): - """ - A |Settings| object providing access to the document-level settings - for this document. - """ - return self._part.settings - - @property - def styles(self): - """ - A |Styles| object providing access to the styles in this document. - """ - return self._part.styles - - @property - def tables(self): - """ - A list of |Table| instances corresponding to the tables in the - document, in document order. Note that only tables appearing at the - top level of the document appear in this list; a table nested inside - a table cell does not appear. A table within revision marks such as - ```` or ```` will also not appear in the list. - """ - return self._body.tables - - @property - def _block_width(self): - """ - Return a |Length| object specifying the width of available "writing" - space between the margins of the last section of this document. - """ - section = self.sections[-1] - return Emu( - section.page_width - section.left_margin - section.right_margin - ) - - @property - def _body(self): - """ - The |_Body| instance containing the content for this document. - """ - if self.__body is None: - self.__body = _Body(self._element.body, self) - return self.__body - - -class _Body(BlockItemContainer): - """ - Proxy for ```` element in this document, having primarily a - container role. - """ - def __init__(self, body_elm, parent): - super(_Body, self).__init__(body_elm, parent) - self._body = body_elm - - def clear_content(self): - """ - Return this |_Body| instance after clearing it of all content. - Section properties for the main document story, if present, are - preserved. - """ - self._body.clear_content() - return self diff --git a/docx/enum/base.py b/docx/enum/base.py deleted file mode 100644 index 36764b1a6..000000000 --- a/docx/enum/base.py +++ /dev/null @@ -1,363 +0,0 @@ -# encoding: utf-8 - -""" -Base classes and other objects used by enumerations -""" - -from __future__ import absolute_import, print_function - -import sys -import textwrap - -from ..exceptions import InvalidXmlError - - -def alias(*aliases): - """ - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to - be referenced by each of the names provided as arguments. - """ - def decorator(cls): - # alias must be set in globals from caller's frame - caller = sys._getframe(1) - globals_dict = caller.f_globals - for alias in aliases: - globals_dict[alias] = cls - return cls - return decorator - - -class _DocsPageFormatter(object): - """Generate an .rst doc page for an enumeration. - - Formats a RestructuredText documention page (string) for the enumeration - class parts passed to the constructor. An immutable one-shot service - object. - """ - - def __init__(self, clsname, clsdict): - self._clsname = clsname - self._clsdict = clsdict - - @property - def page_str(self): - """ - The RestructuredText documentation page for the enumeration. This is - the only API member for the class. - """ - tmpl = '.. _%s:\n\n%s\n\n%s\n\n----\n\n%s' - components = ( - self._ms_name, self._page_title, self._intro_text, - self._member_defs - ) - return tmpl % components - - @property - def _intro_text(self): - """Docstring of the enumeration, formatted for documentation page.""" - try: - cls_docstring = self._clsdict['__doc__'] - except KeyError: - cls_docstring = '' - - if cls_docstring is None: - return '' - - return textwrap.dedent(cls_docstring).strip() - - def _member_def(self, member): - """ - Return an individual member definition formatted as an RST glossary - entry, wrapped to fit within 78 columns. - """ - member_docstring = textwrap.dedent(member.docstring).strip() - member_docstring = textwrap.fill( - member_docstring, width=78, initial_indent=' '*4, - subsequent_indent=' '*4 - ) - return '%s\n%s\n' % (member.name, member_docstring) - - @property - def _member_defs(self): - """ - A single string containing the aggregated member definitions section - of the documentation page - """ - members = self._clsdict['__members__'] - member_defs = [ - self._member_def(member) for member in members - if member.name is not None - ] - return '\n'.join(member_defs) - - @property - def _ms_name(self): - """ - The Microsoft API name for this enumeration - """ - return self._clsdict['__ms_name__'] - - @property - def _page_title(self): - """ - The title for the documentation page, formatted as code (surrounded - in double-backtics) and underlined with '=' characters - """ - title_underscore = '=' * (len(self._clsname)+4) - return '``%s``\n%s' % (self._clsname, title_underscore) - - -class MetaEnumeration(type): - """ - The metaclass for Enumeration and its subclasses. Adds a name for each - named member and compiles state needed by the enumeration class to - respond to other attribute gets - """ - def __new__(meta, clsname, bases, clsdict): - meta._add_enum_members(clsdict) - meta._collect_valid_settings(clsdict) - meta._generate_docs_page(clsname, clsdict) - return type.__new__(meta, clsname, bases, clsdict) - - @classmethod - def _add_enum_members(meta, clsdict): - """ - Dispatch ``.add_to_enum()`` call to each member so it can do its - thing to properly add itself to the enumeration class. This - delegation allows member sub-classes to add specialized behaviors. - """ - enum_members = clsdict['__members__'] - for member in enum_members: - member.add_to_enum(clsdict) - - @classmethod - def _collect_valid_settings(meta, clsdict): - """ - Return a sequence containing the enumeration values that are valid - assignment values. Return-only values are excluded. - """ - enum_members = clsdict['__members__'] - valid_settings = [] - for member in enum_members: - valid_settings.extend(member.valid_settings) - clsdict['_valid_settings'] = valid_settings - - @classmethod - def _generate_docs_page(meta, clsname, clsdict): - """ - Return the RST documentation page for the enumeration. - """ - clsdict['__docs_rst__'] = ( - _DocsPageFormatter(clsname, clsdict).page_str - ) - - -class EnumerationBase(object): - """ - Base class for all enumerations, used directly for enumerations requiring - only basic behavior. It's __dict__ is used below in the Python 2+3 - compatible metaclass definition. - """ - __members__ = () - __ms_name__ = '' - - @classmethod - def validate(cls, value): - """ - Raise |ValueError| if *value* is not an assignable value. - """ - if value not in cls._valid_settings: - raise ValueError( - "%s not a member of %s enumeration" % (value, cls.__name__) - ) - - -Enumeration = MetaEnumeration( - 'Enumeration', (object,), dict(EnumerationBase.__dict__) -) - - -class XmlEnumeration(Enumeration): - """ - Provides ``to_xml()`` and ``from_xml()`` methods in addition to base - enumeration features - """ - __members__ = () - __ms_name__ = '' - - @classmethod - def from_xml(cls, xml_val): - """ - Return the enumeration member corresponding to the XML value - *xml_val*. - """ - if xml_val not in cls._xml_to_member: - raise InvalidXmlError( - "attribute value '%s' not valid for this type" % xml_val - ) - return cls._xml_to_member[xml_val] - - @classmethod - def to_xml(cls, enum_val): - """ - Return the XML value of the enumeration value *enum_val*. - """ - if enum_val not in cls._member_to_xml: - raise ValueError( - "value '%s' not in enumeration %s" % (enum_val, cls.__name__) - ) - return cls._member_to_xml[enum_val] - - -class EnumMember(object): - """ - Used in the enumeration class definition to define a member value and its - mappings - """ - def __init__(self, name, value, docstring): - self._name = name - if isinstance(value, int): - value = EnumValue(name, value, docstring) - self._value = value - self._docstring = docstring - - def add_to_enum(self, clsdict): - """ - Add a name to *clsdict* for this member. - """ - self.register_name(clsdict) - - @property - def docstring(self): - """ - The description of this member - """ - return self._docstring - - @property - def name(self): - """ - The distinguishing name of this member within the enumeration class, - e.g. 'MIDDLE' for MSO_VERTICAL_ANCHOR.MIDDLE, if this is a named - member. Otherwise the primitive value such as |None|, |True| or - |False|. - """ - return self._name - - def register_name(self, clsdict): - """ - Add a member name to the class dict *clsdict* containing the value of - this member object. Where the name of this object is None, do - nothing; this allows out-of-band values to be defined without adding - a name to the class dict. - """ - if self.name is None: - return - clsdict[self.name] = self.value - - @property - def valid_settings(self): - """ - A sequence containing the values valid for assignment for this - member. May be zero, one, or more in number. - """ - return (self._value,) - - @property - def value(self): - """ - The enumeration value for this member, often an instance of - EnumValue, but may be a primitive value such as |None|. - """ - return self._value - - -class EnumValue(int): - """ - A named enumeration value, providing __str__ and __doc__ string values - for its symbolic name and description, respectively. Subclasses int, so - behaves as a regular int unless the strings are asked for. - """ - def __new__(cls, member_name, int_value, docstring): - return super(EnumValue, cls).__new__(cls, int_value) - - def __init__(self, member_name, int_value, docstring): - super(EnumValue, self).__init__() - self._member_name = member_name - self._docstring = docstring - - @property - def __doc__(self): - """ - The description of this enumeration member - """ - return self._docstring.strip() - - def __str__(self): - """ - The symbolic name and string value of this member, e.g. 'MIDDLE (3)' - """ - return "%s (%d)" % (self._member_name, int(self)) - - -class ReturnValueOnlyEnumMember(EnumMember): - """ - Used to define a member of an enumeration that is only valid as a query - result and is not valid as a setting, e.g. MSO_VERTICAL_ANCHOR.MIXED (-2) - """ - @property - def valid_settings(self): - """ - No settings are valid for a return-only value. - """ - return () - - -class XmlMappedEnumMember(EnumMember): - """ - Used to define a member whose value maps to an XML attribute value. - """ - def __init__(self, name, value, xml_value, docstring): - super(XmlMappedEnumMember, self).__init__(name, value, docstring) - self._xml_value = xml_value - - def add_to_enum(self, clsdict): - """ - Compile XML mappings in addition to base add behavior. - """ - super(XmlMappedEnumMember, self).add_to_enum(clsdict) - self.register_xml_mapping(clsdict) - - def register_xml_mapping(self, clsdict): - """ - Add XML mappings to the enumeration class state for this member. - """ - member_to_xml = self._get_or_add_member_to_xml(clsdict) - member_to_xml[self.value] = self.xml_value - xml_to_member = self._get_or_add_xml_to_member(clsdict) - xml_to_member[self.xml_value] = self.value - - @property - def xml_value(self): - """ - The XML attribute value that corresponds to this enumeration value - """ - return self._xml_value - - @staticmethod - def _get_or_add_member_to_xml(clsdict): - """ - Add the enum -> xml value mapping to the enumeration class state - """ - if '_member_to_xml' not in clsdict: - clsdict['_member_to_xml'] = dict() - return clsdict['_member_to_xml'] - - @staticmethod - def _get_or_add_xml_to_member(clsdict): - """ - Add the xml -> enum value mapping to the enumeration class state - """ - if '_xml_to_member' not in clsdict: - clsdict['_xml_to_member'] = dict() - return clsdict['_xml_to_member'] diff --git a/docx/enum/dml.py b/docx/enum/dml.py deleted file mode 100644 index 1ad0eaa87..000000000 --- a/docx/enum/dml.py +++ /dev/null @@ -1,124 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations used by DrawingML objects -""" - -from __future__ import absolute_import - -from .base import ( - alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember -) - - -class MSO_COLOR_TYPE(Enumeration): - """ - Specifies the color specification scheme - - Example:: - - from docx.enum.dml import MSO_COLOR_TYPE - - assert font.color.type == MSO_COLOR_TYPE.SCHEME - """ - - __ms_name__ = 'MsoColorType' - - __url__ = ( - 'http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15' - ').aspx' - ) - - __members__ = ( - EnumMember( - 'RGB', 1, 'Color is specified by an |RGBColor| value.' - ), - EnumMember( - 'THEME', 2, 'Color is one of the preset theme colors.' - ), - EnumMember( - 'AUTO', 101, 'Color is determined automatically by the ' - 'application.' - ), - ) - - -@alias('MSO_THEME_COLOR') -class MSO_THEME_COLOR_INDEX(XmlEnumeration): - """ - Indicates the Office theme color, one of those shown in the color gallery - on the formatting ribbon. - - Alias: ``MSO_THEME_COLOR`` - - Example:: - - from docx.enum.dml import MSO_THEME_COLOR - - font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 - """ - - __ms_name__ = 'MsoThemeColorIndex' - - __url__ = ( - 'http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15' - ').aspx' - ) - - __members__ = ( - EnumMember( - 'NOT_THEME_COLOR', 0, 'Indicates the color is not a theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_1', 5, 'accent1', 'Specifies the Accent 1 theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_2', 6, 'accent2', 'Specifies the Accent 2 theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_3', 7, 'accent3', 'Specifies the Accent 3 theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_4', 8, 'accent4', 'Specifies the Accent 4 theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_5', 9, 'accent5', 'Specifies the Accent 5 theme color.' - ), - XmlMappedEnumMember( - 'ACCENT_6', 10, 'accent6', 'Specifies the Accent 6 theme color.' - ), - XmlMappedEnumMember( - 'BACKGROUND_1', 14, 'background1', 'Specifies the Background 1 ' - 'theme color.' - ), - XmlMappedEnumMember( - 'BACKGROUND_2', 16, 'background2', 'Specifies the Background 2 ' - 'theme color.' - ), - XmlMappedEnumMember( - 'DARK_1', 1, 'dark1', 'Specifies the Dark 1 theme color.' - ), - XmlMappedEnumMember( - 'DARK_2', 3, 'dark2', 'Specifies the Dark 2 theme color.' - ), - XmlMappedEnumMember( - 'FOLLOWED_HYPERLINK', 12, 'followedHyperlink', 'Specifies the ' - 'theme color for a clicked hyperlink.' - ), - XmlMappedEnumMember( - 'HYPERLINK', 11, 'hyperlink', 'Specifies the theme color for a ' - 'hyperlink.' - ), - XmlMappedEnumMember( - 'LIGHT_1', 2, 'light1', 'Specifies the Light 1 theme color.' - ), - XmlMappedEnumMember( - 'LIGHT_2', 4, 'light2', 'Specifies the Light 2 theme color.' - ), - XmlMappedEnumMember( - 'TEXT_1', 13, 'text1', 'Specifies the Text 1 theme color.' - ), - XmlMappedEnumMember( - 'TEXT_2', 15, 'text2', 'Specifies the Text 2 theme color.' - ), - ) diff --git a/docx/enum/section.py b/docx/enum/section.py deleted file mode 100644 index 381e81877..000000000 --- a/docx/enum/section.py +++ /dev/null @@ -1,103 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations related to the main document in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, XmlEnumeration, XmlMappedEnumMember - - -@alias('WD_HEADER_FOOTER') -class WD_HEADER_FOOTER_INDEX(XmlEnumeration): - """ - alias: **WD_HEADER_FOOTER** - - Specifies one of the three possible header/footer definitions for a section. - - For internal use only; not part of the python-docx API. - """ - - __ms_name__ = "WdHeaderFooterIndex" - - __url__ = "https://docs.microsoft.com/en-us/office/vba/api/word.wdheaderfooterindex" - - __members__ = ( - XmlMappedEnumMember( - "PRIMARY", 1, "default", "Header for odd pages or all if no even header." - ), - XmlMappedEnumMember( - "FIRST_PAGE", 2, "first", "Header for first page of section." - ), - XmlMappedEnumMember( - "EVEN_PAGE", 3, "even", "Header for even pages of recto/verso section." - ), - ) - - -@alias('WD_ORIENT') -class WD_ORIENTATION(XmlEnumeration): - """ - alias: **WD_ORIENT** - - Specifies the page layout orientation. - - Example:: - - from docx.enum.section import WD_ORIENT - - section = document.sections[-1] - section.orientation = WD_ORIENT.LANDSCAPE - """ - - __ms_name__ = 'WdOrientation' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff837902.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'PORTRAIT', 0, 'portrait', 'Portrait orientation.' - ), - XmlMappedEnumMember( - 'LANDSCAPE', 1, 'landscape', 'Landscape orientation.' - ), - ) - - -@alias('WD_SECTION') -class WD_SECTION_START(XmlEnumeration): - """ - alias: **WD_SECTION** - - Specifies the start type of a section break. - - Example:: - - from docx.enum.section import WD_SECTION - - section = document.sections[0] - section.start_type = WD_SECTION.NEW_PAGE - """ - - __ms_name__ = 'WdSectionStart' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff840975.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'CONTINUOUS', 0, 'continuous', 'Continuous section break.' - ), - XmlMappedEnumMember( - 'NEW_COLUMN', 1, 'nextColumn', 'New column section break.' - ), - XmlMappedEnumMember( - 'NEW_PAGE', 2, 'nextPage', 'New page section break.' - ), - XmlMappedEnumMember( - 'EVEN_PAGE', 3, 'evenPage', 'Even pages section break.' - ), - XmlMappedEnumMember( - 'ODD_PAGE', 4, 'oddPage', 'Section begins on next odd page.' - ), - ) diff --git a/docx/enum/shape.py b/docx/enum/shape.py deleted file mode 100644 index 937f30a9f..000000000 --- a/docx/enum/shape.py +++ /dev/null @@ -1,22 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations related to DrawingML shapes in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -class WD_INLINE_SHAPE_TYPE(object): - """ - Corresponds to WdInlineShapeType enumeration - http://msdn.microsoft.com/en-us/library/office/ff192587.aspx - """ - CHART = 12 - LINKED_PICTURE = 4 - PICTURE = 3 - SMART_ART = 15 - NOT_IMPLEMENTED = -6 - - -WD_INLINE_SHAPE = WD_INLINE_SHAPE_TYPE diff --git a/docx/enum/style.py b/docx/enum/style.py deleted file mode 100644 index 515c594ce..000000000 --- a/docx/enum/style.py +++ /dev/null @@ -1,466 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations related to styles -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember - - -@alias('WD_STYLE') -class WD_BUILTIN_STYLE(XmlEnumeration): - """ - alias: **WD_STYLE** - - Specifies a built-in Microsoft Word style. - - Example:: - - from docx import Document - from docx.enum.style import WD_STYLE - - document = Document() - styles = document.styles - style = styles[WD_STYLE.BODY_TEXT] - """ - - __ms_name__ = 'WdBuiltinStyle' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff835210.aspx' - - __members__ = ( - EnumMember( - 'BLOCK_QUOTATION', -85, 'Block Text.' - ), - EnumMember( - 'BODY_TEXT', -67, 'Body Text.' - ), - EnumMember( - 'BODY_TEXT_2', -81, 'Body Text 2.' - ), - EnumMember( - 'BODY_TEXT_3', -82, 'Body Text 3.' - ), - EnumMember( - 'BODY_TEXT_FIRST_INDENT', -78, 'Body Text First Indent.' - ), - EnumMember( - 'BODY_TEXT_FIRST_INDENT_2', -79, 'Body Text First Indent 2.' - ), - EnumMember( - 'BODY_TEXT_INDENT', -68, 'Body Text Indent.' - ), - EnumMember( - 'BODY_TEXT_INDENT_2', -83, 'Body Text Indent 2.' - ), - EnumMember( - 'BODY_TEXT_INDENT_3', -84, 'Body Text Indent 3.' - ), - EnumMember( - 'BOOK_TITLE', -265, 'Book Title.' - ), - EnumMember( - 'CAPTION', -35, 'Caption.' - ), - EnumMember( - 'CLOSING', -64, 'Closing.' - ), - EnumMember( - 'COMMENT_REFERENCE', -40, 'Comment Reference.' - ), - EnumMember( - 'COMMENT_TEXT', -31, 'Comment Text.' - ), - EnumMember( - 'DATE', -77, 'Date.' - ), - EnumMember( - 'DEFAULT_PARAGRAPH_FONT', -66, 'Default Paragraph Font.' - ), - EnumMember( - 'EMPHASIS', -89, 'Emphasis.' - ), - EnumMember( - 'ENDNOTE_REFERENCE', -43, 'Endnote Reference.' - ), - EnumMember( - 'ENDNOTE_TEXT', -44, 'Endnote Text.' - ), - EnumMember( - 'ENVELOPE_ADDRESS', -37, 'Envelope Address.' - ), - EnumMember( - 'ENVELOPE_RETURN', -38, 'Envelope Return.' - ), - EnumMember( - 'FOOTER', -33, 'Footer.' - ), - EnumMember( - 'FOOTNOTE_REFERENCE', -39, 'Footnote Reference.' - ), - EnumMember( - 'FOOTNOTE_TEXT', -30, 'Footnote Text.' - ), - EnumMember( - 'HEADER', -32, 'Header.' - ), - EnumMember( - 'HEADING_1', -2, 'Heading 1.' - ), - EnumMember( - 'HEADING_2', -3, 'Heading 2.' - ), - EnumMember( - 'HEADING_3', -4, 'Heading 3.' - ), - EnumMember( - 'HEADING_4', -5, 'Heading 4.' - ), - EnumMember( - 'HEADING_5', -6, 'Heading 5.' - ), - EnumMember( - 'HEADING_6', -7, 'Heading 6.' - ), - EnumMember( - 'HEADING_7', -8, 'Heading 7.' - ), - EnumMember( - 'HEADING_8', -9, 'Heading 8.' - ), - EnumMember( - 'HEADING_9', -10, 'Heading 9.' - ), - EnumMember( - 'HTML_ACRONYM', -96, 'HTML Acronym.' - ), - EnumMember( - 'HTML_ADDRESS', -97, 'HTML Address.' - ), - EnumMember( - 'HTML_CITE', -98, 'HTML Cite.' - ), - EnumMember( - 'HTML_CODE', -99, 'HTML Code.' - ), - EnumMember( - 'HTML_DFN', -100, 'HTML Definition.' - ), - EnumMember( - 'HTML_KBD', -101, 'HTML Keyboard.' - ), - EnumMember( - 'HTML_NORMAL', -95, 'Normal (Web).' - ), - EnumMember( - 'HTML_PRE', -102, 'HTML Preformatted.' - ), - EnumMember( - 'HTML_SAMP', -103, 'HTML Sample.' - ), - EnumMember( - 'HTML_TT', -104, 'HTML Typewriter.' - ), - EnumMember( - 'HTML_VAR', -105, 'HTML Variable.' - ), - EnumMember( - 'HYPERLINK', -86, 'Hyperlink.' - ), - EnumMember( - 'HYPERLINK_FOLLOWED', -87, 'Followed Hyperlink.' - ), - EnumMember( - 'INDEX_1', -11, 'Index 1.' - ), - EnumMember( - 'INDEX_2', -12, 'Index 2.' - ), - EnumMember( - 'INDEX_3', -13, 'Index 3.' - ), - EnumMember( - 'INDEX_4', -14, 'Index 4.' - ), - EnumMember( - 'INDEX_5', -15, 'Index 5.' - ), - EnumMember( - 'INDEX_6', -16, 'Index 6.' - ), - EnumMember( - 'INDEX_7', -17, 'Index 7.' - ), - EnumMember( - 'INDEX_8', -18, 'Index 8.' - ), - EnumMember( - 'INDEX_9', -19, 'Index 9.' - ), - EnumMember( - 'INDEX_HEADING', -34, 'Index Heading' - ), - EnumMember( - 'INTENSE_EMPHASIS', -262, 'Intense Emphasis.' - ), - EnumMember( - 'INTENSE_QUOTE', -182, 'Intense Quote.' - ), - EnumMember( - 'INTENSE_REFERENCE', -264, 'Intense Reference.' - ), - EnumMember( - 'LINE_NUMBER', -41, 'Line Number.' - ), - EnumMember( - 'LIST', -48, 'List.' - ), - EnumMember( - 'LIST_2', -51, 'List 2.' - ), - EnumMember( - 'LIST_3', -52, 'List 3.' - ), - EnumMember( - 'LIST_4', -53, 'List 4.' - ), - EnumMember( - 'LIST_5', -54, 'List 5.' - ), - EnumMember( - 'LIST_BULLET', -49, 'List Bullet.' - ), - EnumMember( - 'LIST_BULLET_2', -55, 'List Bullet 2.' - ), - EnumMember( - 'LIST_BULLET_3', -56, 'List Bullet 3.' - ), - EnumMember( - 'LIST_BULLET_4', -57, 'List Bullet 4.' - ), - EnumMember( - 'LIST_BULLET_5', -58, 'List Bullet 5.' - ), - EnumMember( - 'LIST_CONTINUE', -69, 'List Continue.' - ), - EnumMember( - 'LIST_CONTINUE_2', -70, 'List Continue 2.' - ), - EnumMember( - 'LIST_CONTINUE_3', -71, 'List Continue 3.' - ), - EnumMember( - 'LIST_CONTINUE_4', -72, 'List Continue 4.' - ), - EnumMember( - 'LIST_CONTINUE_5', -73, 'List Continue 5.' - ), - EnumMember( - 'LIST_NUMBER', -50, 'List Number.' - ), - EnumMember( - 'LIST_NUMBER_2', -59, 'List Number 2.' - ), - EnumMember( - 'LIST_NUMBER_3', -60, 'List Number 3.' - ), - EnumMember( - 'LIST_NUMBER_4', -61, 'List Number 4.' - ), - EnumMember( - 'LIST_NUMBER_5', -62, 'List Number 5.' - ), - EnumMember( - 'LIST_PARAGRAPH', -180, 'List Paragraph.' - ), - EnumMember( - 'MACRO_TEXT', -46, 'Macro Text.' - ), - EnumMember( - 'MESSAGE_HEADER', -74, 'Message Header.' - ), - EnumMember( - 'NAV_PANE', -90, 'Document Map.' - ), - EnumMember( - 'NORMAL', -1, 'Normal.' - ), - EnumMember( - 'NORMAL_INDENT', -29, 'Normal Indent.' - ), - EnumMember( - 'NORMAL_OBJECT', -158, 'Normal (applied to an object).' - ), - EnumMember( - 'NORMAL_TABLE', -106, 'Normal (applied within a table).' - ), - EnumMember( - 'NOTE_HEADING', -80, 'Note Heading.' - ), - EnumMember( - 'PAGE_NUMBER', -42, 'Page Number.' - ), - EnumMember( - 'PLAIN_TEXT', -91, 'Plain Text.' - ), - EnumMember( - 'QUOTE', -181, 'Quote.' - ), - EnumMember( - 'SALUTATION', -76, 'Salutation.' - ), - EnumMember( - 'SIGNATURE', -65, 'Signature.' - ), - EnumMember( - 'STRONG', -88, 'Strong.' - ), - EnumMember( - 'SUBTITLE', -75, 'Subtitle.' - ), - EnumMember( - 'SUBTLE_EMPHASIS', -261, 'Subtle Emphasis.' - ), - EnumMember( - 'SUBTLE_REFERENCE', -263, 'Subtle Reference.' - ), - EnumMember( - 'TABLE_COLORFUL_GRID', -172, 'Colorful Grid.' - ), - EnumMember( - 'TABLE_COLORFUL_LIST', -171, 'Colorful List.' - ), - EnumMember( - 'TABLE_COLORFUL_SHADING', -170, 'Colorful Shading.' - ), - EnumMember( - 'TABLE_DARK_LIST', -169, 'Dark List.' - ), - EnumMember( - 'TABLE_LIGHT_GRID', -161, 'Light Grid.' - ), - EnumMember( - 'TABLE_LIGHT_GRID_ACCENT_1', -175, 'Light Grid Accent 1.' - ), - EnumMember( - 'TABLE_LIGHT_LIST', -160, 'Light List.' - ), - EnumMember( - 'TABLE_LIGHT_LIST_ACCENT_1', -174, 'Light List Accent 1.' - ), - EnumMember( - 'TABLE_LIGHT_SHADING', -159, 'Light Shading.' - ), - EnumMember( - 'TABLE_LIGHT_SHADING_ACCENT_1', -173, 'Light Shading Accent 1.' - ), - EnumMember( - 'TABLE_MEDIUM_GRID_1', -166, 'Medium Grid 1.' - ), - EnumMember( - 'TABLE_MEDIUM_GRID_2', -167, 'Medium Grid 2.' - ), - EnumMember( - 'TABLE_MEDIUM_GRID_3', -168, 'Medium Grid 3.' - ), - EnumMember( - 'TABLE_MEDIUM_LIST_1', -164, 'Medium List 1.' - ), - EnumMember( - 'TABLE_MEDIUM_LIST_1_ACCENT_1', -178, 'Medium List 1 Accent 1.' - ), - EnumMember( - 'TABLE_MEDIUM_LIST_2', -165, 'Medium List 2.' - ), - EnumMember( - 'TABLE_MEDIUM_SHADING_1', -162, 'Medium Shading 1.' - ), - EnumMember( - 'TABLE_MEDIUM_SHADING_1_ACCENT_1', -176, - 'Medium Shading 1 Accent 1.' - ), - EnumMember( - 'TABLE_MEDIUM_SHADING_2', -163, 'Medium Shading 2.' - ), - EnumMember( - 'TABLE_MEDIUM_SHADING_2_ACCENT_1', -177, - 'Medium Shading 2 Accent 1.' - ), - EnumMember( - 'TABLE_OF_AUTHORITIES', -45, 'Table of Authorities.' - ), - EnumMember( - 'TABLE_OF_FIGURES', -36, 'Table of Figures.' - ), - EnumMember( - 'TITLE', -63, 'Title.' - ), - EnumMember( - 'TOAHEADING', -47, 'TOA Heading.' - ), - EnumMember( - 'TOC_1', -20, 'TOC 1.' - ), - EnumMember( - 'TOC_2', -21, 'TOC 2.' - ), - EnumMember( - 'TOC_3', -22, 'TOC 3.' - ), - EnumMember( - 'TOC_4', -23, 'TOC 4.' - ), - EnumMember( - 'TOC_5', -24, 'TOC 5.' - ), - EnumMember( - 'TOC_6', -25, 'TOC 6.' - ), - EnumMember( - 'TOC_7', -26, 'TOC 7.' - ), - EnumMember( - 'TOC_8', -27, 'TOC 8.' - ), - EnumMember( - 'TOC_9', -28, 'TOC 9.' - ), - ) - - -class WD_STYLE_TYPE(XmlEnumeration): - """ - Specifies one of the four style types: paragraph, character, list, or - table. - - Example:: - - from docx import Document - from docx.enum.style import WD_STYLE_TYPE - - styles = Document().styles - assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH - """ - - __ms_name__ = 'WdStyleType' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff196870.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'CHARACTER', 2, 'character', 'Character style.' - ), - XmlMappedEnumMember( - 'LIST', 4, 'numbering', 'List style.' - ), - XmlMappedEnumMember( - 'PARAGRAPH', 1, 'paragraph', 'Paragraph style.' - ), - XmlMappedEnumMember( - 'TABLE', 3, 'table', 'Table style.' - ), - ) diff --git a/docx/enum/table.py b/docx/enum/table.py deleted file mode 100644 index eedab082e..000000000 --- a/docx/enum/table.py +++ /dev/null @@ -1,146 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations related to tables in WordprocessingML files -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from .base import ( - alias, Enumeration, EnumMember, XmlEnumeration, XmlMappedEnumMember -) - - -@alias('WD_ALIGN_VERTICAL') -class WD_CELL_VERTICAL_ALIGNMENT(XmlEnumeration): - """ - alias: **WD_ALIGN_VERTICAL** - - Specifies the vertical alignment of text in one or more cells of a table. - - Example:: - - from docx.enum.table import WD_ALIGN_VERTICAL - - table = document.add_table(3, 3) - table.cell(0, 0).vertical_alignment = WD_ALIGN_VERTICAL.BOTTOM - """ - - __ms_name__ = 'WdCellVerticalAlignment' - - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff193345.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'TOP', 0, 'top', 'Text is aligned to the top border of the cell.' - ), - XmlMappedEnumMember( - 'CENTER', 1, 'center', 'Text is aligned to the center of the cel' - 'l.' - ), - XmlMappedEnumMember( - 'BOTTOM', 3, 'bottom', 'Text is aligned to the bottom border of ' - 'the cell.' - ), - XmlMappedEnumMember( - 'BOTH', 101, 'both', 'This is an option in the OpenXml spec, but' - ' not in Word itself. It\'s not clear what Word behavior this se' - 'tting produces. If you find out please let us know and we\'ll u' - 'pdate this documentation. Otherwise, probably best to avoid thi' - 's option.' - ), - ) - - -@alias('WD_ROW_HEIGHT') -class WD_ROW_HEIGHT_RULE(XmlEnumeration): - """ - alias: **WD_ROW_HEIGHT** - - Specifies the rule for determining the height of a table row - - Example:: - - from docx.enum.table import WD_ROW_HEIGHT_RULE - - table = document.add_table(3, 3) - table.rows[0].height_rule = WD_ROW_HEIGHT_RULE.EXACTLY - """ - - __ms_name__ = "WdRowHeightRule" - - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff193620.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'AUTO', 0, 'auto', 'The row height is adjusted to accommodate th' - 'e tallest value in the row.' - ), - XmlMappedEnumMember( - 'AT_LEAST', 1, 'atLeast', 'The row height is at least a minimum ' - 'specified value.' - ), - XmlMappedEnumMember( - 'EXACTLY', 2, 'exact', 'The row height is an exact value.' - ), - ) - - -class WD_TABLE_ALIGNMENT(XmlEnumeration): - """ - Specifies table justification type. - - Example:: - - from docx.enum.table import WD_TABLE_ALIGNMENT - - table = document.add_table(3, 3) - table.alignment = WD_TABLE_ALIGNMENT.CENTER - """ - - __ms_name__ = 'WdRowAlignment' - - __url__ = ' http://office.microsoft.com/en-us/word-help/HV080607259.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'LEFT', 0, 'left', 'Left-aligned' - ), - XmlMappedEnumMember( - 'CENTER', 1, 'center', 'Center-aligned.' - ), - XmlMappedEnumMember( - 'RIGHT', 2, 'right', 'Right-aligned.' - ), - ) - - -class WD_TABLE_DIRECTION(Enumeration): - """ - Specifies the direction in which an application orders cells in the - specified table or row. - - Example:: - - from docx.enum.table import WD_TABLE_DIRECTION - - table = document.add_table(3, 3) - table.direction = WD_TABLE_DIRECTION.RTL - """ - - __ms_name__ = 'WdTableDirection' - - __url__ = ' http://msdn.microsoft.com/en-us/library/ff835141.aspx' - - __members__ = ( - EnumMember( - 'LTR', 0, 'The table or row is arranged with the first column ' - 'in the leftmost position.' - ), - EnumMember( - 'RTL', 1, 'The table or row is arranged with the first column ' - 'in the rightmost position.' - ), - ) diff --git a/docx/enum/text.py b/docx/enum/text.py deleted file mode 100644 index 67f6a66af..000000000 --- a/docx/enum/text.py +++ /dev/null @@ -1,352 +0,0 @@ -# encoding: utf-8 - -""" -Enumerations related to text in WordprocessingML files -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from .base import alias, EnumMember, XmlEnumeration, XmlMappedEnumMember - - -@alias('WD_ALIGN_PARAGRAPH') -class WD_PARAGRAPH_ALIGNMENT(XmlEnumeration): - """ - alias: **WD_ALIGN_PARAGRAPH** - - Specifies paragraph justification type. - - Example:: - - from docx.enum.text import WD_ALIGN_PARAGRAPH - - paragraph = document.add_paragraph() - paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER - """ - - __ms_name__ = 'WdParagraphAlignment' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff835817.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'LEFT', 0, 'left', 'Left-aligned' - ), - XmlMappedEnumMember( - 'CENTER', 1, 'center', 'Center-aligned.' - ), - XmlMappedEnumMember( - 'RIGHT', 2, 'right', 'Right-aligned.' - ), - XmlMappedEnumMember( - 'JUSTIFY', 3, 'both', 'Fully justified.' - ), - XmlMappedEnumMember( - 'DISTRIBUTE', 4, 'distribute', 'Paragraph characters are distrib' - 'uted to fill the entire width of the paragraph.' - ), - XmlMappedEnumMember( - 'JUSTIFY_MED', 5, 'mediumKashida', 'Justified with a medium char' - 'acter compression ratio.' - ), - XmlMappedEnumMember( - 'JUSTIFY_HI', 7, 'highKashida', 'Justified with a high character' - ' compression ratio.' - ), - XmlMappedEnumMember( - 'JUSTIFY_LOW', 8, 'lowKashida', 'Justified with a low character ' - 'compression ratio.' - ), - XmlMappedEnumMember( - 'THAI_JUSTIFY', 9, 'thaiDistribute', 'Justified according to Tha' - 'i formatting layout.' - ), - ) - - -class WD_BREAK_TYPE(object): - """ - Corresponds to WdBreakType enumeration - http://msdn.microsoft.com/en-us/library/office/ff195905.aspx - """ - COLUMN = 8 - LINE = 6 - LINE_CLEAR_LEFT = 9 - LINE_CLEAR_RIGHT = 10 - LINE_CLEAR_ALL = 11 # added for consistency, not in MS version - PAGE = 7 - SECTION_CONTINUOUS = 3 - SECTION_EVEN_PAGE = 4 - SECTION_NEXT_PAGE = 2 - SECTION_ODD_PAGE = 5 - TEXT_WRAPPING = 11 - - -WD_BREAK = WD_BREAK_TYPE - - -@alias('WD_COLOR') -class WD_COLOR_INDEX(XmlEnumeration): - """ - Specifies a standard preset color to apply. Used for font highlighting and - perhaps other applications. - """ - - __ms_name__ = 'WdColorIndex' - - __url__ = 'https://msdn.microsoft.com/EN-US/library/office/ff195343.aspx' - - __members__ = ( - XmlMappedEnumMember( - None, None, None, 'Color is inherited from the style hierarchy.' - ), - XmlMappedEnumMember( - 'AUTO', 0, 'default', 'Automatic color. Default; usually black.' - ), - XmlMappedEnumMember( - 'BLACK', 1, 'black', 'Black color.' - ), - XmlMappedEnumMember( - 'BLUE', 2, 'blue', 'Blue color' - ), - XmlMappedEnumMember( - 'BRIGHT_GREEN', 4, 'green', 'Bright green color.' - ), - XmlMappedEnumMember( - 'DARK_BLUE', 9, 'darkBlue', 'Dark blue color.' - ), - XmlMappedEnumMember( - 'DARK_RED', 13, 'darkRed', 'Dark red color.' - ), - XmlMappedEnumMember( - 'DARK_YELLOW', 14, 'darkYellow', 'Dark yellow color.' - ), - XmlMappedEnumMember( - 'GRAY_25', 16, 'lightGray', '25% shade of gray color.' - ), - XmlMappedEnumMember( - 'GRAY_50', 15, 'darkGray', '50% shade of gray color.' - ), - XmlMappedEnumMember( - 'GREEN', 11, 'darkGreen', 'Green color.' - ), - XmlMappedEnumMember( - 'PINK', 5, 'magenta', 'Pink color.' - ), - XmlMappedEnumMember( - 'RED', 6, 'red', 'Red color.' - ), - XmlMappedEnumMember( - 'TEAL', 10, 'darkCyan', 'Teal color.' - ), - XmlMappedEnumMember( - 'TURQUOISE', 3, 'cyan', 'Turquoise color.' - ), - XmlMappedEnumMember( - 'VIOLET', 12, 'darkMagenta', 'Violet color.' - ), - XmlMappedEnumMember( - 'WHITE', 8, 'white', 'White color.' - ), - XmlMappedEnumMember( - 'YELLOW', 7, 'yellow', 'Yellow color.' - ), - ) - - -class WD_LINE_SPACING(XmlEnumeration): - """ - Specifies a line spacing format to be applied to a paragraph. - - Example:: - - from docx.enum.text import WD_LINE_SPACING - - paragraph = document.add_paragraph() - paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY - """ - - __ms_name__ = 'WdLineSpacing' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff844910.aspx' - - __members__ = ( - EnumMember( - 'ONE_POINT_FIVE', 1, 'Space-and-a-half line spacing.' - ), - XmlMappedEnumMember( - 'AT_LEAST', 3, 'atLeast', 'Line spacing is always at least the s' - 'pecified amount. The amount is specified separately.' - ), - EnumMember( - 'DOUBLE', 2, 'Double spaced.' - ), - XmlMappedEnumMember( - 'EXACTLY', 4, 'exact', 'Line spacing is exactly the specified am' - 'ount. The amount is specified separately.' - ), - XmlMappedEnumMember( - 'MULTIPLE', 5, 'auto', 'Line spacing is specified as a multiple ' - 'of line heights. Changing the font size will change the line sp' - 'acing proportionately.' - ), - EnumMember( - 'SINGLE', 0, 'Single spaced (default).' - ), - ) - - -class WD_TAB_ALIGNMENT(XmlEnumeration): - """ - Specifies the tab stop alignment to apply. - """ - - __ms_name__ = 'WdTabAlignment' - - __url__ = 'https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'LEFT', 0, 'left', 'Left-aligned.' - ), - XmlMappedEnumMember( - 'CENTER', 1, 'center', 'Center-aligned.' - ), - XmlMappedEnumMember( - 'RIGHT', 2, 'right', 'Right-aligned.' - ), - XmlMappedEnumMember( - 'DECIMAL', 3, 'decimal', 'Decimal-aligned.' - ), - XmlMappedEnumMember( - 'BAR', 4, 'bar', 'Bar-aligned.' - ), - XmlMappedEnumMember( - 'LIST', 6, 'list', 'List-aligned. (deprecated)' - ), - XmlMappedEnumMember( - 'CLEAR', 101, 'clear', 'Clear an inherited tab stop.' - ), - XmlMappedEnumMember( - 'END', 102, 'end', 'Right-aligned. (deprecated)' - ), - XmlMappedEnumMember( - 'NUM', 103, 'num', 'Left-aligned. (deprecated)' - ), - XmlMappedEnumMember( - 'START', 104, 'start', 'Left-aligned. (deprecated)' - ), - ) - - -class WD_TAB_LEADER(XmlEnumeration): - """ - Specifies the character to use as the leader with formatted tabs. - """ - - __ms_name__ = 'WdTabLeader' - - __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff845050.aspx' - - __members__ = ( - XmlMappedEnumMember( - 'SPACES', 0, 'none', 'Spaces. Default.' - ), - XmlMappedEnumMember( - 'DOTS', 1, 'dot', 'Dots.' - ), - XmlMappedEnumMember( - 'DASHES', 2, 'hyphen', 'Dashes.' - ), - XmlMappedEnumMember( - 'LINES', 3, 'underscore', 'Double lines.' - ), - XmlMappedEnumMember( - 'HEAVY', 4, 'heavy', 'A heavy line.' - ), - XmlMappedEnumMember( - 'MIDDLE_DOT', 5, 'middleDot', 'A vertically-centered dot.' - ), - ) - - -class WD_UNDERLINE(XmlEnumeration): - """ - Specifies the style of underline applied to a run of characters. - """ - - __ms_name__ = 'WdUnderline' - - __url__ = 'http://msdn.microsoft.com/en-us/library/office/ff822388.aspx' - - __members__ = ( - XmlMappedEnumMember( - None, None, None, 'Inherit underline setting from containing par' - 'agraph.' - ), - XmlMappedEnumMember( - 'NONE', 0, 'none', 'No underline. This setting overrides any inh' - 'erited underline value, so can be used to remove underline from' - ' a run that inherits underlining from its containing paragraph.' - ' Note this is not the same as assigning |None| to Run.underline' - '. |None| is a valid assignment value, but causes the run to inh' - 'erit its underline value. Assigning ``WD_UNDERLINE.NONE`` cause' - 's underlining to be unconditionally turned off.' - ), - XmlMappedEnumMember( - 'SINGLE', 1, 'single', 'A single line. Note that this setting is' - 'write-only in the sense that |True| (rather than ``WD_UNDERLINE' - '.SINGLE``) is returned for a run having this setting.' - ), - XmlMappedEnumMember( - 'WORDS', 2, 'words', 'Underline individual words only.' - ), - XmlMappedEnumMember( - 'DOUBLE', 3, 'double', 'A double line.' - ), - XmlMappedEnumMember( - 'DOTTED', 4, 'dotted', 'Dots.' - ), - XmlMappedEnumMember( - 'THICK', 6, 'thick', 'A single thick line.' - ), - XmlMappedEnumMember( - 'DASH', 7, 'dash', 'Dashes.' - ), - XmlMappedEnumMember( - 'DOT_DASH', 9, 'dotDash', 'Alternating dots and dashes.' - ), - XmlMappedEnumMember( - 'DOT_DOT_DASH', 10, 'dotDotDash', 'An alternating dot-dot-dash p' - 'attern.' - ), - XmlMappedEnumMember( - 'WAVY', 11, 'wave', 'A single wavy line.' - ), - XmlMappedEnumMember( - 'DOTTED_HEAVY', 20, 'dottedHeavy', 'Heavy dots.' - ), - XmlMappedEnumMember( - 'DASH_HEAVY', 23, 'dashedHeavy', 'Heavy dashes.' - ), - XmlMappedEnumMember( - 'DOT_DASH_HEAVY', 25, 'dashDotHeavy', 'Alternating heavy dots an' - 'd heavy dashes.' - ), - XmlMappedEnumMember( - 'DOT_DOT_DASH_HEAVY', 26, 'dashDotDotHeavy', 'An alternating hea' - 'vy dot-dot-dash pattern.' - ), - XmlMappedEnumMember( - 'WAVY_HEAVY', 27, 'wavyHeavy', 'A heavy wavy line.' - ), - XmlMappedEnumMember( - 'DASH_LONG', 39, 'dashLong', 'Long dashes.' - ), - XmlMappedEnumMember( - 'WAVY_DOUBLE', 43, 'wavyDouble', 'A double wavy line.' - ), - XmlMappedEnumMember( - 'DASH_LONG_HEAVY', 55, 'dashLongHeavy', 'Long heavy dashes.' - ), - ) diff --git a/docx/exceptions.py b/docx/exceptions.py deleted file mode 100644 index 7a8b99c81..000000000 --- a/docx/exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -# encoding: utf-8 - -""" -Exceptions used with python-docx. - -The base exception class is PythonDocxError. -""" - - -class PythonDocxError(Exception): - """ - Generic error class. - """ - - -class InvalidSpanError(PythonDocxError): - """ - Raised when an invalid merge region is specified in a request to merge - table cells. - """ - - -class InvalidXmlError(PythonDocxError): - """ - Raised when invalid XML is encountered, such as on attempt to access a - missing required child element - """ diff --git a/docx/image/__init__.py b/docx/image/__init__.py deleted file mode 100644 index 8ab3ada68..000000000 --- a/docx/image/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: utf-8 - -""" -Provides objects that can characterize image streams as to content type and -size, as a required step in including them in a document. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from docx.image.bmp import Bmp -from docx.image.gif import Gif -from docx.image.jpeg import Exif, Jfif -from docx.image.png import Png -from docx.image.tiff import Tiff - - -SIGNATURES = ( - # class, offset, signature_bytes - (Png, 0, b'\x89PNG\x0D\x0A\x1A\x0A'), - (Jfif, 6, b'JFIF'), - (Exif, 6, b'Exif'), - (Gif, 0, b'GIF87a'), - (Gif, 0, b'GIF89a'), - (Tiff, 0, b'MM\x00*'), # big-endian (Motorola) TIFF - (Tiff, 0, b'II*\x00'), # little-endian (Intel) TIFF - (Bmp, 0, b'BM'), -) diff --git a/docx/image/constants.py b/docx/image/constants.py deleted file mode 100644 index 90b469705..000000000 --- a/docx/image/constants.py +++ /dev/null @@ -1,169 +0,0 @@ -# encoding: utf-8 - -""" -Constants specific the the image sub-package -""" - - -class JPEG_MARKER_CODE(object): - """ - JPEG marker codes - """ - TEM = b'\x01' - DHT = b'\xC4' - DAC = b'\xCC' - JPG = b'\xC8' - - SOF0 = b'\xC0' - SOF1 = b'\xC1' - SOF2 = b'\xC2' - SOF3 = b'\xC3' - SOF5 = b'\xC5' - SOF6 = b'\xC6' - SOF7 = b'\xC7' - SOF9 = b'\xC9' - SOFA = b'\xCA' - SOFB = b'\xCB' - SOFD = b'\xCD' - SOFE = b'\xCE' - SOFF = b'\xCF' - - RST0 = b'\xD0' - RST1 = b'\xD1' - RST2 = b'\xD2' - RST3 = b'\xD3' - RST4 = b'\xD4' - RST5 = b'\xD5' - RST6 = b'\xD6' - RST7 = b'\xD7' - - SOI = b'\xD8' - EOI = b'\xD9' - SOS = b'\xDA' - DQT = b'\xDB' # Define Quantization Table(s) - DNL = b'\xDC' - DRI = b'\xDD' - DHP = b'\xDE' - EXP = b'\xDF' - - APP0 = b'\xE0' - APP1 = b'\xE1' - APP2 = b'\xE2' - APP3 = b'\xE3' - APP4 = b'\xE4' - APP5 = b'\xE5' - APP6 = b'\xE6' - APP7 = b'\xE7' - APP8 = b'\xE8' - APP9 = b'\xE9' - APPA = b'\xEA' - APPB = b'\xEB' - APPC = b'\xEC' - APPD = b'\xED' - APPE = b'\xEE' - APPF = b'\xEF' - - STANDALONE_MARKERS = ( - TEM, SOI, EOI, RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7 - ) - - SOF_MARKER_CODES = ( - SOF0, SOF1, SOF2, SOF3, SOF5, SOF6, SOF7, SOF9, SOFA, SOFB, SOFD, - SOFE, SOFF - ) - - marker_names = { - b'\x00': 'UNKNOWN', - b'\xC0': 'SOF0', - b'\xC2': 'SOF2', - b'\xC4': 'DHT', - b'\xDA': 'SOS', # start of scan - b'\xD8': 'SOI', # start of image - b'\xD9': 'EOI', # end of image - b'\xDB': 'DQT', - b'\xE0': 'APP0', - b'\xE1': 'APP1', - b'\xE2': 'APP2', - b'\xED': 'APP13', - b'\xEE': 'APP14', - } - - @classmethod - def is_standalone(cls, marker_code): - return marker_code in cls.STANDALONE_MARKERS - - -class MIME_TYPE(object): - """ - Image content types - """ - BMP = 'image/bmp' - GIF = 'image/gif' - JPEG = 'image/jpeg' - PNG = 'image/png' - TIFF = 'image/tiff' - - -class PNG_CHUNK_TYPE(object): - """ - PNG chunk type names - """ - IHDR = 'IHDR' - pHYs = 'pHYs' - IEND = 'IEND' - - -class TIFF_FLD_TYPE(object): - """ - Tag codes for TIFF Image File Directory (IFD) entries. - """ - BYTE = 1 - ASCII = 2 - SHORT = 3 - LONG = 4 - RATIONAL = 5 - - field_type_names = { - 1: 'BYTE', 2: 'ASCII char', 3: 'SHORT', 4: 'LONG', - 5: 'RATIONAL' - } - - -TIFF_FLD = TIFF_FLD_TYPE - - -class TIFF_TAG(object): - """ - Tag codes for TIFF Image File Directory (IFD) entries. - """ - IMAGE_WIDTH = 0x0100 - IMAGE_LENGTH = 0x0101 - X_RESOLUTION = 0x011A - Y_RESOLUTION = 0x011B - RESOLUTION_UNIT = 0x0128 - - tag_names = { - 0x00FE: 'NewSubfileType', - 0x0100: 'ImageWidth', - 0x0101: 'ImageLength', - 0x0102: 'BitsPerSample', - 0x0103: 'Compression', - 0x0106: 'PhotometricInterpretation', - 0x010E: 'ImageDescription', - 0x010F: 'Make', - 0x0110: 'Model', - 0x0111: 'StripOffsets', - 0x0112: 'Orientation', - 0x0115: 'SamplesPerPixel', - 0x0117: 'StripByteCounts', - 0x011A: 'XResolution', - 0x011B: 'YResolution', - 0x011C: 'PlanarConfiguration', - 0x0128: 'ResolutionUnit', - 0x0131: 'Software', - 0x0132: 'DateTime', - 0x0213: 'YCbCrPositioning', - 0x8769: 'ExifTag', - 0x8825: 'GPS IFD', - 0xC4A5: 'PrintImageMatching', - } diff --git a/docx/image/exceptions.py b/docx/image/exceptions.py deleted file mode 100644 index f233edc4e..000000000 --- a/docx/image/exceptions.py +++ /dev/null @@ -1,23 +0,0 @@ -# encoding: utf-8 - -""" -Exceptions specific the the image sub-package -""" - - -class InvalidImageStreamError(Exception): - """ - The recognized image stream appears to be corrupted - """ - - -class UnexpectedEndOfFileError(Exception): - """ - EOF was unexpectedly encountered while reading an image stream. - """ - - -class UnrecognizedImageError(Exception): - """ - The provided image stream could not be recognized. - """ diff --git a/docx/image/gif.py b/docx/image/gif.py deleted file mode 100644 index 57f037d80..000000000 --- a/docx/image/gif.py +++ /dev/null @@ -1,47 +0,0 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - -from struct import Struct - -from .constants import MIME_TYPE -from .image import BaseImageHeader - - -class Gif(BaseImageHeader): - """ - Image header parser for GIF images. Note that the GIF format does not - support resolution (DPI) information. Both horizontal and vertical DPI - default to 72. - """ - @classmethod - def from_stream(cls, stream): - """ - Return |Gif| instance having header properties parsed from GIF image - in *stream*. - """ - px_width, px_height = cls._dimensions_from_stream(stream) - return cls(px_width, px_height, 72, 72) - - @property - def content_type(self): - """ - MIME content type for this image, unconditionally `image/gif` for - GIF images. - """ - return MIME_TYPE.GIF - - @property - def default_ext(self): - """ - Default filename extension, always 'gif' for GIF images. - """ - return 'gif' - - @classmethod - def _dimensions_from_stream(cls, stream): - stream.seek(6) - bytes_ = stream.read(4) - struct = Struct('L' - return self._read_int(fmt, base, offset) - - def read_short(self, base, offset=0): - """ - Return the int value of the two bytes at the file position determined - by *base* and *offset*, similarly to ``read_long()`` above. - """ - fmt = b'H' - return self._read_int(fmt, base, offset) - - def read_str(self, char_count, base, offset=0): - """ - Return a string containing the *char_count* bytes at the file - position determined by self._base_offset + *base* + *offset*. - """ - def str_struct(char_count): - format_ = '%ds' % char_count - return Struct(format_) - struct = str_struct(char_count) - chars = self._unpack_item(struct, base, offset) - unicode_str = chars.decode('UTF-8') - return unicode_str - - def seek(self, base, offset=0): - location = self._base_offset + base + offset - self._stream.seek(location) - - def tell(self): - """ - Allow pass-through tell() call - """ - return self._stream.tell() - - def _read_bytes(self, byte_count, base, offset): - self.seek(base, offset) - bytes_ = self._stream.read(byte_count) - if len(bytes_) < byte_count: - raise UnexpectedEndOfFileError - return bytes_ - - def _read_int(self, fmt, base, offset): - struct = Struct(fmt) - return self._unpack_item(struct, base, offset) - - def _unpack_item(self, struct, base, offset): - bytes_ = self._read_bytes(struct.size, base, offset) - return struct.unpack(bytes_)[0] diff --git a/docx/image/image.py b/docx/image/image.py deleted file mode 100644 index ba2158e72..000000000 --- a/docx/image/image.py +++ /dev/null @@ -1,263 +0,0 @@ -# encoding: utf-8 - -""" -Provides objects that can characterize image streams as to content type and -size, as a required step in including them in a document. -""" - -from __future__ import absolute_import, division, print_function - -import hashlib -import os - -from ..compat import BytesIO, is_string -from .exceptions import UnrecognizedImageError -from ..shared import Emu, Inches, lazyproperty - - -class Image(object): - """ - Graphical image stream such as JPEG, PNG, or GIF with properties and - methods required by ImagePart. - """ - def __init__(self, blob, filename, image_header): - super(Image, self).__init__() - self._blob = blob - self._filename = filename - self._image_header = image_header - - @classmethod - def from_blob(cls, blob): - """ - Return a new |Image| subclass instance parsed from the image binary - contained in *blob*. - """ - stream = BytesIO(blob) - return cls._from_stream(stream, blob) - - @classmethod - def from_file(cls, image_descriptor): - """ - Return a new |Image| subclass instance loaded from the image file - identified by *image_descriptor*, a path or file-like object. - """ - if is_string(image_descriptor): - path = image_descriptor - with open(path, 'rb') as f: - blob = f.read() - stream = BytesIO(blob) - filename = os.path.basename(path) - else: - stream = image_descriptor - stream.seek(0) - blob = stream.read() - filename = None - return cls._from_stream(stream, blob, filename) - - @property - def blob(self): - """ - The bytes of the image 'file' - """ - return self._blob - - @property - def content_type(self): - """ - MIME content type for this image, e.g. ``'image/jpeg'`` for a JPEG - image - """ - return self._image_header.content_type - - @lazyproperty - def ext(self): - """ - The file extension for the image. If an actual one is available from - a load filename it is used. Otherwise a canonical extension is - assigned based on the content type. Does not contain the leading - period, e.g. 'jpg', not '.jpg'. - """ - return os.path.splitext(self._filename)[1][1:] - - @property - def filename(self): - """ - Original image file name, if loaded from disk, or a generic filename - if loaded from an anonymous stream. - """ - return self._filename - - @property - def px_width(self): - """ - The horizontal pixel dimension of the image - """ - return self._image_header.px_width - - @property - def px_height(self): - """ - The vertical pixel dimension of the image - """ - return self._image_header.px_height - - @property - def horz_dpi(self): - """ - Integer dots per inch for the width of this image. Defaults to 72 - when not present in the file, as is often the case. - """ - return self._image_header.horz_dpi - - @property - def vert_dpi(self): - """ - Integer dots per inch for the height of this image. Defaults to 72 - when not present in the file, as is often the case. - """ - return self._image_header.vert_dpi - - @property - def width(self): - """ - A |Length| value representing the native width of the image, - calculated from the values of `px_width` and `horz_dpi`. - """ - return Inches(self.px_width / self.horz_dpi) - - @property - def height(self): - """ - A |Length| value representing the native height of the image, - calculated from the values of `px_height` and `vert_dpi`. - """ - return Inches(self.px_height / self.vert_dpi) - - def scaled_dimensions(self, width=None, height=None): - """ - Return a (cx, cy) 2-tuple representing the native dimensions of this - image scaled by applying the following rules to *width* and *height*. - If both *width* and *height* are specified, the return value is - (*width*, *height*); no scaling is performed. If only one is - specified, it is used to compute a scaling factor that is then - applied to the unspecified dimension, preserving the aspect ratio of - the image. If both *width* and *height* are |None|, the native - dimensions are returned. The native dimensions are calculated using - the dots-per-inch (dpi) value embedded in the image, defaulting to 72 - dpi if no value is specified, as is often the case. The returned - values are both |Length| objects. - """ - if width is None and height is None: - return self.width, self.height - - if width is None: - scaling_factor = float(height) / float(self.height) - width = round(self.width * scaling_factor) - - if height is None: - scaling_factor = float(width) / float(self.width) - height = round(self.height * scaling_factor) - - return Emu(width), Emu(height) - - @lazyproperty - def sha1(self): - """ - SHA1 hash digest of the image blob - """ - return hashlib.sha1(self._blob).hexdigest() - - @classmethod - def _from_stream(cls, stream, blob, filename=None): - """ - Return an instance of the |Image| subclass corresponding to the - format of the image in *stream*. - """ - image_header = _ImageHeaderFactory(stream) - if filename is None: - filename = 'image.%s' % image_header.default_ext - return cls(blob, filename, image_header) - - -def _ImageHeaderFactory(stream): - """ - Return a |BaseImageHeader| subclass instance that knows how to parse the - headers of the image in *stream*. - """ - from docx.image import SIGNATURES - - def read_32(stream): - stream.seek(0) - return stream.read(32) - - header = read_32(stream) - for cls, offset, signature_bytes in SIGNATURES: - end = offset + len(signature_bytes) - found_bytes = header[offset:end] - if found_bytes == signature_bytes: - return cls.from_stream(stream) - raise UnrecognizedImageError - - -class BaseImageHeader(object): - """ - Base class for image header subclasses like |Jpeg| and |Tiff|. - """ - def __init__(self, px_width, px_height, horz_dpi, vert_dpi): - self._px_width = px_width - self._px_height = px_height - self._horz_dpi = horz_dpi - self._vert_dpi = vert_dpi - - @property - def content_type(self): - """ - Abstract property definition, must be implemented by all subclasses. - """ - msg = ( - 'content_type property must be implemented by all subclasses of ' - 'BaseImageHeader' - ) - raise NotImplementedError(msg) - - @property - def default_ext(self): - """ - Default filename extension for images of this type. An abstract - property definition, must be implemented by all subclasses. - """ - msg = ( - 'default_ext property must be implemented by all subclasses of ' - 'BaseImageHeader' - ) - raise NotImplementedError(msg) - - @property - def px_width(self): - """ - The horizontal pixel dimension of the image - """ - return self._px_width - - @property - def px_height(self): - """ - The vertical pixel dimension of the image - """ - return self._px_height - - @property - def horz_dpi(self): - """ - Integer dots per inch for the width of this image. Defaults to 72 - when not present in the file, as is often the case. - """ - return self._horz_dpi - - @property - def vert_dpi(self): - """ - Integer dots per inch for the height of this image. Defaults to 72 - when not present in the file, as is often the case. - """ - return self._vert_dpi diff --git a/docx/image/tiff.py b/docx/image/tiff.py deleted file mode 100644 index c38242360..000000000 --- a/docx/image/tiff.py +++ /dev/null @@ -1,345 +0,0 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - -from .constants import MIME_TYPE, TIFF_FLD, TIFF_TAG -from .helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader -from .image import BaseImageHeader - - -class Tiff(BaseImageHeader): - """ - Image header parser for TIFF images. Handles both big and little endian - byte ordering. - """ - @property - def content_type(self): - """ - Return the MIME type of this TIFF image, unconditionally the string - ``image/tiff``. - """ - return MIME_TYPE.TIFF - - @property - def default_ext(self): - """ - Default filename extension, always 'tiff' for TIFF images. - """ - return 'tiff' - - @classmethod - def from_stream(cls, stream): - """ - Return a |Tiff| instance containing the properties of the TIFF image - in *stream*. - """ - parser = _TiffParser.parse(stream) - - px_width = parser.px_width - px_height = parser.px_height - horz_dpi = parser.horz_dpi - vert_dpi = parser.vert_dpi - - return cls(px_width, px_height, horz_dpi, vert_dpi) - - -class _TiffParser(object): - """ - Parses a TIFF image stream to extract the image properties found in its - main image file directory (IFD) - """ - def __init__(self, ifd_entries): - super(_TiffParser, self).__init__() - self._ifd_entries = ifd_entries - - @classmethod - def parse(cls, stream): - """ - Return an instance of |_TiffParser| containing the properties parsed - from the TIFF image in *stream*. - """ - stream_rdr = cls._make_stream_reader(stream) - ifd0_offset = stream_rdr.read_long(4) - ifd_entries = _IfdEntries.from_stream(stream_rdr, ifd0_offset) - return cls(ifd_entries) - - @property - def horz_dpi(self): - """ - The horizontal dots per inch value calculated from the XResolution - and ResolutionUnit tags of the IFD; defaults to 72 if those tags are - not present. - """ - return self._dpi(TIFF_TAG.X_RESOLUTION) - - @property - def vert_dpi(self): - """ - The vertical dots per inch value calculated from the XResolution and - ResolutionUnit tags of the IFD; defaults to 72 if those tags are not - present. - """ - return self._dpi(TIFF_TAG.Y_RESOLUTION) - - @property - def px_height(self): - """ - The number of stacked rows of pixels in the image, |None| if the IFD - contains no ``ImageLength`` tag, the expected case when the TIFF is - embeded in an Exif image. - """ - return self._ifd_entries.get(TIFF_TAG.IMAGE_LENGTH) - - @property - def px_width(self): - """ - The number of pixels in each row in the image, |None| if the IFD - contains no ``ImageWidth`` tag, the expected case when the TIFF is - embeded in an Exif image. - """ - return self._ifd_entries.get(TIFF_TAG.IMAGE_WIDTH) - - @classmethod - def _detect_endian(cls, stream): - """ - Return either BIG_ENDIAN or LITTLE_ENDIAN depending on the endian - indicator found in the TIFF *stream* header, either 'MM' or 'II'. - """ - stream.seek(0) - endian_str = stream.read(2) - return BIG_ENDIAN if endian_str == b'MM' else LITTLE_ENDIAN - - def _dpi(self, resolution_tag): - """ - Return the dpi value calculated for *resolution_tag*, which can be - either TIFF_TAG.X_RESOLUTION or TIFF_TAG.Y_RESOLUTION. The - calculation is based on the values of both that tag and the - TIFF_TAG.RESOLUTION_UNIT tag in this parser's |_IfdEntries| instance. - """ - ifd_entries = self._ifd_entries - - if resolution_tag not in ifd_entries: - return 72 - - # resolution unit defaults to inches (2) - resolution_unit = ( - ifd_entries[TIFF_TAG.RESOLUTION_UNIT] - if TIFF_TAG.RESOLUTION_UNIT in ifd_entries else 2 - ) - - if resolution_unit == 1: # aspect ratio only - return 72 - # resolution_unit == 2 for inches, 3 for centimeters - units_per_inch = 1 if resolution_unit == 2 else 2.54 - dots_per_unit = ifd_entries[resolution_tag] - return int(round(dots_per_unit * units_per_inch)) - - @classmethod - def _make_stream_reader(cls, stream): - """ - Return a |StreamReader| instance with wrapping *stream* and having - "endian-ness" determined by the 'MM' or 'II' indicator in the TIFF - stream header. - """ - endian = cls._detect_endian(stream) - return StreamReader(stream, endian) - - -class _IfdEntries(object): - """ - Image File Directory for a TIFF image, having mapping (dict) semantics - allowing "tag" values to be retrieved by tag code. - """ - def __init__(self, entries): - super(_IfdEntries, self).__init__() - self._entries = entries - - def __contains__(self, key): - """ - Provides ``in`` operator, e.g. ``tag in ifd_entries`` - """ - return self._entries.__contains__(key) - - def __getitem__(self, key): - """ - Provides indexed access, e.g. ``tag_value = ifd_entries[tag_code]`` - """ - return self._entries.__getitem__(key) - - @classmethod - def from_stream(cls, stream, offset): - """ - Return a new |_IfdEntries| instance parsed from *stream* starting at - *offset*. - """ - ifd_parser = _IfdParser(stream, offset) - entries = dict((e.tag, e.value) for e in ifd_parser.iter_entries()) - return cls(entries) - - def get(self, tag_code, default=None): - """ - Return value of IFD entry having tag matching *tag_code*, or - *default* if no matching tag found. - """ - return self._entries.get(tag_code, default) - - -class _IfdParser(object): - """ - Service object that knows how to extract directory entries from an Image - File Directory (IFD) - """ - def __init__(self, stream_rdr, offset): - super(_IfdParser, self).__init__() - self._stream_rdr = stream_rdr - self._offset = offset - - def iter_entries(self): - """ - Generate an |_IfdEntry| instance corresponding to each entry in the - directory. - """ - for idx in range(self._entry_count): - dir_entry_offset = self._offset + 2 + (idx*12) - ifd_entry = _IfdEntryFactory(self._stream_rdr, dir_entry_offset) - yield ifd_entry - - @property - def _entry_count(self): - """ - The count of directory entries, read from the top of the IFD header - """ - return self._stream_rdr.read_short(self._offset) - - -def _IfdEntryFactory(stream_rdr, offset): - """ - Return an |_IfdEntry| subclass instance containing the value of the - directory entry at *offset* in *stream_rdr*. - """ - ifd_entry_classes = { - TIFF_FLD.ASCII: _AsciiIfdEntry, - TIFF_FLD.SHORT: _ShortIfdEntry, - TIFF_FLD.LONG: _LongIfdEntry, - TIFF_FLD.RATIONAL: _RationalIfdEntry, - } - field_type = stream_rdr.read_short(offset, 2) - if field_type in ifd_entry_classes: - entry_cls = ifd_entry_classes[field_type] - else: - entry_cls = _IfdEntry - return entry_cls.from_stream(stream_rdr, offset) - - -class _IfdEntry(object): - """ - Base class for IFD entry classes. Subclasses are differentiated by value - type, e.g. ASCII, long int, etc. - """ - def __init__(self, tag_code, value): - super(_IfdEntry, self).__init__() - self._tag_code = tag_code - self._value = value - - @classmethod - def from_stream(cls, stream_rdr, offset): - """ - Return an |_IfdEntry| subclass instance containing the tag and value - of the tag parsed from *stream_rdr* at *offset*. Note this method is - common to all subclasses. Override the ``_parse_value()`` method to - provide distinctive behavior based on field type. - """ - tag_code = stream_rdr.read_short(offset, 0) - value_count = stream_rdr.read_long(offset, 4) - value_offset = stream_rdr.read_long(offset, 8) - value = cls._parse_value( - stream_rdr, offset, value_count, value_offset - ) - return cls(tag_code, value) - - @classmethod - def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the value of this field parsed from *stream_rdr* at *offset*. - Intended to be overridden by subclasses. - """ - return 'UNIMPLEMENTED FIELD TYPE' # pragma: no cover - - @property - def tag(self): - """ - Short int code that identifies this IFD entry - """ - return self._tag_code - - @property - def value(self): - """ - Value of this tag, its type being dependent on the tag. - """ - return self._value - - -class _AsciiIfdEntry(_IfdEntry): - """ - IFD entry having the form of a NULL-terminated ASCII string - """ - @classmethod - def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the ASCII string parsed from *stream_rdr* at *value_offset*. - The length of the string, including a terminating '\x00' (NUL) - character, is in *value_count*. - """ - return stream_rdr.read_str(value_count-1, value_offset) - - -class _ShortIfdEntry(_IfdEntry): - """ - IFD entry expressed as a short (2-byte) integer - """ - @classmethod - def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the short int value contained in the *value_offset* field of - this entry. Only supports single values at present. - """ - if value_count == 1: - return stream_rdr.read_short(offset, 8) - else: # pragma: no cover - return 'Multi-value short integer NOT IMPLEMENTED' - - -class _LongIfdEntry(_IfdEntry): - """ - IFD entry expressed as a long (4-byte) integer - """ - @classmethod - def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the long int value contained in the *value_offset* field of - this entry. Only supports single values at present. - """ - if value_count == 1: - return stream_rdr.read_long(offset, 8) - else: # pragma: no cover - return 'Multi-value long integer NOT IMPLEMENTED' - - -class _RationalIfdEntry(_IfdEntry): - """ - IFD entry expressed as a numerator, denominator pair - """ - @classmethod - def _parse_value(cls, stream_rdr, offset, value_count, value_offset): - """ - Return the rational (numerator / denominator) value at *value_offset* - in *stream_rdr* as a floating-point number. Only supports single - values at present. - """ - if value_count == 1: - numerator = stream_rdr.read_long(value_offset) - denominator = stream_rdr.read_long(value_offset, 4) - return numerator / denominator - else: # pragma: no cover - return 'Multi-value Rational NOT IMPLEMENTED' diff --git a/docx/opc/compat.py b/docx/opc/compat.py deleted file mode 100644 index d5f63ac19..000000000 --- a/docx/opc/compat.py +++ /dev/null @@ -1,49 +0,0 @@ -# encoding: utf-8 - -""" -Provides Python 2/3 compatibility objects -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys - -# =========================================================================== -# Python 3 versions -# =========================================================================== - -if sys.version_info >= (3, 0): - - def cls_method_fn(cls, method_name): - """ - Return the function object associated with the method of *cls* having - *method_name*. - """ - return getattr(cls, method_name) - - def is_string(obj): - """ - Return True if *obj* is a string, False otherwise. - """ - return isinstance(obj, str) - - -# =========================================================================== -# Python 2 versions -# =========================================================================== - -else: - - def cls_method_fn(cls, method_name): - """ - Return the function object associated with the method of *cls* having - *method_name*. - """ - unbound_method = getattr(cls, method_name) - return unbound_method.__func__ - - def is_string(obj): - """ - Return True if *obj* is a string, False otherwise. - """ - return isinstance(obj, basestring) # noqa diff --git a/docx/opc/constants.py b/docx/opc/constants.py deleted file mode 100644 index b90aa394a..000000000 --- a/docx/opc/constants.py +++ /dev/null @@ -1,658 +0,0 @@ -# encoding: utf-8 - -""" -Constant values related to the Open Packaging Convention, in particular, -content types and relationship types. -""" - - -class CONTENT_TYPE(object): - """ - Content type URIs (like MIME-types) that specify a part's format - """ - BMP = ( - 'image/bmp' - ) - DML_CHART = ( - 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml' - ) - DML_CHARTSHAPES = ( - 'application/vnd.openxmlformats-officedocument.drawingml.chartshapes' - '+xml' - ) - DML_DIAGRAM_COLORS = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramColo' - 'rs+xml' - ) - DML_DIAGRAM_DATA = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramData' - '+xml' - ) - DML_DIAGRAM_LAYOUT = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramLayo' - 'ut+xml' - ) - DML_DIAGRAM_STYLE = ( - 'application/vnd.openxmlformats-officedocument.drawingml.diagramStyl' - 'e+xml' - ) - GIF = ( - 'image/gif' - ) - JPEG = ( - 'image/jpeg' - ) - MS_PHOTO = ( - 'image/vnd.ms-photo' - ) - OFC_CUSTOM_PROPERTIES = ( - 'application/vnd.openxmlformats-officedocument.custom-properties+xml' - ) - OFC_CUSTOM_XML_PROPERTIES = ( - 'application/vnd.openxmlformats-officedocument.customXmlProperties+x' - 'ml' - ) - OFC_DRAWING = ( - 'application/vnd.openxmlformats-officedocument.drawing+xml' - ) - OFC_EXTENDED_PROPERTIES = ( - 'application/vnd.openxmlformats-officedocument.extended-properties+x' - 'ml' - ) - OFC_OLE_OBJECT = ( - 'application/vnd.openxmlformats-officedocument.oleObject' - ) - OFC_PACKAGE = ( - 'application/vnd.openxmlformats-officedocument.package' - ) - OFC_THEME = ( - 'application/vnd.openxmlformats-officedocument.theme+xml' - ) - OFC_THEME_OVERRIDE = ( - 'application/vnd.openxmlformats-officedocument.themeOverride+xml' - ) - OFC_VML_DRAWING = ( - 'application/vnd.openxmlformats-officedocument.vmlDrawing' - ) - OPC_CORE_PROPERTIES = ( - 'application/vnd.openxmlformats-package.core-properties+xml' - ) - OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( - 'application/vnd.openxmlformats-package.digital-signature-certificat' - 'e' - ) - OPC_DIGITAL_SIGNATURE_ORIGIN = ( - 'application/vnd.openxmlformats-package.digital-signature-origin' - ) - OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( - 'application/vnd.openxmlformats-package.digital-signature-xmlsignatu' - 're+xml' - ) - OPC_RELATIONSHIPS = ( - 'application/vnd.openxmlformats-package.relationships+xml' - ) - PML_COMMENTS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.commen' - 'ts+xml' - ) - PML_COMMENT_AUTHORS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.commen' - 'tAuthors+xml' - ) - PML_HANDOUT_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.handou' - 'tMaster+xml' - ) - PML_NOTES_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.notesM' - 'aster+xml' - ) - PML_NOTES_SLIDE = ( - 'application/vnd.openxmlformats-officedocument.presentationml.notesS' - 'lide+xml' - ) - PML_PRESENTATION_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.presen' - 'tation.main+xml' - ) - PML_PRES_PROPS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.presPr' - 'ops+xml' - ) - PML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.printe' - 'rSettings' - ) - PML_SLIDE = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slide+' - 'xml' - ) - PML_SLIDESHOW_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slides' - 'how.main+xml' - ) - PML_SLIDE_LAYOUT = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideL' - 'ayout+xml' - ) - PML_SLIDE_MASTER = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideM' - 'aster+xml' - ) - PML_SLIDE_UPDATE_INFO = ( - 'application/vnd.openxmlformats-officedocument.presentationml.slideU' - 'pdateInfo+xml' - ) - PML_TABLE_STYLES = ( - 'application/vnd.openxmlformats-officedocument.presentationml.tableS' - 'tyles+xml' - ) - PML_TAGS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.tags+x' - 'ml' - ) - PML_TEMPLATE_MAIN = ( - 'application/vnd.openxmlformats-officedocument.presentationml.templa' - 'te.main+xml' - ) - PML_VIEW_PROPS = ( - 'application/vnd.openxmlformats-officedocument.presentationml.viewPr' - 'ops+xml' - ) - PNG = ( - 'image/png' - ) - SML_CALC_CHAIN = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.calcCha' - 'in+xml' - ) - SML_CHARTSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.chartsh' - 'eet+xml' - ) - SML_COMMENTS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.comment' - 's+xml' - ) - SML_CONNECTIONS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.connect' - 'ions+xml' - ) - SML_CUSTOM_PROPERTY = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.customP' - 'roperty' - ) - SML_DIALOGSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.dialogs' - 'heet+xml' - ) - SML_EXTERNAL_LINK = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.externa' - 'lLink+xml' - ) - SML_PIVOT_CACHE_DEFINITION = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa' - 'cheDefinition+xml' - ) - SML_PIVOT_CACHE_RECORDS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa' - 'cheRecords+xml' - ) - SML_PIVOT_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTa' - 'ble+xml' - ) - SML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.printer' - 'Settings' - ) - SML_QUERY_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.queryTa' - 'ble+xml' - ) - SML_REVISION_HEADERS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.revisio' - 'nHeaders+xml' - ) - SML_REVISION_LOG = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.revisio' - 'nLog+xml' - ) - SML_SHARED_STRINGS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedS' - 'trings+xml' - ) - SML_SHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ) - SML_SHEET_MAIN = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.m' - 'ain+xml' - ) - SML_SHEET_METADATA = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMe' - 'tadata+xml' - ) - SML_STYLES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+' - 'xml' - ) - SML_TABLE = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+x' - 'ml' - ) - SML_TABLE_SINGLE_CELLS = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.tableSi' - 'ngleCells+xml' - ) - SML_TEMPLATE_MAIN = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.templat' - 'e.main+xml' - ) - SML_USER_NAMES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.userNam' - 'es+xml' - ) - SML_VOLATILE_DEPENDENCIES = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.volatil' - 'eDependencies+xml' - ) - SML_WORKSHEET = ( - 'application/vnd.openxmlformats-officedocument.spreadsheetml.workshe' - 'et+xml' - ) - TIFF = ( - 'image/tiff' - ) - WML_COMMENTS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.comm' - 'ents+xml' - ) - WML_DOCUMENT = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment' - ) - WML_DOCUMENT_GLOSSARY = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment.glossary+xml' - ) - WML_DOCUMENT_MAIN = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.docu' - 'ment.main+xml' - ) - WML_ENDNOTES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.endn' - 'otes+xml' - ) - WML_FONT_TABLE = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.font' - 'Table+xml' - ) - WML_FOOTER = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.foot' - 'er+xml' - ) - WML_FOOTNOTES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.foot' - 'notes+xml' - ) - WML_HEADER = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.head' - 'er+xml' - ) - WML_NUMBERING = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.numb' - 'ering+xml' - ) - WML_PRINTER_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.prin' - 'terSettings' - ) - WML_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.sett' - 'ings+xml' - ) - WML_STYLES = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.styl' - 'es+xml' - ) - WML_WEB_SETTINGS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.webS' - 'ettings+xml' - ) - XML = ( - 'application/xml' - ) - X_EMF = ( - 'image/x-emf' - ) - X_FONTDATA = ( - 'application/x-fontdata' - ) - X_FONT_TTF = ( - 'application/x-font-ttf' - ) - X_WMF = ( - 'image/x-wmf' - ) - - -class NAMESPACE(object): - """Constant values for OPC XML namespaces""" - DML_WORDPROCESSING_DRAWING = ( - 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDraw' - 'ing' - ) - OFC_RELATIONSHIPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - ) - OPC_RELATIONSHIPS = ( - 'http://schemas.openxmlformats.org/package/2006/relationships' - ) - OPC_CONTENT_TYPES = ( - 'http://schemas.openxmlformats.org/package/2006/content-types' - ) - WML_MAIN = ( - 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' - ) - - -class RELATIONSHIP_TARGET_MODE(object): - """Open XML relationship target modes""" - EXTERNAL = 'External' - INTERNAL = 'Internal' - - -class RELATIONSHIP_TYPE(object): - AUDIO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/audio' - ) - A_F_CHUNK = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/aFChunk' - ) - CALC_CHAIN = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/calcChain' - ) - CERTIFICATE = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/certificate' - ) - CHART = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chart' - ) - CHARTSHEET = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chartsheet' - ) - CHART_USER_SHAPES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/chartUserShapes' - ) - COMMENTS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/comments' - ) - COMMENT_AUTHORS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/commentAuthors' - ) - CONNECTIONS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/connections' - ) - CONTROL = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/control' - ) - CORE_PROPERTIES = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/metada' - 'ta/core-properties' - ) - CUSTOM_PROPERTIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/custom-properties' - ) - CUSTOM_PROPERTY = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customProperty' - ) - CUSTOM_XML = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customXml' - ) - CUSTOM_XML_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/customXmlProps' - ) - DIAGRAM_COLORS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramColors' - ) - DIAGRAM_DATA = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramData' - ) - DIAGRAM_LAYOUT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramLayout' - ) - DIAGRAM_QUICK_STYLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/diagramQuickStyle' - ) - DIALOGSHEET = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/dialogsheet' - ) - DRAWING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/drawing' - ) - ENDNOTES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/endnotes' - ) - EXTENDED_PROPERTIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/extended-properties' - ) - EXTERNAL_LINK = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/externalLink' - ) - FONT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/font' - ) - FONT_TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/fontTable' - ) - FOOTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/footer' - ) - FOOTNOTES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/footnotes' - ) - GLOSSARY_DOCUMENT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/glossaryDocument' - ) - HANDOUT_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/handoutMaster' - ) - HEADER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/header' - ) - HYPERLINK = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/hyperlink' - ) - IMAGE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/image' - ) - NOTES_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/notesMaster' - ) - NOTES_SLIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/notesSlide' - ) - NUMBERING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/numbering' - ) - OFFICE_DOCUMENT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/officeDocument' - ) - OLE_OBJECT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/oleObject' - ) - ORIGIN = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/origin' - ) - PACKAGE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/package' - ) - PIVOT_CACHE_DEFINITION = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/pivotCacheDefinition' - ) - PIVOT_CACHE_RECORDS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/spreadsheetml/pivotCacheRecords' - ) - PIVOT_TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/pivotTable' - ) - PRES_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/presProps' - ) - PRINTER_SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/printerSettings' - ) - QUERY_TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/queryTable' - ) - REVISION_HEADERS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/revisionHeaders' - ) - REVISION_LOG = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/revisionLog' - ) - SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/settings' - ) - SHARED_STRINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/sharedStrings' - ) - SHEET_METADATA = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/sheetMetadata' - ) - SIGNATURE = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/digita' - 'l-signature/signature' - ) - SLIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slide' - ) - SLIDE_LAYOUT = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideLayout' - ) - SLIDE_MASTER = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideMaster' - ) - SLIDE_UPDATE_INFO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/slideUpdateInfo' - ) - STYLES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/styles' - ) - TABLE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/table' - ) - TABLE_SINGLE_CELLS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/tableSingleCells' - ) - TABLE_STYLES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/tableStyles' - ) - TAGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/tags' - ) - THEME = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/theme' - ) - THEME_OVERRIDE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/themeOverride' - ) - THUMBNAIL = ( - 'http://schemas.openxmlformats.org/package/2006/relationships/metada' - 'ta/thumbnail' - ) - USERNAMES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/usernames' - ) - VIDEO = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/video' - ) - VIEW_PROPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/viewProps' - ) - VML_DRAWING = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/vmlDrawing' - ) - VOLATILE_DEPENDENCIES = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/volatileDependencies' - ) - WEB_SETTINGS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/webSettings' - ) - WORKSHEET_SOURCE = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/worksheetSource' - ) - XML_MAPS = ( - 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' - '/xmlMaps' - ) diff --git a/docx/opc/exceptions.py b/docx/opc/exceptions.py deleted file mode 100644 index b8e6de43f..000000000 --- a/docx/opc/exceptions.py +++ /dev/null @@ -1,19 +0,0 @@ -# encoding: utf-8 - -""" -Exceptions specific to python-opc - -The base exception class is OpcError. -""" - - -class OpcError(Exception): - """ - Base error class for python-opc - """ - - -class PackageNotFoundError(OpcError): - """ - Raised when a package cannot be found at the specified path. - """ diff --git a/docx/opc/oxml.py b/docx/opc/oxml.py deleted file mode 100644 index 494b31dca..000000000 --- a/docx/opc/oxml.py +++ /dev/null @@ -1,292 +0,0 @@ -# encoding: utf-8 - -""" -Temporary stand-in for main oxml module that came across with the -PackageReader transplant. Probably much will get replaced with objects from -the pptx.oxml.core and then this module will either get deleted or only hold -the package related custom element classes. -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from lxml import etree - -from .constants import NAMESPACE as NS, RELATIONSHIP_TARGET_MODE as RTM - - -# configure XML parser -element_class_lookup = etree.ElementNamespaceClassLookup() -oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False) -oxml_parser.set_element_class_lookup(element_class_lookup) - -nsmap = { - 'ct': NS.OPC_CONTENT_TYPES, - 'pr': NS.OPC_RELATIONSHIPS, - 'r': NS.OFC_RELATIONSHIPS, -} - - -# =========================================================================== -# functions -# =========================================================================== - -def parse_xml(text): - """ - ``etree.fromstring()`` replacement that uses oxml parser - """ - return etree.fromstring(text, oxml_parser) - - -def qn(tag): - """ - Stands for "qualified name", a utility function to turn a namespace - prefixed tag name into a Clark-notation qualified tag name for lxml. For - example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``. - """ - prefix, tagroot = tag.split(':') - uri = nsmap[prefix] - return '{%s}%s' % (uri, tagroot) - - -def serialize_part_xml(part_elm): - """ - Serialize *part_elm* etree element to XML suitable for storage as an XML - part. That is to say, no insignificant whitespace added for readability, - and an appropriate XML declaration added with UTF-8 encoding specified. - """ - return etree.tostring(part_elm, encoding='UTF-8', standalone=True) - - -def serialize_for_reading(element): - """ - Serialize *element* to human-readable XML suitable for tests. No XML - declaration. - """ - return etree.tostring(element, encoding='unicode', pretty_print=True) - - -# =========================================================================== -# Custom element classes -# =========================================================================== - -class BaseOxmlElement(etree.ElementBase): - """ - Base class for all custom element classes, to add standardized behavior - to all classes in one place. - """ - @property - def xml(self): - """ - Return XML string for this element, suitable for testing purposes. - Pretty printed for readability and without an XML declaration at the - top. - """ - return serialize_for_reading(self) - - -class CT_Default(BaseOxmlElement): - """ - ```` element, specifying the default content type to be applied - to a part with the specified extension. - """ - @property - def content_type(self): - """ - String held in the ``ContentType`` attribute of this ```` - element. - """ - return self.get('ContentType') - - @property - def extension(self): - """ - String held in the ``Extension`` attribute of this ```` - element. - """ - return self.get('Extension') - - @staticmethod - def new(ext, content_type): - """ - Return a new ```` element with attributes set to parameter - values. - """ - xml = '' % nsmap['ct'] - default = parse_xml(xml) - default.set('Extension', ext) - default.set('ContentType', content_type) - return default - - -class CT_Override(BaseOxmlElement): - """ - ```` element, specifying the content type to be applied for a - part with the specified partname. - """ - @property - def content_type(self): - """ - String held in the ``ContentType`` attribute of this ```` - element. - """ - return self.get('ContentType') - - @staticmethod - def new(partname, content_type): - """ - Return a new ```` element with attributes set to parameter - values. - """ - xml = '' % nsmap['ct'] - override = parse_xml(xml) - override.set('PartName', partname) - override.set('ContentType', content_type) - return override - - @property - def partname(self): - """ - String held in the ``PartName`` attribute of this ```` - element. - """ - return self.get('PartName') - - -class CT_Relationship(BaseOxmlElement): - """ - ```` element, representing a single relationship from a - source to a target part. - """ - @staticmethod - def new(rId, reltype, target, target_mode=RTM.INTERNAL): - """ - Return a new ```` element. - """ - xml = '' % nsmap['pr'] - relationship = parse_xml(xml) - relationship.set('Id', rId) - relationship.set('Type', reltype) - relationship.set('Target', target) - if target_mode == RTM.EXTERNAL: - relationship.set('TargetMode', RTM.EXTERNAL) - return relationship - - @property - def rId(self): - """ - String held in the ``Id`` attribute of this ```` - element. - """ - return self.get('Id') - - @property - def reltype(self): - """ - String held in the ``Type`` attribute of this ```` - element. - """ - return self.get('Type') - - @property - def target_ref(self): - """ - String held in the ``Target`` attribute of this ```` - element. - """ - return self.get('Target') - - @property - def target_mode(self): - """ - String held in the ``TargetMode`` attribute of this - ```` element, either ``Internal`` or ``External``. - Defaults to ``Internal``. - """ - return self.get('TargetMode', RTM.INTERNAL) - - -class CT_Relationships(BaseOxmlElement): - """ - ```` element, the root element in a .rels file. - """ - def add_rel(self, rId, reltype, target, is_external=False): - """ - Add a child ```` element with attributes set according - to parameter values. - """ - target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL - relationship = CT_Relationship.new(rId, reltype, target, target_mode) - self.append(relationship) - - @staticmethod - def new(): - """ - Return a new ```` element. - """ - xml = '' % nsmap['pr'] - relationships = parse_xml(xml) - return relationships - - @property - def Relationship_lst(self): - """ - Return a list containing all the ```` child elements. - """ - return self.findall(qn('pr:Relationship')) - - @property - def xml(self): - """ - Return XML string for this element, suitable for saving in a .rels - stream, not pretty printed and with an XML declaration at the top. - """ - return serialize_part_xml(self) - - -class CT_Types(BaseOxmlElement): - """ - ```` element, the container element for Default and Override - elements in [Content_Types].xml. - """ - def add_default(self, ext, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ - default = CT_Default.new(ext, content_type) - self.append(default) - - def add_override(self, partname, content_type): - """ - Add a child ```` element with attributes set to parameter - values. - """ - override = CT_Override.new(partname, content_type) - self.append(override) - - @property - def defaults(self): - return self.findall(qn('ct:Default')) - - @staticmethod - def new(): - """ - Return a new ```` element. - """ - xml = '' % nsmap['ct'] - types = parse_xml(xml) - return types - - @property - def overrides(self): - return self.findall(qn('ct:Override')) - - -ct_namespace = element_class_lookup.get_namespace(nsmap['ct']) -ct_namespace['Default'] = CT_Default -ct_namespace['Override'] = CT_Override -ct_namespace['Types'] = CT_Types - -pr_namespace = element_class_lookup.get_namespace(nsmap['pr']) -pr_namespace['Relationship'] = CT_Relationship -pr_namespace['Relationships'] = CT_Relationships diff --git a/docx/opc/packuri.py b/docx/opc/packuri.py deleted file mode 100644 index 621ed92e5..000000000 --- a/docx/opc/packuri.py +++ /dev/null @@ -1,117 +0,0 @@ -# encoding: utf-8 - -""" -Provides the PackURI value type along with some useful known pack URI strings -such as PACKAGE_URI. -""" - -import posixpath -import re - - -class PackURI(str): - """ - Provides access to pack URI components such as the baseURI and the - filename slice. Behaves as |str| otherwise. - """ - _filename_re = re.compile('([a-zA-Z]+)([1-9][0-9]*)?') - - def __new__(cls, pack_uri_str): - if not pack_uri_str[0] == '/': - tmpl = "PackURI must begin with slash, got '%s'" - raise ValueError(tmpl % pack_uri_str) - return str.__new__(cls, pack_uri_str) - - @staticmethod - def from_rel_ref(baseURI, relative_ref): - """ - Return a |PackURI| instance containing the absolute pack URI formed by - translating *relative_ref* onto *baseURI*. - """ - joined_uri = posixpath.join(baseURI, relative_ref) - abs_uri = posixpath.abspath(joined_uri) - return PackURI(abs_uri) - - @property - def baseURI(self): - """ - The base URI of this pack URI, the directory portion, roughly - speaking. E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. - For the package pseudo-partname '/', baseURI is '/'. - """ - return posixpath.split(self)[0] - - @property - def ext(self): - """ - The extension portion of this pack URI, e.g. ``'xml'`` for - ``'/word/document.xml'``. Note the period is not included. - """ - # raw_ext is either empty string or starts with period, e.g. '.xml' - raw_ext = posixpath.splitext(self)[1] - return raw_ext[1:] if raw_ext.startswith('.') else raw_ext - - @property - def filename(self): - """ - The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for - ``'/ppt/slides/slide1.xml'``. For the package pseudo-partname '/', - filename is ''. - """ - return posixpath.split(self)[1] - - @property - def idx(self): - """ - Return partname index as integer for tuple partname or None for - singleton partname, e.g. ``21`` for ``'/ppt/slides/slide21.xml'`` and - |None| for ``'/ppt/presentation.xml'``. - """ - filename = self.filename - if not filename: - return None - name_part = posixpath.splitext(filename)[0] # filename w/ext removed - match = self._filename_re.match(name_part) - if match is None: - return None - if match.group(2): - return int(match.group(2)) - return None - - @property - def membername(self): - """ - The pack URI with the leading slash stripped off, the form used as - the Zip file membername for the package item. Returns '' for the - package pseudo-partname '/'. - """ - return self[1:] - - def relative_ref(self, baseURI): - """ - Return string containing relative reference to package item from - *baseURI*. E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would - return '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'. - """ - # workaround for posixpath bug in 2.6, doesn't generate correct - # relative path when *start* (second) parameter is root ('/') - if baseURI == '/': - relpath = self[1:] - else: - relpath = posixpath.relpath(self, baseURI) - return relpath - - @property - def rels_uri(self): - """ - The pack URI of the .rels part corresponding to the current pack URI. - Only produces sensible output if the pack URI is a partname or the - package pseudo-partname '/'. - """ - rels_filename = '%s.rels' % self.filename - rels_uri_str = posixpath.join(self.baseURI, '_rels', rels_filename) - return PackURI(rels_uri_str) - - -PACKAGE_URI = PackURI('/') -CONTENT_TYPES_URI = PackURI('/[Content_Types].xml') diff --git a/docx/opc/part.py b/docx/opc/part.py deleted file mode 100644 index 928d3c183..000000000 --- a/docx/opc/part.py +++ /dev/null @@ -1,241 +0,0 @@ -# encoding: utf-8 - -""" -Open Packaging Convention (OPC) objects related to package parts. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from .compat import cls_method_fn -from .oxml import serialize_part_xml -from ..oxml import parse_xml -from .packuri import PackURI -from .rel import Relationships -from .shared import lazyproperty - - -class Part(object): - """ - Base class for package parts. Provides common properties and methods, but - intended to be subclassed in client code to implement specific part - behaviors. - """ - def __init__(self, partname, content_type, blob=None, package=None): - super(Part, self).__init__() - self._partname = partname - self._content_type = content_type - self._blob = blob - self._package = package - - def after_unmarshal(self): - """ - Entry point for post-unmarshaling processing, for example to parse - the part XML. May be overridden by subclasses without forwarding call - to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - - def before_marshal(self): - """ - Entry point for pre-serialization processing, for example to finalize - part naming if necessary. May be overridden by subclasses without - forwarding call to super. - """ - # don't place any code here, just catch call if not overridden by - # subclass - pass - - @property - def blob(self): - """ - Contents of this package part as a sequence of bytes. May be text or - binary. Intended to be overridden by subclasses. Default behavior is - to return load blob. - """ - return self._blob - - @property - def content_type(self): - """ - Content type of this part. - """ - return self._content_type - - def drop_rel(self, rId): - """ - Remove the relationship identified by *rId* if its reference count - is less than 2. Relationships with a reference count of 0 are - implicit relationships. - """ - if self._rel_ref_count(rId) < 2: - del self.rels[rId] - - @classmethod - def load(cls, partname, content_type, blob, package): - return cls(partname, content_type, blob, package) - - def load_rel(self, reltype, target, rId, is_external=False): - """ - Return newly added |_Relationship| instance of *reltype* between this - part and *target* with key *rId*. Target mode is set to - ``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during - load from a serialized package, where the rId is well-known. Other - methods exist for adding a new relationship to a part when - manipulating a part. - """ - return self.rels.add_relationship(reltype, target, rId, is_external) - - @property - def package(self): - """ - |OpcPackage| instance this part belongs to. - """ - return self._package - - @property - def partname(self): - """ - |PackURI| instance holding partname of this part, e.g. - '/ppt/slides/slide1.xml' - """ - return self._partname - - @partname.setter - def partname(self, partname): - if not isinstance(partname, PackURI): - tmpl = "partname must be instance of PackURI, got '%s'" - raise TypeError(tmpl % type(partname).__name__) - self._partname = partname - - def part_related_by(self, reltype): - """ - Return part to which this part has a relationship of *reltype*. - Raises |KeyError| if no such relationship is found and |ValueError| - if more than one such relationship is found. Provides ability to - resolve implicitly related part, such as Slide -> SlideLayout. - """ - return self.rels.part_with_reltype(reltype) - - def relate_to(self, target, reltype, is_external=False): - """ - Return rId key of relationship of *reltype* to *target*, from an - existing relationship if there is one, otherwise a newly created one. - """ - if is_external: - return self.rels.get_or_add_ext_rel(reltype, target) - else: - rel = self.rels.get_or_add(reltype, target) - return rel.rId - - @property - def related_parts(self): - """ - Dictionary mapping related parts by rId, so child objects can resolve - explicit relationships present in the part XML, e.g. sldIdLst to a - specific |Slide| instance. - """ - return self.rels.related_parts - - @lazyproperty - def rels(self): - """ - |Relationships| instance holding the relationships for this part. - """ - return Relationships(self._partname.baseURI) - - def target_ref(self, rId): - """ - Return URL contained in target ref of relationship identified by - *rId*. - """ - rel = self.rels[rId] - return rel.target_ref - - def _rel_ref_count(self, rId): - """ - Return the count of references in this part's XML to the relationship - identified by *rId*. - """ - rIds = self._element.xpath('//@r:id') - return len([_rId for _rId in rIds if _rId == rId]) - - -class PartFactory(object): - """ - Provides a way for client code to specify a subclass of |Part| to be - constructed by |Unmarshaller| based on its content type and/or a custom - callable. Setting ``PartFactory.part_class_selector`` to a callable - object will cause that object to be called with the parameters - ``content_type, reltype``, once for each part in the package. If the - callable returns an object, it is used as the class for that part. If it - returns |None|, part class selection falls back to the content type map - defined in ``PartFactory.part_type_for``. If no class is returned from - either of these, the class contained in ``PartFactory.default_part_type`` - is used to construct the part, which is by default ``opc.package.Part``. - """ - part_class_selector = None - part_type_for = {} - default_part_type = Part - - def __new__(cls, partname, content_type, reltype, blob, package): - PartClass = None - if cls.part_class_selector is not None: - part_class_selector = cls_method_fn(cls, 'part_class_selector') - PartClass = part_class_selector(content_type, reltype) - if PartClass is None: - PartClass = cls._part_cls_for(content_type) - return PartClass.load(partname, content_type, blob, package) - - @classmethod - def _part_cls_for(cls, content_type): - """ - Return the custom part class registered for *content_type*, or the - default part class if no custom class is registered for - *content_type*. - """ - if content_type in cls.part_type_for: - return cls.part_type_for[content_type] - return cls.default_part_type - - -class XmlPart(Part): - """ - Base class for package parts containing an XML payload, which is most of - them. Provides additional methods to the |Part| base class that take care - of parsing and reserializing the XML payload and managing relationships - to other parts. - """ - def __init__(self, partname, content_type, element, package): - super(XmlPart, self).__init__( - partname, content_type, package=package - ) - self._element = element - - @property - def blob(self): - return serialize_part_xml(self._element) - - @property - def element(self): - """ - The root XML element of this XML part. - """ - return self._element - - @classmethod - def load(cls, partname, content_type, blob, package): - element = parse_xml(blob) - return cls(partname, content_type, element, package) - - @property - def part(self): - """ - Part of the parent protocol, "children" of the document will not know - the part that contains them so must ask their parent object. That - chain of delegation ends here for child objects. - """ - return self diff --git a/docx/opc/parts/coreprops.py b/docx/opc/parts/coreprops.py deleted file mode 100644 index 3c692fb99..000000000 --- a/docx/opc/parts/coreprops.py +++ /dev/null @@ -1,54 +0,0 @@ -# encoding: utf-8 - -""" -Core properties part, corresponds to ``/docProps/core.xml`` part in package. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from datetime import datetime - -from ..constants import CONTENT_TYPE as CT -from ..coreprops import CoreProperties -from ...oxml.coreprops import CT_CoreProperties -from ..packuri import PackURI -from ..part import XmlPart - - -class CorePropertiesPart(XmlPart): - """ - Corresponds to part named ``/docProps/core.xml``, containing the core - document properties for this document package. - """ - @classmethod - def default(cls, package): - """ - Return a new |CorePropertiesPart| object initialized with default - values for its base properties. - """ - core_properties_part = cls._new(package) - core_properties = core_properties_part.core_properties - core_properties.title = 'Word Document' - core_properties.last_modified_by = 'python-docx' - core_properties.revision = 1 - core_properties.modified = datetime.utcnow() - return core_properties_part - - @property - def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties contained in this core properties part. - """ - return CoreProperties(self.element) - - @classmethod - def _new(cls, package): - partname = PackURI('/docProps/core.xml') - content_type = CT.OPC_CORE_PROPERTIES - coreProperties = CT_CoreProperties.new() - return CorePropertiesPart( - partname, content_type, coreProperties, package - ) diff --git a/docx/opc/phys_pkg.py b/docx/opc/phys_pkg.py deleted file mode 100644 index c86a51994..000000000 --- a/docx/opc/phys_pkg.py +++ /dev/null @@ -1,155 +0,0 @@ -# encoding: utf-8 - -""" -Provides a general interface to a *physical* OPC package, such as a zip file. -""" - -from __future__ import absolute_import - -import os - -from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED - -from .compat import is_string -from .exceptions import PackageNotFoundError -from .packuri import CONTENT_TYPES_URI - - -class PhysPkgReader(object): - """ - Factory for physical package reader objects. - """ - def __new__(cls, pkg_file): - # if *pkg_file* is a string, treat it as a path - if is_string(pkg_file): - if os.path.isdir(pkg_file): - reader_cls = _DirPkgReader - elif is_zipfile(pkg_file): - reader_cls = _ZipPkgReader - else: - raise PackageNotFoundError( - "Package not found at '%s'" % pkg_file - ) - else: # assume it's a stream and pass it to Zip reader to sort out - reader_cls = _ZipPkgReader - - return super(PhysPkgReader, cls).__new__(reader_cls) - - -class PhysPkgWriter(object): - """ - Factory for physical package writer objects. - """ - def __new__(cls, pkg_file): - return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter) - - -class _DirPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for an OPC package extracted into a - directory. - """ - def __init__(self, path): - """ - *path* is the path to a directory containing an expanded package. - """ - super(_DirPkgReader, self).__init__() - self._path = os.path.abspath(path) - - def blob_for(self, pack_uri): - """ - Return contents of file corresponding to *pack_uri* in package - directory. - """ - path = os.path.join(self._path, pack_uri.membername) - with open(path, 'rb') as f: - blob = f.read() - return blob - - def close(self): - """ - Provides interface consistency with |ZipFileSystem|, but does - nothing, a directory file system doesn't need closing. - """ - pass - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri*, or None if the - item has no rels item. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except IOError: - rels_xml = None - return rels_xml - - -class _ZipPkgReader(PhysPkgReader): - """ - Implements |PhysPkgReader| interface for a zip file OPC package. - """ - def __init__(self, pkg_file): - super(_ZipPkgReader, self).__init__() - self._zipf = ZipFile(pkg_file, 'r') - - def blob_for(self, pack_uri): - """ - Return blob corresponding to *pack_uri*. Raises |ValueError| if no - matching member is present in zip archive. - """ - return self._zipf.read(pack_uri.membername) - - def close(self): - """ - Close the zip archive, releasing any resources it is using. - """ - self._zipf.close() - - @property - def content_types_xml(self): - """ - Return the `[Content_Types].xml` blob from the zip package. - """ - return self.blob_for(CONTENT_TYPES_URI) - - def rels_xml_for(self, source_uri): - """ - Return rels item XML for source with *source_uri* or None if no rels - item is present. - """ - try: - rels_xml = self.blob_for(source_uri.rels_uri) - except KeyError: - rels_xml = None - return rels_xml - - -class _ZipPkgWriter(PhysPkgWriter): - """ - Implements |PhysPkgWriter| interface for a zip file OPC package. - """ - def __init__(self, pkg_file): - super(_ZipPkgWriter, self).__init__() - self._zipf = ZipFile(pkg_file, 'w', compression=ZIP_DEFLATED) - - def close(self): - """ - Close the zip archive, flushing any pending physical writes and - releasing any resources it's using. - """ - self._zipf.close() - - def write(self, pack_uri, blob): - """ - Write *blob* to this zip package with the membername corresponding to - *pack_uri*. - """ - self._zipf.writestr(pack_uri.membername, blob) diff --git a/docx/opc/pkgwriter.py b/docx/opc/pkgwriter.py deleted file mode 100644 index fccda6cd8..000000000 --- a/docx/opc/pkgwriter.py +++ /dev/null @@ -1,125 +0,0 @@ -# encoding: utf-8 - -""" -Provides a low-level, write-only API to a serialized Open Packaging -Convention (OPC) package, essentially an implementation of OpcPackage.save() -""" - -from __future__ import absolute_import - -from .constants import CONTENT_TYPE as CT -from .oxml import CT_Types, serialize_part_xml -from .packuri import CONTENT_TYPES_URI, PACKAGE_URI -from .phys_pkg import PhysPkgWriter -from .shared import CaseInsensitiveDict -from .spec import default_content_types - - -class PackageWriter(object): - """ - Writes a zip-format OPC package to *pkg_file*, where *pkg_file* can be - either a path to a zip file (a string) or a file-like object. Its single - API method, :meth:`write`, is static, so this class is not intended to - be instantiated. - """ - @staticmethod - def write(pkg_file, pkg_rels, parts): - """ - Write a physical package (.pptx file) to *pkg_file* containing - *pkg_rels* and *parts* and a content types stream based on the - content types of the parts. - """ - phys_writer = PhysPkgWriter(pkg_file) - PackageWriter._write_content_types_stream(phys_writer, parts) - PackageWriter._write_pkg_rels(phys_writer, pkg_rels) - PackageWriter._write_parts(phys_writer, parts) - phys_writer.close() - - @staticmethod - def _write_content_types_stream(phys_writer, parts): - """ - Write ``[Content_Types].xml`` part to the physical package with an - appropriate content type lookup target for each part in *parts*. - """ - cti = _ContentTypesItem.from_parts(parts) - phys_writer.write(CONTENT_TYPES_URI, cti.blob) - - @staticmethod - def _write_parts(phys_writer, parts): - """ - Write the blob of each part in *parts* to the package, along with a - rels item for its relationships if and only if it has any. - """ - for part in parts: - phys_writer.write(part.partname, part.blob) - if len(part._rels): - phys_writer.write(part.partname.rels_uri, part._rels.xml) - - @staticmethod - def _write_pkg_rels(phys_writer, pkg_rels): - """ - Write the XML rels item for *pkg_rels* ('/_rels/.rels') to the - package. - """ - phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) - - -class _ContentTypesItem(object): - """ - Service class that composes a content types item ([Content_Types].xml) - based on a list of parts. Not meant to be instantiated directly, its - single interface method is xml_for(), e.g. - ``_ContentTypesItem.xml_for(parts)``. - """ - def __init__(self): - self._defaults = CaseInsensitiveDict() - self._overrides = dict() - - @property - def blob(self): - """ - Return XML form of this content types item, suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ - return serialize_part_xml(self._element) - - @classmethod - def from_parts(cls, parts): - """ - Return content types XML mapping each part in *parts* to the - appropriate content type and suitable for storage as - ``[Content_Types].xml`` in an OPC package. - """ - cti = cls() - cti._defaults['rels'] = CT.OPC_RELATIONSHIPS - cti._defaults['xml'] = CT.XML - for part in parts: - cti._add_content_type(part.partname, part.content_type) - return cti - - def _add_content_type(self, partname, content_type): - """ - Add a content type for the part with *partname* and *content_type*, - using a default or override as appropriate. - """ - ext = partname.ext - if (ext.lower(), content_type) in default_content_types: - self._defaults[ext] = content_type - else: - self._overrides[partname] = content_type - - @property - def _element(self): - """ - Return XML form of this content types item, suitable for storage as - ``[Content_Types].xml`` in an OPC package. Although the sequence of - elements is not strictly significant, as an aid to testing and - readability Default elements are sorted by extension and Override - elements are sorted by partname. - """ - _types_elm = CT_Types.new() - for ext in sorted(self._defaults.keys()): - _types_elm.add_default(ext, self._defaults[ext]) - for partname in sorted(self._overrides.keys()): - _types_elm.add_override(partname, self._overrides[partname]) - return _types_elm diff --git a/docx/opc/shared.py b/docx/opc/shared.py deleted file mode 100644 index 55344483d..000000000 --- a/docx/opc/shared.py +++ /dev/null @@ -1,47 +0,0 @@ -# encoding: utf-8 - -""" -Objects shared by opc modules. -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -class CaseInsensitiveDict(dict): - """ - Mapping type that behaves like dict except that it matches without respect - to the case of the key. E.g. cid['A'] == cid['a']. Note this is not - general-purpose, just complete enough to satisfy opc package needs. It - assumes str keys, and that it is created empty; keys passed in constructor - are not accounted for - """ - def __contains__(self, key): - return super(CaseInsensitiveDict, self).__contains__(key.lower()) - - def __getitem__(self, key): - return super(CaseInsensitiveDict, self).__getitem__(key.lower()) - - def __setitem__(self, key, value): - return super(CaseInsensitiveDict, self).__setitem__( - key.lower(), value - ) - - -def lazyproperty(f): - """ - @lazyprop decorator. Decorated method will be called only on first access - to calculate a cached property value. After that, the cached value is - returned. - """ - cache_attr_name = '_%s' % f.__name__ # like '_foobar' for prop 'foobar' - docstring = f.__doc__ - - def get_prop_value(obj): - try: - return getattr(obj, cache_attr_name) - except AttributeError: - value = f(obj) - setattr(obj, cache_attr_name, value) - return value - - return property(get_prop_value, doc=docstring) diff --git a/docx/opc/spec.py b/docx/opc/spec.py deleted file mode 100644 index 60fc38564..000000000 --- a/docx/opc/spec.py +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: utf-8 - -""" -Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500. -""" - -from .constants import CONTENT_TYPE as CT - - -default_content_types = ( - ('bin', CT.PML_PRINTER_SETTINGS), - ('bin', CT.SML_PRINTER_SETTINGS), - ('bin', CT.WML_PRINTER_SETTINGS), - ('bmp', CT.BMP), - ('emf', CT.X_EMF), - ('fntdata', CT.X_FONTDATA), - ('gif', CT.GIF), - ('jpe', CT.JPEG), - ('jpeg', CT.JPEG), - ('jpg', CT.JPEG), - ('png', CT.PNG), - ('rels', CT.OPC_RELATIONSHIPS), - ('tif', CT.TIFF), - ('tiff', CT.TIFF), - ('wdp', CT.MS_PHOTO), - ('wmf', CT.X_WMF), - ('xlsx', CT.SML_SHEET), - ('xml', CT.XML), -) diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py deleted file mode 100644 index 093c1b45b..000000000 --- a/docx/oxml/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -# encoding: utf-8 - -""" -Initializes oxml sub-package, including registering custom element classes -corresponding to Open XML elements. -""" - -from __future__ import absolute_import - -from lxml import etree - -from .ns import NamespacePrefixedTag, nsmap - - -# configure XML parser -element_class_lookup = etree.ElementNamespaceClassLookup() -oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False) -oxml_parser.set_element_class_lookup(element_class_lookup) - - -def parse_xml(xml): - """ - Return root lxml element obtained by parsing XML character string in - *xml*, which can be either a Python 2.x string or unicode. The custom - parser is used, so custom element classes are produced for elements in - *xml* that have them. - """ - root_element = etree.fromstring(xml, oxml_parser) - return root_element - - -def register_element_cls(tag, cls): - """ - Register *cls* to be constructed when the oxml parser encounters an - element with matching *tag*. *tag* is a string of the form - ``nspfx:tagroot``, e.g. ``'w:document'``. - """ - nspfx, tagroot = tag.split(':') - namespace = element_class_lookup.get_namespace(nsmap[nspfx]) - namespace[tagroot] = cls - - -def OxmlElement(nsptag_str, attrs=None, nsdecls=None): - """ - Return a 'loose' lxml element having the tag specified by *nsptag_str*. - *nsptag_str* must contain the standard namespace prefix, e.g. 'a:tbl'. - The resulting element is an instance of the custom element class for this - tag name if one is defined. A dictionary of attribute values may be - provided as *attrs*; they are set if present. All namespaces defined in - the dict *nsdecls* are declared in the element using the key as the - prefix and the value as the namespace name. If *nsdecls* is not provided, - a single namespace declaration is added based on the prefix on - *nsptag_str*. - """ - nsptag = NamespacePrefixedTag(nsptag_str) - if nsdecls is None: - nsdecls = nsptag.nsmap - return oxml_parser.makeelement( - nsptag.clark_name, attrib=attrs, nsmap=nsdecls - ) - - -# =========================================================================== -# custom element class mappings -# =========================================================================== - -from .shared import CT_DecimalNumber, CT_OnOff, CT_String # noqa -register_element_cls("w:evenAndOddHeaders", CT_OnOff) -register_element_cls("w:titlePg", CT_OnOff) - - -from .coreprops import CT_CoreProperties # noqa -register_element_cls('cp:coreProperties', CT_CoreProperties) - -from .document import CT_Body, CT_Document # noqa -register_element_cls('w:body', CT_Body) -register_element_cls('w:document', CT_Document) - -from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr # noqa -register_element_cls('w:abstractNumId', CT_DecimalNumber) -register_element_cls('w:ilvl', CT_DecimalNumber) -register_element_cls('w:lvlOverride', CT_NumLvl) -register_element_cls('w:num', CT_Num) -register_element_cls('w:numId', CT_DecimalNumber) -register_element_cls('w:numPr', CT_NumPr) -register_element_cls('w:numbering', CT_Numbering) -register_element_cls('w:startOverride', CT_DecimalNumber) - -from .section import ( # noqa - CT_HdrFtr, - CT_HdrFtrRef, - CT_PageMar, - CT_PageSz, - CT_SectPr, - CT_SectType, -) -register_element_cls("w:footerReference", CT_HdrFtrRef) -register_element_cls("w:ftr", CT_HdrFtr) -register_element_cls("w:hdr", CT_HdrFtr) -register_element_cls("w:headerReference", CT_HdrFtrRef) -register_element_cls("w:pgMar", CT_PageMar) -register_element_cls("w:pgSz", CT_PageSz) -register_element_cls("w:sectPr", CT_SectPr) -register_element_cls("w:type", CT_SectType) - -from .settings import CT_Settings # noqa -register_element_cls("w:settings", CT_Settings) - -from .shape import ( # noqa - CT_Blip, - CT_BlipFillProperties, - CT_GraphicalObject, - CT_GraphicalObjectData, - CT_Inline, - CT_NonVisualDrawingProps, - CT_Picture, - CT_PictureNonVisual, - CT_Point2D, - CT_PositiveSize2D, - CT_ShapeProperties, - CT_Transform2D, -) -register_element_cls('a:blip', CT_Blip) -register_element_cls('a:ext', CT_PositiveSize2D) -register_element_cls('a:graphic', CT_GraphicalObject) -register_element_cls('a:graphicData', CT_GraphicalObjectData) -register_element_cls('a:off', CT_Point2D) -register_element_cls('a:xfrm', CT_Transform2D) -register_element_cls('pic:blipFill', CT_BlipFillProperties) -register_element_cls('pic:cNvPr', CT_NonVisualDrawingProps) -register_element_cls('pic:nvPicPr', CT_PictureNonVisual) -register_element_cls('pic:pic', CT_Picture) -register_element_cls('pic:spPr', CT_ShapeProperties) -register_element_cls('wp:docPr', CT_NonVisualDrawingProps) -register_element_cls('wp:extent', CT_PositiveSize2D) -register_element_cls('wp:inline', CT_Inline) - -from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa -register_element_cls('w:basedOn', CT_String) -register_element_cls('w:latentStyles', CT_LatentStyles) -register_element_cls('w:locked', CT_OnOff) -register_element_cls('w:lsdException', CT_LsdException) -register_element_cls('w:name', CT_String) -register_element_cls('w:next', CT_String) -register_element_cls('w:qFormat', CT_OnOff) -register_element_cls('w:semiHidden', CT_OnOff) -register_element_cls('w:style', CT_Style) -register_element_cls('w:styles', CT_Styles) -register_element_cls('w:uiPriority', CT_DecimalNumber) -register_element_cls('w:unhideWhenUsed', CT_OnOff) - -from .table import ( # noqa - CT_Height, - CT_Row, - CT_Tbl, - CT_TblGrid, - CT_TblGridCol, - CT_TblLayoutType, - CT_TblPr, - CT_TblWidth, - CT_Tc, - CT_TcPr, - CT_TrPr, - CT_VMerge, - CT_VerticalJc, -) -register_element_cls('w:bidiVisual', CT_OnOff) -register_element_cls('w:gridCol', CT_TblGridCol) -register_element_cls('w:gridSpan', CT_DecimalNumber) -register_element_cls('w:tbl', CT_Tbl) -register_element_cls('w:tblGrid', CT_TblGrid) -register_element_cls('w:tblLayout', CT_TblLayoutType) -register_element_cls('w:tblPr', CT_TblPr) -register_element_cls('w:tblStyle', CT_String) -register_element_cls('w:tc', CT_Tc) -register_element_cls('w:tcPr', CT_TcPr) -register_element_cls('w:tcW', CT_TblWidth) -register_element_cls('w:tr', CT_Row) -register_element_cls('w:trHeight', CT_Height) -register_element_cls('w:trPr', CT_TrPr) -register_element_cls('w:vAlign', CT_VerticalJc) -register_element_cls('w:vMerge', CT_VMerge) - -from .text.font import ( # noqa - CT_Color, - CT_Fonts, - CT_Highlight, - CT_HpsMeasure, - CT_RPr, - CT_Underline, - CT_VerticalAlignRun, -) -register_element_cls('w:b', CT_OnOff) -register_element_cls('w:bCs', CT_OnOff) -register_element_cls('w:caps', CT_OnOff) -register_element_cls('w:color', CT_Color) -register_element_cls('w:cs', CT_OnOff) -register_element_cls('w:dstrike', CT_OnOff) -register_element_cls('w:emboss', CT_OnOff) -register_element_cls('w:highlight', CT_Highlight) -register_element_cls('w:i', CT_OnOff) -register_element_cls('w:iCs', CT_OnOff) -register_element_cls('w:imprint', CT_OnOff) -register_element_cls('w:noProof', CT_OnOff) -register_element_cls('w:oMath', CT_OnOff) -register_element_cls('w:outline', CT_OnOff) -register_element_cls('w:rFonts', CT_Fonts) -register_element_cls('w:rPr', CT_RPr) -register_element_cls('w:rStyle', CT_String) -register_element_cls('w:rtl', CT_OnOff) -register_element_cls('w:shadow', CT_OnOff) -register_element_cls('w:smallCaps', CT_OnOff) -register_element_cls('w:snapToGrid', CT_OnOff) -register_element_cls('w:specVanish', CT_OnOff) -register_element_cls('w:strike', CT_OnOff) -register_element_cls('w:sz', CT_HpsMeasure) -register_element_cls('w:u', CT_Underline) -register_element_cls('w:vanish', CT_OnOff) -register_element_cls('w:vertAlign', CT_VerticalAlignRun) -register_element_cls('w:webHidden', CT_OnOff) - -from .text.paragraph import CT_P # noqa -register_element_cls('w:p', CT_P) - -from .text.parfmt import ( # noqa - CT_Ind, - CT_Jc, - CT_PPr, - CT_Spacing, - CT_TabStop, - CT_TabStops, -) -register_element_cls('w:ind', CT_Ind) -register_element_cls('w:jc', CT_Jc) -register_element_cls('w:keepLines', CT_OnOff) -register_element_cls('w:keepNext', CT_OnOff) -register_element_cls('w:pageBreakBefore', CT_OnOff) -register_element_cls('w:pPr', CT_PPr) -register_element_cls('w:pStyle', CT_String) -register_element_cls('w:spacing', CT_Spacing) -register_element_cls('w:tab', CT_TabStop) -register_element_cls('w:tabs', CT_TabStops) -register_element_cls('w:widowControl', CT_OnOff) - -from .text.run import CT_Br, CT_R, CT_Text # noqa -register_element_cls('w:br', CT_Br) -register_element_cls('w:r', CT_R) -register_element_cls('w:t', CT_Text) diff --git a/docx/oxml/coreprops.py b/docx/oxml/coreprops.py deleted file mode 100644 index ed3dd1001..000000000 --- a/docx/oxml/coreprops.py +++ /dev/null @@ -1,317 +0,0 @@ -# encoding: utf-8 - -"""Custom element classes for core properties-related XML elements""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -import re - -from datetime import datetime, timedelta - -from docx.compat import is_string -from docx.oxml import parse_xml -from docx.oxml.ns import nsdecls, qn -from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne - - -class CT_CoreProperties(BaseOxmlElement): - """ - ```` element, the root element of the Core Properties - part stored as ``/docProps/core.xml``. Implements many of the Dublin Core - document metadata elements. String elements resolve to an empty string - ('') if the element is not present in the XML. String elements are - limited in length to 255 unicode characters. - """ - category = ZeroOrOne('cp:category', successors=()) - contentStatus = ZeroOrOne('cp:contentStatus', successors=()) - created = ZeroOrOne('dcterms:created', successors=()) - creator = ZeroOrOne('dc:creator', successors=()) - description = ZeroOrOne('dc:description', successors=()) - identifier = ZeroOrOne('dc:identifier', successors=()) - keywords = ZeroOrOne('cp:keywords', successors=()) - language = ZeroOrOne('dc:language', successors=()) - lastModifiedBy = ZeroOrOne('cp:lastModifiedBy', successors=()) - lastPrinted = ZeroOrOne('cp:lastPrinted', successors=()) - modified = ZeroOrOne('dcterms:modified', successors=()) - revision = ZeroOrOne('cp:revision', successors=()) - subject = ZeroOrOne('dc:subject', successors=()) - title = ZeroOrOne('dc:title', successors=()) - version = ZeroOrOne('cp:version', successors=()) - - _coreProperties_tmpl = ( - '\n' % nsdecls('cp', 'dc', 'dcterms') - ) - - @classmethod - def new(cls): - """ - Return a new ```` element - """ - xml = cls._coreProperties_tmpl - coreProperties = parse_xml(xml) - return coreProperties - - @property - def author_text(self): - """ - The text in the `dc:creator` child element. - """ - return self._text_of_element('creator') - - @author_text.setter - def author_text(self, value): - self._set_element_text('creator', value) - - @property - def category_text(self): - return self._text_of_element('category') - - @category_text.setter - def category_text(self, value): - self._set_element_text('category', value) - - @property - def comments_text(self): - return self._text_of_element('description') - - @comments_text.setter - def comments_text(self, value): - self._set_element_text('description', value) - - @property - def contentStatus_text(self): - return self._text_of_element('contentStatus') - - @contentStatus_text.setter - def contentStatus_text(self, value): - self._set_element_text('contentStatus', value) - - @property - def created_datetime(self): - return self._datetime_of_element('created') - - @created_datetime.setter - def created_datetime(self, value): - self._set_element_datetime('created', value) - - @property - def identifier_text(self): - return self._text_of_element('identifier') - - @identifier_text.setter - def identifier_text(self, value): - self._set_element_text('identifier', value) - - @property - def keywords_text(self): - return self._text_of_element('keywords') - - @keywords_text.setter - def keywords_text(self, value): - self._set_element_text('keywords', value) - - @property - def language_text(self): - return self._text_of_element('language') - - @language_text.setter - def language_text(self, value): - self._set_element_text('language', value) - - @property - def lastModifiedBy_text(self): - return self._text_of_element('lastModifiedBy') - - @lastModifiedBy_text.setter - def lastModifiedBy_text(self, value): - self._set_element_text('lastModifiedBy', value) - - @property - def lastPrinted_datetime(self): - return self._datetime_of_element('lastPrinted') - - @lastPrinted_datetime.setter - def lastPrinted_datetime(self, value): - self._set_element_datetime('lastPrinted', value) - - @property - def modified_datetime(self): - return self._datetime_of_element('modified') - - @modified_datetime.setter - def modified_datetime(self, value): - self._set_element_datetime('modified', value) - - @property - def revision_number(self): - """ - Integer value of revision property. - """ - revision = self.revision - if revision is None: - return 0 - revision_str = revision.text - try: - revision = int(revision_str) - except ValueError: - # non-integer revision strings also resolve to 0 - revision = 0 - # as do negative integers - if revision < 0: - revision = 0 - return revision - - @revision_number.setter - def revision_number(self, value): - """ - Set revision property to string value of integer *value*. - """ - if not isinstance(value, int) or value < 1: - tmpl = "revision property requires positive int, got '%s'" - raise ValueError(tmpl % value) - revision = self.get_or_add_revision() - revision.text = str(value) - - @property - def subject_text(self): - return self._text_of_element('subject') - - @subject_text.setter - def subject_text(self, value): - self._set_element_text('subject', value) - - @property - def title_text(self): - return self._text_of_element('title') - - @title_text.setter - def title_text(self, value): - self._set_element_text('title', value) - - @property - def version_text(self): - return self._text_of_element('version') - - @version_text.setter - def version_text(self, value): - self._set_element_text('version', value) - - def _datetime_of_element(self, property_name): - element = getattr(self, property_name) - if element is None: - return None - datetime_str = element.text - try: - return self._parse_W3CDTF_to_datetime(datetime_str) - except ValueError: - # invalid datetime strings are ignored - return None - - def _get_or_add(self, prop_name): - """ - Return element returned by 'get_or_add_' method for *prop_name*. - """ - get_or_add_method_name = 'get_or_add_%s' % prop_name - get_or_add_method = getattr(self, get_or_add_method_name) - element = get_or_add_method() - return element - - @classmethod - def _offset_dt(cls, dt, offset_str): - """ - Return a |datetime| instance that is offset from datetime *dt* by - the timezone offset specified in *offset_str*, a string like - ``'-07:00'``. - """ - match = cls._offset_pattern.match(offset_str) - if match is None: - raise ValueError( - "'%s' is not a valid offset string" % offset_str - ) - sign, hours_str, minutes_str = match.groups() - sign_factor = -1 if sign == '+' else 1 - hours = int(hours_str) * sign_factor - minutes = int(minutes_str) * sign_factor - td = timedelta(hours=hours, minutes=minutes) - return dt + td - - _offset_pattern = re.compile(r'([+-])(\d\d):(\d\d)') - - @classmethod - def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): - # valid W3CDTF date cases: - # yyyy e.g. '2003' - # yyyy-mm e.g. '2003-12' - # yyyy-mm-dd e.g. '2003-12-31' - # UTC timezone e.g. '2003-12-31T10:14:55Z' - # numeric timezone e.g. '2003-12-31T10:14:55-08:00' - templates = ( - '%Y-%m-%dT%H:%M:%S', - '%Y-%m-%d', - '%Y-%m', - '%Y', - ) - # strptime isn't smart enough to parse literal timezone offsets like - # '-07:30', so we have to do it ourselves - parseable_part = w3cdtf_str[:19] - offset_str = w3cdtf_str[19:] - dt = None - for tmpl in templates: - try: - dt = datetime.strptime(parseable_part, tmpl) - except ValueError: - continue - if dt is None: - tmpl = "could not parse W3CDTF datetime string '%s'" - raise ValueError(tmpl % w3cdtf_str) - if len(offset_str) == 6: - return cls._offset_dt(dt, offset_str) - return dt - - def _set_element_datetime(self, prop_name, value): - """ - Set date/time value of child element having *prop_name* to *value*. - """ - if not isinstance(value, datetime): - tmpl = ( - "property requires object, got %s" - ) - raise ValueError(tmpl % type(value)) - element = self._get_or_add(prop_name) - dt_str = value.strftime('%Y-%m-%dT%H:%M:%SZ') - element.text = dt_str - if prop_name in ('created', 'modified'): - # These two require an explicit 'xsi:type="dcterms:W3CDTF"' - # attribute. The first and last line are a hack required to add - # the xsi namespace to the root element rather than each child - # element in which it is referenced - self.set(qn('xsi:foo'), 'bar') - element.set(qn('xsi:type'), 'dcterms:W3CDTF') - del self.attrib[qn('xsi:foo')] - - def _set_element_text(self, prop_name, value): - """Set string value of *name* property to *value*.""" - if not is_string(value): - value = str(value) - - if len(value) > 255: - tmpl = ( - "exceeded 255 char limit for property, got:\n\n'%s'" - ) - raise ValueError(tmpl % value) - element = self._get_or_add(prop_name) - element.text = value - - def _text_of_element(self, property_name): - """ - Return the text in the element matching *property_name*, or an empty - string if the element is not present or contains no text. - """ - element = getattr(self, property_name) - if element is None: - return '' - if element.text is None: - return '' - return element.text diff --git a/docx/oxml/document.py b/docx/oxml/document.py deleted file mode 100644 index 4211b8ed1..000000000 --- a/docx/oxml/document.py +++ /dev/null @@ -1,67 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes that correspond to the document part, e.g. -. -""" - -from .xmlchemy import BaseOxmlElement, ZeroOrOne, ZeroOrMore - - -class CT_Document(BaseOxmlElement): - """ - ```` element, the root element of a document.xml file. - """ - body = ZeroOrOne('w:body') - - @property - def sectPr_lst(self): - """ - Return a list containing a reference to each ```` element - in the document, in the order encountered. - """ - return self.xpath('.//w:sectPr') - - -class CT_Body(BaseOxmlElement): - """ - ````, the container element for the main document story in - ``document.xml``. - """ - p = ZeroOrMore('w:p', successors=('w:sectPr',)) - tbl = ZeroOrMore('w:tbl', successors=('w:sectPr',)) - sectPr = ZeroOrOne('w:sectPr', successors=()) - - def add_section_break(self): - """Return `w:sectPr` element for new section added at end of document. - - The last `w:sectPr` becomes the second-to-last, with the new `w:sectPr` being an - exact clone of the previous one, except that all header and footer references - are removed (and are therefore now "inherited" from the prior section). - - A copy of the previously-last `w:sectPr` will now appear in a new `w:p` at the - end of the document. The returned `w:sectPr` is the sentinel `w:sectPr` for the - document (and as implemented, *is* the prior sentinel `w:sectPr` with headers - and footers removed). - """ - # ---get the sectPr at file-end, which controls last section (sections[-1])--- - sentinel_sectPr = self.get_or_add_sectPr() - # ---add exact copy to new `w:p` element; that is now second-to last section--- - self.add_p().set_sectPr(sentinel_sectPr.clone()) - # ---remove any header or footer references from "new" last section--- - for hdrftr_ref in sentinel_sectPr.xpath("w:headerReference|w:footerReference"): - sentinel_sectPr.remove(hdrftr_ref) - # ---the sentinel `w:sectPr` now controls the new last section--- - return sentinel_sectPr - - def clear_content(self): - """ - Remove all content child elements from this element. Leave - the element if it is present. - """ - if self.sectPr is not None: - content_elms = self[:-1] - else: - content_elms = self[:] - for content_elm in content_elms: - self.remove(content_elm) diff --git a/docx/oxml/exceptions.py b/docx/oxml/exceptions.py deleted file mode 100644 index 4696f1e93..000000000 --- a/docx/oxml/exceptions.py +++ /dev/null @@ -1,16 +0,0 @@ -# encoding: utf-8 - -""" -Exceptions for oxml sub-package -""" - - -class XmlchemyError(Exception): - """Generic error class.""" - - -class InvalidXmlError(XmlchemyError): - """ - Raised when invalid XML is encountered, such as on attempt to access a - missing required child element - """ diff --git a/docx/oxml/ns.py b/docx/oxml/ns.py deleted file mode 100644 index 6b0861284..000000000 --- a/docx/oxml/ns.py +++ /dev/null @@ -1,114 +0,0 @@ -# encoding: utf-8 - -""" -Namespace-related objects. -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -nsmap = { - "a": "http://schemas.openxmlformats.org/drawingml/2006/main", - "c": "http://schemas.openxmlformats.org/drawingml/2006/chart", - "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", - "dc": "http://purl.org/dc/elements/1.1/", - "dcmitype": "http://purl.org/dc/dcmitype/", - "dcterms": "http://purl.org/dc/terms/", - "dgm": "http://schemas.openxmlformats.org/drawingml/2006/diagram", - "m": "http://schemas.openxmlformats.org/officeDocument/2006/math", - "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", - "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - "sl": "http://schemas.openxmlformats.org/schemaLibrary/2006/main", - "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", - 'w14': "http://schemas.microsoft.com/office/word/2010/wordml", - "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", - "xml": "http://www.w3.org/XML/1998/namespace", - "xsi": "http://www.w3.org/2001/XMLSchema-instance", -} - -pfxmap = dict((value, key) for key, value in nsmap.items()) - - -class NamespacePrefixedTag(str): - """ - Value object that knows the semantics of an XML tag having a namespace - prefix. - """ - def __new__(cls, nstag, *args): - return super(NamespacePrefixedTag, cls).__new__(cls, nstag) - - def __init__(self, nstag): - self._pfx, self._local_part = nstag.split(':') - self._ns_uri = nsmap[self._pfx] - - @property - def clark_name(self): - return '{%s}%s' % (self._ns_uri, self._local_part) - - @classmethod - def from_clark_name(cls, clark_name): - nsuri, local_name = clark_name[1:].split('}') - nstag = '%s:%s' % (pfxmap[nsuri], local_name) - return cls(nstag) - - @property - def local_part(self): - """ - Return the local part of the tag as a string. E.g. 'foobar' is - returned for tag 'f:foobar'. - """ - return self._local_part - - @property - def nsmap(self): - """ - Return a dict having a single member, mapping the namespace prefix of - this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This - is handy for passing to xpath calls and other uses. - """ - return {self._pfx: self._ns_uri} - - @property - def nspfx(self): - """ - Return the string namespace prefix for the tag, e.g. 'f' is returned - for tag 'f:foobar'. - """ - return self._pfx - - @property - def nsuri(self): - """ - Return the namespace URI for the tag, e.g. 'http://foo/bar' would be - returned for tag 'f:foobar' if the 'f' prefix maps to - 'http://foo/bar' in nsmap. - """ - return self._ns_uri - - -def nsdecls(*prefixes): - """ - Return a string containing a namespace declaration for each of the - namespace prefix strings, e.g. 'p', 'ct', passed as *prefixes*. - """ - return ' '.join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes]) - - -def nspfxmap(*nspfxs): - """ - Return a dict containing the subset namespace prefix mappings specified by - *nspfxs*. Any number of namespace prefixes can be supplied, e.g. - namespaces('a', 'r', 'p'). - """ - return dict((pfx, nsmap[pfx]) for pfx in nspfxs) - - -def qn(tag): - """ - Stands for "qualified name", a utility function to turn a namespace - prefixed tag name into a Clark-notation qualified tag name for lxml. For - example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``. - """ - prefix, tagroot = tag.split(':') - uri = nsmap[prefix] - return '{%s}%s' % (uri, tagroot) diff --git a/docx/oxml/numbering.py b/docx/oxml/numbering.py deleted file mode 100644 index aeedfa9a0..000000000 --- a/docx/oxml/numbering.py +++ /dev/null @@ -1,131 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to the numbering part -""" - -from . import OxmlElement -from .shared import CT_DecimalNumber -from .simpletypes import ST_DecimalNumber -from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrMore, ZeroOrOne -) - - -class CT_Num(BaseOxmlElement): - """ - ```` element, which represents a concrete list definition - instance, having a required child that references an - abstract numbering definition that defines most of the formatting details. - """ - abstractNumId = OneAndOnlyOne('w:abstractNumId') - lvlOverride = ZeroOrMore('w:lvlOverride') - numId = RequiredAttribute('w:numId', ST_DecimalNumber) - - def add_lvlOverride(self, ilvl): - """ - Return a newly added CT_NumLvl () element having its - ``ilvl`` attribute set to *ilvl*. - """ - return self._add_lvlOverride(ilvl=ilvl) - - @classmethod - def new(cls, num_id, abstractNum_id): - """ - Return a new ```` element having numId of *num_id* and having - a ```` child with val attribute set to - *abstractNum_id*. - """ - num = OxmlElement('w:num') - num.numId = num_id - abstractNumId = CT_DecimalNumber.new( - 'w:abstractNumId', abstractNum_id - ) - num.append(abstractNumId) - return num - - -class CT_NumLvl(BaseOxmlElement): - """ - ```` element, which identifies a level in a list - definition to override with settings it contains. - """ - startOverride = ZeroOrOne('w:startOverride', successors=('w:lvl',)) - ilvl = RequiredAttribute('w:ilvl', ST_DecimalNumber) - - def add_startOverride(self, val): - """ - Return a newly added CT_DecimalNumber element having tagname - ``w:startOverride`` and ``val`` attribute set to *val*. - """ - return self._add_startOverride(val=val) - - -class CT_NumPr(BaseOxmlElement): - """ - A ```` element, a container for numbering properties applied to - a paragraph. - """ - ilvl = ZeroOrOne('w:ilvl', successors=( - 'w:numId', 'w:numberingChange', 'w:ins' - )) - numId = ZeroOrOne('w:numId', successors=('w:numberingChange', 'w:ins')) - - # @ilvl.setter - # def _set_ilvl(self, val): - # """ - # Get or add a child and set its ``w:val`` attribute to *val*. - # """ - # ilvl = self.get_or_add_ilvl() - # ilvl.val = val - - # @numId.setter - # def numId(self, val): - # """ - # Get or add a child and set its ``w:val`` attribute to - # *val*. - # """ - # numId = self.get_or_add_numId() - # numId.val = val - - -class CT_Numbering(BaseOxmlElement): - """ - ```` element, the root element of a numbering part, i.e. - numbering.xml - """ - num = ZeroOrMore('w:num', successors=('w:numIdMacAtCleanup',)) - - def add_num(self, abstractNum_id): - """ - Return a newly added CT_Num () element referencing the - abstract numbering definition identified by *abstractNum_id*. - """ - next_num_id = self._next_numId - num = CT_Num.new(next_num_id, abstractNum_id) - return self._insert_num(num) - - def num_having_numId(self, numId): - """ - Return the ```` child element having ``numId`` attribute - matching *numId*. - """ - xpath = './w:num[@w:numId="%d"]' % numId - try: - return self.xpath(xpath)[0] - except IndexError: - raise KeyError('no element with numId %d' % numId) - - @property - def _next_numId(self): - """ - The first ``numId`` unused by a ```` element, starting at - 1 and filling any gaps in numbering between existing ```` - elements. - """ - numId_strs = self.xpath('./w:num/@w:numId') - num_ids = [int(numId_str) for numId_str in numId_strs] - for num in range(1, len(num_ids)+2): - if num not in num_ids: - break - return num diff --git a/docx/oxml/section.py b/docx/oxml/section.py deleted file mode 100644 index fc953e74d..000000000 --- a/docx/oxml/section.py +++ /dev/null @@ -1,351 +0,0 @@ -# encoding: utf-8 - -"""Section-related custom element classes""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from copy import deepcopy - -from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START -from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString -from docx.oxml.xmlchemy import ( - BaseOxmlElement, - OptionalAttribute, - RequiredAttribute, - ZeroOrMore, - ZeroOrOne, -) - - -class CT_HdrFtr(BaseOxmlElement): - """`w:hdr` and `w:ftr`, the root element for header and footer part respectively""" - - p = ZeroOrMore('w:p', successors=()) - tbl = ZeroOrMore('w:tbl', successors=()) - - -class CT_HdrFtrRef(BaseOxmlElement): - """`w:headerReference` and `w:footerReference` elements""" - - type_ = RequiredAttribute('w:type', WD_HEADER_FOOTER) - rId = RequiredAttribute('r:id', XsdString) - - -class CT_PageMar(BaseOxmlElement): - """ - ```` element, defining page margins. - """ - top = OptionalAttribute('w:top', ST_SignedTwipsMeasure) - right = OptionalAttribute('w:right', ST_TwipsMeasure) - bottom = OptionalAttribute('w:bottom', ST_SignedTwipsMeasure) - left = OptionalAttribute('w:left', ST_TwipsMeasure) - header = OptionalAttribute('w:header', ST_TwipsMeasure) - footer = OptionalAttribute('w:footer', ST_TwipsMeasure) - gutter = OptionalAttribute('w:gutter', ST_TwipsMeasure) - - -class CT_PageSz(BaseOxmlElement): - """ - ```` element, defining page dimensions and orientation. - """ - w = OptionalAttribute('w:w', ST_TwipsMeasure) - h = OptionalAttribute('w:h', ST_TwipsMeasure) - orient = OptionalAttribute( - 'w:orient', WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT - ) - - -class CT_SectPr(BaseOxmlElement): - """`w:sectPr` element, the container element for section properties""" - - _tag_seq = ( - 'w:footnotePr', 'w:endnotePr', 'w:type', 'w:pgSz', 'w:pgMar', 'w:paperSrc', - 'w:pgBorders', 'w:lnNumType', 'w:pgNumType', 'w:cols', 'w:formProt', 'w:vAlign', - 'w:noEndnote', 'w:titlePg', 'w:textDirection', 'w:bidi', 'w:rtlGutter', - 'w:docGrid', 'w:printerSettings', 'w:sectPrChange', - ) - headerReference = ZeroOrMore("w:headerReference", successors=_tag_seq) - footerReference = ZeroOrMore("w:footerReference", successors=_tag_seq) - type = ZeroOrOne("w:type", successors=_tag_seq[3:]) - pgSz = ZeroOrOne("w:pgSz", successors=_tag_seq[4:]) - pgMar = ZeroOrOne("w:pgMar", successors=_tag_seq[5:]) - titlePg = ZeroOrOne("w:titlePg", successors=_tag_seq[14:]) - del _tag_seq - - def add_footerReference(self, type_, rId): - """Return newly added CT_HdrFtrRef element of *type_* with *rId*. - - The element tag is `w:footerReference`. - """ - footerReference = self._add_footerReference() - footerReference.type_ = type_ - footerReference.rId = rId - return footerReference - - def add_headerReference(self, type_, rId): - """Return newly added CT_HdrFtrRef element of *type_* with *rId*. - - The element tag is `w:headerReference`. - """ - headerReference = self._add_headerReference() - headerReference.type_ = type_ - headerReference.rId = rId - return headerReference - - @property - def bottom_margin(self): - """ - The value of the ``w:bottom`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.bottom - - @bottom_margin.setter - def bottom_margin(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.bottom = value - - def clone(self): - """ - Return an exact duplicate of this ```` element tree - suitable for use in adding a section break. All rsid* attributes are - removed from the root ```` element. - """ - clone_sectPr = deepcopy(self) - clone_sectPr.attrib.clear() - return clone_sectPr - - @property - def footer(self): - """ - The value of the ``w:footer`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.footer - - @footer.setter - def footer(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.footer = value - - def get_footerReference(self, type_): - """Return footerReference element of *type_* or None if not present.""" - path = "./w:footerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_) - footerReferences = self.xpath(path) - if not footerReferences: - return None - return footerReferences[0] - - def get_headerReference(self, type_): - """Return headerReference element of *type_* or None if not present.""" - matching_headerReferences = self.xpath( - "./w:headerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_) - ) - if len(matching_headerReferences) == 0: - return None - return matching_headerReferences[0] - - @property - def gutter(self): - """ - The value of the ``w:gutter`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.gutter - - @gutter.setter - def gutter(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.gutter = value - - @property - def header(self): - """ - The value of the ``w:header`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.header - - @header.setter - def header(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.header = value - - @property - def left_margin(self): - """ - The value of the ``w:left`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.left - - @left_margin.setter - def left_margin(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.left = value - - @property - def orientation(self): - """ - The member of the ``WD_ORIENTATION`` enumeration corresponding to the - value of the ``orient`` attribute of the ```` child element, - or ``WD_ORIENTATION.PORTRAIT`` if not present. - """ - pgSz = self.pgSz - if pgSz is None: - return WD_ORIENTATION.PORTRAIT - return pgSz.orient - - @orientation.setter - def orientation(self, value): - pgSz = self.get_or_add_pgSz() - pgSz.orient = value - - @property - def page_height(self): - """ - Value in EMU of the ``h`` attribute of the ```` child - element, or |None| if not present. - """ - pgSz = self.pgSz - if pgSz is None: - return None - return pgSz.h - - @page_height.setter - def page_height(self, value): - pgSz = self.get_or_add_pgSz() - pgSz.h = value - - @property - def page_width(self): - """ - Value in EMU of the ``w`` attribute of the ```` child - element, or |None| if not present. - """ - pgSz = self.pgSz - if pgSz is None: - return None - return pgSz.w - - @page_width.setter - def page_width(self, value): - pgSz = self.get_or_add_pgSz() - pgSz.w = value - - @property - def preceding_sectPr(self): - """sectPr immediately preceding this one or None if this is the first.""" - # ---[1] predicate returns list of zero or one value--- - preceding_sectPrs = self.xpath("./preceding::w:sectPr[1]") - return preceding_sectPrs[0] if len(preceding_sectPrs) > 0 else None - - def remove_footerReference(self, type_): - """Return rId of w:footerReference child of *type_* after removing it.""" - footerReference = self.get_footerReference(type_) - rId = footerReference.rId - self.remove(footerReference) - return rId - - def remove_headerReference(self, type_): - """Return rId of w:headerReference child of *type_* after removing it.""" - headerReference = self.get_headerReference(type_) - rId = headerReference.rId - self.remove(headerReference) - return rId - - @property - def right_margin(self): - """ - The value of the ``w:right`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.right - - @right_margin.setter - def right_margin(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.right = value - - @property - def start_type(self): - """ - The member of the ``WD_SECTION_START`` enumeration corresponding to - the value of the ``val`` attribute of the ```` child element, - or ``WD_SECTION_START.NEW_PAGE`` if not present. - """ - type = self.type - if type is None or type.val is None: - return WD_SECTION_START.NEW_PAGE - return type.val - - @start_type.setter - def start_type(self, value): - if value is None or value is WD_SECTION_START.NEW_PAGE: - self._remove_type() - return - type = self.get_or_add_type() - type.val = value - - @property - def titlePg_val(self): - """Value of `w:titlePg/@val` or |None| if not present""" - titlePg = self.titlePg - if titlePg is None: - return False - return titlePg.val - - @titlePg_val.setter - def titlePg_val(self, value): - if value in [None, False]: - self._remove_titlePg() - else: - self.get_or_add_titlePg().val = value - - @property - def top_margin(self): - """ - The value of the ``w:top`` attribute in the ```` child - element, as a |Length| object, or |None| if either the element or the - attribute is not present. - """ - pgMar = self.pgMar - if pgMar is None: - return None - return pgMar.top - - @top_margin.setter - def top_margin(self, value): - pgMar = self.get_or_add_pgMar() - pgMar.top = value - - -class CT_SectType(BaseOxmlElement): - """ - ```` element, defining the section start type. - """ - val = OptionalAttribute('w:val', WD_SECTION_START) diff --git a/docx/oxml/settings.py b/docx/oxml/settings.py deleted file mode 100644 index fd319ad70..000000000 --- a/docx/oxml/settings.py +++ /dev/null @@ -1,64 +0,0 @@ -# encoding: utf-8 - -"""Custom element classes related to document settings""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne - - -class CT_Settings(BaseOxmlElement): - """`w:settings` element, root element for the settings part""" - - _tag_seq = ( - "w:writeProtection", "w:view", "w:zoom", "w:removePersonalInformation", - "w:removeDateAndTime", "w:doNotDisplayPageBoundaries", - "w:displayBackgroundShape", "w:printPostScriptOverText", - "w:printFractionalCharacterWidth", "w:printFormsData", "w:embedTrueTypeFonts", - "w:embedSystemFonts", "w:saveSubsetFonts", "w:saveFormsData", "w:mirrorMargins", - "w:alignBordersAndEdges", "w:bordersDoNotSurroundHeader", - "w:bordersDoNotSurroundFooter", "w:gutterAtTop", "w:hideSpellingErrors", - "w:hideGrammaticalErrors", "w:activeWritingStyle", "w:proofState", - "w:formsDesign", "w:attachedTemplate", "w:linkStyles", - "w:stylePaneFormatFilter", "w:stylePaneSortMethod", "w:documentType", - "w:mailMerge", "w:revisionView", "w:trackRevisions", "w:doNotTrackMoves", - "w:doNotTrackFormatting", "w:documentProtection", "w:autoFormatOverride", - "w:styleLockTheme", "w:styleLockQFSet", "w:defaultTabStop", "w:autoHyphenation", - "w:consecutiveHyphenLimit", "w:hyphenationZone", "w:doNotHyphenateCaps", - "w:showEnvelope", "w:summaryLength", "w:clickAndTypeStyle", - "w:defaultTableStyle", "w:evenAndOddHeaders", "w:bookFoldRevPrinting", - "w:bookFoldPrinting", "w:bookFoldPrintingSheets", - "w:drawingGridHorizontalSpacing", "w:drawingGridVerticalSpacing", - "w:displayHorizontalDrawingGridEvery", "w:displayVerticalDrawingGridEvery", - "w:doNotUseMarginsForDrawingGridOrigin", "w:drawingGridHorizontalOrigin", - "w:drawingGridVerticalOrigin", "w:doNotShadeFormData", "w:noPunctuationKerning", - "w:characterSpacingControl", "w:printTwoOnOne", "w:strictFirstAndLastChars", - "w:noLineBreaksAfter", "w:noLineBreaksBefore", "w:savePreviewPicture", - "w:doNotValidateAgainstSchema", "w:saveInvalidXml", "w:ignoreMixedContent", - "w:alwaysShowPlaceholderText", "w:doNotDemarcateInvalidXml", - "w:saveXmlDataOnly", "w:useXSLTWhenSaving", "w:saveThroughXslt", - "w:showXMLTags", "w:alwaysMergeEmptyNamespace", "w:updateFields", - "w:hdrShapeDefaults", "w:footnotePr", "w:endnotePr", "w:compat", "w:docVars", - "w:rsids", "m:mathPr", "w:attachedSchema", "w:themeFontLang", - "w:clrSchemeMapping", "w:doNotIncludeSubdocsInStats", - "w:doNotAutoCompressPictures", "w:forceUpgrade", "w:captions", - "w:readModeInkLockDown", "w:smartTagType", "sl:schemaLibrary", - "w:shapeDefaults", "w:doNotEmbedSmartTags", "w:decimalSymbol", "w:listSeparator" - ) - evenAndOddHeaders = ZeroOrOne("w:evenAndOddHeaders", successors=_tag_seq[48:]) - del _tag_seq - - @property - def evenAndOddHeaders_val(self): - """value of `w:evenAndOddHeaders/@w:val` or |None| if not present.""" - evenAndOddHeaders = self.evenAndOddHeaders - if evenAndOddHeaders is None: - return False - return evenAndOddHeaders.val - - @evenAndOddHeaders_val.setter - def evenAndOddHeaders_val(self, value): - if value in [None, False]: - self._remove_evenAndOddHeaders() - else: - self.get_or_add_evenAndOddHeaders().val = value diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py deleted file mode 100644 index 77ca7db8a..000000000 --- a/docx/oxml/shape.py +++ /dev/null @@ -1,284 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes for shape-related elements like ```` -""" - -from . import parse_xml -from .ns import nsdecls -from .simpletypes import ( - ST_Coordinate, ST_DrawingElementId, ST_PositiveCoordinate, - ST_RelationshipId, XsdString, XsdToken -) -from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, RequiredAttribute, - ZeroOrOne -) - - -class CT_Blip(BaseOxmlElement): - """ - ```` element, specifies image source and adjustments such as - alpha and tint. - """ - embed = OptionalAttribute('r:embed', ST_RelationshipId) - link = OptionalAttribute('r:link', ST_RelationshipId) - - -class CT_BlipFillProperties(BaseOxmlElement): - """ - ```` element, specifies picture properties - """ - blip = ZeroOrOne('a:blip', successors=( - 'a:srcRect', 'a:tile', 'a:stretch' - )) - - -class CT_GraphicalObject(BaseOxmlElement): - """ - ```` element, container for a DrawingML object - """ - graphicData = OneAndOnlyOne('a:graphicData') - - -class CT_GraphicalObjectData(BaseOxmlElement): - """ - ```` element, container for the XML of a DrawingML object - """ - pic = ZeroOrOne('pic:pic') - uri = RequiredAttribute('uri', XsdToken) - - -class CT_Inline(BaseOxmlElement): - """ - ```` element, container for an inline shape. - """ - extent = OneAndOnlyOne('wp:extent') - docPr = OneAndOnlyOne('wp:docPr') - graphic = OneAndOnlyOne('a:graphic') - - @classmethod - def new(cls, cx, cy, shape_id, pic): - """ - Return a new ```` element populated with the values passed - as parameters. - """ - inline = parse_xml(cls._inline_xml()) - inline.extent.cx = cx - inline.extent.cy = cy - inline.docPr.id = shape_id - inline.docPr.name = 'Picture %d' % shape_id - inline.graphic.graphicData.uri = ( - 'http://schemas.openxmlformats.org/drawingml/2006/picture' - ) - inline.graphic.graphicData._insert_pic(pic) - return inline - - @classmethod - def new_pic_inline(cls, shape_id, rId, filename, cx, cy): - """ - Return a new `wp:inline` element containing the `pic:pic` element - specified by the argument values. - """ - pic_id = 0 # Word doesn't seem to use this, but does not omit it - pic = CT_Picture.new(pic_id, filename, rId, cx, cy) - inline = cls.new(cx, cy, shape_id, pic) - inline.graphic.graphicData._insert_pic(pic) - return inline - - @classmethod - def _inline_xml(cls): - return ( - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - '' % nsdecls('wp', 'a', 'pic', 'r') - ) - - -class CT_NonVisualDrawingProps(BaseOxmlElement): - """ - Used for ```` element, and perhaps others. Specifies the id and - name of a DrawingML drawing. - """ - id = RequiredAttribute('id', ST_DrawingElementId) - name = RequiredAttribute('name', XsdString) - - -class CT_NonVisualPictureProperties(BaseOxmlElement): - """ - ```` element, specifies picture locking and resize - behaviors. - """ - - -class CT_Picture(BaseOxmlElement): - """ - ```` element, a DrawingML picture - """ - nvPicPr = OneAndOnlyOne('pic:nvPicPr') - blipFill = OneAndOnlyOne('pic:blipFill') - spPr = OneAndOnlyOne('pic:spPr') - - @classmethod - def new(cls, pic_id, filename, rId, cx, cy): - """ - Return a new ```` element populated with the minimal - contents required to define a viable picture element, based on the - values passed as parameters. - """ - pic = parse_xml(cls._pic_xml()) - pic.nvPicPr.cNvPr.id = pic_id - pic.nvPicPr.cNvPr.name = filename - pic.blipFill.blip.embed = rId - pic.spPr.cx = cx - pic.spPr.cy = cy - return pic - - @classmethod - def _pic_xml(cls): - return ( - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - '' % nsdecls('pic', 'a', 'r') - ) - - -class CT_PictureNonVisual(BaseOxmlElement): - """ - ```` element, non-visual picture properties - """ - cNvPr = OneAndOnlyOne('pic:cNvPr') - - -class CT_Point2D(BaseOxmlElement): - """ - Used for ```` element, and perhaps others. Specifies an x, y - coordinate (point). - """ - x = RequiredAttribute('x', ST_Coordinate) - y = RequiredAttribute('y', ST_Coordinate) - - -class CT_PositiveSize2D(BaseOxmlElement): - """ - Used for ```` element, and perhaps others later. Specifies the - size of a DrawingML drawing. - """ - cx = RequiredAttribute('cx', ST_PositiveCoordinate) - cy = RequiredAttribute('cy', ST_PositiveCoordinate) - - -class CT_PresetGeometry2D(BaseOxmlElement): - """ - ```` element, specifies an preset autoshape geometry, such - as ``rect``. - """ - - -class CT_RelativeRect(BaseOxmlElement): - """ - ```` element, specifying picture should fill containing - rectangle shape. - """ - - -class CT_ShapeProperties(BaseOxmlElement): - """ - ```` element, specifies size and shape of picture container. - """ - xfrm = ZeroOrOne('a:xfrm', successors=( - 'a:custGeom', 'a:prstGeom', 'a:ln', 'a:effectLst', 'a:effectDag', - 'a:scene3d', 'a:sp3d', 'a:extLst' - )) - - @property - def cx(self): - """ - Shape width as an instance of Emu, or None if not present. - """ - xfrm = self.xfrm - if xfrm is None: - return None - return xfrm.cx - - @cx.setter - def cx(self, value): - xfrm = self.get_or_add_xfrm() - xfrm.cx = value - - @property - def cy(self): - """ - Shape height as an instance of Emu, or None if not present. - """ - xfrm = self.xfrm - if xfrm is None: - return None - return xfrm.cy - - @cy.setter - def cy(self, value): - xfrm = self.get_or_add_xfrm() - xfrm.cy = value - - -class CT_StretchInfoProperties(BaseOxmlElement): - """ - ```` element, specifies how picture should fill its containing - shape. - """ - - -class CT_Transform2D(BaseOxmlElement): - """ - ```` element, specifies size and shape of picture container. - """ - off = ZeroOrOne('a:off', successors=('a:ext',)) - ext = ZeroOrOne('a:ext', successors=()) - - @property - def cx(self): - ext = self.ext - if ext is None: - return None - return ext.cx - - @cx.setter - def cx(self, value): - ext = self.get_or_add_ext() - ext.cx = value - - @property - def cy(self): - ext = self.ext - if ext is None: - return None - return ext.cy - - @cy.setter - def cy(self, value): - ext = self.get_or_add_ext() - ext.cy = value diff --git a/docx/oxml/shared.py b/docx/oxml/shared.py deleted file mode 100644 index 1e21ba366..000000000 --- a/docx/oxml/shared.py +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 - -""" -Objects shared by modules in the docx.oxml subpackage. -""" - -from __future__ import absolute_import - -from . import OxmlElement -from .ns import qn -from .simpletypes import ST_DecimalNumber, ST_OnOff, ST_String -from .xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute - - -class CT_DecimalNumber(BaseOxmlElement): - """ - Used for ````, ````, ```` and several - others, containing a text representation of a decimal number (e.g. 42) in - its ``val`` attribute. - """ - val = RequiredAttribute('w:val', ST_DecimalNumber) - - @classmethod - def new(cls, nsptagname, val): - """ - Return a new ``CT_DecimalNumber`` element having tagname *nsptagname* - and ``val`` attribute set to *val*. - """ - return OxmlElement(nsptagname, attrs={qn('w:val'): str(val)}) - - -class CT_OnOff(BaseOxmlElement): - """ - Used for ````, ```` elements and others, containing a bool-ish - string in its ``val`` attribute, xsd:boolean plus 'on' and 'off'. - """ - val = OptionalAttribute('w:val', ST_OnOff, default=True) - - -class CT_String(BaseOxmlElement): - """ - Used for ```` and ```` elements and others, - containing a style name in its ``val`` attribute. - """ - val = RequiredAttribute('w:val', ST_String) - - @classmethod - def new(cls, nsptagname, val): - """ - Return a new ``CT_String`` element with tagname *nsptagname* and - ``val`` attribute set to *val*. - """ - elm = OxmlElement(nsptagname) - elm.val = val - return elm diff --git a/docx/oxml/styles.py b/docx/oxml/styles.py deleted file mode 100644 index 6f27e45eb..000000000 --- a/docx/oxml/styles.py +++ /dev/null @@ -1,351 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to the styles part -""" - -from ..enum.style import WD_STYLE_TYPE -from .simpletypes import ST_DecimalNumber, ST_OnOff, ST_String -from .xmlchemy import ( - BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore, - ZeroOrOne -) - - -def styleId_from_name(name): - """ - Return the style id corresponding to *name*, taking into account - special-case names such as 'Heading 1'. - """ - return { - 'caption': 'Caption', - 'heading 1': 'Heading1', - 'heading 2': 'Heading2', - 'heading 3': 'Heading3', - 'heading 4': 'Heading4', - 'heading 5': 'Heading5', - 'heading 6': 'Heading6', - 'heading 7': 'Heading7', - 'heading 8': 'Heading8', - 'heading 9': 'Heading9', - }.get(name, name.replace(' ', '')) - - -class CT_LatentStyles(BaseOxmlElement): - """ - `w:latentStyles` element, defining behavior defaults for latent styles - and containing `w:lsdException` child elements that each override those - defaults for a named latent style. - """ - lsdException = ZeroOrMore('w:lsdException', successors=()) - - count = OptionalAttribute('w:count', ST_DecimalNumber) - defLockedState = OptionalAttribute('w:defLockedState', ST_OnOff) - defQFormat = OptionalAttribute('w:defQFormat', ST_OnOff) - defSemiHidden = OptionalAttribute('w:defSemiHidden', ST_OnOff) - defUIPriority = OptionalAttribute('w:defUIPriority', ST_DecimalNumber) - defUnhideWhenUsed = OptionalAttribute('w:defUnhideWhenUsed', ST_OnOff) - - def bool_prop(self, attr_name): - """ - Return the boolean value of the attribute having *attr_name*, or - |False| if not present. - """ - value = getattr(self, attr_name) - if value is None: - return False - return value - - def get_by_name(self, name): - """ - Return the `w:lsdException` child having *name*, or |None| if not - found. - """ - found = self.xpath('w:lsdException[@w:name="%s"]' % name) - if not found: - return None - return found[0] - - def set_bool_prop(self, attr_name, value): - """ - Set the on/off attribute having *attr_name* to *value*. - """ - setattr(self, attr_name, bool(value)) - - -class CT_LsdException(BaseOxmlElement): - """ - ```` element, defining override visibility behaviors for - a named latent style. - """ - locked = OptionalAttribute('w:locked', ST_OnOff) - name = RequiredAttribute('w:name', ST_String) - qFormat = OptionalAttribute('w:qFormat', ST_OnOff) - semiHidden = OptionalAttribute('w:semiHidden', ST_OnOff) - uiPriority = OptionalAttribute('w:uiPriority', ST_DecimalNumber) - unhideWhenUsed = OptionalAttribute('w:unhideWhenUsed', ST_OnOff) - - def delete(self): - """ - Remove this `w:lsdException` element from the XML document. - """ - self.getparent().remove(self) - - def on_off_prop(self, attr_name): - """ - Return the boolean value of the attribute having *attr_name*, or - |None| if not present. - """ - return getattr(self, attr_name) - - def set_on_off_prop(self, attr_name, value): - """ - Set the on/off attribute having *attr_name* to *value*. - """ - setattr(self, attr_name, value) - - -class CT_Style(BaseOxmlElement): - """ - A ```` element, representing a style definition - """ - _tag_seq = ( - 'w:name', 'w:aliases', 'w:basedOn', 'w:next', 'w:link', - 'w:autoRedefine', 'w:hidden', 'w:uiPriority', 'w:semiHidden', - 'w:unhideWhenUsed', 'w:qFormat', 'w:locked', 'w:personal', - 'w:personalCompose', 'w:personalReply', 'w:rsid', 'w:pPr', 'w:rPr', - 'w:tblPr', 'w:trPr', 'w:tcPr', 'w:tblStylePr' - ) - name = ZeroOrOne('w:name', successors=_tag_seq[1:]) - basedOn = ZeroOrOne('w:basedOn', successors=_tag_seq[3:]) - next = ZeroOrOne('w:next', successors=_tag_seq[4:]) - uiPriority = ZeroOrOne('w:uiPriority', successors=_tag_seq[8:]) - semiHidden = ZeroOrOne('w:semiHidden', successors=_tag_seq[9:]) - unhideWhenUsed = ZeroOrOne('w:unhideWhenUsed', successors=_tag_seq[10:]) - qFormat = ZeroOrOne('w:qFormat', successors=_tag_seq[11:]) - locked = ZeroOrOne('w:locked', successors=_tag_seq[12:]) - pPr = ZeroOrOne('w:pPr', successors=_tag_seq[17:]) - rPr = ZeroOrOne('w:rPr', successors=_tag_seq[18:]) - del _tag_seq - - type = OptionalAttribute('w:type', WD_STYLE_TYPE) - styleId = OptionalAttribute('w:styleId', ST_String) - default = OptionalAttribute('w:default', ST_OnOff) - customStyle = OptionalAttribute('w:customStyle', ST_OnOff) - - @property - def basedOn_val(self): - """ - Value of `w:basedOn/@w:val` or |None| if not present. - """ - basedOn = self.basedOn - if basedOn is None: - return None - return basedOn.val - - @basedOn_val.setter - def basedOn_val(self, value): - if value is None: - self._remove_basedOn() - else: - self.get_or_add_basedOn().val = value - - @property - def base_style(self): - """ - Sibling CT_Style element this style is based on or |None| if no base - style or base style not found. - """ - basedOn = self.basedOn - if basedOn is None: - return None - styles = self.getparent() - base_style = styles.get_by_id(basedOn.val) - if base_style is None: - return None - return base_style - - def delete(self): - """ - Remove this `w:style` element from its parent `w:styles` element. - """ - self.getparent().remove(self) - - @property - def locked_val(self): - """ - Value of `w:locked/@w:val` or |False| if not present. - """ - locked = self.locked - if locked is None: - return False - return locked.val - - @locked_val.setter - def locked_val(self, value): - self._remove_locked() - if bool(value) is True: - locked = self._add_locked() - locked.val = value - - @property - def name_val(self): - """ - Value of ```` child or |None| if not present. - """ - name = self.name - if name is None: - return None - return name.val - - @name_val.setter - def name_val(self, value): - self._remove_name() - if value is not None: - name = self._add_name() - name.val = value - - @property - def next_style(self): - """ - Sibling CT_Style element identified by the value of `w:name/@w:val` - or |None| if no value is present or no style with that style id - is found. - """ - next = self.next - if next is None: - return None - styles = self.getparent() - return styles.get_by_id(next.val) # None if not found - - @property - def qFormat_val(self): - """ - Value of `w:qFormat/@w:val` or |False| if not present. - """ - qFormat = self.qFormat - if qFormat is None: - return False - return qFormat.val - - @qFormat_val.setter - def qFormat_val(self, value): - self._remove_qFormat() - if bool(value): - self._add_qFormat() - - @property - def semiHidden_val(self): - """ - Value of ```` child or |False| if not present. - """ - semiHidden = self.semiHidden - if semiHidden is None: - return False - return semiHidden.val - - @semiHidden_val.setter - def semiHidden_val(self, value): - self._remove_semiHidden() - if bool(value) is True: - semiHidden = self._add_semiHidden() - semiHidden.val = value - - @property - def uiPriority_val(self): - """ - Value of ```` child or |None| if not present. - """ - uiPriority = self.uiPriority - if uiPriority is None: - return None - return uiPriority.val - - @uiPriority_val.setter - def uiPriority_val(self, value): - self._remove_uiPriority() - if value is not None: - uiPriority = self._add_uiPriority() - uiPriority.val = value - - @property - def unhideWhenUsed_val(self): - """ - Value of `w:unhideWhenUsed/@w:val` or |False| if not present. - """ - unhideWhenUsed = self.unhideWhenUsed - if unhideWhenUsed is None: - return False - return unhideWhenUsed.val - - @unhideWhenUsed_val.setter - def unhideWhenUsed_val(self, value): - self._remove_unhideWhenUsed() - if bool(value) is True: - unhideWhenUsed = self._add_unhideWhenUsed() - unhideWhenUsed.val = value - - -class CT_Styles(BaseOxmlElement): - """ - ```` element, the root element of a styles part, i.e. - styles.xml - """ - _tag_seq = ('w:docDefaults', 'w:latentStyles', 'w:style') - latentStyles = ZeroOrOne('w:latentStyles', successors=_tag_seq[2:]) - style = ZeroOrMore('w:style', successors=()) - del _tag_seq - - def add_style_of_type(self, name, style_type, builtin): - """ - Return a newly added `w:style` element having *name* and - *style_type*. `w:style/@customStyle` is set based on the value of - *builtin*. - """ - style = self.add_style() - style.type = style_type - style.customStyle = None if builtin else True - style.styleId = styleId_from_name(name) - style.name_val = name - return style - - def default_for(self, style_type): - """ - Return `w:style[@w:type="*{style_type}*][-1]` or |None| if not found. - """ - default_styles_for_type = [ - s for s in self._iter_styles() - if s.type == style_type and s.default - ] - if not default_styles_for_type: - return None - # spec calls for last default in document order - return default_styles_for_type[-1] - - def get_by_id(self, styleId): - """ - Return the ```` child element having ``styleId`` attribute - matching *styleId*, or |None| if not found. - """ - xpath = 'w:style[@w:styleId="%s"]' % styleId - try: - return self.xpath(xpath)[0] - except IndexError: - return None - - def get_by_name(self, name): - """ - Return the ```` child element having ```` child - element with value *name*, or |None| if not found. - """ - xpath = 'w:style[w:name/@w:val="%s"]' % name - try: - return self.xpath(xpath)[0] - except IndexError: - return None - - def _iter_styles(self): - """ - Generate each of the `w:style` child elements in document order. - """ - return (style for style in self.xpath('w:style')) diff --git a/docx/oxml/text/font.py b/docx/oxml/text/font.py deleted file mode 100644 index 810ec2b30..000000000 --- a/docx/oxml/text/font.py +++ /dev/null @@ -1,320 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to run properties (font). -""" - -from .. import parse_xml -from ...enum.dml import MSO_THEME_COLOR -from ...enum.text import WD_COLOR, WD_UNDERLINE -from ..ns import nsdecls, qn -from ..simpletypes import ( - ST_HexColor, ST_HpsMeasure, ST_String, ST_VerticalAlignRun -) -from ..xmlchemy import ( - BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrOne -) - - -class CT_Color(BaseOxmlElement): - """ - `w:color` element, specifying the color of a font and perhaps other - objects. - """ - val = RequiredAttribute('w:val', ST_HexColor) - themeColor = OptionalAttribute('w:themeColor', MSO_THEME_COLOR) - - -class CT_Fonts(BaseOxmlElement): - """ - ```` element, specifying typeface name for the various language - types. - """ - ascii = OptionalAttribute('w:ascii', ST_String) - hAnsi = OptionalAttribute('w:hAnsi', ST_String) - - -class CT_Highlight(BaseOxmlElement): - """ - `w:highlight` element, specifying font highlighting/background color. - """ - val = RequiredAttribute('w:val', WD_COLOR) - - -class CT_HpsMeasure(BaseOxmlElement): - """ - Used for ```` element and others, specifying font size in - half-points. - """ - val = RequiredAttribute('w:val', ST_HpsMeasure) - - -class CT_RPr(BaseOxmlElement): - """ - ```` element, containing the properties for a run. - """ - _tag_seq = ( - 'w:rStyle', 'w:rFonts', 'w:b', 'w:bCs', 'w:i', 'w:iCs', 'w:caps', - 'w:smallCaps', 'w:strike', 'w:dstrike', 'w:outline', 'w:shadow', - 'w:emboss', 'w:imprint', 'w:noProof', 'w:snapToGrid', 'w:vanish', - 'w:webHidden', 'w:color', 'w:spacing', 'w:w', 'w:kern', 'w:position', - 'w:sz', 'w:szCs', 'w:highlight', 'w:u', 'w:effect', 'w:bdr', 'w:shd', - 'w:fitText', 'w:vertAlign', 'w:rtl', 'w:cs', 'w:em', 'w:lang', - 'w:eastAsianLayout', 'w:specVanish', 'w:oMath' - ) - rStyle = ZeroOrOne('w:rStyle', successors=_tag_seq[1:]) - rFonts = ZeroOrOne('w:rFonts', successors=_tag_seq[2:]) - b = ZeroOrOne('w:b', successors=_tag_seq[3:]) - bCs = ZeroOrOne('w:bCs', successors=_tag_seq[4:]) - i = ZeroOrOne('w:i', successors=_tag_seq[5:]) - iCs = ZeroOrOne('w:iCs', successors=_tag_seq[6:]) - caps = ZeroOrOne('w:caps', successors=_tag_seq[7:]) - smallCaps = ZeroOrOne('w:smallCaps', successors=_tag_seq[8:]) - strike = ZeroOrOne('w:strike', successors=_tag_seq[9:]) - dstrike = ZeroOrOne('w:dstrike', successors=_tag_seq[10:]) - outline = ZeroOrOne('w:outline', successors=_tag_seq[11:]) - shadow = ZeroOrOne('w:shadow', successors=_tag_seq[12:]) - emboss = ZeroOrOne('w:emboss', successors=_tag_seq[13:]) - imprint = ZeroOrOne('w:imprint', successors=_tag_seq[14:]) - noProof = ZeroOrOne('w:noProof', successors=_tag_seq[15:]) - snapToGrid = ZeroOrOne('w:snapToGrid', successors=_tag_seq[16:]) - vanish = ZeroOrOne('w:vanish', successors=_tag_seq[17:]) - webHidden = ZeroOrOne('w:webHidden', successors=_tag_seq[18:]) - color = ZeroOrOne('w:color', successors=_tag_seq[19:]) - sz = ZeroOrOne('w:sz', successors=_tag_seq[24:]) - highlight = ZeroOrOne('w:highlight', successors=_tag_seq[26:]) - u = ZeroOrOne('w:u', successors=_tag_seq[27:]) - vertAlign = ZeroOrOne('w:vertAlign', successors=_tag_seq[32:]) - rtl = ZeroOrOne('w:rtl', successors=_tag_seq[33:]) - cs = ZeroOrOne('w:cs', successors=_tag_seq[34:]) - specVanish = ZeroOrOne('w:specVanish', successors=_tag_seq[38:]) - oMath = ZeroOrOne('w:oMath', successors=_tag_seq[39:]) - del _tag_seq - - def _new_color(self): - """ - Override metaclass method to set `w:color/@val` to RGB black on - create. - """ - return parse_xml('' % nsdecls('w')) - - @property - def highlight_val(self): - """ - Value of `w:highlight/@val` attribute, specifying a font's highlight - color, or `None` if the text is not highlighted. - """ - highlight = self.highlight - if highlight is None: - return None - return highlight.val - - @highlight_val.setter - def highlight_val(self, value): - if value is None: - self._remove_highlight() - return - highlight = self.get_or_add_highlight() - highlight.val = value - - @property - def rFonts_ascii(self): - """ - The value of `w:rFonts/@w:ascii` or |None| if not present. Represents - the assigned typeface name. The rFonts element also specifies other - special-case typeface names; this method handles the case where just - the common name is required. - """ - rFonts = self.rFonts - if rFonts is None: - return None - return rFonts.ascii - - @rFonts_ascii.setter - def rFonts_ascii(self, value): - if value is None: - self._remove_rFonts() - return - rFonts = self.get_or_add_rFonts() - rFonts.ascii = value - - @property - def rFonts_hAnsi(self): - """ - The value of `w:rFonts/@w:hAnsi` or |None| if not present. - """ - rFonts = self.rFonts - if rFonts is None: - return None - return rFonts.hAnsi - - @rFonts_hAnsi.setter - def rFonts_hAnsi(self, value): - if value is None and self.rFonts is None: - return - rFonts = self.get_or_add_rFonts() - rFonts.hAnsi = value - - @property - def style(self): - """ - String contained in child, or None if that element is not - present. - """ - rStyle = self.rStyle - if rStyle is None: - return None - return rStyle.val - - @style.setter - def style(self, style): - """ - Set val attribute of child element to *style*, adding a - new element if necessary. If *style* is |None|, remove the - element if present. - """ - if style is None: - self._remove_rStyle() - elif self.rStyle is None: - self._add_rStyle(val=style) - else: - self.rStyle.val = style - - @property - def subscript(self): - """ - |True| if `w:vertAlign/@w:val` is 'subscript'. |False| if - `w:vertAlign/@w:val` contains any other value. |None| if - `w:vertAlign` is not present. - """ - vertAlign = self.vertAlign - if vertAlign is None: - return None - if vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT: - return True - return False - - @subscript.setter - def subscript(self, value): - if value is None: - self._remove_vertAlign() - elif bool(value) is True: - self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUBSCRIPT - elif self.vertAlign is None: - return - elif self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT: - self._remove_vertAlign() - - @property - def superscript(self): - """ - |True| if `w:vertAlign/@w:val` is 'superscript'. |False| if - `w:vertAlign/@w:val` contains any other value. |None| if - `w:vertAlign` is not present. - """ - vertAlign = self.vertAlign - if vertAlign is None: - return None - if vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT: - return True - return False - - @superscript.setter - def superscript(self, value): - if value is None: - self._remove_vertAlign() - elif bool(value) is True: - self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUPERSCRIPT - elif self.vertAlign is None: - return - elif self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT: - self._remove_vertAlign() - - @property - def sz_val(self): - """ - The value of `w:sz/@w:val` or |None| if not present. - """ - sz = self.sz - if sz is None: - return None - return sz.val - - @sz_val.setter - def sz_val(self, value): - if value is None: - self._remove_sz() - return - sz = self.get_or_add_sz() - sz.val = value - - @property - def u_val(self): - """ - Value of `w:u/@val`, or None if not present. - """ - u = self.u - if u is None: - return None - return u.val - - @u_val.setter - def u_val(self, value): - self._remove_u() - if value is not None: - self._add_u().val = value - - def _get_bool_val(self, name): - """ - Return the value of the boolean child element having *name*, e.g. - 'b', 'i', and 'smallCaps'. - """ - element = getattr(self, name) - if element is None: - return None - return element.val - - def _set_bool_val(self, name, value): - if value is None: - getattr(self, '_remove_%s' % name)() - return - element = getattr(self, 'get_or_add_%s' % name)() - element.val = value - - -class CT_Underline(BaseOxmlElement): - """ - ```` element, specifying the underlining style for a run. - """ - @property - def val(self): - """ - The underline type corresponding to the ``w:val`` attribute value. - """ - val = self.get(qn('w:val')) - underline = WD_UNDERLINE.from_xml(val) - if underline == WD_UNDERLINE.SINGLE: - return True - if underline == WD_UNDERLINE.NONE: - return False - return underline - - @val.setter - def val(self, value): - # works fine without these two mappings, but only because True == 1 - # and False == 0, which happen to match the mapping for WD_UNDERLINE - # .SINGLE and .NONE respectively. - if value is True: - value = WD_UNDERLINE.SINGLE - elif value is False: - value = WD_UNDERLINE.NONE - - val = WD_UNDERLINE.to_xml(value) - self.set(qn('w:val'), val) - - -class CT_VerticalAlignRun(BaseOxmlElement): - """ - ```` element, specifying subscript or superscript. - """ - val = RequiredAttribute('w:val', ST_VerticalAlignRun) diff --git a/docx/oxml/text/paragraph.py b/docx/oxml/text/paragraph.py deleted file mode 100644 index 5e4213776..000000000 --- a/docx/oxml/text/paragraph.py +++ /dev/null @@ -1,78 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to paragraphs (CT_P). -""" - -from ..ns import qn -from ..xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore, ZeroOrOne - - -class CT_P(BaseOxmlElement): - """ - ```` element, containing the properties and text for a paragraph. - """ - pPr = ZeroOrOne('w:pPr') - r = ZeroOrMore('w:r') - - def _insert_pPr(self, pPr): - self.insert(0, pPr) - return pPr - - def add_p_before(self): - """ - Return a new ```` element inserted directly prior to this one. - """ - new_p = OxmlElement('w:p') - self.addprevious(new_p) - return new_p - - @property - def alignment(self): - """ - The value of the ```` grandchild element or |None| if not - present. - """ - pPr = self.pPr - if pPr is None: - return None - return pPr.jc_val - - @alignment.setter - def alignment(self, value): - pPr = self.get_or_add_pPr() - pPr.jc_val = value - - def clear_content(self): - """ - Remove all child elements, except the ```` element if present. - """ - for child in self[:]: - if child.tag == qn('w:pPr'): - continue - self.remove(child) - - def set_sectPr(self, sectPr): - """ - Unconditionally replace or add *sectPr* as a grandchild in the - correct sequence. - """ - pPr = self.get_or_add_pPr() - pPr._remove_sectPr() - pPr._insert_sectPr(sectPr) - - @property - def style(self): - """ - String contained in w:val attribute of ./w:pPr/w:pStyle grandchild, - or |None| if not present. - """ - pPr = self.pPr - if pPr is None: - return None - return pPr.style - - @style.setter - def style(self, style): - pPr = self.get_or_add_pPr() - pPr.style = style diff --git a/docx/oxml/text/parfmt.py b/docx/oxml/text/parfmt.py deleted file mode 100644 index 466b11b1b..000000000 --- a/docx/oxml/text/parfmt.py +++ /dev/null @@ -1,348 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to paragraph properties (CT_PPr). -""" - -from ...enum.text import ( - WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_TAB_ALIGNMENT, WD_TAB_LEADER -) -from ...shared import Length -from ..simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure -from ..xmlchemy import ( - BaseOxmlElement, OneOrMore, OptionalAttribute, RequiredAttribute, - ZeroOrOne -) - - -class CT_Ind(BaseOxmlElement): - """ - ```` element, specifying paragraph indentation. - """ - left = OptionalAttribute('w:left', ST_SignedTwipsMeasure) - right = OptionalAttribute('w:right', ST_SignedTwipsMeasure) - firstLine = OptionalAttribute('w:firstLine', ST_TwipsMeasure) - hanging = OptionalAttribute('w:hanging', ST_TwipsMeasure) - - -class CT_Jc(BaseOxmlElement): - """ - ```` element, specifying paragraph justification. - """ - val = RequiredAttribute('w:val', WD_ALIGN_PARAGRAPH) - - -class CT_PPr(BaseOxmlElement): - """ - ```` element, containing the properties for a paragraph. - """ - _tag_seq = ( - 'w:pStyle', 'w:keepNext', 'w:keepLines', 'w:pageBreakBefore', - 'w:framePr', 'w:widowControl', 'w:numPr', 'w:suppressLineNumbers', - 'w:pBdr', 'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', - 'w:wordWrap', 'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', - 'w:autoSpaceDN', 'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', - 'w:spacing', 'w:ind', 'w:contextualSpacing', 'w:mirrorIndents', - 'w:suppressOverlap', 'w:jc', 'w:textDirection', 'w:textAlignment', - 'w:textboxTightWrap', 'w:outlineLvl', 'w:divId', 'w:cnfStyle', - 'w:rPr', 'w:sectPr', 'w:pPrChange' - ) - pStyle = ZeroOrOne('w:pStyle', successors=_tag_seq[1:]) - keepNext = ZeroOrOne('w:keepNext', successors=_tag_seq[2:]) - keepLines = ZeroOrOne('w:keepLines', successors=_tag_seq[3:]) - pageBreakBefore = ZeroOrOne('w:pageBreakBefore', successors=_tag_seq[4:]) - widowControl = ZeroOrOne('w:widowControl', successors=_tag_seq[6:]) - numPr = ZeroOrOne('w:numPr', successors=_tag_seq[7:]) - tabs = ZeroOrOne('w:tabs', successors=_tag_seq[11:]) - spacing = ZeroOrOne('w:spacing', successors=_tag_seq[22:]) - ind = ZeroOrOne('w:ind', successors=_tag_seq[23:]) - jc = ZeroOrOne('w:jc', successors=_tag_seq[27:]) - sectPr = ZeroOrOne('w:sectPr', successors=_tag_seq[35:]) - del _tag_seq - - @property - def first_line_indent(self): - """ - A |Length| value calculated from the values of `w:ind/@w:firstLine` - and `w:ind/@w:hanging`. Returns |None| if the `w:ind` child is not - present. - """ - ind = self.ind - if ind is None: - return None - hanging = ind.hanging - if hanging is not None: - return Length(-hanging) - firstLine = ind.firstLine - if firstLine is None: - return None - return firstLine - - @first_line_indent.setter - def first_line_indent(self, value): - if self.ind is None and value is None: - return - ind = self.get_or_add_ind() - ind.firstLine = ind.hanging = None - if value is None: - return - elif value < 0: - ind.hanging = -value - else: - ind.firstLine = value - - @property - def ind_left(self): - """ - The value of `w:ind/@w:left` or |None| if not present. - """ - ind = self.ind - if ind is None: - return None - return ind.left - - @ind_left.setter - def ind_left(self, value): - if value is None and self.ind is None: - return - ind = self.get_or_add_ind() - ind.left = value - - @property - def ind_right(self): - """ - The value of `w:ind/@w:right` or |None| if not present. - """ - ind = self.ind - if ind is None: - return None - return ind.right - - @ind_right.setter - def ind_right(self, value): - if value is None and self.ind is None: - return - ind = self.get_or_add_ind() - ind.right = value - - @property - def jc_val(self): - """ - The value of the ```` child element or |None| if not present. - """ - jc = self.jc - if jc is None: - return None - return jc.val - - @jc_val.setter - def jc_val(self, value): - if value is None: - self._remove_jc() - return - self.get_or_add_jc().val = value - - @property - def keepLines_val(self): - """ - The value of `keepLines/@val` or |None| if not present. - """ - keepLines = self.keepLines - if keepLines is None: - return None - return keepLines.val - - @keepLines_val.setter - def keepLines_val(self, value): - if value is None: - self._remove_keepLines() - else: - self.get_or_add_keepLines().val = value - - @property - def keepNext_val(self): - """ - The value of `keepNext/@val` or |None| if not present. - """ - keepNext = self.keepNext - if keepNext is None: - return None - return keepNext.val - - @keepNext_val.setter - def keepNext_val(self, value): - if value is None: - self._remove_keepNext() - else: - self.get_or_add_keepNext().val = value - - @property - def pageBreakBefore_val(self): - """ - The value of `pageBreakBefore/@val` or |None| if not present. - """ - pageBreakBefore = self.pageBreakBefore - if pageBreakBefore is None: - return None - return pageBreakBefore.val - - @pageBreakBefore_val.setter - def pageBreakBefore_val(self, value): - if value is None: - self._remove_pageBreakBefore() - else: - self.get_or_add_pageBreakBefore().val = value - - @property - def spacing_after(self): - """ - The value of `w:spacing/@w:after` or |None| if not present. - """ - spacing = self.spacing - if spacing is None: - return None - return spacing.after - - @spacing_after.setter - def spacing_after(self, value): - if value is None and self.spacing is None: - return - self.get_or_add_spacing().after = value - - @property - def spacing_before(self): - """ - The value of `w:spacing/@w:before` or |None| if not present. - """ - spacing = self.spacing - if spacing is None: - return None - return spacing.before - - @spacing_before.setter - def spacing_before(self, value): - if value is None and self.spacing is None: - return - self.get_or_add_spacing().before = value - - @property - def spacing_line(self): - """ - The value of `w:spacing/@w:line` or |None| if not present. - """ - spacing = self.spacing - if spacing is None: - return None - return spacing.line - - @spacing_line.setter - def spacing_line(self, value): - if value is None and self.spacing is None: - return - self.get_or_add_spacing().line = value - - @property - def spacing_lineRule(self): - """ - The value of `w:spacing/@w:lineRule` as a member of the - :ref:`WdLineSpacing` enumeration. Only the `MULTIPLE`, `EXACTLY`, and - `AT_LEAST` members are used. It is the responsibility of the client - to calculate the use of `SINGLE`, `DOUBLE`, and `MULTIPLE` based on - the value of `w:spacing/@w:line` if that behavior is desired. - """ - spacing = self.spacing - if spacing is None: - return None - lineRule = spacing.lineRule - if lineRule is None and spacing.line is not None: - return WD_LINE_SPACING.MULTIPLE - return lineRule - - @spacing_lineRule.setter - def spacing_lineRule(self, value): - if value is None and self.spacing is None: - return - self.get_or_add_spacing().lineRule = value - - @property - def style(self): - """ - String contained in child, or None if that element is not - present. - """ - pStyle = self.pStyle - if pStyle is None: - return None - return pStyle.val - - @style.setter - def style(self, style): - """ - Set val attribute of child element to *style*, adding a - new element if necessary. If *style* is |None|, remove the - element if present. - """ - if style is None: - self._remove_pStyle() - return - pStyle = self.get_or_add_pStyle() - pStyle.val = style - - @property - def widowControl_val(self): - """ - The value of `widowControl/@val` or |None| if not present. - """ - widowControl = self.widowControl - if widowControl is None: - return None - return widowControl.val - - @widowControl_val.setter - def widowControl_val(self, value): - if value is None: - self._remove_widowControl() - else: - self.get_or_add_widowControl().val = value - - -class CT_Spacing(BaseOxmlElement): - """ - ```` element, specifying paragraph spacing attributes such as - space before and line spacing. - """ - after = OptionalAttribute('w:after', ST_TwipsMeasure) - before = OptionalAttribute('w:before', ST_TwipsMeasure) - line = OptionalAttribute('w:line', ST_SignedTwipsMeasure) - lineRule = OptionalAttribute('w:lineRule', WD_LINE_SPACING) - - -class CT_TabStop(BaseOxmlElement): - """ - ```` element, representing an individual tab stop. - """ - val = RequiredAttribute('w:val', WD_TAB_ALIGNMENT) - leader = OptionalAttribute( - 'w:leader', WD_TAB_LEADER, default=WD_TAB_LEADER.SPACES - ) - pos = RequiredAttribute('w:pos', ST_SignedTwipsMeasure) - - -class CT_TabStops(BaseOxmlElement): - """ - ```` element, container for a sorted sequence of tab stops. - """ - tab = OneOrMore('w:tab', successors=()) - - def insert_tab_in_order(self, pos, align, leader): - """ - Insert a newly created `w:tab` child element in *pos* order. - """ - new_tab = self._new_tab() - new_tab.pos, new_tab.val, new_tab.leader = pos, align, leader - for tab in self.tab_lst: - if new_tab.pos < tab.pos: - tab.addprevious(new_tab) - return new_tab - self.append(new_tab) - return new_tab diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py deleted file mode 100644 index 8f0a62e82..000000000 --- a/docx/oxml/text/run.py +++ /dev/null @@ -1,166 +0,0 @@ -# encoding: utf-8 - -""" -Custom element classes related to text runs (CT_R). -""" - -from ..ns import qn -from ..simpletypes import ST_BrClear, ST_BrType -from ..xmlchemy import ( - BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne -) - - -class CT_Br(BaseOxmlElement): - """ - ```` element, indicating a line, page, or column break in a run. - """ - type = OptionalAttribute('w:type', ST_BrType) - clear = OptionalAttribute('w:clear', ST_BrClear) - - -class CT_R(BaseOxmlElement): - """ - ```` element, containing the properties and text for a run. - """ - rPr = ZeroOrOne('w:rPr') - t = ZeroOrMore('w:t') - br = ZeroOrMore('w:br') - cr = ZeroOrMore('w:cr') - tab = ZeroOrMore('w:tab') - drawing = ZeroOrMore('w:drawing') - - def _insert_rPr(self, rPr): - self.insert(0, rPr) - return rPr - - def add_t(self, text): - """ - Return a newly added ```` element containing *text*. - """ - t = self._add_t(text=text) - if len(text.strip()) < len(text): - t.set(qn('xml:space'), 'preserve') - return t - - def add_drawing(self, inline_or_anchor): - """ - Return a newly appended ``CT_Drawing`` (````) child - element having *inline_or_anchor* as its child. - """ - drawing = self._add_drawing() - drawing.append(inline_or_anchor) - return drawing - - def clear_content(self): - """ - Remove all child elements except the ```` element if present. - """ - content_child_elms = self[1:] if self.rPr is not None else self[:] - for child in content_child_elms: - self.remove(child) - - @property - def style(self): - """ - String contained in w:val attribute of grandchild, or - |None| if that element is not present. - """ - rPr = self.rPr - if rPr is None: - return None - return rPr.style - - @style.setter - def style(self, style): - """ - Set the character style of this element to *style*. If *style* - is None, remove the style element. - """ - rPr = self.get_or_add_rPr() - rPr.style = style - - @property - def text(self): - """ - A string representing the textual content of this run, with content - child elements like ```` translated to their Python - equivalent. - """ - text = '' - for child in self: - if child.tag == qn('w:t'): - t_text = child.text - text += t_text if t_text is not None else '' - elif child.tag == qn('w:tab'): - text += '\t' - elif child.tag in (qn('w:br'), qn('w:cr')): - text += '\n' - return text - - @text.setter - def text(self, text): - self.clear_content() - _RunContentAppender.append_to_run_from_text(self, text) - - -class CT_Text(BaseOxmlElement): - """ - ```` element, containing a sequence of characters within a run. - """ - - -class _RunContentAppender(object): - """ - Service object that knows how to translate a Python string into run - content elements appended to a specified ```` element. Contiguous - sequences of regular characters are appended in a single ```` - element. Each tab character ('\t') causes a ```` element to be - appended. Likewise a newline or carriage return character ('\n', '\r') - causes a ```` element to be appended. - """ - def __init__(self, r): - self._r = r - self._bfr = [] - - @classmethod - def append_to_run_from_text(cls, r, text): - """ - Create a "one-shot" ``_RunContentAppender`` instance and use it to - append the run content elements corresponding to *text* to the - ```` element *r*. - """ - appender = cls(r) - appender.add_text(text) - - def add_text(self, text): - """ - Append the run content elements corresponding to *text* to the - ```` element of this instance. - """ - for char in text: - self.add_char(char) - self.flush() - - def add_char(self, char): - """ - Process the next character of input through the translation finite - state maching (FSM). There are two possible states, buffer pending - and not pending, but those are hidden behind the ``.flush()`` method - which must be called at the end of text to ensure any pending - ```` element is written. - """ - if char == '\t': - self.flush() - self._r.add_tab() - elif char in '\r\n': - self.flush() - self._r.add_br() - else: - self._bfr.append(char) - - def flush(self): - text = ''.join(self._bfr) - if text: - self._r.add_t(text) - del self._bfr[:] diff --git a/docx/parts/image.py b/docx/parts/image.py deleted file mode 100644 index 6ece20d80..000000000 --- a/docx/parts/image.py +++ /dev/null @@ -1,89 +0,0 @@ -# encoding: utf-8 - -""" -The proxy class for an image part, and related objects. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -import hashlib - -from docx.image.image import Image -from docx.opc.part import Part -from docx.shared import Emu, Inches - - -class ImagePart(Part): - """ - An image part. Corresponds to the target part of a relationship with type - RELATIONSHIP_TYPE.IMAGE. - """ - def __init__(self, partname, content_type, blob, image=None): - super(ImagePart, self).__init__(partname, content_type, blob) - self._image = image - - @property - def default_cx(self): - """ - Native width of this image, calculated from its width in pixels and - horizontal dots per inch (dpi). - """ - px_width = self.image.px_width - horz_dpi = self.image.horz_dpi - width_in_inches = px_width / horz_dpi - return Inches(width_in_inches) - - @property - def default_cy(self): - """ - Native height of this image, calculated from its height in pixels and - vertical dots per inch (dpi). - """ - px_height = self.image.px_height - horz_dpi = self.image.horz_dpi - height_in_emu = 914400 * px_height / horz_dpi - return Emu(height_in_emu) - - @property - def filename(self): - """ - Filename from which this image part was originally created. A generic - name, e.g. 'image.png', is substituted if no name is available, for - example when the image was loaded from an unnamed stream. In that - case a default extension is applied based on the detected MIME type - of the image. - """ - if self._image is not None: - return self._image.filename - return 'image.%s' % self.partname.ext - - @classmethod - def from_image(cls, image, partname): - """ - Return an |ImagePart| instance newly created from *image* and - assigned *partname*. - """ - return ImagePart(partname, image.content_type, image.blob, image) - - @property - def image(self): - if self._image is None: - self._image = Image.from_blob(self.blob) - return self._image - - @classmethod - def load(cls, partname, content_type, blob, package): - """ - Called by ``docx.opc.package.PartFactory`` to load an image part from - a package being opened by ``Document(...)`` call. - """ - return cls(partname, content_type, blob) - - @property - def sha1(self): - """ - SHA1 hash digest of the blob of this image part. - """ - return hashlib.sha1(self._blob).hexdigest() diff --git a/docx/parts/numbering.py b/docx/parts/numbering.py deleted file mode 100644 index e324c5aac..000000000 --- a/docx/parts/numbering.py +++ /dev/null @@ -1,47 +0,0 @@ -# encoding: utf-8 - -""" -|NumberingPart| and closely related objects -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..opc.part import XmlPart -from ..shared import lazyproperty - - -class NumberingPart(XmlPart): - """ - Proxy for the numbering.xml part containing numbering definitions for - a document or glossary. - """ - @classmethod - def new(cls): - """ - Return newly created empty numbering part, containing only the root - ```` element. - """ - raise NotImplementedError - - @lazyproperty - def numbering_definitions(self): - """ - The |_NumberingDefinitions| instance containing the numbering - definitions ( element proxies) for this numbering part. - """ - return _NumberingDefinitions(self._element) - - -class _NumberingDefinitions(object): - """ - Collection of |_NumberingDefinition| instances corresponding to the - ```` elements in a numbering part. - """ - def __init__(self, numbering_elm): - super(_NumberingDefinitions, self).__init__() - self._numbering = numbering_elm - - def __len__(self): - return len(self._numbering.num_lst) diff --git a/docx/parts/settings.py b/docx/parts/settings.py deleted file mode 100644 index a701b1726..000000000 --- a/docx/parts/settings.py +++ /dev/null @@ -1,54 +0,0 @@ -# encoding: utf-8 - -""" -|SettingsPart| and closely related objects -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -import os - -from ..opc.constants import CONTENT_TYPE as CT -from ..opc.packuri import PackURI -from ..opc.part import XmlPart -from ..oxml import parse_xml -from ..settings import Settings - - -class SettingsPart(XmlPart): - """ - Document-level settings part of a WordprocessingML (WML) package. - """ - @classmethod - def default(cls, package): - """ - Return a newly created settings part, containing a default - `w:settings` element tree. - """ - partname = PackURI('/word/settings.xml') - content_type = CT.WML_SETTINGS - element = parse_xml(cls._default_settings_xml()) - return cls(partname, content_type, element, package) - - @property - def settings(self): - """ - A |Settings| proxy object for the `w:settings` element in this part, - containing the document-level settings for this document. - """ - return Settings(self.element) - - @classmethod - def _default_settings_xml(cls): - """ - Return a bytestream containing XML for a default settings part. - """ - path = os.path.join( - os.path.split(__file__)[0], '..', 'templates', - 'default-settings.xml' - ) - with open(path, 'rb') as f: - xml_bytes = f.read() - return xml_bytes diff --git a/docx/parts/story.py b/docx/parts/story.py deleted file mode 100644 index 129b8f1cc..000000000 --- a/docx/parts/story.py +++ /dev/null @@ -1,78 +0,0 @@ -# encoding: utf-8 - -"""|BaseStoryPart| and related objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from docx.opc.constants import RELATIONSHIP_TYPE as RT -from docx.opc.part import XmlPart -from docx.oxml.shape import CT_Inline -from docx.shared import lazyproperty - - -class BaseStoryPart(XmlPart): - """Base class for story parts. - - A story part is one that can contain textual content, such as the document-part and - header or footer parts. These all share content behaviors like `.paragraphs`, - `.add_paragraph()`, `.add_table()` etc. - """ - - def get_or_add_image(self, image_descriptor): - """Return (rId, image) pair for image identified by *image_descriptor*. - - *rId* is the str key (often like "rId7") for the relationship between this story - part and the image part, reused if already present, newly created if not. - *image* is an |Image| instance providing access to the properties of the image, - such as dimensions and image type. - """ - image_part = self._package.get_or_add_image_part(image_descriptor) - rId = self.relate_to(image_part, RT.IMAGE) - return rId, image_part.image - - def get_style(self, style_id, style_type): - """Return the style in this document matching *style_id*. - - Returns the default style for *style_type* if *style_id* is |None| or does not - match a defined style of *style_type*. - """ - return self._document_part.get_style(style_id, style_type) - - def get_style_id(self, style_or_name, style_type): - """Return str style_id for *style_or_name* of *style_type*. - - Returns |None| if the style resolves to the default style for *style_type* or if - *style_or_name* is itself |None|. Raises if *style_or_name* is a style of the - wrong type or names a style not present in the document. - """ - return self._document_part.get_style_id(style_or_name, style_type) - - def new_pic_inline(self, image_descriptor, width, height): - """Return a newly-created `w:inline` element. - - The element contains the image specified by *image_descriptor* and is scaled - based on the values of *width* and *height*. - """ - rId, image = self.get_or_add_image(image_descriptor) - cx, cy = image.scaled_dimensions(width, height) - shape_id, filename = self.next_id, image.filename - return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy) - - @property - def next_id(self): - """Next available positive integer id value in this story XML document. - - The value is determined by incrementing the maximum existing id value. Gaps in - the existing id sequence are not filled. The id attribute value is unique in the - document, without regard to the element type it appears on. - """ - id_str_lst = self._element.xpath('//@id') - used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()] - if not used_ids: - return 1 - return max(used_ids) + 1 - - @lazyproperty - def _document_part(self): - """|DocumentPart| object for this package.""" - return self.package.main_document_part diff --git a/docx/parts/styles.py b/docx/parts/styles.py deleted file mode 100644 index 00c7cb3c3..000000000 --- a/docx/parts/styles.py +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 - -""" -Provides StylesPart and related objects -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -import os - -from ..opc.constants import CONTENT_TYPE as CT -from ..opc.packuri import PackURI -from ..opc.part import XmlPart -from ..oxml import parse_xml -from ..styles.styles import Styles - - -class StylesPart(XmlPart): - """ - Proxy for the styles.xml part containing style definitions for a document - or glossary. - """ - @classmethod - def default(cls, package): - """ - Return a newly created styles part, containing a default set of - elements. - """ - partname = PackURI('/word/styles.xml') - content_type = CT.WML_STYLES - element = parse_xml(cls._default_styles_xml()) - return cls(partname, content_type, element, package) - - @property - def styles(self): - """ - The |_Styles| instance containing the styles ( element - proxies) for this styles part. - """ - return Styles(self.element) - - @classmethod - def _default_styles_xml(cls): - """ - Return a bytestream containing XML for a default styles part. - """ - path = os.path.join( - os.path.split(__file__)[0], '..', 'templates', - 'default-styles.xml' - ) - with open(path, 'rb') as f: - xml_bytes = f.read() - return xml_bytes diff --git a/docx/shared.py b/docx/shared.py deleted file mode 100644 index 919964325..000000000 --- a/docx/shared.py +++ /dev/null @@ -1,250 +0,0 @@ -# encoding: utf-8 - -""" -Objects shared by docx modules. -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -class Length(int): - """ - Base class for length constructor classes Inches, Cm, Mm, Px, and Emu. - Behaves as an int count of English Metric Units, 914,400 to the inch, - 36,000 to the mm. Provides convenience unit conversion methods in the form - of read-only properties. Immutable. - """ - _EMUS_PER_INCH = 914400 - _EMUS_PER_CM = 360000 - _EMUS_PER_MM = 36000 - _EMUS_PER_PT = 12700 - _EMUS_PER_TWIP = 635 - - def __new__(cls, emu): - return int.__new__(cls, emu) - - @property - def cm(self): - """ - The equivalent length expressed in centimeters (float). - """ - return self / float(self._EMUS_PER_CM) - - @property - def emu(self): - """ - The equivalent length expressed in English Metric Units (int). - """ - return self - - @property - def inches(self): - """ - The equivalent length expressed in inches (float). - """ - return self / float(self._EMUS_PER_INCH) - - @property - def mm(self): - """ - The equivalent length expressed in millimeters (float). - """ - return self / float(self._EMUS_PER_MM) - - @property - def pt(self): - """ - Floating point length in points - """ - return self / float(self._EMUS_PER_PT) - - @property - def twips(self): - """ - The equivalent length expressed in twips (int). - """ - return int(round(self / float(self._EMUS_PER_TWIP))) - - -class Inches(Length): - """ - Convenience constructor for length in inches, e.g. - ``width = Inches(0.5)``. - """ - def __new__(cls, inches): - emu = int(inches * Length._EMUS_PER_INCH) - return Length.__new__(cls, emu) - - -class Cm(Length): - """ - Convenience constructor for length in centimeters, e.g. - ``height = Cm(12)``. - """ - def __new__(cls, cm): - emu = int(cm * Length._EMUS_PER_CM) - return Length.__new__(cls, emu) - - -class Emu(Length): - """ - Convenience constructor for length in English Metric Units, e.g. - ``width = Emu(457200)``. - """ - def __new__(cls, emu): - return Length.__new__(cls, int(emu)) - - -class Mm(Length): - """ - Convenience constructor for length in millimeters, e.g. - ``width = Mm(240.5)``. - """ - def __new__(cls, mm): - emu = int(mm * Length._EMUS_PER_MM) - return Length.__new__(cls, emu) - - -class Pt(Length): - """ - Convenience value class for specifying a length in points - """ - def __new__(cls, points): - emu = int(points * Length._EMUS_PER_PT) - return Length.__new__(cls, emu) - - -class Twips(Length): - """ - Convenience constructor for length in twips, e.g. ``width = Twips(42)``. - A twip is a twentieth of a point, 635 EMU. - """ - def __new__(cls, twips): - emu = int(twips * Length._EMUS_PER_TWIP) - return Length.__new__(cls, emu) - - -class RGBColor(tuple): - """ - Immutable value object defining a particular RGB color. - """ - def __new__(cls, r, g, b): - msg = 'RGBColor() takes three integer values 0-255' - for val in (r, g, b): - if not isinstance(val, int) or val < 0 or val > 255: - raise ValueError(msg) - return super(RGBColor, cls).__new__(cls, (r, g, b)) - - def __repr__(self): - return 'RGBColor(0x%02x, 0x%02x, 0x%02x)' % self - - def __str__(self): - """ - Return a hex string rgb value, like '3C2F80' - """ - return '%02X%02X%02X' % self - - @classmethod - def from_string(cls, rgb_hex_str): - """ - Return a new instance from an RGB color hex string like ``'3C2F80'``. - """ - r = int(rgb_hex_str[:2], 16) - g = int(rgb_hex_str[2:4], 16) - b = int(rgb_hex_str[4:], 16) - return cls(r, g, b) - - -def lazyproperty(f): - """ - @lazyprop decorator. Decorated method will be called only on first access - to calculate a cached property value. After that, the cached value is - returned. - """ - cache_attr_name = '_%s' % f.__name__ # like '_foobar' for prop 'foobar' - docstring = f.__doc__ - - def get_prop_value(obj): - try: - return getattr(obj, cache_attr_name) - except AttributeError: - value = f(obj) - setattr(obj, cache_attr_name, value) - return value - - return property(get_prop_value, doc=docstring) - - -def write_only_property(f): - """ - @write_only_property decorator. Creates a property (descriptor attribute) - that accepts assignment, but not getattr (use in an expression). - """ - docstring = f.__doc__ - - return property(fset=f, doc=docstring) - - -class ElementProxy(object): - """ - Base class for lxml element proxy classes. An element proxy class is one - whose primary responsibilities are fulfilled by manipulating the - attributes and child elements of an XML element. They are the most common - type of class in python-docx other than custom element (oxml) classes. - """ - - __slots__ = ('_element', '_parent') - - def __init__(self, element, parent=None): - self._element = element - self._parent = parent - - def __eq__(self, other): - """ - Return |True| if this proxy object refers to the same oxml element as - does *other*. ElementProxy objects are value objects and should - maintain no mutable local state. Equality for proxy objects is - defined as referring to the same XML element, whether or not they are - the same proxy object instance. - """ - if not isinstance(other, ElementProxy): - return False - return self._element is other._element - - def __ne__(self, other): - if not isinstance(other, ElementProxy): - return True - return self._element is not other._element - - @property - def element(self): - """ - The lxml element proxied by this object. - """ - return self._element - - @property - def part(self): - """ - The package part containing this object - """ - return self._parent.part - - -class Parented(object): - """ - Provides common services for document elements that occur below a part - but may occasionally require an ancestor object to provide a service, - such as add or drop a relationship. Provides ``self._parent`` attribute - to subclasses. - """ - def __init__(self, parent): - super(Parented, self).__init__() - self._parent = parent - - @property - def part(self): - """ - The package part containing this object - """ - return self._parent.part diff --git a/docx/styles/__init__.py b/docx/styles/__init__.py deleted file mode 100644 index 63ebaa2b6..000000000 --- a/docx/styles/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -# encoding: utf-8 - -""" -Sub-package module for docx.styles sub-package. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - - -class BabelFish(object): - """ - Translates special-case style names from UI name (e.g. Heading 1) to - internal/styles.xml name (e.g. heading 1) and back. - """ - - style_aliases = ( - ('Caption', 'caption'), - ('Footer', 'footer'), - ('Header', 'header'), - ('Heading 1', 'heading 1'), - ('Heading 2', 'heading 2'), - ('Heading 3', 'heading 3'), - ('Heading 4', 'heading 4'), - ('Heading 5', 'heading 5'), - ('Heading 6', 'heading 6'), - ('Heading 7', 'heading 7'), - ('Heading 8', 'heading 8'), - ('Heading 9', 'heading 9'), - ) - - internal_style_names = dict(style_aliases) - ui_style_names = dict((item[1], item[0]) for item in style_aliases) - - @classmethod - def ui2internal(cls, ui_style_name): - """ - Return the internal style name corresponding to *ui_style_name*, such - as 'heading 1' for 'Heading 1'. - """ - return cls.internal_style_names.get(ui_style_name, ui_style_name) - - @classmethod - def internal2ui(cls, internal_style_name): - """ - Return the user interface style name corresponding to - *internal_style_name*, such as 'Heading 1' for 'heading 1'. - """ - return cls.ui_style_names.get( - internal_style_name, internal_style_name - ) diff --git a/docx/styles/latent.py b/docx/styles/latent.py deleted file mode 100644 index 99b1514ff..000000000 --- a/docx/styles/latent.py +++ /dev/null @@ -1,224 +0,0 @@ -# encoding: utf-8 - -""" -Latent style-related objects. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from . import BabelFish -from ..shared import ElementProxy - - -class LatentStyles(ElementProxy): - """ - Provides access to the default behaviors for latent styles in this - document and to the collection of |_LatentStyle| objects that define - overrides of those defaults for a particular named latent style. - """ - - __slots__ = () - - def __getitem__(self, key): - """ - Enables dictionary-style access to a latent style by name. - """ - style_name = BabelFish.ui2internal(key) - lsdException = self._element.get_by_name(style_name) - if lsdException is None: - raise KeyError("no latent style with name '%s'" % key) - return _LatentStyle(lsdException) - - def __iter__(self): - return (_LatentStyle(ls) for ls in self._element.lsdException_lst) - - def __len__(self): - return len(self._element.lsdException_lst) - - def add_latent_style(self, name): - """ - Return a newly added |_LatentStyle| object to override the inherited - defaults defined in this latent styles object for the built-in style - having *name*. - """ - lsdException = self._element.add_lsdException() - lsdException.name = BabelFish.ui2internal(name) - return _LatentStyle(lsdException) - - @property - def default_priority(self): - """ - Integer between 0 and 99 inclusive specifying the default sort order - for latent styles in style lists and the style gallery. |None| if no - value is assigned, which causes Word to use the default value 99. - """ - return self._element.defUIPriority - - @default_priority.setter - def default_priority(self, value): - self._element.defUIPriority = value - - @property - def default_to_hidden(self): - """ - Boolean specifying whether the default behavior for latent styles is - to be hidden. A hidden style does not appear in the recommended list - or in the style gallery. - """ - return self._element.bool_prop('defSemiHidden') - - @default_to_hidden.setter - def default_to_hidden(self, value): - self._element.set_bool_prop('defSemiHidden', value) - - @property - def default_to_locked(self): - """ - Boolean specifying whether the default behavior for latent styles is - to be locked. A locked style does not appear in the styles panel or - the style gallery and cannot be applied to document content. This - behavior is only active when formatting protection is turned on for - the document (via the Developer menu). - """ - return self._element.bool_prop('defLockedState') - - @default_to_locked.setter - def default_to_locked(self, value): - self._element.set_bool_prop('defLockedState', value) - - @property - def default_to_quick_style(self): - """ - Boolean specifying whether the default behavior for latent styles is - to appear in the style gallery when not hidden. - """ - return self._element.bool_prop('defQFormat') - - @default_to_quick_style.setter - def default_to_quick_style(self, value): - self._element.set_bool_prop('defQFormat', value) - - @property - def default_to_unhide_when_used(self): - """ - Boolean specifying whether the default behavior for latent styles is - to be unhidden when first applied to content. - """ - return self._element.bool_prop('defUnhideWhenUsed') - - @default_to_unhide_when_used.setter - def default_to_unhide_when_used(self, value): - self._element.set_bool_prop('defUnhideWhenUsed', value) - - @property - def load_count(self): - """ - Integer specifying the number of built-in styles to initialize to the - defaults specified in this |LatentStyles| object. |None| if there is - no setting in the XML (very uncommon). The default Word 2011 template - sets this value to 276, accounting for the built-in styles in Word - 2010. - """ - return self._element.count - - @load_count.setter - def load_count(self, value): - self._element.count = value - - -class _LatentStyle(ElementProxy): - """ - Proxy for an `w:lsdException` element, which specifies display behaviors - for a built-in style when no definition for that style is stored yet in - the `styles.xml` part. The values in this element override the defaults - specified in the parent `w:latentStyles` element. - """ - - __slots__ = () - - def delete(self): - """ - Remove this latent style definition such that the defaults defined in - the containing |LatentStyles| object provide the effective value for - each of its attributes. Attempting to access any attributes on this - object after calling this method will raise |AttributeError|. - """ - self._element.delete() - self._element = None - - @property - def hidden(self): - """ - Tri-state value specifying whether this latent style should appear in - the recommended list. |None| indicates the effective value is - inherited from the parent ```` element. - """ - return self._element.on_off_prop('semiHidden') - - @hidden.setter - def hidden(self, value): - self._element.set_on_off_prop('semiHidden', value) - - @property - def locked(self): - """ - Tri-state value specifying whether this latent styles is locked. - A locked style does not appear in the styles panel or the style - gallery and cannot be applied to document content. This behavior is - only active when formatting protection is turned on for the document - (via the Developer menu). - """ - return self._element.on_off_prop('locked') - - @locked.setter - def locked(self, value): - self._element.set_on_off_prop('locked', value) - - @property - def name(self): - """ - The name of the built-in style this exception applies to. - """ - return BabelFish.internal2ui(self._element.name) - - @property - def priority(self): - """ - The integer sort key for this latent style in the Word UI. - """ - return self._element.uiPriority - - @priority.setter - def priority(self, value): - self._element.uiPriority = value - - @property - def quick_style(self): - """ - Tri-state value specifying whether this latent style should appear in - the Word styles gallery when not hidden. |None| indicates the - effective value should be inherited from the default values in its - parent |LatentStyles| object. - """ - return self._element.on_off_prop('qFormat') - - @quick_style.setter - def quick_style(self, value): - self._element.set_on_off_prop('qFormat', value) - - @property - def unhide_when_used(self): - """ - Tri-state value specifying whether this style should have its - :attr:`hidden` attribute set |False| the next time the style is - applied to content. |None| indicates the effective value should be - inherited from the default specified by its parent |LatentStyles| - object. - """ - return self._element.on_off_prop('unhideWhenUsed') - - @unhide_when_used.setter - def unhide_when_used(self, value): - self._element.set_on_off_prop('unhideWhenUsed', value) diff --git a/docx/styles/style.py b/docx/styles/style.py deleted file mode 100644 index 24371b231..000000000 --- a/docx/styles/style.py +++ /dev/null @@ -1,265 +0,0 @@ -# encoding: utf-8 - -""" -Style object hierarchy. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from . import BabelFish -from ..enum.style import WD_STYLE_TYPE -from ..shared import ElementProxy -from ..text.font import Font -from ..text.parfmt import ParagraphFormat - - -def StyleFactory(style_elm): - """ - Return a style object of the appropriate |BaseStyle| subclass, according - to the type of *style_elm*. - """ - style_cls = { - WD_STYLE_TYPE.PARAGRAPH: _ParagraphStyle, - WD_STYLE_TYPE.CHARACTER: _CharacterStyle, - WD_STYLE_TYPE.TABLE: _TableStyle, - WD_STYLE_TYPE.LIST: _NumberingStyle - }[style_elm.type] - - return style_cls(style_elm) - - -class BaseStyle(ElementProxy): - """ - Base class for the various types of style object, paragraph, character, - table, and numbering. These properties and methods are inherited by all - style objects. - """ - - __slots__ = () - - @property - def builtin(self): - """ - Read-only. |True| if this style is a built-in style. |False| - indicates it is a custom (user-defined) style. Note this value is - based on the presence of a `customStyle` attribute in the XML, not on - specific knowledge of which styles are built into Word. - """ - return not self._element.customStyle - - def delete(self): - """ - Remove this style definition from the document. Note that calling - this method does not remove or change the style applied to any - document content. Content items having the deleted style will be - rendered using the default style, as is any content with a style not - defined in the document. - """ - self._element.delete() - self._element = None - - @property - def hidden(self): - """ - |True| if display of this style in the style gallery and list of - recommended styles is suppressed. |False| otherwise. In order to be - shown in the style gallery, this value must be |False| and - :attr:`.quick_style` must be |True|. - """ - return self._element.semiHidden_val - - @hidden.setter - def hidden(self, value): - self._element.semiHidden_val = value - - @property - def locked(self): - """ - Read/write Boolean. |True| if this style is locked. A locked style - does not appear in the styles panel or the style gallery and cannot - be applied to document content. This behavior is only active when - formatting protection is turned on for the document (via the - Developer menu). - """ - return self._element.locked_val - - @locked.setter - def locked(self, value): - self._element.locked_val = value - - @property - def name(self): - """ - The UI name of this style. - """ - name = self._element.name_val - if name is None: - return None - return BabelFish.internal2ui(name) - - @name.setter - def name(self, value): - self._element.name_val = value - - @property - def priority(self): - """ - The integer sort key governing display sequence of this style in the - Word UI. |None| indicates no setting is defined, causing Word to use - the default value of 0. Style name is used as a secondary sort key to - resolve ordering of styles having the same priority value. - """ - return self._element.uiPriority_val - - @priority.setter - def priority(self, value): - self._element.uiPriority_val = value - - @property - def quick_style(self): - """ - |True| if this style should be displayed in the style gallery when - :attr:`.hidden` is |False|. Read/write Boolean. - """ - return self._element.qFormat_val - - @quick_style.setter - def quick_style(self, value): - self._element.qFormat_val = value - - @property - def style_id(self): - """ - The unique key name (string) for this style. This value is subject to - rewriting by Word and should generally not be changed unless you are - familiar with the internals involved. - """ - return self._element.styleId - - @style_id.setter - def style_id(self, value): - self._element.styleId = value - - @property - def type(self): - """ - Member of :ref:`WdStyleType` corresponding to the type of this style, - e.g. ``WD_STYLE_TYPE.PARAGRAPH``. - """ - type = self._element.type - if type is None: - return WD_STYLE_TYPE.PARAGRAPH - return type - - @property - def unhide_when_used(self): - """ - |True| if an application should make this style visible the next time - it is applied to content. False otherwise. Note that |docx| does not - automatically unhide a style having |True| for this attribute when it - is applied to content. - """ - return self._element.unhideWhenUsed_val - - @unhide_when_used.setter - def unhide_when_used(self, value): - self._element.unhideWhenUsed_val = value - - -class _CharacterStyle(BaseStyle): - """ - A character style. A character style is applied to a |Run| object and - primarily provides character-level formatting via the |Font| object in - its :attr:`.font` property. - """ - - __slots__ = () - - @property - def base_style(self): - """ - Style object this style inherits from or |None| if this style is - not based on another style. - """ - base_style = self._element.base_style - if base_style is None: - return None - return StyleFactory(base_style) - - @base_style.setter - def base_style(self, style): - style_id = style.style_id if style is not None else None - self._element.basedOn_val = style_id - - @property - def font(self): - """ - The |Font| object providing access to the character formatting - properties for this style, such as font name and size. - """ - return Font(self._element) - - -class _ParagraphStyle(_CharacterStyle): - """ - A paragraph style. A paragraph style provides both character formatting - and paragraph formatting such as indentation and line-spacing. - """ - - __slots__ = () - - def __repr__(self): - return '_ParagraphStyle(\'%s\') id: %s' % (self.name, id(self)) - - @property - def next_paragraph_style(self): - """ - |_ParagraphStyle| object representing the style to be applied - automatically to a new paragraph inserted after a paragraph of this - style. Returns self if no next paragraph style is defined. Assigning - |None| or *self* removes the setting such that new paragraphs are - created using this same style. - """ - next_style_elm = self._element.next_style - if next_style_elm is None: - return self - if next_style_elm.type != WD_STYLE_TYPE.PARAGRAPH: - return self - return StyleFactory(next_style_elm) - - @next_paragraph_style.setter - def next_paragraph_style(self, style): - if style is None or style.style_id == self.style_id: - self._element._remove_next() - else: - self._element.get_or_add_next().val = style.style_id - - @property - def paragraph_format(self): - """ - The |ParagraphFormat| object providing access to the paragraph - formatting properties for this style such as indentation. - """ - return ParagraphFormat(self._element) - - -class _TableStyle(_ParagraphStyle): - """ - A table style. A table style provides character and paragraph formatting - for its contents as well as special table formatting properties. - """ - - __slots__ = () - - def __repr__(self): - return '_TableStyle(\'%s\') id: %s' % (self.name, id(self)) - - -class _NumberingStyle(BaseStyle): - """ - A numbering style. Not yet implemented. - """ - - __slots__ = () diff --git a/docx/styles/styles.py b/docx/styles/styles.py deleted file mode 100644 index f9f1cd2fb..000000000 --- a/docx/styles/styles.py +++ /dev/null @@ -1,153 +0,0 @@ -# encoding: utf-8 - -"""Styles object, container for all objects in the styles part""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from warnings import warn - -from docx.shared import ElementProxy -from docx.styles import BabelFish -from docx.styles.latent import LatentStyles -from docx.styles.style import BaseStyle, StyleFactory - - -class Styles(ElementProxy): - """Provides access to the styles defined in a document. - - Accessed using the :attr:`.Document.styles` property. Supports ``len()``, iteration, - and dictionary-style access by style name. - """ - - __slots__ = () - - def __contains__(self, name): - """ - Enables `in` operator on style name. - """ - internal_name = BabelFish.ui2internal(name) - for style in self._element.style_lst: - if style.name_val == internal_name: - return True - return False - - def __getitem__(self, key): - """ - Enables dictionary-style access by UI name. Lookup by style id is - deprecated, triggers a warning, and will be removed in a near-future - release. - """ - style_elm = self._element.get_by_name(BabelFish.ui2internal(key)) - if style_elm is not None: - return StyleFactory(style_elm) - - style_elm = self._element.get_by_id(key) - if style_elm is not None: - msg = ( - 'style lookup by style_id is deprecated. Use style name as ' - 'key instead.' - ) - warn(msg, UserWarning, stacklevel=2) - return StyleFactory(style_elm) - - raise KeyError("no style with name '%s'" % key) - - def __iter__(self): - return (StyleFactory(style) for style in self._element.style_lst) - - def __len__(self): - return len(self._element.style_lst) - - def add_style(self, name, style_type, builtin=False): - """ - Return a newly added style object of *style_type* and identified - by *name*. A builtin style can be defined by passing True for the - optional *builtin* argument. - """ - style_name = BabelFish.ui2internal(name) - if style_name in self: - raise ValueError("document already contains style '%s'" % name) - style = self._element.add_style_of_type( - style_name, style_type, builtin - ) - return StyleFactory(style) - - def default(self, style_type): - """ - Return the default style for *style_type* or |None| if no default is - defined for that type (not common). - """ - style = self._element.default_for(style_type) - if style is None: - return None - return StyleFactory(style) - - def get_by_id(self, style_id, style_type): - """Return the style of *style_type* matching *style_id*. - - Returns the default for *style_type* if *style_id* is not found or is |None|, or - if the style having *style_id* is not of *style_type*. - """ - if style_id is None: - return self.default(style_type) - return self._get_by_id(style_id, style_type) - - def get_style_id(self, style_or_name, style_type): - """ - Return the id of the style corresponding to *style_or_name*, or - |None| if *style_or_name* is |None|. If *style_or_name* is not - a style object, the style is looked up using *style_or_name* as - a style name, raising |ValueError| if no style with that name is - defined. Raises |ValueError| if the target style is not of - *style_type*. - """ - if style_or_name is None: - return None - elif isinstance(style_or_name, BaseStyle): - return self._get_style_id_from_style(style_or_name, style_type) - else: - return self._get_style_id_from_name(style_or_name, style_type) - - @property - def latent_styles(self): - """ - A |LatentStyles| object providing access to the default behaviors for - latent styles and the collection of |_LatentStyle| objects that - define overrides of those defaults for a particular named latent - style. - """ - return LatentStyles(self._element.get_or_add_latentStyles()) - - def _get_by_id(self, style_id, style_type): - """ - Return the style of *style_type* matching *style_id*. Returns the - default for *style_type* if *style_id* is not found or if the style - having *style_id* is not of *style_type*. - """ - style = self._element.get_by_id(style_id) - if style is None or style.type != style_type: - return self.default(style_type) - return StyleFactory(style) - - def _get_style_id_from_name(self, style_name, style_type): - """ - Return the id of the style of *style_type* corresponding to - *style_name*. Returns |None| if that style is the default style for - *style_type*. Raises |ValueError| if the named style is not found in - the document or does not match *style_type*. - """ - return self._get_style_id_from_style(self[style_name], style_type) - - def _get_style_id_from_style(self, style, style_type): - """ - Return the id of *style*, or |None| if it is the default style of - *style_type*. Raises |ValueError| if style is not of *style_type*. - """ - if style.type != style_type: - raise ValueError( - "assigned style is type %s, need type %s" % - (style.type, style_type) - ) - if style == self.default(style_type): - return None - return style.style_id diff --git a/docx/text/font.py b/docx/text/font.py deleted file mode 100644 index 162832101..000000000 --- a/docx/text/font.py +++ /dev/null @@ -1,411 +0,0 @@ -# encoding: utf-8 - -""" -Font-related proxy objects. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..dml.color import ColorFormat -from ..shared import ElementProxy - - -class Font(ElementProxy): - """ - Proxy object wrapping the parent of a ```` element and providing - access to character properties such as font name, font size, bold, and - subscript. - """ - - __slots__ = () - - @property - def all_caps(self): - """ - Read/write. Causes text in this font to appear in capital letters. - """ - return self._get_bool_prop('caps') - - @all_caps.setter - def all_caps(self, value): - self._set_bool_prop('caps', value) - - @property - def bold(self): - """ - Read/write. Causes text in this font to appear in bold. - """ - return self._get_bool_prop('b') - - @bold.setter - def bold(self, value): - self._set_bool_prop('b', value) - - @property - def color(self): - """ - A |ColorFormat| object providing a way to get and set the text color - for this font. - """ - return ColorFormat(self._element) - - @property - def complex_script(self): - """ - Read/write tri-state value. When |True|, causes the characters in the - run to be treated as complex script regardless of their Unicode - values. - """ - return self._get_bool_prop('cs') - - @complex_script.setter - def complex_script(self, value): - self._set_bool_prop('cs', value) - - @property - def cs_bold(self): - """ - Read/write tri-state value. When |True|, causes the complex script - characters in the run to be displayed in bold typeface. - """ - return self._get_bool_prop('bCs') - - @cs_bold.setter - def cs_bold(self, value): - self._set_bool_prop('bCs', value) - - @property - def cs_italic(self): - """ - Read/write tri-state value. When |True|, causes the complex script - characters in the run to be displayed in italic typeface. - """ - return self._get_bool_prop('iCs') - - @cs_italic.setter - def cs_italic(self, value): - self._set_bool_prop('iCs', value) - - @property - def double_strike(self): - """ - Read/write tri-state value. When |True|, causes the text in the run - to appear with double strikethrough. - """ - return self._get_bool_prop('dstrike') - - @double_strike.setter - def double_strike(self, value): - self._set_bool_prop('dstrike', value) - - @property - def emboss(self): - """ - Read/write tri-state value. When |True|, causes the text in the run - to appear as if raised off the page in relief. - """ - return self._get_bool_prop('emboss') - - @emboss.setter - def emboss(self, value): - self._set_bool_prop('emboss', value) - - @property - def hidden(self): - """ - Read/write tri-state value. When |True|, causes the text in the run - to be hidden from display, unless applications settings force hidden - text to be shown. - """ - return self._get_bool_prop('vanish') - - @hidden.setter - def hidden(self, value): - self._set_bool_prop('vanish', value) - - @property - def highlight_color(self): - """ - A member of :ref:`WdColorIndex` indicating the color of highlighting - applied, or `None` if no highlighting is applied. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.highlight_val - - @highlight_color.setter - def highlight_color(self, value): - rPr = self._element.get_or_add_rPr() - rPr.highlight_val = value - - @property - def italic(self): - """ - Read/write tri-state value. When |True|, causes the text of the run - to appear in italics. |None| indicates the effective value is - inherited from the style hierarchy. - """ - return self._get_bool_prop('i') - - @italic.setter - def italic(self, value): - self._set_bool_prop('i', value) - - @property - def imprint(self): - """ - Read/write tri-state value. When |True|, causes the text in the run - to appear as if pressed into the page. - """ - return self._get_bool_prop('imprint') - - @imprint.setter - def imprint(self, value): - self._set_bool_prop('imprint', value) - - @property - def math(self): - """ - Read/write tri-state value. When |True|, specifies this run contains - WML that should be handled as though it was Office Open XML Math. - """ - return self._get_bool_prop('oMath') - - @math.setter - def math(self, value): - self._set_bool_prop('oMath', value) - - @property - def name(self): - """ - Get or set the typeface name for this |Font| instance, causing the - text it controls to appear in the named font, if a matching font is - found. |None| indicates the typeface is inherited from the style - hierarchy. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.rFonts_ascii - - @name.setter - def name(self, value): - rPr = self._element.get_or_add_rPr() - rPr.rFonts_ascii = value - rPr.rFonts_hAnsi = value - - @property - def no_proof(self): - """ - Read/write tri-state value. When |True|, specifies that the contents - of this run should not report any errors when the document is scanned - for spelling and grammar. - """ - return self._get_bool_prop('noProof') - - @no_proof.setter - def no_proof(self, value): - self._set_bool_prop('noProof', value) - - @property - def outline(self): - """ - Read/write tri-state value. When |True| causes the characters in the - run to appear as if they have an outline, by drawing a one pixel wide - border around the inside and outside borders of each character glyph. - """ - return self._get_bool_prop('outline') - - @outline.setter - def outline(self, value): - self._set_bool_prop('outline', value) - - @property - def rtl(self): - """ - Read/write tri-state value. When |True| causes the text in the run - to have right-to-left characteristics. - """ - return self._get_bool_prop('rtl') - - @rtl.setter - def rtl(self, value): - self._set_bool_prop('rtl', value) - - @property - def shadow(self): - """ - Read/write tri-state value. When |True| causes the text in the run - to appear as if each character has a shadow. - """ - return self._get_bool_prop('shadow') - - @shadow.setter - def shadow(self, value): - self._set_bool_prop('shadow', value) - - @property - def size(self): - """ - Read/write |Length| value or |None|, indicating the font height in - English Metric Units (EMU). |None| indicates the font size should be - inherited from the style hierarchy. |Length| is a subclass of |int| - having properties for convenient conversion into points or other - length units. The :class:`docx.shared.Pt` class allows convenient - specification of point values:: - - >> font.size = Pt(24) - >> font.size - 304800 - >> font.size.pt - 24.0 - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.sz_val - - @size.setter - def size(self, emu): - rPr = self._element.get_or_add_rPr() - rPr.sz_val = emu - - @property - def small_caps(self): - """ - Read/write tri-state value. When |True| causes the lowercase - characters in the run to appear as capital letters two points smaller - than the font size specified for the run. - """ - return self._get_bool_prop('smallCaps') - - @small_caps.setter - def small_caps(self, value): - self._set_bool_prop('smallCaps', value) - - @property - def snap_to_grid(self): - """ - Read/write tri-state value. When |True| causes the run to use the - document grid characters per line settings defined in the docGrid - element when laying out the characters in this run. - """ - return self._get_bool_prop('snapToGrid') - - @snap_to_grid.setter - def snap_to_grid(self, value): - self._set_bool_prop('snapToGrid', value) - - @property - def spec_vanish(self): - """ - Read/write tri-state value. When |True|, specifies that the given run - shall always behave as if it is hidden, even when hidden text is - being displayed in the current document. The property has a very - narrow, specialized use related to the table of contents. Consult the - spec (§17.3.2.36) for more details. - """ - return self._get_bool_prop('specVanish') - - @spec_vanish.setter - def spec_vanish(self, value): - self._set_bool_prop('specVanish', value) - - @property - def strike(self): - """ - Read/write tri-state value. When |True| causes the text in the run - to appear with a single horizontal line through the center of the - line. - """ - return self._get_bool_prop('strike') - - @strike.setter - def strike(self, value): - self._set_bool_prop('strike', value) - - @property - def subscript(self): - """ - Boolean indicating whether the characters in this |Font| appear as - subscript. |None| indicates the subscript/subscript value is - inherited from the style hierarchy. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.subscript - - @subscript.setter - def subscript(self, value): - rPr = self._element.get_or_add_rPr() - rPr.subscript = value - - @property - def superscript(self): - """ - Boolean indicating whether the characters in this |Font| appear as - superscript. |None| indicates the subscript/superscript value is - inherited from the style hierarchy. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.superscript - - @superscript.setter - def superscript(self, value): - rPr = self._element.get_or_add_rPr() - rPr.superscript = value - - @property - def underline(self): - """ - The underline style for this |Font|, one of |None|, |True|, |False|, - or a value from :ref:`WdUnderline`. |None| indicates the font - inherits its underline value from the style hierarchy. |False| - indicates no underline. |True| indicates single underline. The values - from :ref:`WdUnderline` are used to specify other outline styles such - as double, wavy, and dotted. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr.u_val - - @underline.setter - def underline(self, value): - rPr = self._element.get_or_add_rPr() - rPr.u_val = value - - @property - def web_hidden(self): - """ - Read/write tri-state value. When |True|, specifies that the contents - of this run shall be hidden when the document is displayed in web - page view. - """ - return self._get_bool_prop('webHidden') - - @web_hidden.setter - def web_hidden(self, value): - self._set_bool_prop('webHidden', value) - - def _get_bool_prop(self, name): - """ - Return the value of boolean child of `w:rPr` having *name*. - """ - rPr = self._element.rPr - if rPr is None: - return None - return rPr._get_bool_val(name) - - def _set_bool_prop(self, name, value): - """ - Assign *value* to the boolean child *name* of `w:rPr`. - """ - rPr = self._element.get_or_add_rPr() - rPr._set_bool_val(name, value) diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py deleted file mode 100644 index 4fb583b94..000000000 --- a/docx/text/paragraph.py +++ /dev/null @@ -1,145 +0,0 @@ -# encoding: utf-8 - -""" -Paragraph-related proxy types. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..enum.style import WD_STYLE_TYPE -from .parfmt import ParagraphFormat -from .run import Run -from ..shared import Parented - - -class Paragraph(Parented): - """ - Proxy object wrapping ```` element. - """ - def __init__(self, p, parent): - super(Paragraph, self).__init__(parent) - self._p = self._element = p - - def add_run(self, text=None, style=None): - """ - Append a run to this paragraph containing *text* and having character - style identified by style ID *style*. *text* can contain tab - (``\\t``) characters, which are converted to the appropriate XML form - for a tab. *text* can also include newline (``\\n``) or carriage - return (``\\r``) characters, each of which is converted to a line - break. - """ - r = self._p.add_r() - run = Run(r, self) - if text: - run.text = text - if style: - run.style = style - return run - - @property - def alignment(self): - """ - A member of the :ref:`WdParagraphAlignment` enumeration specifying - the justification setting for this paragraph. A value of |None| - indicates the paragraph has no directly-applied alignment value and - will inherit its alignment value from its style hierarchy. Assigning - |None| to this property removes any directly-applied alignment value. - """ - return self._p.alignment - - @alignment.setter - def alignment(self, value): - self._p.alignment = value - - def clear(self): - """ - Return this same paragraph after removing all its content. - Paragraph-level formatting, such as style, is preserved. - """ - self._p.clear_content() - return self - - def insert_paragraph_before(self, text=None, style=None): - """ - Return a newly created paragraph, inserted directly before this - paragraph. If *text* is supplied, the new paragraph contains that - text in a single run. If *style* is provided, that style is assigned - to the new paragraph. - """ - paragraph = self._insert_paragraph_before() - if text: - paragraph.add_run(text) - if style is not None: - paragraph.style = style - return paragraph - - @property - def paragraph_format(self): - """ - The |ParagraphFormat| object providing access to the formatting - properties for this paragraph, such as line spacing and indentation. - """ - return ParagraphFormat(self._element) - - @property - def runs(self): - """ - Sequence of |Run| instances corresponding to the elements in - this paragraph. - """ - return [Run(r, self) for r in self._p.r_lst] - - @property - def style(self): - """ - Read/Write. |_ParagraphStyle| object representing the style assigned - to this paragraph. If no explicit style is assigned to this - paragraph, its value is the default paragraph style for the document. - A paragraph style name can be assigned in lieu of a paragraph style - object. Assigning |None| removes any applied style, making its - effective value the default paragraph style for the document. - """ - style_id = self._p.style - return self.part.get_style(style_id, WD_STYLE_TYPE.PARAGRAPH) - - @style.setter - def style(self, style_or_name): - style_id = self.part.get_style_id( - style_or_name, WD_STYLE_TYPE.PARAGRAPH - ) - self._p.style = style_id - - @property - def text(self): - """ - String formed by concatenating the text of each run in the paragraph. - Tabs and line breaks in the XML are mapped to ``\\t`` and ``\\n`` - characters respectively. - - Assigning text to this property causes all existing paragraph content - to be replaced with a single run containing the assigned text. - A ``\\t`` character in the text is mapped to a ```` element - and each ``\\n`` or ``\\r`` character is mapped to a line break. - Paragraph-level formatting, such as style, is preserved. All - run-level formatting, such as bold or italic, is removed. - """ - text = '' - for run in self.runs: - text += run.text - return text - - @text.setter - def text(self, text): - self.clear() - self.add_run(text) - - def _insert_paragraph_before(self): - """ - Return a newly created paragraph, inserted directly before this - paragraph. - """ - p = self._p.add_p_before() - return Paragraph(p, self._parent) diff --git a/docx/text/run.py b/docx/text/run.py deleted file mode 100644 index 97d6da7db..000000000 --- a/docx/text/run.py +++ /dev/null @@ -1,191 +0,0 @@ -# encoding: utf-8 - -""" -Run-related proxy objects for python-docx, Run in particular. -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from ..enum.style import WD_STYLE_TYPE -from ..enum.text import WD_BREAK -from .font import Font -from ..shape import InlineShape -from ..shared import Parented - - -class Run(Parented): - """ - Proxy object wrapping ```` element. Several of the properties on Run - take a tri-state value, |True|, |False|, or |None|. |True| and |False| - correspond to on and off respectively. |None| indicates the property is - not specified directly on the run and its effective value is taken from - the style hierarchy. - """ - def __init__(self, r, parent): - super(Run, self).__init__(parent) - self._r = self._element = self.element = r - - def add_break(self, break_type=WD_BREAK.LINE): - """ - Add a break element of *break_type* to this run. *break_type* can - take the values `WD_BREAK.LINE`, `WD_BREAK.PAGE`, and - `WD_BREAK.COLUMN` where `WD_BREAK` is imported from `docx.enum.text`. - *break_type* defaults to `WD_BREAK.LINE`. - """ - type_, clear = { - WD_BREAK.LINE: (None, None), - WD_BREAK.PAGE: ('page', None), - WD_BREAK.COLUMN: ('column', None), - WD_BREAK.LINE_CLEAR_LEFT: ('textWrapping', 'left'), - WD_BREAK.LINE_CLEAR_RIGHT: ('textWrapping', 'right'), - WD_BREAK.LINE_CLEAR_ALL: ('textWrapping', 'all'), - }[break_type] - br = self._r.add_br() - if type_ is not None: - br.type = type_ - if clear is not None: - br.clear = clear - - def add_picture(self, image_path_or_stream, width=None, height=None): - """ - Return an |InlineShape| instance containing the image identified by - *image_path_or_stream*, added to the end of this run. - *image_path_or_stream* can be a path (a string) or a file-like object - containing a binary image. If neither width nor height is specified, - the picture appears at its native size. If only one is specified, it - is used to compute a scaling factor that is then applied to the - unspecified dimension, preserving the aspect ratio of the image. The - native size of the picture is calculated using the dots-per-inch - (dpi) value specified in the image file, defaulting to 72 dpi if no - value is specified, as is often the case. - """ - inline = self.part.new_pic_inline(image_path_or_stream, width, height) - self._r.add_drawing(inline) - return InlineShape(inline) - - def add_tab(self): - """ - Add a ```` element at the end of the run, which Word - interprets as a tab character. - """ - self._r._add_tab() - - def add_text(self, text): - """ - Returns a newly appended |_Text| object (corresponding to a new - ```` child element) to the run, containing *text*. Compare with - the possibly more friendly approach of assigning text to the - :attr:`Run.text` property. - """ - t = self._r.add_t(text) - return _Text(t) - - @property - def bold(self): - """ - Read/write. Causes the text of the run to appear in bold. - """ - return self.font.bold - - @bold.setter - def bold(self, value): - self.font.bold = value - - def clear(self): - """ - Return reference to this run after removing all its content. All run - formatting is preserved. - """ - self._r.clear_content() - return self - - @property - def font(self): - """ - The |Font| object providing access to the character formatting - properties for this run, such as font name and size. - """ - return Font(self._element) - - @property - def italic(self): - """ - Read/write tri-state value. When |True|, causes the text of the run - to appear in italics. - """ - return self.font.italic - - @italic.setter - def italic(self, value): - self.font.italic = value - - @property - def style(self): - """ - Read/write. A |_CharacterStyle| object representing the character - style applied to this run. The default character style for the - document (often `Default Character Font`) is returned if the run has - no directly-applied character style. Setting this property to |None| - removes any directly-applied character style. - """ - style_id = self._r.style - return self.part.get_style(style_id, WD_STYLE_TYPE.CHARACTER) - - @style.setter - def style(self, style_or_name): - style_id = self.part.get_style_id( - style_or_name, WD_STYLE_TYPE.CHARACTER - ) - self._r.style = style_id - - @property - def text(self): - """ - String formed by concatenating the text equivalent of each run - content child element into a Python string. Each ```` element - adds the text characters it contains. A ```` element adds - a ``\\t`` character. A ```` or ```` element each add - a ``\\n`` character. Note that a ```` element can indicate - a page break or column break as well as a line break. All ```` - elements translate to a single ``\\n`` character regardless of their - type. All other content child elements, such as ````, are - ignored. - - Assigning text to this property has the reverse effect, translating - each ``\\t`` character to a ```` element and each ``\\n`` or - ``\\r`` character to a ```` element. Any existing run content - is replaced. Run formatting is preserved. - """ - return self._r.text - - @text.setter - def text(self, text): - self._r.text = text - - @property - def underline(self): - """ - The underline style for this |Run|, one of |None|, |True|, |False|, - or a value from :ref:`WdUnderline`. A value of |None| indicates the - run has no directly-applied underline value and so will inherit the - underline value of its containing paragraph. Assigning |None| to this - property removes any directly-applied underline value. A value of - |False| indicates a directly-applied setting of no underline, - overriding any inherited value. A value of |True| indicates single - underline. The values from :ref:`WdUnderline` are used to specify - other outline styles such as double, wavy, and dotted. - """ - return self.font.underline - - @underline.setter - def underline(self, value): - self.font.underline = value - - -class _Text(object): - """ - Proxy object wrapping ```` element. - """ - def __init__(self, t_elm): - super(_Text, self).__init__() - self._t = t_elm diff --git a/docx/text/tabstops.py b/docx/text/tabstops.py deleted file mode 100644 index c22b9bc91..000000000 --- a/docx/text/tabstops.py +++ /dev/null @@ -1,143 +0,0 @@ -# encoding: utf-8 - -""" -Tabstop-related proxy types. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..shared import ElementProxy -from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER - - -class TabStops(ElementProxy): - """ - A sequence of |TabStop| objects providing access to the tab stops of - a paragraph or paragraph style. Supports iteration, indexed access, del, - and len(). It is accesed using the :attr:`~.ParagraphFormat.tab_stops` - property of ParagraphFormat; it is not intended to be constructed - directly. - """ - - __slots__ = ('_pPr') - - def __init__(self, element): - super(TabStops, self).__init__(element, None) - self._pPr = element - - def __delitem__(self, idx): - """ - Remove the tab at offset *idx* in this sequence. - """ - tabs = self._pPr.tabs - try: - tabs.remove(tabs[idx]) - except (AttributeError, IndexError): - raise IndexError('tab index out of range') - - if len(tabs) == 0: - self._pPr.remove(tabs) - - def __getitem__(self, idx): - """ - Enables list-style access by index. - """ - tabs = self._pPr.tabs - if tabs is None: - raise IndexError('TabStops object is empty') - tab = tabs.tab_lst[idx] - return TabStop(tab) - - def __iter__(self): - """ - Generate a TabStop object for each of the w:tab elements, in XML - document order. - """ - tabs = self._pPr.tabs - if tabs is not None: - for tab in tabs.tab_lst: - yield TabStop(tab) - - def __len__(self): - tabs = self._pPr.tabs - if tabs is None: - return 0 - return len(tabs.tab_lst) - - def add_tab_stop(self, position, alignment=WD_TAB_ALIGNMENT.LEFT, - leader=WD_TAB_LEADER.SPACES): - """ - Add a new tab stop at *position*, a |Length| object specifying the - location of the tab stop relative to the paragraph edge. A negative - *position* value is valid and appears in hanging indentation. Tab - alignment defaults to left, but may be specified by passing a member - of the :ref:`WdTabAlignment` enumeration as *alignment*. An optional - leader character can be specified by passing a member of the - :ref:`WdTabLeader` enumeration as *leader*. - """ - tabs = self._pPr.get_or_add_tabs() - tab = tabs.insert_tab_in_order(position, alignment, leader) - return TabStop(tab) - - def clear_all(self): - """ - Remove all custom tab stops. - """ - self._pPr._remove_tabs() - - -class TabStop(ElementProxy): - """ - An individual tab stop applying to a paragraph or style. Accessed using - list semantics on its containing |TabStops| object. - """ - - __slots__ = ('_tab') - - def __init__(self, element): - super(TabStop, self).__init__(element, None) - self._tab = element - - @property - def alignment(self): - """ - A member of :ref:`WdTabAlignment` specifying the alignment setting - for this tab stop. Read/write. - """ - return self._tab.val - - @alignment.setter - def alignment(self, value): - self._tab.val = value - - @property - def leader(self): - """ - A member of :ref:`WdTabLeader` specifying a repeating character used - as a "leader", filling in the space spanned by this tab. Assigning - |None| produces the same result as assigning `WD_TAB_LEADER.SPACES`. - Read/write. - """ - return self._tab.leader - - @leader.setter - def leader(self, value): - self._tab.leader = value - - @property - def position(self): - """ - A |Length| object representing the distance of this tab stop from the - inside edge of the paragraph. May be positive or negative. - Read/write. - """ - return self._tab.pos - - @position.setter - def position(self, value): - tab = self._tab - tabs = tab.getparent() - self._tab = tabs.insert_tab_in_order(value, tab.val, tab.leader) - tabs.remove(tab) diff --git a/features/environment.py b/features/environment.py index e144106cf..dfd2028a3 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,15 +1,8 @@ -# encoding: utf-8 - -""" -Used by behave to set testing environment before and after running acceptance -tests. -""" +"""Set testing environment before and after behave acceptance test runs.""" import os -scratch_dir = os.path.abspath( - os.path.join(os.path.split(__file__)[0], '_scratch') -) +scratch_dir = os.path.abspath(os.path.join(os.path.split(__file__)[0], "_scratch")) def before_all(context): diff --git a/features/hlk-props.feature b/features/hlk-props.feature new file mode 100644 index 000000000..5472f49a3 --- /dev/null +++ b/features/hlk-props.feature @@ -0,0 +1,35 @@ +Feature: Access hyperlink properties + In order to access the URL and other details for a hyperlink + As a developer using python-docx + I need properties on Hyperlink + + + Scenario: Hyperlink.address has the URL of the hyperlink + Given a hyperlink + Then hyperlink.address is the URL of the hyperlink + + + Scenario Outline: Hyperlink.contains_page_break reports presence of page-break + Given a hyperlink having rendered page breaks + Then hyperlink.contains_page_break is + + Examples: Hyperlink.contains_page_break cases + | zero-or-more | value | + | no | False | + | one | True | + + + Scenario Outline: Hyperlink.runs contains Run for each run in hyperlink + Given a hyperlink having runs + Then hyperlink.runs has length + And hyperlink.runs contains only Run instances + + Examples: Hyperlink.runs cases + | zero-or-more | value | + | one | 1 | + | two | 2 | + + + Scenario: Hyperlink.text has the visible text of the hyperlink + Given a hyperlink + Then hyperlink.text is the visible text of the hyperlink diff --git a/features/par-access-inner-content.feature b/features/par-access-inner-content.feature new file mode 100644 index 000000000..047168fcf --- /dev/null +++ b/features/par-access-inner-content.feature @@ -0,0 +1,49 @@ +Feature: Access paragraph inner-content including hyperlinks + In order to extract paragraph content with high-fidelity + As a developer using python-docx + I need to access differentiated paragraph content in document order + + + Scenario Outline: Paragraph.contains_page_break reports presence of page-break + Given a paragraph having rendered page breaks + Then paragraph.contains_page_break is + + Examples: Paragraph.contains_page_break cases + | zero-or-more | value | + | no | False | + | one | True | + | two | True | + + + Scenario Outline: Paragraph.hyperlinks contains Hyperlink for each link in paragraph + Given a paragraph having hyperlinks + Then paragraph.hyperlinks has length + And paragraph.hyperlinks contains only Hyperlink instances + + Examples: Paragraph.hyperlinks cases + | zero-or-more | value | + | no | 0 | + | one | 1 | + | three | 3 | + + + Scenario: Paragraph.iter_inner_content() generates the paragraph's runs and hyperlinks + Given a paragraph having three hyperlinks + Then paragraph.iter_inner_content() generates the paragraph runs and hyperlinks + + + Scenario Outline: Paragraph.rendered_page_breaks contains paragraph RenderedPageBreaks + Given a paragraph having rendered page breaks + Then paragraph.rendered_page_breaks has length + And paragraph.rendered_page_breaks contains only RenderedPageBreak instances + + Examples: Paragraph.rendered_page_breaks cases + | zero-or-more | value | + | no | 0 | + | one | 1 | + | two | 2 | + + + Scenario: Paragraph.text contains both run-text and hyperlink-text + Given a paragraph having three hyperlinks + Then paragraph.text contains the text of both the runs and the hyperlinks diff --git a/features/pbk-split-para.feature b/features/pbk-split-para.feature new file mode 100644 index 000000000..8ce048a40 --- /dev/null +++ b/features/pbk-split-para.feature @@ -0,0 +1,24 @@ +Feature: Split paragraph on rendered page-breaks + In order to extract document content with high page-attribution fidelity + As a developer using python-docx + I need to a way to split a paragraph on its first rendered page break + + + Scenario: RenderedPageBreak.preceding_paragraph_fragment is the content before break + Given a rendered_page_break in a paragraph + Then rendered_page_break.preceding_paragraph_fragment is the content before break + + + Scenario: RenderedPageBreak.preceding_paragraph_fragment includes the hyperlink + Given a rendered_page_break in a hyperlink + Then rendered_page_break.preceding_paragraph_fragment includes the hyperlink + + + Scenario: RenderedPageBreak.following_paragraph_fragment is the content after break + Given a rendered_page_break in a paragraph + Then rendered_page_break.following_paragraph_fragment is the content after break + + + Scenario: RenderedPageBreak.following_paragraph_fragment excludes the hyperlink + Given a rendered_page_break in a hyperlink + Then rendered_page_break.following_paragraph_fragment excludes the hyperlink diff --git a/features/run-access-content.feature b/features/run-access-content.feature deleted file mode 100644 index ad30f6feb..000000000 --- a/features/run-access-content.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: Access run content - In order to discover or locate existing inline content - As a developer using python-docx - I need ways to access the run content - - - Scenario: Get run content as Python text - Given a run having mixed text content - Then the text of the run represents the textual run content diff --git a/features/run-access-inner-content.feature b/features/run-access-inner-content.feature new file mode 100644 index 000000000..a9bbb170c --- /dev/null +++ b/features/run-access-inner-content.feature @@ -0,0 +1,25 @@ +Feature: Access run inner-content including rendered page-breaks + In order to extract run content with high-fidelity + As a developer using python-docx + I need to access differentiated run content in document order + + + Scenario Outline: Run.contains_page_break reports presence of page-break + Given a run having rendered page breaks + Then run.contains_page_break is + + Examples: Run.contains_page_break cases + | zero-or-more | value | + | no | False | + | one | True | + | two | True | + + + Scenario: Run.iter_inner_content() generates the run's text and rendered page-breaks + Given a run having two rendered page breaks + Then run.iter_inner_content() generates the run text and rendered page-breaks + + + Scenario: Run.text contains the text content of the run + Given a run having mixed text content + Then run.text contains the text content of the run diff --git a/features/run-add-content.feature b/features/run-add-content.feature index d4257925c..078dccd33 100644 --- a/features/run-add-content.feature +++ b/features/run-add-content.feature @@ -11,4 +11,4 @@ Feature: Add content to a run Scenario: Assign mixed text to text property Given a run When I assign mixed text to the text property - Then the text of the run represents the textual run content + Then run.text contains the text content of the run diff --git a/features/sct-section.feature b/features/sct-section.feature index d00287403..7017d77aa 100644 --- a/features/sct-section.feature +++ b/features/sct-section.feature @@ -57,6 +57,11 @@ Feature: Access and change section properties Then section.header is a _Header object + Scenario: Section.iter_inner_content() + Given a Section object of a multi-section document as section + Then section.iter_inner_content() produces the paragraphs and tables in section + + Scenario Outline: Get section start type Given a section having start type Then the reported section start type is diff --git a/features/steps/api.py b/features/steps/api.py index a3325567b..16038ffe7 100644 --- a/features/steps/api.py +++ b/features/steps/api.py @@ -1,46 +1,43 @@ -# encoding: utf-8 - -""" -Step implementations for basic API features -""" +"""Step implementations for basic API features.""" from behave import given, then, when import docx - from docx import Document from helpers import test_docx - # given ==================================================== -@given('I have python-docx installed') + +@given("I have python-docx installed") def given_I_have_python_docx_installed(context): pass # when ===================================================== -@when('I call docx.Document() with no arguments') + +@when("I call docx.Document() with no arguments") def when_I_call_docx_Document_with_no_arguments(context): context.document = Document() -@when('I call docx.Document() with the path of a .docx file') +@when("I call docx.Document() with the path of a .docx file") def when_I_call_docx_Document_with_the_path_of_a_docx_file(context): - context.document = Document(test_docx('doc-default')) + context.document = Document(test_docx("doc-default")) # then ===================================================== -@then('document is a Document object') + +@then("document is a Document object") def then_document_is_a_Document_object(context): document = context.document assert isinstance(document, docx.document.Document) -@then('the last paragraph contains the text I specified') +@then("the last paragraph contains the text I specified") def then_last_p_contains_specified_text(context): document = context.document text = context.paragraph_text @@ -48,15 +45,15 @@ def then_last_p_contains_specified_text(context): assert p.text == text -@then('the last paragraph has the style I specified') +@then("the last paragraph has the style I specified") def then_the_last_paragraph_has_the_style_I_specified(context): document, expected_style = context.document, context.style paragraph = document.paragraphs[-1] assert paragraph.style == expected_style -@then('the last paragraph is the empty paragraph I added') +@then("the last paragraph is the empty paragraph I added") def then_last_p_is_empty_paragraph_added(context): document = context.document p = document.paragraphs[-1] - assert p.text == '' + assert p.text == "" diff --git a/features/steps/block.py b/features/steps/block.py index 1eee70cd2..a091694ad 100644 --- a/features/steps/block.py +++ b/features/steps/block.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for block content containers -""" +"""Step implementations for block content containers.""" from behave import given, then, when @@ -11,15 +7,15 @@ from helpers import test_docx - # given =================================================== -@given('a document containing a table') + +@given("a document containing a table") def given_a_document_containing_a_table(context): - context.document = Document(test_docx('blk-containing-table')) + context.document = Document(test_docx("blk-containing-table")) -@given('a paragraph') +@given("a paragraph") def given_a_paragraph(context): context.document = Document() context.paragraph = context.document.add_paragraph() @@ -27,13 +23,14 @@ def given_a_paragraph(context): # when ==================================================== -@when('I add a paragraph') + +@when("I add a paragraph") def when_add_paragraph(context): document = context.document context.p = document.add_paragraph() -@when('I add a table') +@when("I add a table") def when_add_table(context): rows, cols = 2, 2 context.document.add_table(rows, cols) @@ -41,13 +38,14 @@ def when_add_table(context): # then ===================================================== -@then('I can access the table') + +@then("I can access the table") def then_can_access_table(context): table = context.document.tables[-1] assert isinstance(table, Table) -@then('the new table appears in the document') +@then("the new table appears in the document") def then_new_table_appears_in_document(context): table = context.document.tables[-1] assert isinstance(table, Table) diff --git a/features/steps/cell.py b/features/steps/cell.py index d1385c921..10896872b 100644 --- a/features/steps/cell.py +++ b/features/steps/cell.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for table cell-related features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for table cell-related features.""" from behave import given, then, when @@ -12,33 +6,35 @@ from helpers import test_docx - # given =================================================== -@given('a table cell') + +@given("a table cell") def given_a_table_cell(context): - table = Document(test_docx('tbl-2x2-table')).tables[0] + table = Document(test_docx("tbl-2x2-table")).tables[0] context.cell = table.cell(0, 0) # when ===================================================== -@when('I add a 2 x 2 table into the first cell') + +@when("I add a 2 x 2 table into the first cell") def when_I_add_a_2x2_table_into_the_first_cell(context): context.table_ = context.cell.add_table(2, 2) -@when('I assign a string to the cell text attribute') +@when("I assign a string to the cell text attribute") def when_assign_string_to_cell_text_attribute(context): cell = context.cell - text = 'foobar' + text = "foobar" cell.text = text context.expected_text = text # then ===================================================== -@then('cell.tables[0] is a 2 x 2 table') + +@then("cell.tables[0] is a 2 x 2 table") def then_cell_tables_0_is_a_2x2_table(context): cell = context.cell table = cell.tables[0] @@ -46,7 +42,7 @@ def then_cell_tables_0_is_a_2x2_table(context): assert len(table.columns) == 2 -@then('the cell contains the string I assigned') +@then("the cell contains the string I assigned") def then_cell_contains_string_assigned(context): cell, expected_text = context.cell, context.expected_text text = cell.paragraphs[0].runs[0].text diff --git a/features/steps/coreprops.py b/features/steps/coreprops.py index dc6be2e6c..0f6b6a854 100644 --- a/features/steps/coreprops.py +++ b/features/steps/coreprops.py @@ -1,12 +1,4 @@ -# encoding: utf-8 - -""" -Gherkin step implementations for core properties-related features. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Gherkin step implementations for core properties-related features.""" from datetime import datetime, timedelta @@ -17,22 +9,23 @@ from helpers import test_docx - # given =================================================== -@given('a document having known core properties') + +@given("a document having known core properties") def given_a_document_having_known_core_properties(context): - context.document = Document(test_docx('doc-coreprops')) + context.document = Document(test_docx("doc-coreprops")) -@given('a document having no core properties part') +@given("a document having no core properties part") def given_a_document_having_no_core_properties_part(context): - context.document = Document(test_docx('doc-no-coreprops')) + context.document = Document(test_docx("doc-no-coreprops")) # when ==================================================== -@when('I access the core properties object') + +@when("I access the core properties object") def when_I_access_the_core_properties_object(context): context.document.core_properties @@ -40,21 +33,21 @@ def when_I_access_the_core_properties_object(context): @when("I assign new values to the properties") def when_I_assign_new_values_to_the_properties(context): context.propvals = ( - ('author', 'Creator'), - ('category', 'Category'), - ('comments', 'Description'), - ('content_status', 'Content Status'), - ('created', datetime(2013, 6, 15, 12, 34, 56)), - ('identifier', 'Identifier'), - ('keywords', 'key; word; keyword'), - ('language', 'Language'), - ('last_modified_by', 'Last Modified By'), - ('last_printed', datetime(2013, 6, 15, 12, 34, 56)), - ('modified', datetime(2013, 6, 15, 12, 34, 56)), - ('revision', 9), - ('subject', 'Subject'), - ('title', 'Title'), - ('version', 'Version'), + ("author", "Creator"), + ("category", "Category"), + ("comments", "Description"), + ("content_status", "Content Status"), + ("created", datetime(2013, 6, 15, 12, 34, 56)), + ("identifier", "Identifier"), + ("keywords", "key; word; keyword"), + ("language", "Language"), + ("last_modified_by", "Last Modified By"), + ("last_printed", datetime(2013, 6, 15, 12, 34, 56)), + ("modified", datetime(2013, 6, 15, 12, 34, 56)), + ("revision", 9), + ("subject", "Subject"), + ("title", "Title"), + ("version", "Version"), ) core_properties = context.document.core_properties for name, value in context.propvals: @@ -63,11 +56,12 @@ def when_I_assign_new_values_to_the_properties(context): # then ==================================================== -@then('a core properties part with default values is added') + +@then("a core properties part with default values is added") def then_a_core_properties_part_with_default_values_is_added(context): core_properties = context.document.core_properties - assert core_properties.title == 'Word Document' - assert core_properties.last_modified_by == 'python-docx' + assert core_properties.title == "Word Document" + assert core_properties.last_modified_by == "python-docx" assert core_properties.revision == 1 # core_properties.modified only stores time with seconds resolution, so # comparison needs to be a little loose (within two seconds) @@ -76,45 +70,47 @@ def then_a_core_properties_part_with_default_values_is_added(context): assert modified_timedelta < max_expected_timedelta -@then('I can access the core properties object') +@then("I can access the core properties object") def then_I_can_access_the_core_properties_object(context): document = context.document core_properties = document.core_properties assert isinstance(core_properties, CoreProperties) -@then('the core property values match the known values') +@then("the core property values match the known values") def then_the_core_property_values_match_the_known_values(context): known_propvals = ( - ('author', 'Steve Canny'), - ('category', 'Category'), - ('comments', 'Description'), - ('content_status', 'Content Status'), - ('created', datetime(2014, 12, 13, 22, 2, 0)), - ('identifier', 'Identifier'), - ('keywords', 'key; word; keyword'), - ('language', 'Language'), - ('last_modified_by', 'Steve Canny'), - ('last_printed', datetime(2014, 12, 13, 22, 2, 42)), - ('modified', datetime(2014, 12, 13, 22, 6, 0)), - ('revision', 2), - ('subject', 'Subject'), - ('title', 'Title'), - ('version', '0.7.1a3'), + ("author", "Steve Canny"), + ("category", "Category"), + ("comments", "Description"), + ("content_status", "Content Status"), + ("created", datetime(2014, 12, 13, 22, 2, 0)), + ("identifier", "Identifier"), + ("keywords", "key; word; keyword"), + ("language", "Language"), + ("last_modified_by", "Steve Canny"), + ("last_printed", datetime(2014, 12, 13, 22, 2, 42)), + ("modified", datetime(2014, 12, 13, 22, 6, 0)), + ("revision", 2), + ("subject", "Subject"), + ("title", "Title"), + ("version", "0.7.1a3"), ) core_properties = context.document.core_properties for name, expected_value in known_propvals: value = getattr(core_properties, name) - assert value == expected_value, ( - "got '%s' for core property '%s'" % (value, name) + assert value == expected_value, "got '%s' for core property '%s'" % ( + value, + name, ) -@then('the core property values match the new values') +@then("the core property values match the new values") def then_the_core_property_values_match_the_new_values(context): core_properties = context.document.core_properties for name, expected_value in context.propvals: value = getattr(core_properties, name) - assert value == expected_value, ( - "got '%s' for core property '%s'" % (value, name) + assert value == expected_value, "got '%s' for core property '%s'" % ( + value, + name, ) diff --git a/features/steps/document.py b/features/steps/document.py index a8e4d1adf..49165efc3 100644 --- a/features/steps/document.py +++ b/features/steps/document.py @@ -1,60 +1,54 @@ -# encoding: utf-8 - -""" -Step implementations for document-related features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for document-related features.""" from behave import given, then, when from docx import Document from docx.enum.section import WD_ORIENT, WD_SECTION +from docx.section import Sections from docx.shape import InlineShapes from docx.shared import Inches -from docx.section import Sections from docx.styles.styles import Styles from docx.table import Table from docx.text.paragraph import Paragraph from helpers import test_docx, test_file - # given =================================================== -@given('a blank document') + +@given("a blank document") def given_a_blank_document(context): - context.document = Document(test_docx('doc-word-default-blank')) + context.document = Document(test_docx("doc-word-default-blank")) -@given('a document having built-in styles') +@given("a document having built-in styles") def given_a_document_having_builtin_styles(context): context.document = Document() -@given('a document having inline shapes') +@given("a document having inline shapes") def given_a_document_having_inline_shapes(context): - context.document = Document(test_docx('shp-inline-shape-access')) + context.document = Document(test_docx("shp-inline-shape-access")) -@given('a document having sections') +@given("a document having sections") def given_a_document_having_sections(context): - context.document = Document(test_docx('doc-access-sections')) + context.document = Document(test_docx("doc-access-sections")) -@given('a document having styles') +@given("a document having styles") def given_a_document_having_styles(context): - context.document = Document(test_docx('sty-having-styles-part')) + context.document = Document(test_docx("sty-having-styles-part")) -@given('a document having three tables') +@given("a document having three tables") def given_a_document_having_three_tables(context): - context.document = Document(test_docx('tbl-having-tables')) + context.document = Document(test_docx("tbl-having-tables")) -@given('a single-section document having portrait layout') +@given("a single-section document having portrait layout") def given_a_single_section_document_having_portrait_layout(context): - context.document = Document(test_docx('doc-add-section')) + context.document = Document(test_docx("doc-add-section")) section = context.document.sections[-1] context.original_dimensions = (section.page_width, section.page_height) @@ -66,55 +60,56 @@ def given_a_single_section_Document_object_with_headers_and_footers(context): # when ==================================================== -@when('I add a 2 x 2 table specifying only row and column count') + +@when("I add a 2 x 2 table specifying only row and column count") def when_add_2x2_table_specifying_only_row_and_col_count(context): document = context.document document.add_table(rows=2, cols=2) -@when('I add a 2 x 2 table specifying style \'{style_name}\'') +@when("I add a 2 x 2 table specifying style '{style_name}'") def when_add_2x2_table_specifying_style_name(context, style_name): document = context.document document.add_table(rows=2, cols=2, style=style_name) -@when('I add a heading specifying level={level}') +@when("I add a heading specifying level={level}") def when_add_heading_specifying_level(context, level): context.document.add_heading(level=int(level)) -@when('I add a heading specifying only its text') +@when("I add a heading specifying only its text") def when_add_heading_specifying_only_its_text(context): document = context.document - context.heading_text = text = 'Spam vs. Eggs' + context.heading_text = text = "Spam vs. Eggs" document.add_heading(text) -@when('I add a page break to the document') +@when("I add a page break to the document") def when_add_page_break_to_document(context): document = context.document document.add_page_break() -@when('I add a paragraph specifying its style as a {kind}') +@when("I add a paragraph specifying its style as a {kind}") def when_I_add_a_paragraph_specifying_its_style_as_a(context, kind): document = context.document - style = context.style = document.styles['Heading 1'] + style = context.style = document.styles["Heading 1"] style_spec = { - 'style object': style, - 'style name': 'Heading 1', + "style object": style, + "style name": "Heading 1", }[kind] document.add_paragraph(style=style_spec) -@when('I add a paragraph specifying its text') +@when("I add a paragraph specifying its text") def when_add_paragraph_specifying_text(context): document = context.document - context.paragraph_text = 'foobar' + context.paragraph_text = "foobar" document.add_paragraph(context.paragraph_text) -@when('I add a paragraph without specifying text or style') +@when("I add a paragraph without specifying text or style") def when_add_paragraph_without_specifying_text_or_style(context): document = context.document document.add_paragraph() @@ -124,39 +119,38 @@ def when_add_paragraph_without_specifying_text_or_style(context): def when_add_picture_specifying_width_and_height(context): document = context.document context.picture = document.add_picture( - test_file('monty-truth.png'), - width=Inches(1.75), height=Inches(2.5) + test_file("monty-truth.png"), width=Inches(1.75), height=Inches(2.5) ) -@when('I add a picture specifying a height of 1.5 inches') +@when("I add a picture specifying a height of 1.5 inches") def when_add_picture_specifying_height(context): document = context.document context.picture = document.add_picture( - test_file('monty-truth.png'), height=Inches(1.5) + test_file("monty-truth.png"), height=Inches(1.5) ) -@when('I add a picture specifying a width of 1.5 inches') +@when("I add a picture specifying a width of 1.5 inches") def when_add_picture_specifying_width(context): document = context.document context.picture = document.add_picture( - test_file('monty-truth.png'), width=Inches(1.5) + test_file("monty-truth.png"), width=Inches(1.5) ) -@when('I add a picture specifying only the image file') +@when("I add a picture specifying only the image file") def when_add_picture_specifying_only_image_file(context): document = context.document - context.picture = document.add_picture(test_file('monty-truth.png')) + context.picture = document.add_picture(test_file("monty-truth.png")) -@when('I add an even-page section to the document') +@when("I add an even-page section to the document") def when_I_add_an_even_page_section_to_the_document(context): context.section = context.document.add_section(WD_SECTION.EVEN_PAGE) -@when('I change the new section layout to landscape') +@when("I change the new section layout to landscape") def when_I_change_the_new_section_layout_to_landscape(context): new_height, new_width = context.original_dimensions section = context.section @@ -172,14 +166,15 @@ def when_I_execute_section_eq_document_add_section(context): # then ==================================================== -@then('document.inline_shapes is an InlineShapes object') + +@then("document.inline_shapes is an InlineShapes object") def then_document_inline_shapes_is_an_InlineShapes_object(context): document = context.document inline_shapes = document.inline_shapes assert isinstance(inline_shapes, InlineShapes) -@then('document.paragraphs is a list containing three paragraphs') +@then("document.paragraphs is a list containing three paragraphs") def then_document_paragraphs_is_a_list_containing_three_paragraphs(context): document = context.document paragraphs = document.paragraphs @@ -189,20 +184,20 @@ def then_document_paragraphs_is_a_list_containing_three_paragraphs(context): assert isinstance(paragraph, Paragraph) -@then('document.sections is a Sections object') +@then("document.sections is a Sections object") def then_document_sections_is_a_Sections_object(context): sections = context.document.sections - msg = 'document.sections not instance of Sections' + msg = "document.sections not instance of Sections" assert isinstance(sections, Sections), msg -@then('document.styles is a Styles object') +@then("document.styles is a Styles object") def then_document_styles_is_a_Styles_object(context): styles = context.document.styles assert isinstance(styles, Styles) -@then('document.tables is a list containing three tables') +@then("document.tables is a list containing three tables") def then_document_tables_is_a_list_containing_three_tables(context): document = context.document tables = document.tables @@ -212,7 +207,7 @@ def then_document_tables_is_a_list_containing_three_tables(context): assert isinstance(table, Table) -@then('the document contains a 2 x 2 table') +@then("the document contains a 2 x 2 table") def then_the_document_contains_a_2x2_table(context): table = context.document.tables[-1] assert isinstance(table, Table) @@ -221,12 +216,12 @@ def then_the_document_contains_a_2x2_table(context): context.table_ = table -@then('the document has two sections') +@then("the document has two sections") def then_the_document_has_two_sections(context): assert len(context.document.sections) == 2 -@then('the first section is portrait') +@then("the first section is portrait") def then_the_first_section_is_portrait(context): first_section = context.document.sections[0] expected_width, expected_height = context.original_dimensions @@ -235,16 +230,16 @@ def then_the_first_section_is_portrait(context): assert first_section.page_height == expected_height -@then('the last paragraph contains only a page break') +@then("the last paragraph contains only a page break") def then_last_paragraph_contains_only_a_page_break(context): document = context.document paragraph = document.paragraphs[-1] assert len(paragraph.runs) == 1 assert len(paragraph.runs[0]._r) == 1 - assert paragraph.runs[0]._r[0].type == 'page' + assert paragraph.runs[0]._r[0].type == "page" -@then('the last paragraph contains the heading text') +@then("the last paragraph contains the heading text") def then_last_p_contains_heading_text(context): document = context.document text = context.heading_text @@ -252,7 +247,7 @@ def then_last_p_contains_heading_text(context): assert paragraph.text == text -@then('the second section is landscape') +@then("the second section is landscape") def then_the_second_section_is_landscape(context): new_section = context.document.sections[-1] expected_height, expected_width = context.original_dimensions @@ -261,10 +256,8 @@ def then_the_second_section_is_landscape(context): assert new_section.page_height == expected_height -@then('the style of the last paragraph is \'{style_name}\'') +@then("the style of the last paragraph is '{style_name}'") def then_the_style_of_the_last_paragraph_is_style(context, style_name): document = context.document paragraph = document.paragraphs[-1] - assert paragraph.style.name == style_name, ( - 'got %s' % paragraph.style.name - ) + assert paragraph.style.name == style_name, "got %s" % paragraph.style.name diff --git a/features/steps/font.py b/features/steps/font.py index 60f308d86..63ca6b48e 100644 --- a/features/steps/font.py +++ b/features/steps/font.py @@ -1,12 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for font-related features. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Step implementations for font-related features.""" from behave import given, then, when @@ -18,140 +10,139 @@ from helpers import test_docx - # given =================================================== -@given('a font') + +@given("a font") def given_a_font(context): - document = Document(test_docx('txt-font-props')) + document = Document(test_docx("txt-font-props")) context.font = document.paragraphs[0].runs[0].font -@given('a font having {color} highlighting') +@given("a font having {color} highlighting") def given_a_font_having_color_highlighting(context, color): paragraph_index = { - 'no': 0, - 'yellow': 1, - 'bright green': 2, + "no": 0, + "yellow": 1, + "bright green": 2, }[color] - document = Document(test_docx('txt-font-highlight-color')) + document = Document(test_docx("txt-font-highlight-color")) context.font = document.paragraphs[paragraph_index].runs[0].font -@given('a font having {type} color') +@given("a font having {type} color") def given_a_font_having_type_color(context, type): - run_idx = ['no', 'auto', 'an RGB', 'a theme'].index(type) - document = Document(test_docx('fnt-color')) + run_idx = ["no", "auto", "an RGB", "a theme"].index(type) + document = Document(test_docx("fnt-color")) context.font = document.paragraphs[0].runs[run_idx].font -@given('a font having typeface name {name}') +@given("a font having typeface name {name}") def given_a_font_having_typeface_name(context, name): - document = Document(test_docx('txt-font-props')) + document = Document(test_docx("txt-font-props")) style_name = { - 'not specified': 'Normal', - 'Avenir Black': 'Having Typeface', + "not specified": "Normal", + "Avenir Black": "Having Typeface", }[name] context.font = document.styles[style_name].font -@given('a font having {underline_type} underline') +@given("a font having {underline_type} underline") def given_a_font_having_type_underline(context, underline_type): style_name = { - 'inherited': 'Normal', - 'no': 'None Underlined', - 'single': 'Underlined', - 'double': 'Double Underlined', + "inherited": "Normal", + "no": "None Underlined", + "single": "Underlined", + "double": "Double Underlined", }[underline_type] - document = Document(test_docx('txt-font-props')) + document = Document(test_docx("txt-font-props")) context.font = document.styles[style_name].font -@given('a font having {vertAlign_state} vertical alignment') +@given("a font having {vertAlign_state} vertical alignment") def given_a_font_having_vertAlign_state(context, vertAlign_state): style_name = { - 'inherited': 'Normal', - 'subscript': 'Subscript', - 'superscript': 'Superscript', + "inherited": "Normal", + "subscript": "Subscript", + "superscript": "Superscript", }[vertAlign_state] - document = Document(test_docx('txt-font-props')) + document = Document(test_docx("txt-font-props")) context.font = document.styles[style_name].font -@given('a font of size {size}') +@given("a font of size {size}") def given_a_font_of_size(context, size): - document = Document(test_docx('txt-font-props')) + document = Document(test_docx("txt-font-props")) style_name = { - 'unspecified': 'Normal', - '14 pt': 'Having Typeface', - '18 pt': 'Large Size', + "unspecified": "Normal", + "14 pt": "Having Typeface", + "18 pt": "Large Size", }[size] context.font = document.styles[style_name].font # when ==================================================== -@when('I assign {value} to font.color.rgb') + +@when("I assign {value} to font.color.rgb") def when_I_assign_value_to_font_color_rgb(context, value): font = context.font - new_value = None if value == 'None' else RGBColor.from_string(value) + new_value = None if value == "None" else RGBColor.from_string(value) font.color.rgb = new_value -@when('I assign {value} to font.color.theme_color') +@when("I assign {value} to font.color.theme_color") def when_I_assign_value_to_font_color_theme_color(context, value): font = context.font - new_value = None if value == 'None' else getattr(MSO_THEME_COLOR, value) + new_value = None if value == "None" else getattr(MSO_THEME_COLOR, value) font.color.theme_color = new_value -@when('I assign {value} to font.highlight_color') +@when("I assign {value} to font.highlight_color") def when_I_assign_value_to_font_highlight_color(context, value): font = context.font - expected_value = ( - None if value == 'None' else getattr(WD_COLOR_INDEX, value) - ) + expected_value = None if value == "None" else getattr(WD_COLOR_INDEX, value) font.highlight_color = expected_value -@when('I assign {value} to font.name') +@when("I assign {value} to font.name") def when_I_assign_value_to_font_name(context, value): font = context.font - value = None if value == 'None' else value + value = None if value == "None" else value font.name = value -@when('I assign {value} to font.size') +@when("I assign {value} to font.size") def when_I_assign_value_str_to_font_size(context, value): - value = None if value == 'None' else int(value) + value = None if value == "None" else int(value) font = context.font font.size = value -@when('I assign {value} to font.underline') +@when("I assign {value} to font.underline") def when_I_assign_value_to_font_underline(context, value): new_value = { - 'True': True, - 'False': False, - 'None': None, - 'WD_UNDERLINE.SINGLE': WD_UNDERLINE.SINGLE, - 'WD_UNDERLINE.DOUBLE': WD_UNDERLINE.DOUBLE, + "True": True, + "False": False, + "None": None, + "WD_UNDERLINE.SINGLE": WD_UNDERLINE.SINGLE, + "WD_UNDERLINE.DOUBLE": WD_UNDERLINE.DOUBLE, }[value] font = context.font font.underline = new_value -@when('I assign {value} to font.{sub_super}script') +@when("I assign {value} to font.{sub_super}script") def when_I_assign_value_to_font_sub_super(context, value, sub_super): font = context.font name = { - 'sub': 'subscript', - 'super': 'superscript', + "sub": "subscript", + "super": "superscript", }[sub_super] new_value = { - 'None': None, - 'True': True, - 'False': False, + "None": None, + "True": True, + "False": False, }[value] setattr(font, name, new_value) @@ -159,82 +150,77 @@ def when_I_assign_value_to_font_sub_super(context, value, sub_super): # then ===================================================== -@then('font.color is a ColorFormat object') + +@then("font.color is a ColorFormat object") def then_font_color_is_a_ColorFormat_object(context): font = context.font assert isinstance(font.color, ColorFormat) -@then('font.color.rgb is {value}') +@then("font.color.rgb is {value}") def then_font_color_rgb_is_value(context, value): font = context.font - expected_value = None if value == 'None' else RGBColor.from_string(value) + expected_value = None if value == "None" else RGBColor.from_string(value) assert font.color.rgb == expected_value -@then('font.color.theme_color is {value}') +@then("font.color.theme_color is {value}") def then_font_color_theme_color_is_value(context, value): font = context.font - expected_value = ( - None if value == 'None' else getattr(MSO_THEME_COLOR, value) - ) + expected_value = None if value == "None" else getattr(MSO_THEME_COLOR, value) assert font.color.theme_color == expected_value -@then('font.color.type is {value}') +@then("font.color.type is {value}") def then_font_color_type_is_value(context, value): font = context.font - expected_value = ( - None if value == 'None' else getattr(MSO_COLOR_TYPE, value) - ) + expected_value = None if value == "None" else getattr(MSO_COLOR_TYPE, value) assert font.color.type == expected_value -@then('font.highlight_color is {value}') +@then("font.highlight_color is {value}") def then_font_highlight_color_is_value(context, value): font = context.font - expected_value = ( - None if value == 'None' else getattr(WD_COLOR_INDEX, value) - ) + expected_value = None if value == "None" else getattr(WD_COLOR_INDEX, value) assert font.highlight_color == expected_value -@then('font.name is {value}') +@then("font.name is {value}") def then_font_name_is_value(context, value): font = context.font - value = None if value == 'None' else value + value = None if value == "None" else value assert font.name == value -@then('font.size is {value}') +@then("font.size is {value}") def then_font_size_is_value(context, value): - value = None if value == 'None' else int(value) + value = None if value == "None" else int(value) font = context.font assert font.size == value -@then('font.underline is {value}') +@then("font.underline is {value}") def then_font_underline_is_value(context, value): expected_value = { - 'None': None, - 'True': True, - 'False': False, - 'WD_UNDERLINE.DOUBLE': WD_UNDERLINE.DOUBLE, + "None": None, + "True": True, + "False": False, + "WD_UNDERLINE.DOUBLE": WD_UNDERLINE.DOUBLE, }[value] font = context.font assert font.underline == expected_value -@then('font.{sub_super}script is {value}') +@then("font.{sub_super}script is {value}") def then_font_sub_super_is_value(context, sub_super, value): name = { - 'sub': 'subscript', - 'super': 'superscript', + "sub": "subscript", + "super": "superscript", }[sub_super] expected_value = { - 'None': None, - 'True': True, - 'False': False, + "None": None, + "True": True, + "False": False, }[value] font = context.font actual_value = getattr(font, name) diff --git a/features/steps/hdrftr.py b/features/steps/hdrftr.py index 786673dbd..5949f961c 100644 --- a/features/steps/hdrftr.py +++ b/features/steps/hdrftr.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Step implementations for header and footer-related features""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Step implementations for header and footer-related features.""" from behave import given, then, when @@ -10,9 +6,9 @@ from helpers import test_docx, test_file - # given ==================================================== + @given("a _Footer object {with_or_no} footer definition as footer") def given_a_Footer_object_with_or_no_footer_definition(context, with_or_no): section_idx = {"with a": 0, "with no": 1}[with_or_no] @@ -51,12 +47,13 @@ def given_the_next_Header_object_with_no_header_definition(context): # when ===================================================== -@when("I assign \"Normal\" to footer.paragraphs[0].style") + +@when('I assign "Normal" to footer.paragraphs[0].style') def when_I_assign_Body_Text_to_footer_style(context): context.footer.paragraphs[0].style = "Normal" -@when("I assign \"Normal\" to header.paragraphs[0].style") +@when('I assign "Normal" to header.paragraphs[0].style') def when_I_assign_Body_Text_to_header_style(context): context.header.paragraphs[0].style = "Normal" @@ -78,6 +75,7 @@ def when_I_call_run_add_picture(context): # then ===================================================== + @then("footer.is_linked_to_previous is {value}") def then_footer_is_linked_to_previous_is_value(context, value): actual = context.footer.is_linked_to_previous @@ -85,7 +83,7 @@ def then_footer_is_linked_to_previous_is_value(context, value): assert actual == expected, "footer.is_linked_to_previous is %s" % actual -@then("footer.paragraphs[0].style.name == \"Normal\"") +@then('footer.paragraphs[0].style.name == "Normal"') def then_footer_paragraphs_0_style_name_eq_Normal(context): actual = context.footer.paragraphs[0].style.name expected = "Normal" @@ -113,7 +111,7 @@ def then_header_is_linked_to_previous_is_value(context, value): assert actual == expected, "header.is_linked_to_previous is %s" % actual -@then("header.paragraphs[0].style.name == \"Normal\"") +@then('header.paragraphs[0].style.name == "Normal"') def then_header_paragraphs_0_style_name_eq_Normal(context): actual = context.header.paragraphs[0].style.name expected = "Normal" diff --git a/features/steps/helpers.py b/features/steps/helpers.py index 6733bfc13..fc40697b2 100644 --- a/features/steps/helpers.py +++ b/features/steps/helpers.py @@ -1,45 +1,34 @@ -# encoding: utf-8 - -""" -Helper methods and variables for acceptance tests. -""" +"""Helper methods and variables for acceptance tests.""" import os -def absjoin(*paths): +def absjoin(*paths: str) -> str: return os.path.abspath(os.path.join(*paths)) -thisdir = os.path.split(__file__)[0] -scratch_dir = absjoin(thisdir, '../_scratch') +thisdir: str = os.path.split(__file__)[0] +scratch_dir: str = absjoin(thisdir, "../_scratch") # scratch output docx file ------------- -saved_docx_path = absjoin(scratch_dir, 'test_out.docx') +saved_docx_path: str = absjoin(scratch_dir, "test_out.docx") -bool_vals = { - 'True': True, - 'False': False -} +bool_vals = {"True": True, "False": False} -test_text = 'python-docx was here!' +test_text = "python-docx was here!" tri_state_vals = { - 'True': True, - 'False': False, - 'None': None, + "True": True, + "False": False, + "None": None, } -def test_docx(name): - """ - Return the absolute path to test .docx file with root name *name*. - """ - return absjoin(thisdir, 'test_files', '%s.docx' % name) +def test_docx(name: str): + """Return the absolute path to test .docx file with root name `name`.""" + return absjoin(thisdir, "test_files", "%s.docx" % name) -def test_file(name): - """ - Return the absolute path to file with *name* in test_files directory - """ - return absjoin(thisdir, 'test_files', '%s' % name) +def test_file(name: str): + """Return the absolute path to file with `name` in test_files directory""" + return absjoin(thisdir, "test_files", "%s" % name) diff --git a/features/steps/hyperlink.py b/features/steps/hyperlink.py new file mode 100644 index 000000000..0596a3cd6 --- /dev/null +++ b/features/steps/hyperlink.py @@ -0,0 +1,88 @@ +"""Step implementations for hyperlink-related features.""" + +from __future__ import annotations + +from behave import given, then +from behave.runner import Context + +from docx import Document + +from helpers import test_docx + +# given =================================================== + + +@given("a hyperlink") +def given_a_hyperlink(context: Context): + document = Document(test_docx("par-hyperlinks")) + context.hyperlink = document.paragraphs[1].hyperlinks[0] + + +@given("a hyperlink having {zero_or_more} rendered page breaks") +def given_a_hyperlink_having_rendered_page_breaks(context: Context, zero_or_more: str): + paragraph_idx = { + "no": 1, + "one": 2, + }[zero_or_more] + document = Document(test_docx("par-hyperlinks")) + paragraph = document.paragraphs[paragraph_idx] + context.hyperlink = paragraph.hyperlinks[0] + + +@given("a hyperlink having {one_or_more} runs") +def given_a_hyperlink_having_one_or_more_runs(context: Context, one_or_more: str): + paragraph_idx, hyperlink_idx = { + "one": (1, 0), + "two": (2, 1), + }[one_or_more] + document = Document(test_docx("par-hyperlinks")) + paragraph = document.paragraphs[paragraph_idx] + context.hyperlink = paragraph.hyperlinks[hyperlink_idx] + + +# then ===================================================== + + +@then("hyperlink.address is the URL of the hyperlink") +def then_hyperlink_address_is_the_URL_of_the_hyperlink(context: Context): + actual_value = context.hyperlink.address + expected_value = "http://yahoo.com/" + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("hyperlink.contains_page_break is {value}") +def then_hyperlink_contains_page_break_is_value(context: Context, value: str): + actual_value = context.hyperlink.contains_page_break + expected_value = {"True": True, "False": False}[value] + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("hyperlink.runs contains only Run instances") +def then_hyperlink_runs_contains_only_Run_instances(context: Context): + actual_value = [type(item).__name__ for item in context.hyperlink.runs] + expected_value = ["Run" for _ in context.hyperlink.runs] + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("hyperlink.runs has length {value}") +def then_hyperlink_runs_has_length(context: Context, value: str): + actual_value = len(context.hyperlink.runs) + expected_value = int(value) + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("hyperlink.text is the visible text of the hyperlink") +def then_hyperlink_text_is_the_visible_text_of_the_hyperlink(context: Context): + actual_value = context.hyperlink.text + expected_value = "awesome hyperlink" + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" diff --git a/features/steps/image.py b/features/steps/image.py index ee2a35c17..5ac54169b 100644 --- a/features/steps/image.py +++ b/features/steps/image.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for image characterization features -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Step implementations for image characterization features.""" from behave import given, then, when @@ -12,62 +6,69 @@ from helpers import test_file - # given =================================================== -@given('the image file \'{filename}\'') + +@given("the image file '{filename}'") def given_image_filename(context, filename): context.image_path = test_file(filename) # when ==================================================== -@when('I construct an image using the image path') + +@when("I construct an image using the image path") def when_construct_image_using_path(context): context.image = Image.from_file(context.image_path) # then ==================================================== -@then('the image has content type \'{mime_type}\'') + +@then("the image has content type '{mime_type}'") def then_image_has_content_type(context, mime_type): content_type = context.image.content_type - assert content_type == mime_type, ( - "expected MIME type '%s', got '%s'" % (mime_type, content_type) + assert content_type == mime_type, "expected MIME type '%s', got '%s'" % ( + mime_type, + content_type, ) -@then('the image has {horz_dpi_str} horizontal dpi') +@then("the image has {horz_dpi_str} horizontal dpi") def then_image_has_horizontal_dpi(context, horz_dpi_str): expected_horz_dpi = int(horz_dpi_str) horz_dpi = context.image.horz_dpi - assert horz_dpi == expected_horz_dpi, ( - "expected horizontal dpi %d, got %d" % (expected_horz_dpi, horz_dpi) + assert horz_dpi == expected_horz_dpi, "expected horizontal dpi %d, got %d" % ( + expected_horz_dpi, + horz_dpi, ) -@then('the image has {vert_dpi_str} vertical dpi') +@then("the image has {vert_dpi_str} vertical dpi") def then_image_has_vertical_dpi(context, vert_dpi_str): expected_vert_dpi = int(vert_dpi_str) vert_dpi = context.image.vert_dpi - assert vert_dpi == expected_vert_dpi, ( - "expected vertical dpi %d, got %d" % (expected_vert_dpi, vert_dpi) + assert vert_dpi == expected_vert_dpi, "expected vertical dpi %d, got %d" % ( + expected_vert_dpi, + vert_dpi, ) -@then('the image is {px_height_str} pixels high') +@then("the image is {px_height_str} pixels high") def then_image_is_cx_pixels_high(context, px_height_str): expected_px_height = int(px_height_str) px_height = context.image.px_height - assert px_height == expected_px_height, ( - "expected pixel height %d, got %d" % (expected_px_height, px_height) + assert px_height == expected_px_height, "expected pixel height %d, got %d" % ( + expected_px_height, + px_height, ) -@then('the image is {px_width_str} pixels wide') +@then("the image is {px_width_str} pixels wide") def then_image_is_cx_pixels_wide(context, px_width_str): expected_px_width = int(px_width_str) px_width = context.image.px_width - assert px_width == expected_px_width, ( - "expected pixel width %d, got %d" % (expected_px_width, px_width) + assert px_width == expected_px_width, "expected pixel width %d, got %d" % ( + expected_px_width, + px_width, ) diff --git a/features/steps/numbering.py b/features/steps/numbering.py index ea41cdeb5..be88ceee7 100644 --- a/features/steps/numbering.py +++ b/features/steps/numbering.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Step implementations for numbering-related features -""" +"""Step implementations for numbering-related features.""" from behave import given, then, when @@ -10,17 +6,18 @@ from helpers import test_docx - # given =================================================== -@given('a document having a numbering part') + +@given("a document having a numbering part") def given_a_document_having_a_numbering_part(context): - context.document = Document(test_docx('num-having-numbering-part')) + context.document = Document(test_docx("num-having-numbering-part")) # when ==================================================== -@when('I get the numbering part from the document') + +@when("I get the numbering part from the document") def when_get_numbering_part_from_document(context): document = context.document context.numbering_part = document.part.numbering_part @@ -28,7 +25,8 @@ def when_get_numbering_part_from_document(context): # then ===================================================== -@then('the numbering part has the expected numbering definitions') + +@then("the numbering part has the expected numbering definitions") def then_numbering_part_has_expected_numbering_definitions(context): numbering_part = context.numbering_part assert len(numbering_part.numbering_definitions) == 10 diff --git a/features/steps/pagebreak.py b/features/steps/pagebreak.py new file mode 100644 index 000000000..7d443da46 --- /dev/null +++ b/features/steps/pagebreak.py @@ -0,0 +1,171 @@ +"""Step implementations for rendered page-break related features.""" + +from __future__ import annotations + +from behave import given, then +from behave.runner import Context + +from docx import Document +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + +from helpers import test_docx + +# given =================================================== + + +@given("a rendered_page_break in a hyperlink") +def given_a_rendered_page_break_in_a_hyperlink(context: Context): + document = Document(test_docx("par-rendered-page-breaks")) + paragraph = document.paragraphs[2] + context.rendered_page_break = paragraph.rendered_page_breaks[0] + + +@given("a rendered_page_break in a paragraph") +def given_a_rendered_page_break_in_a_paragraph(context: Context): + document = Document(test_docx("par-rendered-page-breaks")) + paragraph = document.paragraphs[1] + context.rendered_page_break = paragraph.rendered_page_breaks[0] + + +# then ===================================================== + + +@then("rendered_page_break.preceding_paragraph_fragment includes the hyperlink") +def then_rendered_page_break_preceding_paragraph_fragment_includes_the_hyperlink( + context: Context, +): + para_frag = context.rendered_page_break.preceding_paragraph_fragment + + actual_value = type(para_frag).__name__ + expected_value = "Paragraph" + assert ( + actual_value == expected_value + ), f"expected: '{expected_value}', got: '{actual_value}'" + + actual_value = para_frag.text + expected_value = "Page break in>><""" % nsdecls( + "w" + ) r = parse_xml(r_xml) context.run = Run(r, None) -@given('a run having {underline_type} underline') +@given("a run having {underline_type} underline") def given_a_run_having_underline_type(context, underline_type): - run_idx = { - 'inherited': 0, 'no': 1, 'single': 2, 'double': 3 - }[underline_type] - document = Document(test_docx('run-enumerated-props')) + run_idx = {"inherited": 0, "no": 1, "single": 2, "double": 3}[underline_type] + document = Document(test_docx("run-enumerated-props")) context.run = document.paragraphs[0].runs[run_idx] -@given('a run having {style} style') +@given("a run having {style} style") def given_a_run_having_style(context, style): run_idx = { - 'no explicit': 0, - 'Emphasis': 1, - 'Strong': 2, + "no explicit": 0, + "Emphasis": 1, + "Strong": 2, }[style] - context.document = document = Document(test_docx('run-char-style')) + context.document = document = Document(test_docx("run-char-style")) context.run = document.paragraphs[0].runs[run_idx] -@given('a run inside a table cell retrieved from {cell_source}') +@given("a run having {zero_or_more} rendered page breaks") +def given_a_run_having_rendered_page_breaks(context: Context, zero_or_more: str): + paragraph_idx = {"no": 0, "one": 1, "two": 3}[zero_or_more] + document = Document(test_docx("par-rendered-page-breaks")) + paragraph = document.paragraphs[paragraph_idx] + context.run = paragraph.runs[0] + + +@given("a run inside a table cell retrieved from {cell_source}") def given_a_run_inside_a_table_cell_from_source(context, cell_source): document = Document() table = document.add_table(rows=2, cols=2) - if cell_source == 'Table.cell': + if cell_source == "Table.cell": cell = table.cell(0, 0) - elif cell_source == 'Table.row.cells': + elif cell_source == "Table.row.cells": cell = table.rows[0].cells[1] - elif cell_source == 'Table.column.cells': + elif cell_source == "Table.column.cells": cell = table.columns[1].cells[0] run = cell.paragraphs[0].add_run() context.document = document @@ -102,202 +110,221 @@ def given_a_run_inside_a_table_cell_from_source(context, cell_source): # when ==================================================== -@when('I add a column break') + +@when("I add a column break") def when_add_column_break(context): run = context.run run.add_break(WD_BREAK.COLUMN) -@when('I add a line break') +@when("I add a line break") def when_add_line_break(context): run = context.run run.add_break() -@when('I add a page break') +@when("I add a page break") def when_add_page_break(context): run = context.run run.add_break(WD_BREAK.PAGE) -@when('I add a picture to the run') +@when("I add a picture to the run") def when_I_add_a_picture_to_the_run(context): run = context.run - run.add_picture(test_file('monty-truth.png')) + run.add_picture(test_file("monty-truth.png")) -@when('I add a run specifying its text') +@when("I add a run specifying its text") def when_I_add_a_run_specifying_its_text(context): context.run = context.paragraph.add_run(test_text) -@when('I add a run specifying the character style Emphasis') +@when("I add a run specifying the character style Emphasis") def when_I_add_a_run_specifying_the_character_style_Emphasis(context): - context.run = context.paragraph.add_run(test_text, 'Emphasis') + context.run = context.paragraph.add_run(test_text, "Emphasis") -@when('I add a tab') +@when("I add a tab") def when_I_add_a_tab(context): context.run.add_tab() -@when('I add text to the run') +@when("I add text to the run") def when_I_add_text_to_the_run(context): context.run.add_text(test_text) -@when('I assign mixed text to the text property') +@when("I assign mixed text to the text property") def when_I_assign_mixed_text_to_the_text_property(context): - context.run.text = 'abc\tdef\nghi\rjkl' + context.run.text = "abc\ndef\rghijkl\tmno-pqr\tstu" -@when('I assign {value_str} to its {bool_prop_name} property') +@when("I assign {value_str} to its {bool_prop_name} property") def when_assign_true_to_bool_run_prop(context, value_str, bool_prop_name): - value = {'True': True, 'False': False, 'None': None}[value_str] + value = {"True": True, "False": False, "None": None}[value_str] run = context.run setattr(run, bool_prop_name, value) -@when('I assign {value} to run.style') +@when("I assign {value} to run.style") def when_I_assign_value_to_run_style(context, value): - if value == 'None': + if value == "None": new_value = None - elif value.startswith('styles['): - new_value = context.document.styles[value.split('\'')[1]] + elif value.startswith("styles["): + new_value = context.document.styles[value.split("'")[1]] else: new_value = context.document.styles[value] context.run.style = new_value -@when('I clear the run') +@when("I clear the run") def when_I_clear_the_run(context): context.run.clear() -@when('I set the run underline to {underline_value}') +@when("I set the run underline to {underline_value}") def when_I_set_the_run_underline_to_value(context, underline_value): new_value = { - 'True': True, 'False': False, 'None': None, - 'WD_UNDERLINE.SINGLE': WD_UNDERLINE.SINGLE, - 'WD_UNDERLINE.DOUBLE': WD_UNDERLINE.DOUBLE, + "True": True, + "False": False, + "None": None, + "WD_UNDERLINE.SINGLE": WD_UNDERLINE.SINGLE, + "WD_UNDERLINE.DOUBLE": WD_UNDERLINE.DOUBLE, }[underline_value] context.run.underline = new_value # then ===================================================== -@then('it is a column break') + +@then("it is a column break") def then_type_is_column_break(context): attrib = context.last_child.attrib - assert attrib == {qn('w:type'): 'column'} + assert attrib == {qn("w:type"): "column"} -@then('it is a line break') +@then("it is a line break") def then_type_is_line_break(context): attrib = context.last_child.attrib assert attrib == {} -@then('it is a page break') +@then("it is a page break") def then_type_is_page_break(context): attrib = context.last_child.attrib - assert attrib == {qn('w:type'): 'page'} + assert attrib == {qn("w:type"): "page"} + + +@then("run.contains_page_break is {value}") +def then_run_contains_page_break_is_value(context: Context, value: str): + actual = context.run.contains_page_break + expected = {"True": True, "False": False}[value] + assert actual == expected, f"expected: {expected}, got: {actual}" -@then('run.font is the Font object for the run') +@then("run.font is the Font object for the run") def then_run_font_is_the_Font_object_for_the_run(context): run, font = context.run, context.run.font assert isinstance(font, Font) assert font.element is run.element -@then('run.style is styles[\'{style_name}\']') +@then("run.iter_inner_content() generates the run text and rendered page-breaks") +def then_run_iter_inner_content_generates_text_and_page_breaks(context: Context): + actual_value = [type(item).__name__ for item in context.run.iter_inner_content()] + expected_value = ["str", "RenderedPageBreak", "str", "RenderedPageBreak", "str"] + assert ( + actual_value == expected_value + ), f"expected: {expected_value}, got: {actual_value}" + + +@then("run.style is styles['{style_name}']") def then_run_style_is_style(context, style_name): expected_value = context.document.styles[style_name] run = context.run - assert run.style == expected_value, 'got %s' % run.style + assert run.style == expected_value, "got %s" % run.style + +@then("run.text contains the text content of the run") +def then_run_text_contains_the_text_content_of_the_run(context): + actual = context.run.text + expected = "abc\ndef\nghijkl\tmno-pqr\tstu" + assert actual == expected, f"expected:\n'{expected}'\n\ngot:\n'{actual}'" -@then('the last item in the run is a break') + +@then("the last item in the run is a break") def then_last_item_in_run_is_a_break(context): run = context.run context.last_child = run._r[-1] - expected_tag = ( - '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}br' - ) + expected_tag = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}br" assert context.last_child.tag == expected_tag -@then('the picture appears at the end of the run') +@then("the picture appears at the end of the run") def then_the_picture_appears_at_the_end_of_the_run(context): run = context.run r = run._r blip_rId = r.xpath( - './w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/' - 'a:blip/@r:embed' + "./w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/" + "a:blip/@r:embed" )[0] image_part = run.part.related_parts[blip_rId] image_sha1 = hashlib.sha1(image_part.blob).hexdigest() - expected_sha1 = '79769f1e202add2e963158b532e36c2c0f76a70c' - assert image_sha1 == expected_sha1, ( - "image SHA1 doesn't match, expected %s, got %s" % - (expected_sha1, image_sha1) - ) + expected_sha1 = "79769f1e202add2e963158b532e36c2c0f76a70c" + assert ( + image_sha1 == expected_sha1 + ), "image SHA1 doesn't match, expected %s, got %s" % (expected_sha1, image_sha1) -@then('the run appears in {boolean_prop_name} unconditionally') +@then("the run appears in {boolean_prop_name} unconditionally") def then_run_appears_in_boolean_prop_name(context, boolean_prop_name): run = context.run assert getattr(run, boolean_prop_name) is True -@then('the run appears with its inherited {boolean_prop_name} setting') +@then("the run appears with its inherited {boolean_prop_name} setting") def then_run_inherits_bool_prop_value(context, boolean_prop_name): run = context.run assert getattr(run, boolean_prop_name) is None -@then('the run appears without {boolean_prop_name} unconditionally') +@then("the run appears without {boolean_prop_name} unconditionally") def then_run_appears_without_bool_prop(context, boolean_prop_name): run = context.run assert getattr(run, boolean_prop_name) is False -@then('the run contains no text') +@then("the run contains no text") def then_the_run_contains_no_text(context): - assert context.run.text == '' + assert context.run.text == "" -@then('the run contains the text I specified') +@then("the run contains the text I specified") def then_the_run_contains_the_text_I_specified(context): assert context.run.text == test_text -@then('the run formatting is preserved') +@then("the run formatting is preserved") def then_the_run_formatting_is_preserved(context): assert context.run.bold is True assert context.run.italic is True -@then('the run underline property value is {underline_value}') +@then("the run underline property value is {underline_value}") def then_the_run_underline_property_value_is(context, underline_value): expected_value = { - 'None': None, 'False': False, 'True': True, - 'WD_UNDERLINE.DOUBLE': WD_UNDERLINE.DOUBLE + "None": None, + "False": False, + "True": True, + "WD_UNDERLINE.DOUBLE": WD_UNDERLINE.DOUBLE, }[underline_value] assert context.run.underline == expected_value -@then('the tab appears at the end of the run') +@then("the tab appears at the end of the run") def then_the_tab_appears_at_the_end_of_the_run(context): r = context.run._r - tab = r.find(qn('w:tab')) + tab = r.find(qn("w:tab")) assert tab is not None - - -@then('the text of the run represents the textual run content') -def then_the_text_of_the_run_represents_the_textual_run_content(context): - assert context.run.text == 'abc\tdef\nghi\njkl', ( - 'got \'%s\'' % context.run.text - ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..dc9884a9b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,86 @@ +[build-system] +requires = ["setuptools>=61.0.0"] + +[project] +name = "python-docx" +authors = [{name = "Steve Canny", email = "stcanny@gmail.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Office/Business :: Office Suites", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "lxml>=3.1.0", + "typing_extensions", +] +description = "Create, read, and update Microsoft Word .docx files." +dynamic = ["version"] +keywords = ["docx", "office", "openxml", "word"] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.7" + +[project.urls] +Changelog = "https://github.com/python-openxml/python-docx/blob/master/HISTORY.rst" +Documentation = "https://python-docx.readthedocs.org/en/latest/" +Homepage = "https://github.com/python-openxml/python-docx" +Repository = "https://github.com/python-openxml/python-docx" + +[tool.black] +target-version = ["py37", "py38", "py39", "py310", "py311"] + +[tool.pytest.ini_options] +norecursedirs = [ + "doc", + "docx", + "*.egg-info", + "features", + ".git", + "ref", + "_scratch", + ".tox", +] +python_files = ["test_*.py"] +python_classes = ["Test", "Describe"] +python_functions = ["it_", "its_", "they_", "and_", "but_"] + +[tool.ruff] +exclude = [] +ignore = [ + "COM812", # -- over-aggressively insists on trailing commas where not desired -- + "PT001", # -- wants @pytest.fixture() instead of @pytest.fixture -- + "PT005", # -- wants @pytest.fixture() instead of @pytest.fixture -- +] +select = [ + "C4", # -- flake8-comprehensions -- + "COM", # -- flake8-commas -- + "E", # -- pycodestyle errors -- + "F", # -- pyflakes -- + "I", # -- isort (imports) -- + "PLR0402", # -- Name compared with itself like `foo == foo` -- + "PT", # -- flake8-pytest-style -- + "SIM", # -- flake8-simplify -- + "UP015", # -- redundant `open()` mode parameter (like "r" is default) -- + "UP018", # -- Unnecessary {literal_type} call like `str("abc")`. (rewrite as a literal) -- + "UP032", # -- Use f-string instead of `.format()` call -- + "UP034", # -- Avoid extraneous parentheses -- +] +target-version = "py37" + +[tool.ruff.isort] +known-first-party = ["docx"] +known-local-folder = ["helpers"] + +[tool.setuptools.dynamic] +version = {attr = "docx.__version__"} diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..161e49d2b --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,21 @@ +{ + "exclude": [ + "**/__pycache__", + "**/.*" + ], + "ignore": [ + ], + "include": [ + "src/docx/", + "tests" + ], + "pythonPlatform": "All", + "pythonVersion": "3.7", + "reportImportCycles": true, + "reportUnnecessaryCast": true, + "reportUnnecessaryTypeIgnoreComment": true, + "stubPath": "./typings", + "typeCheckingMode": "strict", + "useLibraryCodeForTypes": true, + "verboseOutput": true +} diff --git a/ref/xsd/vml-wordprocessingDrawing.xsd b/ref/xsd/vml-wordprocessingDrawing.xsd index f1041e34e..f36e1acde 100644 --- a/ref/xsd/vml-wordprocessingDrawing.xsd +++ b/ref/xsd/vml-wordprocessingDrawing.xsd @@ -1,8 +1,11 @@ - + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + attributeFormDefault="unqualified" + elementFormDefault="qualified" + targetNamespace="urn:schemas-microsoft-com:office:word" +> diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..45e5f78c3 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements-test.txt +build +setuptools>=61.0.0 +tox +twine diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..357592950 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,3 @@ +Sphinx==1.8.6 +Jinja2==2.11.3 +MarkupSafe==0.23 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 000000000..85d9f6ba3 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +-r requirements.txt +behave>=1.2.3 +pyparsing>=2.0.1 +pytest>=2.5 +ruff diff --git a/requirements.txt b/requirements.txt index de244afa3..a156cfe60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,2 @@ -behave>=1.2.3 -flake8>=2.0 lxml>=3.1.0 -mock>=1.0.1 -pyparsing>=2.0.1 -pytest>=2.5 +typing-extensions diff --git a/setup.py b/setup.py deleted file mode 100644 index 7c34edcca..000000000 --- a/setup.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python - -import os -import re - -from setuptools import find_packages, setup - - -def text_of(relpath): - """ - Return string containing the contents of the file at *relpath* relative to - this file. - """ - thisdir = os.path.dirname(__file__) - file_path = os.path.join(thisdir, os.path.normpath(relpath)) - with open(file_path) as f: - text = f.read() - return text - - -# Read the version from docx.__version__ without importing the package -# (and thus attempting to import packages it depends on that may not be -# installed yet) -version = re.search(r'__version__ = "([^"]+)"', text_of("docx/__init__.py")).group(1) - - -NAME = "python-docx" -VERSION = version -DESCRIPTION = "Create and update Microsoft Word .docx files." -KEYWORDS = "docx office openxml word" -AUTHOR = "Steve Canny" -AUTHOR_EMAIL = "python-docx@googlegroups.com" -URL = "https://github.com/python-openxml/python-docx" -LICENSE = text_of("LICENSE") -PACKAGES = find_packages(exclude=["tests", "tests.*"]) -PACKAGE_DATA = {"docx": ["templates/*.xml", "templates/*.docx"]} - -INSTALL_REQUIRES = ["lxml>=2.3.2"] -TEST_SUITE = "tests" -TESTS_REQUIRE = ["behave", "mock", "pyparsing", "pytest"] - -CLASSIFIERS = [ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Topic :: Office/Business :: Office Suites", - "Topic :: Software Development :: Libraries", -] - -LONG_DESCRIPTION = text_of("README.rst") + "\n\n" + text_of("HISTORY.rst") - -ZIP_SAFE = False - -params = { - "name": NAME, - "version": VERSION, - "description": DESCRIPTION, - "keywords": KEYWORDS, - "long_description": LONG_DESCRIPTION, - "author": AUTHOR, - "author_email": AUTHOR_EMAIL, - "url": URL, - "license": LICENSE, - "packages": PACKAGES, - "package_data": PACKAGE_DATA, - "install_requires": INSTALL_REQUIRES, - "tests_require": TESTS_REQUIRE, - "test_suite": TEST_SUITE, - "classifiers": CLASSIFIERS, - "zip_safe": ZIP_SAFE, -} - -setup(**params) diff --git a/docx/__init__.py b/src/docx/__init__.py similarity index 90% rename from docx/__init__.py rename to src/docx/__init__.py index 59756c021..f6f7b5b5a 100644 --- a/docx/__init__.py +++ b/src/docx/__init__.py @@ -1,16 +1,14 @@ -# encoding: utf-8 - from docx.api import Document # noqa -__version__ = "0.8.11" +__version__ = "1.0.0" # register custom Part classes with opc package reader -from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.part import PartFactory from docx.opc.parts.coreprops import CorePropertiesPart - from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart from docx.parts.image import ImagePart diff --git a/docx/api.py b/src/docx/api.py similarity index 50% rename from docx/api.py rename to src/docx/api.py index 63e18c406..a17c1dad4 100644 --- a/docx/api.py +++ b/src/docx/api.py @@ -1,25 +1,23 @@ -# encoding: utf-8 +"""Directly exposed API functions and classes, :func:`Document` for now. -""" -Directly exposed API functions and classes, :func:`Document` for now. -Provides a syntactically more convenient API for interacting with the -OpcPackage graph. +Provides a syntactically more convenient API for interacting with the OpcPackage graph. """ -from __future__ import absolute_import, division, print_function +from __future__ import annotations import os +from typing import IO from docx.opc.constants import CONTENT_TYPE as CT from docx.package import Package -def Document(docx=None): - """ - Return a |Document| object loaded from *docx*, where *docx* can be - either a path to a ``.docx`` file (a string) or a file-like object. If - *docx* is missing or ``None``, the built-in default document "template" - is loaded. +def Document(docx: str | IO[bytes] | None = None): + """Return a |Document| object loaded from `docx`, where `docx` can be either a path + to a ``.docx`` file (a string) or a file-like object. + + If `docx` is missing or ``None``, the built-in default document "template" is + loaded. """ docx = _default_docx_path() if docx is None else docx document_part = Package.open(docx).main_document_part @@ -30,8 +28,6 @@ def Document(docx=None): def _default_docx_path(): - """ - Return the path to the built-in default .docx package. - """ + """Return the path to the built-in default .docx package.""" _thisdir = os.path.split(__file__)[0] - return os.path.join(_thisdir, 'templates', 'default.docx') + return os.path.join(_thisdir, "templates", "default.docx") diff --git a/docx/blkcntnr.py b/src/docx/blkcntnr.py similarity index 57% rename from docx/blkcntnr.py rename to src/docx/blkcntnr.py index a80903e52..81166556a 100644 --- a/docx/blkcntnr.py +++ b/src/docx/blkcntnr.py @@ -1,13 +1,9 @@ -# encoding: utf-8 - """Block item container, used by body, cell, header, etc. Block level items are things like paragraph and table, although there are a few other specialized ones like structured document tags. """ -from __future__ import absolute_import, division, print_function, unicode_literals - from docx.oxml.table import CT_Tbl from docx.shared import Parented from docx.text.paragraph import Paragraph @@ -17,20 +13,22 @@ class BlockItemContainer(Parented): """Base class for proxy objects that can contain block items. These containers include _Body, _Cell, header, footer, footnote, endnote, comment, - and text box objects. Provides the shared functionality to add a block item like - a paragraph or table. + and text box objects. Provides the shared functionality to add a block item like a + paragraph or table. """ def __init__(self, element, parent): super(BlockItemContainer, self).__init__(parent) self._element = element - def add_paragraph(self, text='', style=None): - """ - Return a paragraph newly added to the end of the content in this - container, having *text* in a single run if present, and having - paragraph style *style*. If *style* is |None|, no paragraph style is - applied, which has the same effect as applying the 'Normal' style. + def add_paragraph(self, text="", style=None): + """Return paragraph newly added to the end of the content in this container. + + The paragraph has `text` in a single run if present, and is given paragraph + style `style`. + + If `style` is |None|, no paragraph style is applied, which has the same effect + as applying the 'Normal' style. """ paragraph = self._add_paragraph() if text: @@ -40,36 +38,36 @@ def add_paragraph(self, text='', style=None): return paragraph def add_table(self, rows, cols, width): + """Return table of `width` having `rows` rows and `cols` columns. + + The table is appended appended at the end of the content in this container. + + `width` is evenly distributed between the table columns. """ - Return a table of *width* having *rows* rows and *cols* columns, - newly appended to the content in this container. *width* is evenly - distributed between the table columns. - """ - from .table import Table + from docx.table import Table + tbl = CT_Tbl.new_tbl(rows, cols, width) self._element._insert_tbl(tbl) return Table(tbl, self) @property def paragraphs(self): - """ - A list containing the paragraphs in this container, in document - order. Read-only. + """A list containing the paragraphs in this container, in document order. + + Read-only. """ return [Paragraph(p, self) for p in self._element.p_lst] @property def tables(self): - """ - A list containing the tables in this container, in document order. + """A list containing the tables in this container, in document order. + Read-only. """ from .table import Table + return [Table(tbl, self) for tbl in self._element.tbl_lst] def _add_paragraph(self): - """ - Return a paragraph newly added to the end of the content in this - container. - """ + """Return paragraph newly added to the end of the content in this container.""" return Paragraph(self._element.add_p(), self) diff --git a/docx/dml/__init__.py b/src/docx/dml/__init__.py similarity index 100% rename from docx/dml/__init__.py rename to src/docx/dml/__init__.py diff --git a/src/docx/dml/color.py b/src/docx/dml/color.py new file mode 100644 index 000000000..d7ee0a21c --- /dev/null +++ b/src/docx/dml/color.py @@ -0,0 +1,100 @@ +"""DrawingML objects related to color, ColorFormat being the most prominent.""" + +from ..enum.dml import MSO_COLOR_TYPE +from ..oxml.simpletypes import ST_HexColorAuto +from ..shared import ElementProxy + + +class ColorFormat(ElementProxy): + """Provides access to color settings such as RGB color, theme color, and luminance + adjustments.""" + + def __init__(self, rPr_parent): + super(ColorFormat, self).__init__(rPr_parent) + + @property + def rgb(self): + """An |RGBColor| value or |None| if no RGB color is specified. + + When :attr:`type` is `MSO_COLOR_TYPE.RGB`, the value of this property will + always be an |RGBColor| value. It may also be an |RGBColor| value if + :attr:`type` is `MSO_COLOR_TYPE.THEME`, as Word writes the current value of a + theme color when one is assigned. In that case, the RGB value should be + interpreted as no more than a good guess however, as the theme color takes + precedence at rendering time. Its value is |None| whenever :attr:`type` is + either |None| or `MSO_COLOR_TYPE.AUTO`. + + Assigning an |RGBColor| value causes :attr:`type` to become `MSO_COLOR_TYPE.RGB` + and any theme color is removed. Assigning |None| causes any color to be removed + such that the effective color is inherited from the style hierarchy. + """ + color = self._color + if color is None: + return None + if color.val == ST_HexColorAuto.AUTO: + return None + return color.val + + @rgb.setter + def rgb(self, value): + if value is None and self._color is None: + return + rPr = self._element.get_or_add_rPr() + rPr._remove_color() + if value is not None: + rPr.get_or_add_color().val = value + + @property + def theme_color(self): + """Member of :ref:`MsoThemeColorIndex` or |None| if no theme color is specified. + + When :attr:`type` is `MSO_COLOR_TYPE.THEME`, the value of this property will + always be a member of :ref:`MsoThemeColorIndex`. When :attr:`type` has any other + value, the value of this property is |None|. + + Assigning a member of :ref:`MsoThemeColorIndex` causes :attr:`type` to become + `MSO_COLOR_TYPE.THEME`. Any existing RGB value is retained but ignored by Word. + Assigning |None| causes any color specification to be removed such that the + effective color is inherited from the style hierarchy. + """ + color = self._color + if color is None or color.themeColor is None: + return None + return color.themeColor + + @theme_color.setter + def theme_color(self, value): + if value is None: + if self._color is not None: + self._element.rPr._remove_color() + return + self._element.get_or_add_rPr().get_or_add_color().themeColor = value + + @property + def type(self) -> MSO_COLOR_TYPE: + """Read-only. + + A member of :ref:`MsoColorType`, one of RGB, THEME, or AUTO, corresponding to + the way this color is defined. Its value is |None| if no color is applied at + this level, which causes the effective color to be inherited from the style + hierarchy. + """ + color = self._color + if color is None: + return None + if color.themeColor is not None: + return MSO_COLOR_TYPE.THEME + if color.val == ST_HexColorAuto.AUTO: + return MSO_COLOR_TYPE.AUTO + return MSO_COLOR_TYPE.RGB + + @property + def _color(self): + """Return `w:rPr/w:color` or |None| if not present. + + Helper to factor out repetitive element access. + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.color diff --git a/src/docx/document.py b/src/docx/document.py new file mode 100644 index 000000000..07751f155 --- /dev/null +++ b/src/docx/document.py @@ -0,0 +1,181 @@ +"""|Document| and closely related objects.""" + +from docx.blkcntnr import BlockItemContainer +from docx.enum.section import WD_SECTION +from docx.enum.text import WD_BREAK +from docx.section import Section, Sections +from docx.shared import ElementProxy, Emu +from docx.text.paragraph import Paragraph + + +class Document(ElementProxy): + """WordprocessingML (WML) document. + + Not intended to be constructed directly. Use :func:`docx.Document` to open or create + a document. + """ + + def __init__(self, element, part): + super(Document, self).__init__(element) + self._part = part + self.__body = None + + def add_heading(self, text="", level=1): + """Return a heading paragraph newly added to the end of the document. + + The heading paragraph will contain `text` and have its paragraph style + determined by `level`. If `level` is 0, the style is set to `Title`. If `level` + is 1 (or omitted), `Heading 1` is used. Otherwise the style is set to `Heading + {level}`. Raises |ValueError| if `level` is outside the range 0-9. + """ + if not 0 <= level <= 9: + raise ValueError("level must be in range 0-9, got %d" % level) + style = "Title" if level == 0 else "Heading %d" % level + return self.add_paragraph(text, style) + + def add_page_break(self): + """Return newly |Paragraph| object containing only a page break.""" + paragraph = self.add_paragraph() + paragraph.add_run().add_break(WD_BREAK.PAGE) + return paragraph + + def add_paragraph(self, text: str = "", style=None) -> Paragraph: + """Return paragraph newly added to the end of the document. + + The paragraph is populated with `text` and having paragraph style `style`. + + `text` can contain tab (``\\t``) characters, which are converted to the + appropriate XML form for a tab. `text` can also include newline (``\\n``) or + carriage return (``\\r``) characters, each of which is converted to a line + break. + """ + return self._body.add_paragraph(text, style) + + def add_picture(self, image_path_or_stream, width=None, height=None): + """Return new picture shape added in its own paragraph at end of the document. + + The picture contains the image at `image_path_or_stream`, scaled based on + `width` and `height`. If neither width nor height is specified, the picture + appears at its native size. If only one is specified, it is used to compute a + scaling factor that is then applied to the unspecified dimension, preserving the + aspect ratio of the image. The native size of the picture is calculated using + the dots-per-inch (dpi) value specified in the image file, defaulting to 72 dpi + if no value is specified, as is often the case. + """ + run = self.add_paragraph().add_run() + return run.add_picture(image_path_or_stream, width, height) + + def add_section(self, start_type=WD_SECTION.NEW_PAGE): + """Return a |Section| object newly added at the end of the document. + + The optional `start_type` argument must be a member of the :ref:`WdSectionStart` + enumeration, and defaults to ``WD_SECTION.NEW_PAGE`` if not provided. + """ + new_sectPr = self._element.body.add_section_break() + new_sectPr.start_type = start_type + return Section(new_sectPr, self._part) + + def add_table(self, rows, cols, style=None): + """Add a table having row and column counts of `rows` and `cols` respectively. + + `style` may be a table style object or a table style name. If `style` is |None|, + the table inherits the default table style of the document. + """ + table = self._body.add_table(rows, cols, self._block_width) + table.style = style + return table + + @property + def core_properties(self): + """A |CoreProperties| object providing Dublin Core properties of document.""" + return self._part.core_properties + + @property + def inline_shapes(self): + """The |InlineShapes| collectoin for this document. + + An inline shape is a graphical object, such as a picture, contained in a run of + text and behaving like a character glyph, being flowed like other text in a + paragraph. + """ + return self._part.inline_shapes + + @property + def paragraphs(self): + """The |Paragraph| instances in the document, in document order. + + Note that paragraphs within revision marks such as ```` or ```` do + not appear in this list. + """ + return self._body.paragraphs + + @property + def part(self): + """The |DocumentPart| object of this document.""" + return self._part + + def save(self, path_or_stream): + """Save this document to `path_or_stream`. + + `path_or_stream` can be either a path to a filesystem location (a string) or a + file-like object. + """ + self._part.save(path_or_stream) + + @property + def sections(self): + """|Sections| object providing access to each section in this document.""" + return Sections(self._element, self._part) + + @property + def settings(self): + """A |Settings| object providing access to the document-level settings.""" + return self._part.settings + + @property + def styles(self): + """A |Styles| object providing access to the styles in this document.""" + return self._part.styles + + @property + def tables(self): + """All |Table| instances in the document, in document order. + + Note that only tables appearing at the top level of the document appear in this + list; a table nested inside a table cell does not appear. A table within + revision marks such as ```` or ```` will also not appear in the + list. + """ + return self._body.tables + + @property + def _block_width(self): + """A |Length| object specifying the space between margins in last section.""" + section = self.sections[-1] + return Emu(section.page_width - section.left_margin - section.right_margin) + + @property + def _body(self): + """The |_Body| instance containing the content for this document.""" + if self.__body is None: + self.__body = _Body(self._element.body, self) + return self.__body + + +class _Body(BlockItemContainer): + """Proxy for `` element in this document. + + It's primary role is a container for document content. + """ + + def __init__(self, body_elm, parent): + super(_Body, self).__init__(body_elm, parent) + self._body = body_elm + + def clear_content(self): + """Return this |_Body| instance after clearing it of all content. + + Section properties for the main document story, if present, are preserved. + """ + self._body.clear_content() + return self diff --git a/src/docx/drawing/__init__.py b/src/docx/drawing/__init__.py new file mode 100644 index 000000000..71bda0413 --- /dev/null +++ b/src/docx/drawing/__init__.py @@ -0,0 +1,16 @@ +"""DrawingML-related objects are in this subpackage.""" + +from __future__ import annotations + +from docx import types as t +from docx.oxml.drawing import CT_Drawing +from docx.shared import Parented + + +class Drawing(Parented): + """Container for a DrawingML object.""" + + def __init__(self, drawing: CT_Drawing, parent: t.StoryChild): + super().__init__(parent) + self._parent = parent + self._drawing = self._element = drawing diff --git a/docx/enum/__init__.py b/src/docx/enum/__init__.py similarity index 52% rename from docx/enum/__init__.py rename to src/docx/enum/__init__.py index dd49faafd..bfab52d36 100644 --- a/docx/enum/__init__.py +++ b/src/docx/enum/__init__.py @@ -1,14 +1,7 @@ -# encoding: utf-8 +"""Enumerations used in python-docx.""" -""" -Enumerations used in python-docx -""" - -from __future__ import absolute_import, print_function, unicode_literals - - -class Enumeration(object): +class Enumeration: @classmethod def from_xml(cls, xml_val): return cls._xml_to_idx[xml_val] diff --git a/src/docx/enum/base.py b/src/docx/enum/base.py new file mode 100644 index 000000000..4c20af644 --- /dev/null +++ b/src/docx/enum/base.py @@ -0,0 +1,145 @@ +"""Base classes and other objects used by enumerations.""" + +from __future__ import annotations + +import enum +import textwrap +from typing import Any, Dict, Type, TypeVar + +from typing_extensions import Self + +_T = TypeVar("_T", bound="BaseXmlEnum") + + +class BaseEnum(int, enum.Enum): + """Base class for Enums that do not map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. + """ + + def __new__(cls, ms_api_value: int, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.__doc__ = docstr.strip() + return self + + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" + + +class BaseXmlEnum(int, enum.Enum): + """Base class for Enums that also map XML attr values. + + The enum's value will be an integer, corresponding to the integer assigned the + corresponding member in the MS API enum of the same name. + """ + + xml_value: str + + def __new__(cls, ms_api_value: int, xml_value: str, docstr: str): + self = int.__new__(cls, ms_api_value) + self._value_ = ms_api_value + self.xml_value = xml_value + self.__doc__ = docstr.strip() + return self + + def __str__(self): + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" + return f"{self.name} ({self.value})" + + @classmethod + def from_xml(cls, xml_value: str | None) -> Self: + """Enumeration member corresponding to XML attribute value `xml_value`. + + Example:: + + >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center") + WD_PARAGRAPH_ALIGNMENT.CENTER + + """ + member = next((member for member in cls if member.xml_value == xml_value), None) + if member is None: + raise ValueError(f"{cls.__name__} has no XML mapping for '{xml_value}'") + return member + + @classmethod + def to_xml(cls: Type[_T], value: int | _T | None) -> str | None: + """XML value of this enum member, generally an XML attribute value.""" + return cls(value).xml_value + + +class DocsPageFormatter: + """Generate an .rst doc page for an enumeration. + + Formats a RestructuredText documention page (string) for the enumeration class parts + passed to the constructor. An immutable one-shot service object. + """ + + def __init__(self, clsname: str, clsdict: Dict[str, Any]): + self._clsname = clsname + self._clsdict = clsdict + + @property + def page_str(self): + """The RestructuredText documentation page for the enumeration. + + This is the only API member for the class. + """ + tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s" + components = ( + self._ms_name, + self._page_title, + self._intro_text, + self._member_defs, + ) + return tmpl % components + + @property + def _intro_text(self): + """Docstring of the enumeration, formatted for documentation page.""" + try: + cls_docstring = self._clsdict["__doc__"] + except KeyError: + cls_docstring = "" + + if cls_docstring is None: + return "" + + return textwrap.dedent(cls_docstring).strip() + + def _member_def(self, member: BaseEnum | BaseXmlEnum): + """Return an individual member definition formatted as an RST glossary entry, + wrapped to fit within 78 columns.""" + assert member.__doc__ is not None + member_docstring = textwrap.dedent(member.__doc__).strip() + member_docstring = textwrap.fill( + member_docstring, + width=78, + initial_indent=" " * 4, + subsequent_indent=" " * 4, + ) + return "%s\n%s\n" % (member.name, member_docstring) + + @property + def _member_defs(self): + """A single string containing the aggregated member definitions section of the + documentation page.""" + members = self._clsdict["__members__"] + member_defs = [ + self._member_def(member) for member in members if member.name is not None + ] + return "\n".join(member_defs) + + @property + def _ms_name(self): + """The Microsoft API name for this enumeration.""" + return self._clsdict["__ms_name__"] + + @property + def _page_title(self): + """The title for the documentation page, formatted as code (surrounded in + double-backtics) and underlined with '=' characters.""" + title_underscore = "=" * (len(self._clsname) + 4) + return "``%s``\n%s" % (self._clsname, title_underscore) diff --git a/src/docx/enum/dml.py b/src/docx/enum/dml.py new file mode 100644 index 000000000..27c63a283 --- /dev/null +++ b/src/docx/enum/dml.py @@ -0,0 +1,103 @@ +"""Enumerations used by DrawingML objects.""" + +from .base import BaseEnum, BaseXmlEnum + + +class MSO_COLOR_TYPE(BaseEnum): + """Specifies the color specification scheme. + + Example:: + + from docx.enum.dml import MSO_COLOR_TYPE + + assert font.color.type == MSO_COLOR_TYPE.SCHEME + + MS API name: `MsoColorType` + + http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx + """ + + RGB = (1, "Color is specified by an |RGBColor| value.") + """Color is specified by an |RGBColor| value.""" + + THEME = (2, "Color is one of the preset theme colors.") + """Color is one of the preset theme colors.""" + + AUTO = (101, "Color is determined automatically by the application.") + """Color is determined automatically by the application.""" + + +class MSO_THEME_COLOR_INDEX(BaseXmlEnum): + """Indicates the Office theme color, one of those shown in the color gallery on the + formatting ribbon. + + Alias: ``MSO_THEME_COLOR`` + + Example:: + + from docx.enum.dml import MSO_THEME_COLOR + + font.color.theme_color = MSO_THEME_COLOR.ACCENT_1 + + MS API name: `MsoThemeColorIndex` + + http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx + """ + + NOT_THEME_COLOR = (0, "UNMAPPED", "Indicates the color is not a theme color.") + """Indicates the color is not a theme color.""" + + ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.") + """Specifies the Accent 1 theme color.""" + + ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.") + """Specifies the Accent 2 theme color.""" + + ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.") + """Specifies the Accent 3 theme color.""" + + ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.") + """Specifies the Accent 4 theme color.""" + + ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.") + """Specifies the Accent 5 theme color.""" + + ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.") + """Specifies the Accent 6 theme color.""" + + BACKGROUND_1 = (14, "background1", "Specifies the Background 1 theme color.") + """Specifies the Background 1 theme color.""" + + BACKGROUND_2 = (16, "background2", "Specifies the Background 2 theme color.") + """Specifies the Background 2 theme color.""" + + DARK_1 = (1, "dark1", "Specifies the Dark 1 theme color.") + """Specifies the Dark 1 theme color.""" + + DARK_2 = (3, "dark2", "Specifies the Dark 2 theme color.") + """Specifies the Dark 2 theme color.""" + + FOLLOWED_HYPERLINK = ( + 12, + "followedHyperlink", + "Specifies the theme color for a clicked hyperlink.", + ) + """Specifies the theme color for a clicked hyperlink.""" + + HYPERLINK = (11, "hyperlink", "Specifies the theme color for a hyperlink.") + """Specifies the theme color for a hyperlink.""" + + LIGHT_1 = (2, "light1", "Specifies the Light 1 theme color.") + """Specifies the Light 1 theme color.""" + + LIGHT_2 = (4, "light2", "Specifies the Light 2 theme color.") + """Specifies the Light 2 theme color.""" + + TEXT_1 = (13, "text1", "Specifies the Text 1 theme color.") + """Specifies the Text 1 theme color.""" + + TEXT_2 = (15, "text2", "Specifies the Text 2 theme color.") + """Specifies the Text 2 theme color.""" + + +MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX diff --git a/src/docx/enum/section.py b/src/docx/enum/section.py new file mode 100644 index 000000000..982e19111 --- /dev/null +++ b/src/docx/enum/section.py @@ -0,0 +1,86 @@ +"""Enumerations related to the main document in WordprocessingML files.""" + +from .base import BaseXmlEnum + + +class WD_HEADER_FOOTER_INDEX(BaseXmlEnum): + """Alias: **WD_HEADER_FOOTER** + + Specifies one of the three possible header/footer definitions for a section. + + For internal use only; not part of the python-docx API. + + MS API name: `WdHeaderFooterIndex` + URL: https://docs.microsoft.com/en-us/office/vba/api/word.wdheaderfooterindex + """ + + PRIMARY = (1, "default", "Header for odd pages or all if no even header.") + """Header for odd pages or all if no even header.""" + + FIRST_PAGE = (2, "first", "Header for first page of section.") + """Header for first page of section.""" + + EVEN_PAGE = (3, "even", "Header for even pages of recto/verso section.") + """Header for even pages of recto/verso section.""" + + +WD_HEADER_FOOTER = WD_HEADER_FOOTER_INDEX + + +class WD_ORIENTATION(BaseXmlEnum): + """Alias: **WD_ORIENT** + + Specifies the page layout orientation. + + Example:: + + from docx.enum.section import WD_ORIENT + + section = document.sections[-1] section.orientation = WD_ORIENT.LANDSCAPE + + MS API name: `WdOrientation` + MS API URL: http://msdn.microsoft.com/en-us/library/office/ff837902.aspx + """ + + PORTRAIT = (0, "portrait", "Portrait orientation.") + """Portrait orientation.""" + + LANDSCAPE = (1, "landscape", "Landscape orientation.") + """Landscape orientation.""" + + +WD_ORIENT = WD_ORIENTATION + + +class WD_SECTION_START(BaseXmlEnum): + """Alias: **WD_SECTION** + + Specifies the start type of a section break. + + Example:: + + from docx.enum.section import WD_SECTION + + section = document.sections[0] section.start_type = WD_SECTION.NEW_PAGE + + MS API name: `WdSectionStart` + MS API URL: http://msdn.microsoft.com/en-us/library/office/ff840975.aspx + """ + + CONTINUOUS = (0, "continuous", "Continuous section break.") + """Continuous section break.""" + + NEW_COLUMN = (1, "nextColumn", "New column section break.") + """New column section break.""" + + NEW_PAGE = (2, "nextPage", "New page section break.") + """New page section break.""" + + EVEN_PAGE = (3, "evenPage", "Even pages section break.") + """Even pages section break.""" + + ODD_PAGE = (4, "oddPage", "Section begins on next odd page.") + """Section begins on next odd page.""" + + +WD_SECTION = WD_SECTION_START diff --git a/src/docx/enum/shape.py b/src/docx/enum/shape.py new file mode 100644 index 000000000..ed086c38d --- /dev/null +++ b/src/docx/enum/shape.py @@ -0,0 +1,19 @@ +"""Enumerations related to DrawingML shapes in WordprocessingML files.""" + +import enum + + +class WD_INLINE_SHAPE_TYPE(enum.Enum): + """Corresponds to WdInlineShapeType enumeration. + + http://msdn.microsoft.com/en-us/library/office/ff192587.aspx. + """ + + CHART = 12 + LINKED_PICTURE = 4 + PICTURE = 3 + SMART_ART = 15 + NOT_IMPLEMENTED = -6 + + +WD_INLINE_SHAPE = WD_INLINE_SHAPE_TYPE diff --git a/src/docx/enum/style.py b/src/docx/enum/style.py new file mode 100644 index 000000000..d2474611d --- /dev/null +++ b/src/docx/enum/style.py @@ -0,0 +1,452 @@ +"""Enumerations related to styles.""" + +from .base import BaseEnum, BaseXmlEnum + + +class WD_BUILTIN_STYLE(BaseEnum): + """Alias: **WD_STYLE** + + Specifies a built-in Microsoft Word style. + + Example:: + + from docx import Document + from docx.enum.style import WD_STYLE + + document = Document() + styles = document.styles + style = styles[WD_STYLE.BODY_TEXT] + + + MS API name: `WdBuiltinStyle` + + http://msdn.microsoft.com/en-us/library/office/ff835210.aspx + """ + + BLOCK_QUOTATION = (-85, "Block Text.") + """Block Text.""" + + BODY_TEXT = (-67, "Body Text.") + """Body Text.""" + + BODY_TEXT_2 = (-81, "Body Text 2.") + """Body Text 2.""" + + BODY_TEXT_3 = (-82, "Body Text 3.") + """Body Text 3.""" + + BODY_TEXT_FIRST_INDENT = (-78, "Body Text First Indent.") + """Body Text First Indent.""" + + BODY_TEXT_FIRST_INDENT_2 = (-79, "Body Text First Indent 2.") + """Body Text First Indent 2.""" + + BODY_TEXT_INDENT = (-68, "Body Text Indent.") + """Body Text Indent.""" + + BODY_TEXT_INDENT_2 = (-83, "Body Text Indent 2.") + """Body Text Indent 2.""" + + BODY_TEXT_INDENT_3 = (-84, "Body Text Indent 3.") + """Body Text Indent 3.""" + + BOOK_TITLE = (-265, "Book Title.") + """Book Title.""" + + CAPTION = (-35, "Caption.") + """Caption.""" + + CLOSING = (-64, "Closing.") + """Closing.""" + + COMMENT_REFERENCE = (-40, "Comment Reference.") + """Comment Reference.""" + + COMMENT_TEXT = (-31, "Comment Text.") + """Comment Text.""" + + DATE = (-77, "Date.") + """Date.""" + + DEFAULT_PARAGRAPH_FONT = (-66, "Default Paragraph Font.") + """Default Paragraph Font.""" + + EMPHASIS = (-89, "Emphasis.") + """Emphasis.""" + + ENDNOTE_REFERENCE = (-43, "Endnote Reference.") + """Endnote Reference.""" + + ENDNOTE_TEXT = (-44, "Endnote Text.") + """Endnote Text.""" + + ENVELOPE_ADDRESS = (-37, "Envelope Address.") + """Envelope Address.""" + + ENVELOPE_RETURN = (-38, "Envelope Return.") + """Envelope Return.""" + + FOOTER = (-33, "Footer.") + """Footer.""" + + FOOTNOTE_REFERENCE = (-39, "Footnote Reference.") + """Footnote Reference.""" + + FOOTNOTE_TEXT = (-30, "Footnote Text.") + """Footnote Text.""" + + HEADER = (-32, "Header.") + """Header.""" + + HEADING_1 = (-2, "Heading 1.") + """Heading 1.""" + + HEADING_2 = (-3, "Heading 2.") + """Heading 2.""" + + HEADING_3 = (-4, "Heading 3.") + """Heading 3.""" + + HEADING_4 = (-5, "Heading 4.") + """Heading 4.""" + + HEADING_5 = (-6, "Heading 5.") + """Heading 5.""" + + HEADING_6 = (-7, "Heading 6.") + """Heading 6.""" + + HEADING_7 = (-8, "Heading 7.") + """Heading 7.""" + + HEADING_8 = (-9, "Heading 8.") + """Heading 8.""" + + HEADING_9 = (-10, "Heading 9.") + """Heading 9.""" + + HTML_ACRONYM = (-96, "HTML Acronym.") + """HTML Acronym.""" + + HTML_ADDRESS = (-97, "HTML Address.") + """HTML Address.""" + + HTML_CITE = (-98, "HTML Cite.") + """HTML Cite.""" + + HTML_CODE = (-99, "HTML Code.") + """HTML Code.""" + + HTML_DFN = (-100, "HTML Definition.") + """HTML Definition.""" + + HTML_KBD = (-101, "HTML Keyboard.") + """HTML Keyboard.""" + + HTML_NORMAL = (-95, "Normal (Web).") + """Normal (Web).""" + + HTML_PRE = (-102, "HTML Preformatted.") + """HTML Preformatted.""" + + HTML_SAMP = (-103, "HTML Sample.") + """HTML Sample.""" + + HTML_TT = (-104, "HTML Typewriter.") + """HTML Typewriter.""" + + HTML_VAR = (-105, "HTML Variable.") + """HTML Variable.""" + + HYPERLINK = (-86, "Hyperlink.") + """Hyperlink.""" + + HYPERLINK_FOLLOWED = (-87, "Followed Hyperlink.") + """Followed Hyperlink.""" + + INDEX_1 = (-11, "Index 1.") + """Index 1.""" + + INDEX_2 = (-12, "Index 2.") + """Index 2.""" + + INDEX_3 = (-13, "Index 3.") + """Index 3.""" + + INDEX_4 = (-14, "Index 4.") + """Index 4.""" + + INDEX_5 = (-15, "Index 5.") + """Index 5.""" + + INDEX_6 = (-16, "Index 6.") + """Index 6.""" + + INDEX_7 = (-17, "Index 7.") + """Index 7.""" + + INDEX_8 = (-18, "Index 8.") + """Index 8.""" + + INDEX_9 = (-19, "Index 9.") + """Index 9.""" + + INDEX_HEADING = (-34, "Index Heading") + """Index Heading""" + + INTENSE_EMPHASIS = (-262, "Intense Emphasis.") + """Intense Emphasis.""" + + INTENSE_QUOTE = (-182, "Intense Quote.") + """Intense Quote.""" + + INTENSE_REFERENCE = (-264, "Intense Reference.") + """Intense Reference.""" + + LINE_NUMBER = (-41, "Line Number.") + """Line Number.""" + + LIST = (-48, "List.") + """List.""" + + LIST_2 = (-51, "List 2.") + """List 2.""" + + LIST_3 = (-52, "List 3.") + """List 3.""" + + LIST_4 = (-53, "List 4.") + """List 4.""" + + LIST_5 = (-54, "List 5.") + """List 5.""" + + LIST_BULLET = (-49, "List Bullet.") + """List Bullet.""" + + LIST_BULLET_2 = (-55, "List Bullet 2.") + """List Bullet 2.""" + + LIST_BULLET_3 = (-56, "List Bullet 3.") + """List Bullet 3.""" + + LIST_BULLET_4 = (-57, "List Bullet 4.") + """List Bullet 4.""" + + LIST_BULLET_5 = (-58, "List Bullet 5.") + """List Bullet 5.""" + + LIST_CONTINUE = (-69, "List Continue.") + """List Continue.""" + + LIST_CONTINUE_2 = (-70, "List Continue 2.") + """List Continue 2.""" + + LIST_CONTINUE_3 = (-71, "List Continue 3.") + """List Continue 3.""" + + LIST_CONTINUE_4 = (-72, "List Continue 4.") + """List Continue 4.""" + + LIST_CONTINUE_5 = (-73, "List Continue 5.") + """List Continue 5.""" + + LIST_NUMBER = (-50, "List Number.") + """List Number.""" + + LIST_NUMBER_2 = (-59, "List Number 2.") + """List Number 2.""" + + LIST_NUMBER_3 = (-60, "List Number 3.") + """List Number 3.""" + + LIST_NUMBER_4 = (-61, "List Number 4.") + """List Number 4.""" + + LIST_NUMBER_5 = (-62, "List Number 5.") + """List Number 5.""" + + LIST_PARAGRAPH = (-180, "List Paragraph.") + """List Paragraph.""" + + MACRO_TEXT = (-46, "Macro Text.") + """Macro Text.""" + + MESSAGE_HEADER = (-74, "Message Header.") + """Message Header.""" + + NAV_PANE = (-90, "Document Map.") + """Document Map.""" + + NORMAL = (-1, "Normal.") + """Normal.""" + + NORMAL_INDENT = (-29, "Normal Indent.") + """Normal Indent.""" + + NORMAL_OBJECT = (-158, "Normal (applied to an object).") + """Normal (applied to an object).""" + + NORMAL_TABLE = (-106, "Normal (applied within a table).") + """Normal (applied within a table).""" + + NOTE_HEADING = (-80, "Note Heading.") + """Note Heading.""" + + PAGE_NUMBER = (-42, "Page Number.") + """Page Number.""" + + PLAIN_TEXT = (-91, "Plain Text.") + """Plain Text.""" + + QUOTE = (-181, "Quote.") + """Quote.""" + + SALUTATION = (-76, "Salutation.") + """Salutation.""" + + SIGNATURE = (-65, "Signature.") + """Signature.""" + + STRONG = (-88, "Strong.") + """Strong.""" + + SUBTITLE = (-75, "Subtitle.") + """Subtitle.""" + + SUBTLE_EMPHASIS = (-261, "Subtle Emphasis.") + """Subtle Emphasis.""" + + SUBTLE_REFERENCE = (-263, "Subtle Reference.") + """Subtle Reference.""" + + TABLE_COLORFUL_GRID = (-172, "Colorful Grid.") + """Colorful Grid.""" + + TABLE_COLORFUL_LIST = (-171, "Colorful List.") + """Colorful List.""" + + TABLE_COLORFUL_SHADING = (-170, "Colorful Shading.") + """Colorful Shading.""" + + TABLE_DARK_LIST = (-169, "Dark List.") + """Dark List.""" + + TABLE_LIGHT_GRID = (-161, "Light Grid.") + """Light Grid.""" + + TABLE_LIGHT_GRID_ACCENT_1 = (-175, "Light Grid Accent 1.") + """Light Grid Accent 1.""" + + TABLE_LIGHT_LIST = (-160, "Light List.") + """Light List.""" + + TABLE_LIGHT_LIST_ACCENT_1 = (-174, "Light List Accent 1.") + """Light List Accent 1.""" + + TABLE_LIGHT_SHADING = (-159, "Light Shading.") + """Light Shading.""" + + TABLE_LIGHT_SHADING_ACCENT_1 = (-173, "Light Shading Accent 1.") + """Light Shading Accent 1.""" + + TABLE_MEDIUM_GRID_1 = (-166, "Medium Grid 1.") + """Medium Grid 1.""" + + TABLE_MEDIUM_GRID_2 = (-167, "Medium Grid 2.") + """Medium Grid 2.""" + + TABLE_MEDIUM_GRID_3 = (-168, "Medium Grid 3.") + """Medium Grid 3.""" + + TABLE_MEDIUM_LIST_1 = (-164, "Medium List 1.") + """Medium List 1.""" + + TABLE_MEDIUM_LIST_1_ACCENT_1 = (-178, "Medium List 1 Accent 1.") + """Medium List 1 Accent 1.""" + + TABLE_MEDIUM_LIST_2 = (-165, "Medium List 2.") + """Medium List 2.""" + + TABLE_MEDIUM_SHADING_1 = (-162, "Medium Shading 1.") + """Medium Shading 1.""" + + TABLE_MEDIUM_SHADING_1_ACCENT_1 = (-176, "Medium Shading 1 Accent 1.") + """Medium Shading 1 Accent 1.""" + + TABLE_MEDIUM_SHADING_2 = (-163, "Medium Shading 2.") + """Medium Shading 2.""" + + TABLE_MEDIUM_SHADING_2_ACCENT_1 = (-177, "Medium Shading 2 Accent 1.") + """Medium Shading 2 Accent 1.""" + + TABLE_OF_AUTHORITIES = (-45, "Table of Authorities.") + """Table of Authorities.""" + + TABLE_OF_FIGURES = (-36, "Table of Figures.") + """Table of Figures.""" + + TITLE = (-63, "Title.") + """Title.""" + + TOAHEADING = (-47, "TOA Heading.") + """TOA Heading.""" + + TOC_1 = (-20, "TOC 1.") + """TOC 1.""" + + TOC_2 = (-21, "TOC 2.") + """TOC 2.""" + + TOC_3 = (-22, "TOC 3.") + """TOC 3.""" + + TOC_4 = (-23, "TOC 4.") + """TOC 4.""" + + TOC_5 = (-24, "TOC 5.") + """TOC 5.""" + + TOC_6 = (-25, "TOC 6.") + """TOC 6.""" + + TOC_7 = (-26, "TOC 7.") + """TOC 7.""" + + TOC_8 = (-27, "TOC 8.") + """TOC 8.""" + + TOC_9 = (-28, "TOC 9.") + """TOC 9.""" + + +WD_STYLE = WD_BUILTIN_STYLE + + +class WD_STYLE_TYPE(BaseXmlEnum): + """Specifies one of the four style types: paragraph, character, list, or table. + + Example:: + + from docx import Document + from docx.enum.style import WD_STYLE_TYPE + + styles = Document().styles + assert styles[0].type == WD_STYLE_TYPE.PARAGRAPH + + MS API name: `WdStyleType` + + http://msdn.microsoft.com/en-us/library/office/ff196870.aspx + """ + + CHARACTER = (2, "character", "Character style.") + """Character style.""" + + LIST = (4, "numbering", "List style.") + """List style.""" + + PARAGRAPH = (1, "paragraph", "Paragraph style.") + """Paragraph style.""" + + TABLE = (3, "table", "Table style.") + """Table style.""" diff --git a/src/docx/enum/table.py b/src/docx/enum/table.py new file mode 100644 index 000000000..eb1eb9dc0 --- /dev/null +++ b/src/docx/enum/table.py @@ -0,0 +1,136 @@ +"""Enumerations related to tables in WordprocessingML files.""" + +from docx.enum.base import BaseEnum, BaseXmlEnum + + +class WD_CELL_VERTICAL_ALIGNMENT(BaseXmlEnum): + """Alias: **WD_ALIGN_VERTICAL** + + Specifies the vertical alignment of text in one or more cells of a table. + + Example:: + + from docx.enum.table import WD_ALIGN_VERTICAL + + table = document.add_table(3, 3) + table.cell(0, 0).vertical_alignment = WD_ALIGN_VERTICAL.BOTTOM + + MS API name: `WdCellVerticalAlignment` + + https://msdn.microsoft.com/en-us/library/office/ff193345.aspx + """ + + TOP = (0, "top", "Text is aligned to the top border of the cell.") + """Text is aligned to the top border of the cell.""" + + CENTER = (1, "center", "Text is aligned to the center of the cell.") + """Text is aligned to the center of the cell.""" + + BOTTOM = (3, "bottom", "Text is aligned to the bottom border of the cell.") + """Text is aligned to the bottom border of the cell.""" + + BOTH = ( + 101, + "both", + "This is an option in the OpenXml spec, but not in Word itself. It's not" + " clear what Word behavior this setting produces. If you find out please" + " let us know and we'll update this documentation. Otherwise, probably best" + " to avoid this option.", + ) + """This is an option in the OpenXml spec, but not in Word itself. + + It's not clear what Word behavior this setting produces. If you find out please let + us know and we'll update this documentation. Otherwise, probably best to avoid this + option. + """ + + +WD_ALIGN_VERTICAL = WD_CELL_VERTICAL_ALIGNMENT + + +class WD_ROW_HEIGHT_RULE(BaseXmlEnum): + """Alias: **WD_ROW_HEIGHT** + + Specifies the rule for determining the height of a table row + + Example:: + + from docx.enum.table import WD_ROW_HEIGHT_RULE + + table = document.add_table(3, 3) + table.rows[0].height_rule = WD_ROW_HEIGHT_RULE.EXACTLY + + MS API name: `WdRowHeightRule` + + https://msdn.microsoft.com/en-us/library/office/ff193620.aspx + """ + + AUTO = ( + 0, + "auto", + "The row height is adjusted to accommodate the tallest value in the row.", + ) + """The row height is adjusted to accommodate the tallest value in the row.""" + + AT_LEAST = (1, "atLeast", "The row height is at least a minimum specified value.") + """The row height is at least a minimum specified value.""" + + EXACTLY = (2, "exact", "The row height is an exact value.") + """The row height is an exact value.""" + + +WD_ROW_HEIGHT = WD_ROW_HEIGHT_RULE + + +class WD_TABLE_ALIGNMENT(BaseXmlEnum): + """Specifies table justification type. + + Example:: + + from docx.enum.table import WD_TABLE_ALIGNMENT + + table = document.add_table(3, 3) + table.alignment = WD_TABLE_ALIGNMENT.CENTER + + MS API name: `WdRowAlignment` + + http://office.microsoft.com/en-us/word-help/HV080607259.aspx + """ + + LEFT = (0, "left", "Left-aligned") + """Left-aligned""" + + CENTER = (1, "center", "Center-aligned.") + """Center-aligned.""" + + RIGHT = (2, "right", "Right-aligned.") + """Right-aligned.""" + + +class WD_TABLE_DIRECTION(BaseEnum): + """Specifies the direction in which an application orders cells in the specified + table or row. + + Example:: + + from docx.enum.table import WD_TABLE_DIRECTION + + table = document.add_table(3, 3) + table.direction = WD_TABLE_DIRECTION.RTL + + MS API name: `WdTableDirection` + + http://msdn.microsoft.com/en-us/library/ff835141.aspx + """ + + LTR = ( + 0, + "The table or row is arranged with the first column in the leftmost position.", + ) + """The table or row is arranged with the first column in the leftmost position.""" + + RTL = ( + 1, + "The table or row is arranged with the first column in the rightmost position.", + ) + """The table or row is arranged with the first column in the rightmost position.""" diff --git a/src/docx/enum/text.py b/src/docx/enum/text.py new file mode 100644 index 000000000..99e776fea --- /dev/null +++ b/src/docx/enum/text.py @@ -0,0 +1,367 @@ +"""Enumerations related to text in WordprocessingML files.""" + +from __future__ import annotations + +import enum + +from docx.enum.base import BaseXmlEnum + + +class WD_PARAGRAPH_ALIGNMENT(BaseXmlEnum): + """Alias: **WD_ALIGN_PARAGRAPH** + + Specifies paragraph justification type. + + Example:: + + from docx.enum.text import WD_ALIGN_PARAGRAPH + + paragraph = document.add_paragraph() + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + """ + + LEFT = (0, "left", "Left-aligned") + """Left-aligned""" + + CENTER = (1, "center", "Center-aligned.") + """Center-aligned.""" + + RIGHT = (2, "right", "Right-aligned.") + """Right-aligned.""" + + JUSTIFY = (3, "both", "Fully justified.") + """Fully justified.""" + + DISTRIBUTE = ( + 4, + "distribute", + "Paragraph characters are distributed to fill entire width of paragraph.", + ) + """Paragraph characters are distributed to fill entire width of paragraph.""" + + JUSTIFY_MED = ( + 5, + "mediumKashida", + "Justified with a medium character compression ratio.", + ) + """Justified with a medium character compression ratio.""" + + JUSTIFY_HI = ( + 7, + "highKashida", + "Justified with a high character compression ratio.", + ) + """Justified with a high character compression ratio.""" + + JUSTIFY_LOW = (8, "lowKashida", "Justified with a low character compression ratio.") + """Justified with a low character compression ratio.""" + + THAI_JUSTIFY = ( + 9, + "thaiDistribute", + "Justified according to Thai formatting layout.", + ) + """Justified according to Thai formatting layout.""" + + +WD_ALIGN_PARAGRAPH = WD_PARAGRAPH_ALIGNMENT + + +class WD_BREAK_TYPE(enum.Enum): + """Corresponds to WdBreakType enumeration. + + http://msdn.microsoft.com/en-us/library/office/ff195905.aspx. + """ + + COLUMN = 8 + LINE = 6 + LINE_CLEAR_LEFT = 9 + LINE_CLEAR_RIGHT = 10 + LINE_CLEAR_ALL = 11 # -- added for consistency, not in MS version -- + PAGE = 7 + SECTION_CONTINUOUS = 3 + SECTION_EVEN_PAGE = 4 + SECTION_NEXT_PAGE = 2 + SECTION_ODD_PAGE = 5 + TEXT_WRAPPING = 11 + + +WD_BREAK = WD_BREAK_TYPE + + +class WD_COLOR_INDEX(BaseXmlEnum): + """Specifies a standard preset color to apply. + + Used for font highlighting and perhaps other applications. + + * MS API name: `WdColorIndex` + * URL: https://msdn.microsoft.com/EN-US/library/office/ff195343.aspx + """ + + INHERITED = (-1, None, "Color is inherited from the style hierarchy.") + """Color is inherited from the style hierarchy.""" + + AUTO = (0, "default", "Automatic color. Default; usually black.") + """Automatic color. Default; usually black.""" + + BLACK = (1, "black", "Black color.") + """Black color.""" + + BLUE = (2, "blue", "Blue color") + """Blue color""" + + BRIGHT_GREEN = (4, "green", "Bright green color.") + """Bright green color.""" + + DARK_BLUE = (9, "darkBlue", "Dark blue color.") + """Dark blue color.""" + + DARK_RED = (13, "darkRed", "Dark red color.") + """Dark red color.""" + + DARK_YELLOW = (14, "darkYellow", "Dark yellow color.") + """Dark yellow color.""" + + GRAY_25 = (16, "lightGray", "25% shade of gray color.") + """25% shade of gray color.""" + + GRAY_50 = (15, "darkGray", "50% shade of gray color.") + """50% shade of gray color.""" + + GREEN = (11, "darkGreen", "Green color.") + """Green color.""" + + PINK = (5, "magenta", "Pink color.") + """Pink color.""" + + RED = (6, "red", "Red color.") + """Red color.""" + + TEAL = (10, "darkCyan", "Teal color.") + """Teal color.""" + + TURQUOISE = (3, "cyan", "Turquoise color.") + """Turquoise color.""" + + VIOLET = (12, "darkMagenta", "Violet color.") + """Violet color.""" + + WHITE = (8, "white", "White color.") + """White color.""" + + YELLOW = (7, "yellow", "Yellow color.") + """Yellow color.""" + + +WD_COLOR = WD_COLOR_INDEX + + +class WD_LINE_SPACING(BaseXmlEnum): + """Specifies a line spacing format to be applied to a paragraph. + + Example:: + + from docx.enum.text import WD_LINE_SPACING + + paragraph = document.add_paragraph() + paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY + + + MS API name: `WdLineSpacing` + + URL: http://msdn.microsoft.com/en-us/library/office/ff844910.aspx + """ + + SINGLE = (0, "UNMAPPED", "Single spaced (default).") + """Single spaced (default).""" + + ONE_POINT_FIVE = (1, "UNMAPPED", "Space-and-a-half line spacing.") + """Space-and-a-half line spacing.""" + + DOUBLE = (2, "UNMAPPED", "Double spaced.") + """Double spaced.""" + + AT_LEAST = ( + 3, + "atLeast", + "Minimum line spacing is specified amount. Amount is specified separately.", + ) + """Minimum line spacing is specified amount. Amount is specified separately.""" + + EXACTLY = ( + 4, + "exact", + "Line spacing is exactly specified amount. Amount is specified separately.", + ) + """Line spacing is exactly specified amount. Amount is specified separately.""" + + MULTIPLE = ( + 5, + "auto", + "Line spacing is specified as multiple of line heights. Changing font size" + " will change line spacing proportionately.", + ) + """Line spacing is specified as multiple of line heights. Changing font size will + change the line spacing proportionately.""" + + +class WD_TAB_ALIGNMENT(BaseXmlEnum): + """Specifies the tab stop alignment to apply. + + MS API name: `WdTabAlignment` + + URL: https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx + """ + + LEFT = (0, "left", "Left-aligned.") + """Left-aligned.""" + + CENTER = (1, "center", "Center-aligned.") + """Center-aligned.""" + + RIGHT = (2, "right", "Right-aligned.") + """Right-aligned.""" + + DECIMAL = (3, "decimal", "Decimal-aligned.") + """Decimal-aligned.""" + + BAR = (4, "bar", "Bar-aligned.") + """Bar-aligned.""" + + LIST = (6, "list", "List-aligned. (deprecated)") + """List-aligned. (deprecated)""" + + CLEAR = (101, "clear", "Clear an inherited tab stop.") + """Clear an inherited tab stop.""" + + END = (102, "end", "Right-aligned. (deprecated)") + """Right-aligned. (deprecated)""" + + NUM = (103, "num", "Left-aligned. (deprecated)") + """Left-aligned. (deprecated)""" + + START = (104, "start", "Left-aligned. (deprecated)") + """Left-aligned. (deprecated)""" + + +class WD_TAB_LEADER(BaseXmlEnum): + """Specifies the character to use as the leader with formatted tabs. + + MS API name: `WdTabLeader` + + URL: https://msdn.microsoft.com/en-us/library/office/ff845050.aspx + """ + + SPACES = (0, "none", "Spaces. Default.") + """Spaces. Default.""" + + DOTS = (1, "dot", "Dots.") + """Dots.""" + + DASHES = (2, "hyphen", "Dashes.") + """Dashes.""" + + LINES = (3, "underscore", "Double lines.") + """Double lines.""" + + HEAVY = (4, "heavy", "A heavy line.") + """A heavy line.""" + + MIDDLE_DOT = (5, "middleDot", "A vertically-centered dot.") + """A vertically-centered dot.""" + + +class WD_UNDERLINE(BaseXmlEnum): + """Specifies the style of underline applied to a run of characters. + + MS API name: `WdUnderline` + + URL: http://msdn.microsoft.com/en-us/library/office/ff822388.aspx + """ + + INHERITED = (-1, None, "Inherit underline setting from containing paragraph.") + """Inherit underline setting from containing paragraph.""" + + NONE = ( + 0, + "none", + "No underline.\n\nThis setting overrides any inherited underline value, so can" + " be used to remove underline from a run that inherits underlining from its" + " containing paragraph. Note this is not the same as assigning |None| to" + " Run.underline. |None| is a valid assignment value, but causes the run to" + " inherit its underline value. Assigning `WD_UNDERLINE.NONE` causes" + " underlining to be unconditionally turned off.", + ) + """No underline. + + This setting overrides any inherited underline value, so can be used to remove + underline from a run that inherits underlining from its containing paragraph. Note + this is not the same as assigning |None| to Run.underline. |None| is a valid + assignment value, but causes the run to inherit its underline value. Assigning + ``WD_UNDERLINE.NONE`` causes underlining to be unconditionally turned off. + """ + + SINGLE = ( + 1, + "single", + "A single line.\n\nNote that this setting is write-only in the sense that" + " |True| (rather than `WD_UNDERLINE.SINGLE`) is returned for a run having" + " this setting.", + ) + """A single line. + + Note that this setting is write-only in the sense that |True| + (rather than ``WD_UNDERLINE.SINGLE``) is returned for a run having this setting. + """ + + WORDS = (2, "words", "Underline individual words only.") + """Underline individual words only.""" + + DOUBLE = (3, "double", "A double line.") + """A double line.""" + + DOTTED = (4, "dotted", "Dots.") + """Dots.""" + + THICK = (6, "thick", "A single thick line.") + """A single thick line.""" + + DASH = (7, "dash", "Dashes.") + """Dashes.""" + + DOT_DASH = (9, "dotDash", "Alternating dots and dashes.") + """Alternating dots and dashes.""" + + DOT_DOT_DASH = (10, "dotDotDash", "An alternating dot-dot-dash pattern.") + """An alternating dot-dot-dash pattern.""" + + WAVY = (11, "wave", "A single wavy line.") + """A single wavy line.""" + + DOTTED_HEAVY = (20, "dottedHeavy", "Heavy dots.") + """Heavy dots.""" + + DASH_HEAVY = (23, "dashedHeavy", "Heavy dashes.") + """Heavy dashes.""" + + DOT_DASH_HEAVY = (25, "dashDotHeavy", "Alternating heavy dots and heavy dashes.") + """Alternating heavy dots and heavy dashes.""" + + DOT_DOT_DASH_HEAVY = ( + 26, + "dashDotDotHeavy", + "An alternating heavy dot-dot-dash pattern.", + ) + """An alternating heavy dot-dot-dash pattern.""" + + WAVY_HEAVY = (27, "wavyHeavy", "A heavy wavy line.") + """A heavy wavy line.""" + + DASH_LONG = (39, "dashLong", "Long dashes.") + """Long dashes.""" + + WAVY_DOUBLE = (43, "wavyDouble", "A double wavy line.") + """A double wavy line.""" + + DASH_LONG_HEAVY = (55, "dashLongHeavy", "Long heavy dashes.") + """Long heavy dashes.""" diff --git a/src/docx/exceptions.py b/src/docx/exceptions.py new file mode 100644 index 000000000..e26f4c3bf --- /dev/null +++ b/src/docx/exceptions.py @@ -0,0 +1,18 @@ +"""Exceptions used with python-docx. + +The base exception class is PythonDocxError. +""" + + +class PythonDocxError(Exception): + """Generic error class.""" + + +class InvalidSpanError(PythonDocxError): + """Raised when an invalid merge region is specified in a request to merge table + cells.""" + + +class InvalidXmlError(PythonDocxError): + """Raised when invalid XML is encountered, such as on attempt to access a missing + required child element.""" diff --git a/src/docx/image/__init__.py b/src/docx/image/__init__.py new file mode 100644 index 000000000..d28033ef1 --- /dev/null +++ b/src/docx/image/__init__.py @@ -0,0 +1,23 @@ +"""Provides objects that can characterize image streams. + +That characterization is as to content type and size, as a required step in including +them in a document. +""" + +from docx.image.bmp import Bmp +from docx.image.gif import Gif +from docx.image.jpeg import Exif, Jfif +from docx.image.png import Png +from docx.image.tiff import Tiff + +SIGNATURES = ( + # class, offset, signature_bytes + (Png, 0, b"\x89PNG\x0D\x0A\x1A\x0A"), + (Jfif, 6, b"JFIF"), + (Exif, 6, b"Exif"), + (Gif, 0, b"GIF87a"), + (Gif, 0, b"GIF89a"), + (Tiff, 0, b"MM\x00*"), # big-endian (Motorola) TIFF + (Tiff, 0, b"II*\x00"), # little-endian (Intel) TIFF + (Bmp, 0, b"BM"), +) diff --git a/docx/image/bmp.py b/src/docx/image/bmp.py similarity index 58% rename from docx/image/bmp.py rename to src/docx/image/bmp.py index d22f25871..115b01d51 100644 --- a/docx/image/bmp.py +++ b/src/docx/image/bmp.py @@ -1,22 +1,15 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - from .constants import MIME_TYPE from .helpers import LITTLE_ENDIAN, StreamReader from .image import BaseImageHeader class Bmp(BaseImageHeader): - """ - Image header parser for BMP images - """ + """Image header parser for BMP images.""" + @classmethod def from_stream(cls, stream): - """ - Return |Bmp| instance having header properties parsed from the BMP - image in *stream*. - """ + """Return |Bmp| instance having header properties parsed from the BMP image in + `stream`.""" stream_rdr = StreamReader(stream, LITTLE_ENDIAN) px_width = stream_rdr.read_long(0x12) @@ -32,25 +25,19 @@ def from_stream(cls, stream): @property def content_type(self): - """ - MIME content type for this image, unconditionally `image/bmp` for - BMP images. - """ + """MIME content type for this image, unconditionally `image/bmp` for BMP + images.""" return MIME_TYPE.BMP @property def default_ext(self): - """ - Default filename extension, always 'bmp' for BMP images. - """ - return 'bmp' + """Default filename extension, always 'bmp' for BMP images.""" + return "bmp" @staticmethod def _dpi(px_per_meter): - """ - Return the integer pixels per inch from *px_per_meter*, defaulting to - 96 if *px_per_meter* is zero. - """ + """Return the integer pixels per inch from `px_per_meter`, defaulting to 96 if + `px_per_meter` is zero.""" if px_per_meter == 0: return 96 return int(round(px_per_meter * 0.0254)) diff --git a/src/docx/image/constants.py b/src/docx/image/constants.py new file mode 100644 index 000000000..729a828b2 --- /dev/null +++ b/src/docx/image/constants.py @@ -0,0 +1,172 @@ +"""Constants specific the the image sub-package.""" + + +class JPEG_MARKER_CODE: + """JPEG marker codes.""" + + TEM = b"\x01" + DHT = b"\xC4" + DAC = b"\xCC" + JPG = b"\xC8" + + SOF0 = b"\xC0" + SOF1 = b"\xC1" + SOF2 = b"\xC2" + SOF3 = b"\xC3" + SOF5 = b"\xC5" + SOF6 = b"\xC6" + SOF7 = b"\xC7" + SOF9 = b"\xC9" + SOFA = b"\xCA" + SOFB = b"\xCB" + SOFD = b"\xCD" + SOFE = b"\xCE" + SOFF = b"\xCF" + + RST0 = b"\xD0" + RST1 = b"\xD1" + RST2 = b"\xD2" + RST3 = b"\xD3" + RST4 = b"\xD4" + RST5 = b"\xD5" + RST6 = b"\xD6" + RST7 = b"\xD7" + + SOI = b"\xD8" + EOI = b"\xD9" + SOS = b"\xDA" + DQT = b"\xDB" # Define Quantization Table(s) + DNL = b"\xDC" + DRI = b"\xDD" + DHP = b"\xDE" + EXP = b"\xDF" + + APP0 = b"\xE0" + APP1 = b"\xE1" + APP2 = b"\xE2" + APP3 = b"\xE3" + APP4 = b"\xE4" + APP5 = b"\xE5" + APP6 = b"\xE6" + APP7 = b"\xE7" + APP8 = b"\xE8" + APP9 = b"\xE9" + APPA = b"\xEA" + APPB = b"\xEB" + APPC = b"\xEC" + APPD = b"\xED" + APPE = b"\xEE" + APPF = b"\xEF" + + STANDALONE_MARKERS = (TEM, SOI, EOI, RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7) + + SOF_MARKER_CODES = ( + SOF0, + SOF1, + SOF2, + SOF3, + SOF5, + SOF6, + SOF7, + SOF9, + SOFA, + SOFB, + SOFD, + SOFE, + SOFF, + ) + + marker_names = { + b"\x00": "UNKNOWN", + b"\xC0": "SOF0", + b"\xC2": "SOF2", + b"\xC4": "DHT", + b"\xDA": "SOS", # start of scan + b"\xD8": "SOI", # start of image + b"\xD9": "EOI", # end of image + b"\xDB": "DQT", + b"\xE0": "APP0", + b"\xE1": "APP1", + b"\xE2": "APP2", + b"\xED": "APP13", + b"\xEE": "APP14", + } + + @classmethod + def is_standalone(cls, marker_code): + return marker_code in cls.STANDALONE_MARKERS + + +class MIME_TYPE: + """Image content types.""" + + BMP = "image/bmp" + GIF = "image/gif" + JPEG = "image/jpeg" + PNG = "image/png" + TIFF = "image/tiff" + + +class PNG_CHUNK_TYPE: + """PNG chunk type names.""" + + IHDR = "IHDR" + pHYs = "pHYs" + IEND = "IEND" + + +class TIFF_FLD_TYPE: + """Tag codes for TIFF Image File Directory (IFD) entries.""" + + BYTE = 1 + ASCII = 2 + SHORT = 3 + LONG = 4 + RATIONAL = 5 + + field_type_names = { + 1: "BYTE", + 2: "ASCII char", + 3: "SHORT", + 4: "LONG", + 5: "RATIONAL", + } + + +TIFF_FLD = TIFF_FLD_TYPE + + +class TIFF_TAG: + """Tag codes for TIFF Image File Directory (IFD) entries.""" + + IMAGE_WIDTH = 0x0100 + IMAGE_LENGTH = 0x0101 + X_RESOLUTION = 0x011A + Y_RESOLUTION = 0x011B + RESOLUTION_UNIT = 0x0128 + + tag_names = { + 0x00FE: "NewSubfileType", + 0x0100: "ImageWidth", + 0x0101: "ImageLength", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x010E: "ImageDescription", + 0x010F: "Make", + 0x0110: "Model", + 0x0111: "StripOffsets", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x0117: "StripByteCounts", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x011C: "PlanarConfiguration", + 0x0128: "ResolutionUnit", + 0x0131: "Software", + 0x0132: "DateTime", + 0x0213: "YCbCrPositioning", + 0x8769: "ExifTag", + 0x8825: "GPS IFD", + 0xC4A5: "PrintImageMatching", + } diff --git a/src/docx/image/exceptions.py b/src/docx/image/exceptions.py new file mode 100644 index 000000000..2b35187d1 --- /dev/null +++ b/src/docx/image/exceptions.py @@ -0,0 +1,13 @@ +"""Exceptions specific the the image sub-package.""" + + +class InvalidImageStreamError(Exception): + """The recognized image stream appears to be corrupted.""" + + +class UnexpectedEndOfFileError(Exception): + """EOF was unexpectedly encountered while reading an image stream.""" + + +class UnrecognizedImageError(Exception): + """The provided image stream could not be recognized.""" diff --git a/src/docx/image/gif.py b/src/docx/image/gif.py new file mode 100644 index 000000000..e16487264 --- /dev/null +++ b/src/docx/image/gif.py @@ -0,0 +1,38 @@ +from struct import Struct + +from .constants import MIME_TYPE +from .image import BaseImageHeader + + +class Gif(BaseImageHeader): + """Image header parser for GIF images. + + Note that the GIF format does not support resolution (DPI) information. Both + horizontal and vertical DPI default to 72. + """ + + @classmethod + def from_stream(cls, stream): + """Return |Gif| instance having header properties parsed from GIF image in + `stream`.""" + px_width, px_height = cls._dimensions_from_stream(stream) + return cls(px_width, px_height, 72, 72) + + @property + def content_type(self): + """MIME content type for this image, unconditionally `image/gif` for GIF + images.""" + return MIME_TYPE.GIF + + @property + def default_ext(self): + """Default filename extension, always 'gif' for GIF images.""" + return "gif" + + @classmethod + def _dimensions_from_stream(cls, stream): + stream.seek(6) + bytes_ = stream.read(4) + struct = Struct("L" + return self._read_int(fmt, base, offset) + + def read_short(self, base, offset=0): + """Return the int value of the two bytes at the file position determined by + `base` and `offset`, similarly to ``read_long()`` above.""" + fmt = b"H" + return self._read_int(fmt, base, offset) + + def read_str(self, char_count, base, offset=0): + """Return a string containing the `char_count` bytes at the file position + determined by self._base_offset + `base` + `offset`.""" + + def str_struct(char_count): + format_ = "%ds" % char_count + return Struct(format_) + + struct = str_struct(char_count) + chars = self._unpack_item(struct, base, offset) + unicode_str = chars.decode("UTF-8") + return unicode_str + + def seek(self, base, offset=0): + location = self._base_offset + base + offset + self._stream.seek(location) + + def tell(self): + """Allow pass-through tell() call.""" + return self._stream.tell() + + def _read_bytes(self, byte_count, base, offset): + self.seek(base, offset) + bytes_ = self._stream.read(byte_count) + if len(bytes_) < byte_count: + raise UnexpectedEndOfFileError + return bytes_ + + def _read_int(self, fmt, base, offset): + struct = Struct(fmt) + return self._unpack_item(struct, base, offset) + + def _unpack_item(self, struct, base, offset): + bytes_ = self._read_bytes(struct.size, base, offset) + return struct.unpack(bytes_)[0] diff --git a/src/docx/image/image.py b/src/docx/image/image.py new file mode 100644 index 000000000..945432872 --- /dev/null +++ b/src/docx/image/image.py @@ -0,0 +1,242 @@ +"""Provides objects that can characterize image streams. + +That characterization is as to content type and size, as a required step in including +them in a document. +""" + +from __future__ import annotations + +import hashlib +import io +import os +from typing import IO, Tuple + +from typing_extensions import Self + +from docx.image.exceptions import UnrecognizedImageError +from docx.shared import Emu, Inches, Length, lazyproperty + + +class Image: + """Graphical image stream such as JPEG, PNG, or GIF with properties and methods + required by ImagePart.""" + + def __init__(self, blob: bytes, filename: str, image_header: BaseImageHeader): + super(Image, self).__init__() + self._blob = blob + self._filename = filename + self._image_header = image_header + + @classmethod + def from_blob(cls, blob: bytes) -> Self: + """Return a new |Image| subclass instance parsed from the image binary contained + in `blob`.""" + stream = io.BytesIO(blob) + return cls._from_stream(stream, blob) + + @classmethod + def from_file(cls, image_descriptor): + """Return a new |Image| subclass instance loaded from the image file identified + by `image_descriptor`, a path or file-like object.""" + if isinstance(image_descriptor, str): + path = image_descriptor + with open(path, "rb") as f: + blob = f.read() + stream = io.BytesIO(blob) + filename = os.path.basename(path) + else: + stream = image_descriptor + stream.seek(0) + blob = stream.read() + filename = None + return cls._from_stream(stream, blob, filename) + + @property + def blob(self): + """The bytes of the image 'file'.""" + return self._blob + + @property + def content_type(self): + """MIME content type for this image, e.g. ``'image/jpeg'`` for a JPEG image.""" + return self._image_header.content_type + + @lazyproperty + def ext(self): + """The file extension for the image. + + If an actual one is available from a load filename it is used. Otherwise a + canonical extension is assigned based on the content type. Does not contain the + leading period, e.g. 'jpg', not '.jpg'. + """ + return os.path.splitext(self._filename)[1][1:] + + @property + def filename(self): + """Original image file name, if loaded from disk, or a generic filename if + loaded from an anonymous stream.""" + return self._filename + + @property + def px_width(self) -> int: + """The horizontal pixel dimension of the image.""" + return self._image_header.px_width + + @property + def px_height(self) -> int: + """The vertical pixel dimension of the image.""" + return self._image_header.px_height + + @property + def horz_dpi(self) -> int: + """Integer dots per inch for the width of this image. + + Defaults to 72 when not present in the file, as is often the case. + """ + return self._image_header.horz_dpi + + @property + def vert_dpi(self) -> int: + """Integer dots per inch for the height of this image. + + Defaults to 72 when not present in the file, as is often the case. + """ + return self._image_header.vert_dpi + + @property + def width(self) -> Inches: + """A |Length| value representing the native width of the image, calculated from + the values of `px_width` and `horz_dpi`.""" + return Inches(self.px_width / self.horz_dpi) + + @property + def height(self) -> Inches: + """A |Length| value representing the native height of the image, calculated from + the values of `px_height` and `vert_dpi`.""" + return Inches(self.px_height / self.vert_dpi) + + def scaled_dimensions( + self, width: int | None = None, height: int | None = None + ) -> Tuple[Length, Length]: + """(cx, cy) pair representing scaled dimensions of this image. + + The native dimensions of the image are scaled by applying the following rules to + the `width` and `height` arguments. + + * If both `width` and `height` are specified, the return value is (`width`, + `height`); no scaling is performed. + * If only one is specified, it is used to compute a scaling factor that is then + applied to the unspecified dimension, preserving the aspect ratio of the image. + * If both `width` and `height` are |None|, the native dimensions are returned. + + The native dimensions are calculated using the dots-per-inch (dpi) value + embedded in the image, defaulting to 72 dpi if no value is specified, as is + often the case. The returned values are both |Length| objects. + """ + if width is None and height is None: + return self.width, self.height + + if width is None: + assert height is not None + scaling_factor = float(height) / float(self.height) + width = round(self.width * scaling_factor) + + if height is None: + scaling_factor = float(width) / float(self.width) + height = round(self.height * scaling_factor) + + return Emu(width), Emu(height) + + @lazyproperty + def sha1(self): + """SHA1 hash digest of the image blob.""" + return hashlib.sha1(self._blob).hexdigest() + + @classmethod + def _from_stream( + cls, + stream: IO[bytes], + blob: bytes, + filename: str | None = None, + ) -> Image: + """Return an instance of the |Image| subclass corresponding to the format of the + image in `stream`.""" + image_header = _ImageHeaderFactory(stream) + if filename is None: + filename = "image.%s" % image_header.default_ext + return cls(blob, filename, image_header) + + +def _ImageHeaderFactory(stream): + """Return a |BaseImageHeader| subclass instance that knows how to parse the headers + of the image in `stream`.""" + from docx.image import SIGNATURES + + def read_32(stream): + stream.seek(0) + return stream.read(32) + + header = read_32(stream) + for cls, offset, signature_bytes in SIGNATURES: + end = offset + len(signature_bytes) + found_bytes = header[offset:end] + if found_bytes == signature_bytes: + return cls.from_stream(stream) + raise UnrecognizedImageError + + +class BaseImageHeader: + """Base class for image header subclasses like |Jpeg| and |Tiff|.""" + + def __init__(self, px_width, px_height, horz_dpi, vert_dpi): + self._px_width = px_width + self._px_height = px_height + self._horz_dpi = horz_dpi + self._vert_dpi = vert_dpi + + @property + def content_type(self): + """Abstract property definition, must be implemented by all subclasses.""" + msg = ( + "content_type property must be implemented by all subclasses of " + "BaseImageHeader" + ) + raise NotImplementedError(msg) + + @property + def default_ext(self): + """Default filename extension for images of this type. + + An abstract property definition, must be implemented by all subclasses. + """ + msg = ( + "default_ext property must be implemented by all subclasses of " + "BaseImageHeader" + ) + raise NotImplementedError(msg) + + @property + def px_width(self): + """The horizontal pixel dimension of the image.""" + return self._px_width + + @property + def px_height(self): + """The vertical pixel dimension of the image.""" + return self._px_height + + @property + def horz_dpi(self): + """Integer dots per inch for the width of this image. + + Defaults to 72 when not present in the file, as is often the case. + """ + return self._horz_dpi + + @property + def vert_dpi(self): + """Integer dots per inch for the height of this image. + + Defaults to 72 when not present in the file, as is often the case. + """ + return self._vert_dpi diff --git a/docx/image/jpeg.py b/src/docx/image/jpeg.py similarity index 57% rename from docx/image/jpeg.py rename to src/docx/image/jpeg.py index 8a263b6c5..b0114a998 100644 --- a/docx/image/jpeg.py +++ b/src/docx/image/jpeg.py @@ -1,49 +1,38 @@ -# encoding: utf-8 +"""Objects related to parsing headers of JPEG image streams. -""" -Objects related to parsing headers of JPEG image streams, both JFIF and Exif -sub-formats. +Includes both JFIF and Exif sub-formats. """ -from __future__ import absolute_import, division, print_function +import io -from ..compat import BytesIO -from .constants import JPEG_MARKER_CODE, MIME_TYPE -from .helpers import BIG_ENDIAN, StreamReader -from .image import BaseImageHeader -from .tiff import Tiff +from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE +from docx.image.helpers import BIG_ENDIAN, StreamReader +from docx.image.image import BaseImageHeader +from docx.image.tiff import Tiff class Jpeg(BaseImageHeader): - """ - Base class for JFIF and EXIF subclasses. - """ + """Base class for JFIF and EXIF subclasses.""" + @property def content_type(self): - """ - MIME content type for this image, unconditionally `image/jpeg` for - JPEG images. - """ + """MIME content type for this image, unconditionally `image/jpeg` for JPEG + images.""" return MIME_TYPE.JPEG @property def default_ext(self): - """ - Default filename extension, always 'jpg' for JPG images. - """ - return 'jpg' + """Default filename extension, always 'jpg' for JPG images.""" + return "jpg" class Exif(Jpeg): - """ - Image header parser for Exif image format - """ + """Image header parser for Exif image format.""" + @classmethod def from_stream(cls, stream): - """ - Return |Exif| instance having header properties parsed from Exif - image in *stream*. - """ + """Return |Exif| instance having header properties parsed from Exif image in + `stream`.""" markers = _JfifMarkers.from_stream(stream) # print('\n%s' % markers) @@ -56,15 +45,12 @@ def from_stream(cls, stream): class Jfif(Jpeg): - """ - Image header parser for JFIF image format - """ + """Image header parser for JFIF image format.""" + @classmethod def from_stream(cls, stream): - """ - Return a |Jfif| instance having header properties parsed from image - in *stream*. - """ + """Return a |Jfif| instance having header properties parsed from image in + `stream`.""" markers = _JfifMarkers.from_stream(stream) px_width = markers.sof.px_width @@ -75,37 +61,37 @@ def from_stream(cls, stream): return cls(px_width, px_height, horz_dpi, vert_dpi) -class _JfifMarkers(object): - """ - Sequence of markers in a JPEG file, perhaps truncated at first SOS marker - for performance reasons. - """ +class _JfifMarkers: + """Sequence of markers in a JPEG file, perhaps truncated at first SOS marker for + performance reasons.""" + def __init__(self, markers): super(_JfifMarkers, self).__init__() self._markers = list(markers) def __str__(self): # pragma: no cover - """ - Returns a tabular listing of the markers in this instance, which can - be handy for debugging and perhaps other uses. - """ - header = ' offset seglen mc name\n======= ====== == =====' - tmpl = '%7d %6d %02X %s' + """Returns a tabular listing of the markers in this instance, which can be handy + for debugging and perhaps other uses.""" + header = " offset seglen mc name\n======= ====== == =====" + tmpl = "%7d %6d %02X %s" rows = [] for marker in self._markers: - rows.append(tmpl % ( - marker.offset, marker.segment_length, - ord(marker.marker_code), marker.name - )) + rows.append( + tmpl + % ( + marker.offset, + marker.segment_length, + ord(marker.marker_code), + marker.name, + ) + ) lines = [header] + rows - return '\n'.join(lines) + return "\n".join(lines) @classmethod def from_stream(cls, stream): - """ - Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass - instance for each marker in *stream*. - """ + """Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass instance + for each marker in `stream`.""" marker_parser = _MarkerParser.from_stream(stream) markers = [] for marker in marker_parser.iter_markers(): @@ -116,150 +102,132 @@ def from_stream(cls, stream): @property def app0(self): - """ - First APP0 marker in image markers. - """ + """First APP0 marker in image markers.""" for m in self._markers: if m.marker_code == JPEG_MARKER_CODE.APP0: return m - raise KeyError('no APP0 marker in image') + raise KeyError("no APP0 marker in image") @property def app1(self): - """ - First APP1 marker in image markers. - """ + """First APP1 marker in image markers.""" for m in self._markers: if m.marker_code == JPEG_MARKER_CODE.APP1: return m - raise KeyError('no APP1 marker in image') + raise KeyError("no APP1 marker in image") @property def sof(self): - """ - First start of frame (SOFn) marker in this sequence. - """ + """First start of frame (SOFn) marker in this sequence.""" for m in self._markers: if m.marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES: return m - raise KeyError('no start of frame (SOFn) marker in image') + raise KeyError("no start of frame (SOFn) marker in image") -class _MarkerParser(object): - """ - Service class that knows how to parse a JFIF stream and iterate over its - markers. - """ +class _MarkerParser: + """Service class that knows how to parse a JFIF stream and iterate over its + markers.""" + def __init__(self, stream_reader): super(_MarkerParser, self).__init__() self._stream = stream_reader @classmethod def from_stream(cls, stream): - """ - Return a |_MarkerParser| instance to parse JFIF markers from - *stream*. - """ + """Return a |_MarkerParser| instance to parse JFIF markers from `stream`.""" stream_reader = StreamReader(stream, BIG_ENDIAN) return cls(stream_reader) def iter_markers(self): - """ - Generate a (marker_code, segment_offset) 2-tuple for each marker in - the JPEG *stream*, in the order they occur in the stream. - """ + """Generate a (marker_code, segment_offset) 2-tuple for each marker in the JPEG + `stream`, in the order they occur in the stream.""" marker_finder = _MarkerFinder.from_stream(self._stream) start = 0 marker_code = None while marker_code != JPEG_MARKER_CODE.EOI: marker_code, segment_offset = marker_finder.next(start) - marker = _MarkerFactory( - marker_code, self._stream, segment_offset - ) + marker = _MarkerFactory(marker_code, self._stream, segment_offset) yield marker start = segment_offset + marker.segment_length -class _MarkerFinder(object): - """ - Service class that knows how to find the next JFIF marker in a stream. - """ +class _MarkerFinder: + """Service class that knows how to find the next JFIF marker in a stream.""" + def __init__(self, stream): super(_MarkerFinder, self).__init__() self._stream = stream @classmethod def from_stream(cls, stream): - """ - Return a |_MarkerFinder| instance to find JFIF markers in *stream*. - """ + """Return a |_MarkerFinder| instance to find JFIF markers in `stream`.""" return cls(stream) def next(self, start): - """ - Return a (marker_code, segment_offset) 2-tuple identifying and - locating the first marker in *stream* occuring after offset *start*. - The returned *segment_offset* points to the position immediately - following the 2-byte marker code, the start of the marker segment, - for those markers that have a segment. + """Return a (marker_code, segment_offset) 2-tuple identifying and locating the + first marker in `stream` occuring after offset `start`. + + The returned `segment_offset` points to the position immediately following the + 2-byte marker code, the start of the marker segment, for those markers that have + a segment. """ position = start while True: # skip over any non-\xFF bytes position = self._offset_of_next_ff_byte(start=position) # skip over any \xFF padding bytes - position, byte_ = self._next_non_ff_byte(start=position+1) + position, byte_ = self._next_non_ff_byte(start=position + 1) # 'FF 00' sequence is not a marker, start over if found - if byte_ == b'\x00': + if byte_ == b"\x00": continue # this is a marker, gather return values and break out of scan - marker_code, segment_offset = byte_, position+1 + marker_code, segment_offset = byte_, position + 1 break return marker_code, segment_offset def _next_non_ff_byte(self, start): - """ - Return an offset, byte 2-tuple for the next byte in *stream* that is - not '\xFF', starting with the byte at offset *start*. If the byte at - offset *start* is not '\xFF', *start* and the returned *offset* will - be the same. + """Return an offset, byte 2-tuple for the next byte in `stream` that is not + '\xFF', starting with the byte at offset `start`. + + If the byte at offset `start` is not '\xFF', `start` and the returned `offset` + will be the same. """ self._stream.seek(start) byte_ = self._read_byte() - while byte_ == b'\xFF': + while byte_ == b"\xFF": byte_ = self._read_byte() offset_of_non_ff_byte = self._stream.tell() - 1 return offset_of_non_ff_byte, byte_ def _offset_of_next_ff_byte(self, start): - """ - Return the offset of the next '\xFF' byte in *stream* starting with - the byte at offset *start*. Returns *start* if the byte at that - offset is a hex 255; it does not necessarily advance in the stream. + """Return the offset of the next '\xFF' byte in `stream` starting with the byte + at offset `start`. + + Returns `start` if the byte at that offset is a hex 255; it does not necessarily + advance in the stream. """ self._stream.seek(start) byte_ = self._read_byte() - while byte_ != b'\xFF': + while byte_ != b"\xFF": byte_ = self._read_byte() offset_of_ff_byte = self._stream.tell() - 1 return offset_of_ff_byte def _read_byte(self): - """ - Return the next byte read from stream. Raise Exception if stream is - at end of file. + """Return the next byte read from stream. + + Raise Exception if stream is at end of file. """ byte_ = self._stream.read(1) if not byte_: # pragma: no cover - raise Exception('unexpected end of file') + raise Exception("unexpected end of file") return byte_ def _MarkerFactory(marker_code, stream, offset): - """ - Return |_Marker| or subclass instance appropriate for marker at *offset* - in *stream* having *marker_code*. - """ + """Return |_Marker| or subclass instance appropriate for marker at `offset` in + `stream` having `marker_code`.""" if marker_code == JPEG_MARKER_CODE.APP0: marker_cls = _App0Marker elif marker_code == JPEG_MARKER_CODE.APP1: @@ -271,11 +239,12 @@ def _MarkerFactory(marker_code, stream, offset): return marker_cls.from_stream(stream, marker_code, offset) -class _Marker(object): - """ - Base class for JFIF marker classes. Represents a marker and its segment - occuring in a JPEG byte stream. +class _Marker: + """Base class for JFIF marker classes. + + Represents a marker and its segment occuring in a JPEG byte stream. """ + def __init__(self, marker_code, offset, segment_length): super(_Marker, self).__init__() self._marker_code = marker_code @@ -284,10 +253,8 @@ def __init__(self, marker_code, offset, segment_length): @classmethod def from_stream(cls, stream, marker_code, offset): - """ - Return a generic |_Marker| instance for the marker at *offset* in - *stream* having *marker_code*. - """ + """Return a generic |_Marker| instance for the marker at `offset` in `stream` + having `marker_code`.""" if JPEG_MARKER_CODE.is_standalone(marker_code): segment_length = 0 else: @@ -296,10 +263,8 @@ def from_stream(cls, stream, marker_code, offset): @property def marker_code(self): - """ - The single-byte code that identifies the type of this marker, e.g. - ``'\xE0'`` for start of image (SOI). - """ + """The single-byte code that identifies the type of this marker, e.g. ``'\xE0'`` + for start of image (SOI).""" return self._marker_code @property @@ -312,19 +277,16 @@ def offset(self): # pragma: no cover @property def segment_length(self): - """ - The length in bytes of this marker's segment - """ + """The length in bytes of this marker's segment.""" return self._segment_length class _App0Marker(_Marker): - """ - Represents a JFIF APP0 marker segment. - """ + """Represents a JFIF APP0 marker segment.""" + def __init__( - self, marker_code, offset, length, density_units, x_density, - y_density): + self, marker_code, offset, length, density_units, x_density, y_density + ): super(_App0Marker, self).__init__(marker_code, offset, length) self._density_units = density_units self._x_density = x_density @@ -332,24 +294,18 @@ def __init__( @property def horz_dpi(self): - """ - Horizontal dots per inch specified in this marker, defaults to 72 if - not specified. - """ + """Horizontal dots per inch specified in this marker, defaults to 72 if not + specified.""" return self._dpi(self._x_density) @property def vert_dpi(self): - """ - Vertical dots per inch specified in this marker, defaults to 72 if - not specified. - """ + """Vertical dots per inch specified in this marker, defaults to 72 if not + specified.""" return self._dpi(self._y_density) def _dpi(self, density): - """ - Return dots per inch corresponding to *density* value. - """ + """Return dots per inch corresponding to `density` value.""" if self._density_units == 1: dpi = density elif self._density_units == 2: @@ -360,10 +316,8 @@ def _dpi(self, density): @classmethod def from_stream(cls, stream, marker_code, offset): - """ - Return an |_App0Marker| instance for the APP0 marker at *offset* in - *stream*. - """ + """Return an |_App0Marker| instance for the APP0 marker at `offset` in + `stream`.""" # field off type notes # ------------------ --- ----- ------------------- # segment length 0 short @@ -379,15 +333,13 @@ def from_stream(cls, stream, marker_code, offset): x_density = stream.read_short(offset, 10) y_density = stream.read_short(offset, 12) return cls( - marker_code, offset, segment_length, density_units, x_density, - y_density + marker_code, offset, segment_length, density_units, x_density, y_density ) class _App1Marker(_Marker): - """ - Represents a JFIF APP1 (Exif) marker segment. - """ + """Represents a JFIF APP1 (Exif) marker segment.""" + def __init__(self, marker_code, offset, length, horz_dpi, vert_dpi): super(_App1Marker, self).__init__(marker_code, offset, length) self._horz_dpi = horz_dpi @@ -395,10 +347,8 @@ def __init__(self, marker_code, offset, length, horz_dpi, vert_dpi): @classmethod def from_stream(cls, stream, marker_code, offset): - """ - Extract the horizontal and vertical dots-per-inch value from the APP1 - header at *offset* in *stream*. - """ + """Extract the horizontal and vertical dots-per-inch value from the APP1 header + at `offset` in `stream`.""" # field off len type notes # -------------------- --- --- ----- ---------------------------- # segment length 0 2 short @@ -411,66 +361,51 @@ def from_stream(cls, stream, marker_code, offset): if cls._is_non_Exif_APP1_segment(stream, offset): return cls(marker_code, offset, segment_length, 72, 72) tiff = cls._tiff_from_exif_segment(stream, offset, segment_length) - return cls( - marker_code, offset, segment_length, tiff.horz_dpi, tiff.vert_dpi - ) + return cls(marker_code, offset, segment_length, tiff.horz_dpi, tiff.vert_dpi) @property def horz_dpi(self): - """ - Horizontal dots per inch specified in this marker, defaults to 72 if - not specified. - """ + """Horizontal dots per inch specified in this marker, defaults to 72 if not + specified.""" return self._horz_dpi @property def vert_dpi(self): - """ - Vertical dots per inch specified in this marker, defaults to 72 if - not specified. - """ + """Vertical dots per inch specified in this marker, defaults to 72 if not + specified.""" return self._vert_dpi @classmethod def _is_non_Exif_APP1_segment(cls, stream, offset): - """ - Return True if the APP1 segment at *offset* in *stream* is NOT an - Exif segment, as determined by the ``'Exif\x00\x00'`` signature at - offset 2 in the segment. - """ - stream.seek(offset+2) + """Return True if the APP1 segment at `offset` in `stream` is NOT an Exif + segment, as determined by the ``'Exif\x00\x00'`` signature at offset 2 in the + segment.""" + stream.seek(offset + 2) exif_signature = stream.read(6) - return exif_signature != b'Exif\x00\x00' + return exif_signature != b"Exif\x00\x00" @classmethod def _tiff_from_exif_segment(cls, stream, offset, segment_length): - """ - Return a |Tiff| instance parsed from the Exif APP1 segment of - *segment_length* at *offset* in *stream*. - """ + """Return a |Tiff| instance parsed from the Exif APP1 segment of + `segment_length` at `offset` in `stream`.""" # wrap full segment in its own stream and feed to Tiff() - stream.seek(offset+8) - segment_bytes = stream.read(segment_length-8) - substream = BytesIO(segment_bytes) + stream.seek(offset + 8) + segment_bytes = stream.read(segment_length - 8) + substream = io.BytesIO(segment_bytes) return Tiff.from_stream(substream) class _SofMarker(_Marker): - """ - Represents a JFIF start of frame (SOFx) marker segment. - """ - def __init__( - self, marker_code, offset, segment_length, px_width, px_height): + """Represents a JFIF start of frame (SOFx) marker segment.""" + + def __init__(self, marker_code, offset, segment_length, px_width, px_height): super(_SofMarker, self).__init__(marker_code, offset, segment_length) self._px_width = px_width self._px_height = px_height @classmethod def from_stream(cls, stream, marker_code, offset): - """ - Return an |_SofMarker| instance for the SOFn marker at *offset* in - stream. - """ + """Return an |_SofMarker| instance for the SOFn marker at `offset` in stream.""" # field off type notes # ------------------ --- ----- ---------------------------- # segment length 0 short @@ -485,14 +420,10 @@ def from_stream(cls, stream, marker_code, offset): @property def px_height(self): - """ - Image height in pixels - """ + """Image height in pixels.""" return self._px_height @property def px_width(self): - """ - Image width in pixels - """ + """Image width in pixels.""" return self._px_width diff --git a/docx/image/png.py b/src/docx/image/png.py similarity index 59% rename from docx/image/png.py rename to src/docx/image/png.py index 4e899fa5c..dd3cf819e 100644 --- a/docx/image/png.py +++ b/src/docx/image/png.py @@ -1,7 +1,3 @@ -# encoding: utf-8 - -from __future__ import absolute_import, division, print_function - from .constants import MIME_TYPE, PNG_CHUNK_TYPE from .exceptions import InvalidImageStreamError from .helpers import BIG_ENDIAN, StreamReader @@ -9,30 +5,23 @@ class Png(BaseImageHeader): - """ - Image header parser for PNG images - """ + """Image header parser for PNG images.""" + @property def content_type(self): - """ - MIME content type for this image, unconditionally `image/png` for - PNG images. - """ + """MIME content type for this image, unconditionally `image/png` for PNG + images.""" return MIME_TYPE.PNG @property def default_ext(self): - """ - Default filename extension, always 'png' for PNG images. - """ - return 'png' + """Default filename extension, always 'png' for PNG images.""" + return "png" @classmethod def from_stream(cls, stream): - """ - Return a |Png| instance having header properties parsed from image in - *stream*. - """ + """Return a |Png| instance having header properties parsed from image in + `stream`.""" parser = _PngParser.parse(stream) px_width = parser.px_width @@ -43,45 +32,37 @@ def from_stream(cls, stream): return cls(px_width, px_height, horz_dpi, vert_dpi) -class _PngParser(object): - """ - Parses a PNG image stream to extract the image properties found in its - chunks. - """ +class _PngParser: + """Parses a PNG image stream to extract the image properties found in its chunks.""" + def __init__(self, chunks): super(_PngParser, self).__init__() self._chunks = chunks @classmethod def parse(cls, stream): - """ - Return a |_PngParser| instance containing the header properties - parsed from the PNG image in *stream*. - """ + """Return a |_PngParser| instance containing the header properties parsed from + the PNG image in `stream`.""" chunks = _Chunks.from_stream(stream) return cls(chunks) @property def px_width(self): - """ - The number of pixels in each row of the image. - """ + """The number of pixels in each row of the image.""" IHDR = self._chunks.IHDR return IHDR.px_width @property def px_height(self): - """ - The number of stacked rows of pixels in the image. - """ + """The number of stacked rows of pixels in the image.""" IHDR = self._chunks.IHDR return IHDR.px_height @property def horz_dpi(self): - """ - Integer dots per inch for the width of this image. Defaults to 72 - when not present in the file, as is often the case. + """Integer dots per inch for the width of this image. + + Defaults to 72 when not present in the file, as is often the case. """ pHYs = self._chunks.pHYs if pHYs is None: @@ -90,9 +71,9 @@ def horz_dpi(self): @property def vert_dpi(self): - """ - Integer dots per inch for the height of this image. Defaults to 72 - when not present in the file, as is often the case. + """Integer dots per inch for the height of this image. + + Defaults to 72 when not present in the file, as is often the case. """ pHYs = self._chunks.pHYs if pHYs is None: @@ -101,93 +82,76 @@ def vert_dpi(self): @staticmethod def _dpi(units_specifier, px_per_unit): - """ - Return dots per inch value calculated from *units_specifier* and - *px_per_unit*. - """ + """Return dots per inch value calculated from `units_specifier` and + `px_per_unit`.""" if units_specifier == 1 and px_per_unit: return int(round(px_per_unit * 0.0254)) return 72 -class _Chunks(object): - """ - Collection of the chunks parsed from a PNG image stream - """ +class _Chunks: + """Collection of the chunks parsed from a PNG image stream.""" + def __init__(self, chunk_iterable): super(_Chunks, self).__init__() self._chunks = list(chunk_iterable) @classmethod def from_stream(cls, stream): - """ - Return a |_Chunks| instance containing the PNG chunks in *stream*. - """ + """Return a |_Chunks| instance containing the PNG chunks in `stream`.""" chunk_parser = _ChunkParser.from_stream(stream) - chunks = [chunk for chunk in chunk_parser.iter_chunks()] + chunks = list(chunk_parser.iter_chunks()) return cls(chunks) @property def IHDR(self): - """ - IHDR chunk in PNG image - """ + """IHDR chunk in PNG image.""" match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.IHDR # noqa IHDR = self._find_first(match) if IHDR is None: - raise InvalidImageStreamError('no IHDR chunk in PNG image') + raise InvalidImageStreamError("no IHDR chunk in PNG image") return IHDR @property def pHYs(self): - """ - pHYs chunk in PNG image, or |None| if not present - """ + """PHYs chunk in PNG image, or |None| if not present.""" match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.pHYs # noqa return self._find_first(match) def _find_first(self, match): - """ - Return first chunk in stream order returning True for function - *match*. - """ + """Return first chunk in stream order returning True for function `match`.""" for chunk in self._chunks: if match(chunk): return chunk return None -class _ChunkParser(object): - """ - Extracts chunks from a PNG image stream - """ +class _ChunkParser: + """Extracts chunks from a PNG image stream.""" + def __init__(self, stream_rdr): super(_ChunkParser, self).__init__() self._stream_rdr = stream_rdr @classmethod def from_stream(cls, stream): - """ - Return a |_ChunkParser| instance that can extract the chunks from the - PNG image in *stream*. - """ + """Return a |_ChunkParser| instance that can extract the chunks from the PNG + image in `stream`.""" stream_rdr = StreamReader(stream, BIG_ENDIAN) return cls(stream_rdr) def iter_chunks(self): - """ - Generate a |_Chunk| subclass instance for each chunk in this parser's - PNG stream, in the order encountered in the stream. - """ + """Generate a |_Chunk| subclass instance for each chunk in this parser's PNG + stream, in the order encountered in the stream.""" for chunk_type, offset in self._iter_chunk_offsets(): chunk = _ChunkFactory(chunk_type, self._stream_rdr, offset) yield chunk def _iter_chunk_offsets(self): - """ - Generate a (chunk_type, chunk_offset) 2-tuple for each of the chunks - in the PNG image stream. Iteration stops after the IEND chunk is - returned. + """Generate a (chunk_type, chunk_offset) 2-tuple for each of the chunks in the + PNG image stream. + + Iteration stops after the IEND chunk is returned. """ chunk_offset = 8 while True: @@ -195,17 +159,15 @@ def _iter_chunk_offsets(self): chunk_type = self._stream_rdr.read_str(4, chunk_offset, 4) data_offset = chunk_offset + 8 yield chunk_type, data_offset - if chunk_type == 'IEND': + if chunk_type == "IEND": break # incr offset for chunk len long, chunk type, chunk data, and CRC - chunk_offset += (4 + 4 + chunk_data_len + 4) + chunk_offset += 4 + 4 + chunk_data_len + 4 def _ChunkFactory(chunk_type, stream_rdr, offset): - """ - Return a |_Chunk| subclass instance appropriate to *chunk_type* parsed - from *stream_rdr* at *offset*. - """ + """Return a |_Chunk| subclass instance appropriate to `chunk_type` parsed from + `stream_rdr` at `offset`.""" chunk_cls_map = { PNG_CHUNK_TYPE.IHDR: _IHDRChunk, PNG_CHUNK_TYPE.pHYs: _pHYsChunk, @@ -214,34 +176,30 @@ def _ChunkFactory(chunk_type, stream_rdr, offset): return chunk_cls.from_offset(chunk_type, stream_rdr, offset) -class _Chunk(object): - """ - Base class for specific chunk types. Also serves as the default chunk - type. +class _Chunk: + """Base class for specific chunk types. + + Also serves as the default chunk type. """ + def __init__(self, chunk_type): super(_Chunk, self).__init__() self._chunk_type = chunk_type @classmethod def from_offset(cls, chunk_type, stream_rdr, offset): - """ - Return a default _Chunk instance that only knows its chunk type. - """ + """Return a default _Chunk instance that only knows its chunk type.""" return cls(chunk_type) @property def type_name(self): - """ - The chunk type name, e.g. 'IHDR', 'pHYs', etc. - """ + """The chunk type name, e.g. 'IHDR', 'pHYs', etc.""" return self._chunk_type class _IHDRChunk(_Chunk): - """ - IHDR chunk, contains the image dimensions - """ + """IHDR chunk, contains the image dimensions.""" + def __init__(self, chunk_type, px_width, px_height): super(_IHDRChunk, self).__init__(chunk_type) self._px_width = px_width @@ -249,10 +207,8 @@ def __init__(self, chunk_type, px_width, px_height): @classmethod def from_offset(cls, chunk_type, stream_rdr, offset): - """ - Return an _IHDRChunk instance containing the image dimensions - extracted from the IHDR chunk in *stream* at *offset*. - """ + """Return an _IHDRChunk instance containing the image dimensions extracted from + the IHDR chunk in `stream` at `offset`.""" px_width = stream_rdr.read_long(offset) px_height = stream_rdr.read_long(offset, 4) return cls(chunk_type, px_width, px_height) @@ -267,11 +223,9 @@ def px_height(self): class _pHYsChunk(_Chunk): - """ - pYHs chunk, contains the image dpi information - """ - def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, - units_specifier): + """PYHs chunk, contains the image dpi information.""" + + def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier): super(_pHYsChunk, self).__init__(chunk_type) self._horz_px_per_unit = horz_px_per_unit self._vert_px_per_unit = vert_px_per_unit @@ -279,16 +233,12 @@ def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, @classmethod def from_offset(cls, chunk_type, stream_rdr, offset): - """ - Return a _pHYsChunk instance containing the image resolution - extracted from the pHYs chunk in *stream* at *offset*. - """ + """Return a _pHYsChunk instance containing the image resolution extracted from + the pHYs chunk in `stream` at `offset`.""" horz_px_per_unit = stream_rdr.read_long(offset) vert_px_per_unit = stream_rdr.read_long(offset, 4) units_specifier = stream_rdr.read_byte(offset, 8) - return cls( - chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier - ) + return cls(chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier) @property def horz_px_per_unit(self): diff --git a/src/docx/image/tiff.py b/src/docx/image/tiff.py new file mode 100644 index 000000000..b84d9f10f --- /dev/null +++ b/src/docx/image/tiff.py @@ -0,0 +1,293 @@ +from .constants import MIME_TYPE, TIFF_FLD, TIFF_TAG +from .helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader +from .image import BaseImageHeader + + +class Tiff(BaseImageHeader): + """Image header parser for TIFF images. + + Handles both big and little endian byte ordering. + """ + + @property + def content_type(self): + """Return the MIME type of this TIFF image, unconditionally the string + ``image/tiff``.""" + return MIME_TYPE.TIFF + + @property + def default_ext(self): + """Default filename extension, always 'tiff' for TIFF images.""" + return "tiff" + + @classmethod + def from_stream(cls, stream): + """Return a |Tiff| instance containing the properties of the TIFF image in + `stream`.""" + parser = _TiffParser.parse(stream) + + px_width = parser.px_width + px_height = parser.px_height + horz_dpi = parser.horz_dpi + vert_dpi = parser.vert_dpi + + return cls(px_width, px_height, horz_dpi, vert_dpi) + + +class _TiffParser: + """Parses a TIFF image stream to extract the image properties found in its main + image file directory (IFD)""" + + def __init__(self, ifd_entries): + super(_TiffParser, self).__init__() + self._ifd_entries = ifd_entries + + @classmethod + def parse(cls, stream): + """Return an instance of |_TiffParser| containing the properties parsed from the + TIFF image in `stream`.""" + stream_rdr = cls._make_stream_reader(stream) + ifd0_offset = stream_rdr.read_long(4) + ifd_entries = _IfdEntries.from_stream(stream_rdr, ifd0_offset) + return cls(ifd_entries) + + @property + def horz_dpi(self): + """The horizontal dots per inch value calculated from the XResolution and + ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present.""" + return self._dpi(TIFF_TAG.X_RESOLUTION) + + @property + def vert_dpi(self): + """The vertical dots per inch value calculated from the XResolution and + ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present.""" + return self._dpi(TIFF_TAG.Y_RESOLUTION) + + @property + def px_height(self): + """The number of stacked rows of pixels in the image, |None| if the IFD contains + no ``ImageLength`` tag, the expected case when the TIFF is embeded in an Exif + image.""" + return self._ifd_entries.get(TIFF_TAG.IMAGE_LENGTH) + + @property + def px_width(self): + """The number of pixels in each row in the image, |None| if the IFD contains no + ``ImageWidth`` tag, the expected case when the TIFF is embeded in an Exif + image.""" + return self._ifd_entries.get(TIFF_TAG.IMAGE_WIDTH) + + @classmethod + def _detect_endian(cls, stream): + """Return either BIG_ENDIAN or LITTLE_ENDIAN depending on the endian indicator + found in the TIFF `stream` header, either 'MM' or 'II'.""" + stream.seek(0) + endian_str = stream.read(2) + return BIG_ENDIAN if endian_str == b"MM" else LITTLE_ENDIAN + + def _dpi(self, resolution_tag): + """Return the dpi value calculated for `resolution_tag`, which can be either + TIFF_TAG.X_RESOLUTION or TIFF_TAG.Y_RESOLUTION. + + The calculation is based on the values of both that tag and the + TIFF_TAG.RESOLUTION_UNIT tag in this parser's |_IfdEntries| instance. + """ + ifd_entries = self._ifd_entries + + if resolution_tag not in ifd_entries: + return 72 + + # resolution unit defaults to inches (2) + resolution_unit = ( + ifd_entries[TIFF_TAG.RESOLUTION_UNIT] + if TIFF_TAG.RESOLUTION_UNIT in ifd_entries + else 2 + ) + + if resolution_unit == 1: # aspect ratio only + return 72 + # resolution_unit == 2 for inches, 3 for centimeters + units_per_inch = 1 if resolution_unit == 2 else 2.54 + dots_per_unit = ifd_entries[resolution_tag] + return int(round(dots_per_unit * units_per_inch)) + + @classmethod + def _make_stream_reader(cls, stream): + """Return a |StreamReader| instance with wrapping `stream` and having "endian- + ness" determined by the 'MM' or 'II' indicator in the TIFF stream header.""" + endian = cls._detect_endian(stream) + return StreamReader(stream, endian) + + +class _IfdEntries: + """Image File Directory for a TIFF image, having mapping (dict) semantics allowing + "tag" values to be retrieved by tag code.""" + + def __init__(self, entries): + super(_IfdEntries, self).__init__() + self._entries = entries + + def __contains__(self, key): + """Provides ``in`` operator, e.g. ``tag in ifd_entries``""" + return self._entries.__contains__(key) + + def __getitem__(self, key): + """Provides indexed access, e.g. ``tag_value = ifd_entries[tag_code]``""" + return self._entries.__getitem__(key) + + @classmethod + def from_stream(cls, stream, offset): + """Return a new |_IfdEntries| instance parsed from `stream` starting at + `offset`.""" + ifd_parser = _IfdParser(stream, offset) + entries = {e.tag: e.value for e in ifd_parser.iter_entries()} + return cls(entries) + + def get(self, tag_code, default=None): + """Return value of IFD entry having tag matching `tag_code`, or `default` if no + matching tag found.""" + return self._entries.get(tag_code, default) + + +class _IfdParser: + """Service object that knows how to extract directory entries from an Image File + Directory (IFD)""" + + def __init__(self, stream_rdr, offset): + super(_IfdParser, self).__init__() + self._stream_rdr = stream_rdr + self._offset = offset + + def iter_entries(self): + """Generate an |_IfdEntry| instance corresponding to each entry in the + directory.""" + for idx in range(self._entry_count): + dir_entry_offset = self._offset + 2 + (idx * 12) + ifd_entry = _IfdEntryFactory(self._stream_rdr, dir_entry_offset) + yield ifd_entry + + @property + def _entry_count(self): + """The count of directory entries, read from the top of the IFD header.""" + return self._stream_rdr.read_short(self._offset) + + +def _IfdEntryFactory(stream_rdr, offset): + """Return an |_IfdEntry| subclass instance containing the value of the directory + entry at `offset` in `stream_rdr`.""" + ifd_entry_classes = { + TIFF_FLD.ASCII: _AsciiIfdEntry, + TIFF_FLD.SHORT: _ShortIfdEntry, + TIFF_FLD.LONG: _LongIfdEntry, + TIFF_FLD.RATIONAL: _RationalIfdEntry, + } + field_type = stream_rdr.read_short(offset, 2) + EntryCls = ifd_entry_classes.get(field_type, _IfdEntry) + return EntryCls.from_stream(stream_rdr, offset) + + +class _IfdEntry: + """Base class for IFD entry classes. + + Subclasses are differentiated by value type, e.g. ASCII, long int, etc. + """ + + def __init__(self, tag_code, value): + super(_IfdEntry, self).__init__() + self._tag_code = tag_code + self._value = value + + @classmethod + def from_stream(cls, stream_rdr, offset): + """Return an |_IfdEntry| subclass instance containing the tag and value of the + tag parsed from `stream_rdr` at `offset`. + + Note this method is common to all subclasses. Override the ``_parse_value()`` + method to provide distinctive behavior based on field type. + """ + tag_code = stream_rdr.read_short(offset, 0) + value_count = stream_rdr.read_long(offset, 4) + value_offset = stream_rdr.read_long(offset, 8) + value = cls._parse_value(stream_rdr, offset, value_count, value_offset) + return cls(tag_code, value) + + @classmethod + def _parse_value(cls, stream_rdr, offset, value_count, value_offset): + """Return the value of this field parsed from `stream_rdr` at `offset`. + + Intended to be overridden by subclasses. + """ + return "UNIMPLEMENTED FIELD TYPE" # pragma: no cover + + @property + def tag(self): + """Short int code that identifies this IFD entry.""" + return self._tag_code + + @property + def value(self): + """Value of this tag, its type being dependent on the tag.""" + return self._value + + +class _AsciiIfdEntry(_IfdEntry): + """IFD entry having the form of a NULL-terminated ASCII string.""" + + @classmethod + def _parse_value(cls, stream_rdr, offset, value_count, value_offset): + """Return the ASCII string parsed from `stream_rdr` at `value_offset`. + + The length of the string, including a terminating '\x00' (NUL) character, is in + `value_count`. + """ + return stream_rdr.read_str(value_count - 1, value_offset) + + +class _ShortIfdEntry(_IfdEntry): + """IFD entry expressed as a short (2-byte) integer.""" + + @classmethod + def _parse_value(cls, stream_rdr, offset, value_count, value_offset): + """Return the short int value contained in the `value_offset` field of this + entry. + + Only supports single values at present. + """ + if value_count == 1: + return stream_rdr.read_short(offset, 8) + else: # pragma: no cover + return "Multi-value short integer NOT IMPLEMENTED" + + +class _LongIfdEntry(_IfdEntry): + """IFD entry expressed as a long (4-byte) integer.""" + + @classmethod + def _parse_value(cls, stream_rdr, offset, value_count, value_offset): + """Return the long int value contained in the `value_offset` field of this + entry. + + Only supports single values at present. + """ + if value_count == 1: + return stream_rdr.read_long(offset, 8) + else: # pragma: no cover + return "Multi-value long integer NOT IMPLEMENTED" + + +class _RationalIfdEntry(_IfdEntry): + """IFD entry expressed as a numerator, denominator pair.""" + + @classmethod + def _parse_value(cls, stream_rdr, offset, value_count, value_offset): + """Return the rational (numerator / denominator) value at `value_offset` in + `stream_rdr` as a floating-point number. + + Only supports single values at present. + """ + if value_count == 1: + numerator = stream_rdr.read_long(value_offset) + denominator = stream_rdr.read_long(value_offset, 4) + return numerator / denominator + else: # pragma: no cover + return "Multi-value Rational NOT IMPLEMENTED" diff --git a/docx/opc/__init__.py b/src/docx/opc/__init__.py similarity index 100% rename from docx/opc/__init__.py rename to src/docx/opc/__init__.py diff --git a/src/docx/opc/constants.py b/src/docx/opc/constants.py new file mode 100644 index 000000000..89d3c16cc --- /dev/null +++ b/src/docx/opc/constants.py @@ -0,0 +1,532 @@ +"""Constant values related to the Open Packaging Convention. + +In particular it includes content types and relationship types. +""" + + +class CONTENT_TYPE: + """Content type URIs (like MIME-types) that specify a part's format.""" + + BMP = "image/bmp" + DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + DML_CHARTSHAPES = ( + "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" + ) + DML_DIAGRAM_COLORS = ( + "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" + ) + DML_DIAGRAM_DATA = ( + "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" + ) + DML_DIAGRAM_LAYOUT = ( + "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" + ) + DML_DIAGRAM_STYLE = ( + "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" + ) + GIF = "image/gif" + JPEG = "image/jpeg" + MS_PHOTO = "image/vnd.ms-photo" + OFC_CUSTOM_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.custom-properties+xml" + ) + OFC_CUSTOM_XML_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" + ) + OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml" + OFC_EXTENDED_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.extended-properties+xml" + ) + OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" + OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" + OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" + OFC_THEME_OVERRIDE = ( + "application/vnd.openxmlformats-officedocument.themeOverride+xml" + ) + OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" + OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" + OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( + "application/vnd.openxmlformats-package.digital-signature-certificate" + ) + OPC_DIGITAL_SIGNATURE_ORIGIN = ( + "application/vnd.openxmlformats-package.digital-signature-origin" + ) + OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" + ) + OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" + PML_COMMENTS = ( + "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" + ) + PML_COMMENT_AUTHORS = ( + "application/vnd.openxmlformats-officedocument.presentationml.commen" + "tAuthors+xml" + ) + PML_HANDOUT_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.handou" + "tMaster+xml" + ) + PML_NOTES_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.notesM" + "aster+xml" + ) + PML_NOTES_SLIDE = ( + "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + ) + PML_PRESENTATION_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.presen" + "tation.main+xml" + ) + PML_PRES_PROPS = ( + "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + ) + PML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.presentationml.printe" + "rSettings" + ) + PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" + PML_SLIDESHOW_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.slides" + "how.main+xml" + ) + PML_SLIDE_LAYOUT = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideL" + "ayout+xml" + ) + PML_SLIDE_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideM" + "aster+xml" + ) + PML_SLIDE_UPDATE_INFO = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideU" + "pdateInfo+xml" + ) + PML_TABLE_STYLES = ( + "application/vnd.openxmlformats-officedocument.presentationml.tableS" + "tyles+xml" + ) + PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" + PML_TEMPLATE_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.templa" + "te.main+xml" + ) + PML_VIEW_PROPS = ( + "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" + ) + PNG = "image/png" + SML_CALC_CHAIN = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" + ) + SML_CHARTSHEET = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + ) + SML_COMMENTS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + ) + SML_CONNECTIONS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" + ) + SML_CUSTOM_PROPERTY = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" + ) + SML_DIALOGSHEET = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" + ) + SML_EXTERNAL_LINK = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.externa" + "lLink+xml" + ) + SML_PIVOT_CACHE_DEFINITION = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" + "cheDefinition+xml" + ) + SML_PIVOT_CACHE_RECORDS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa" + "cheRecords+xml" + ) + SML_PIVOT_TABLE = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + ) + SML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" + ) + SML_QUERY_TABLE = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" + ) + SML_REVISION_HEADERS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisio" + "nHeaders+xml" + ) + SML_REVISION_LOG = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" + ) + SML_SHARED_STRINGS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedS" + "trings+xml" + ) + SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + SML_SHEET_MAIN = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + ) + SML_SHEET_METADATA = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMe" + "tadata+xml" + ) + SML_STYLES = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" + ) + SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + SML_TABLE_SINGLE_CELLS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSi" + "ngleCells+xml" + ) + SML_TEMPLATE_MAIN = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.templat" + "e.main+xml" + ) + SML_USER_NAMES = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" + ) + SML_VOLATILE_DEPENDENCIES = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatil" + "eDependencies+xml" + ) + SML_WORKSHEET = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + ) + TIFF = "image/tiff" + WML_COMMENTS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" + ) + WML_DOCUMENT = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + WML_DOCUMENT_GLOSSARY = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" + "ment.glossary+xml" + ) + WML_DOCUMENT_MAIN = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.docu" + "ment.main+xml" + ) + WML_ENDNOTES = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + ) + WML_FONT_TABLE = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.font" + "Table+xml" + ) + WML_FOOTER = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + ) + WML_FOOTNOTES = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.foot" + "notes+xml" + ) + WML_HEADER = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" + ) + WML_NUMBERING = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.numb" + "ering+xml" + ) + WML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.prin" + "terSettings" + ) + WML_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" + ) + WML_STYLES = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" + ) + WML_WEB_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.webS" + "ettings+xml" + ) + XML = "application/xml" + X_EMF = "image/x-emf" + X_FONTDATA = "application/x-fontdata" + X_FONT_TTF = "application/x-font-ttf" + X_WMF = "image/x-wmf" + + +class NAMESPACE: + """Constant values for OPC XML namespaces.""" + + DML_WORDPROCESSING_DRAWING = ( + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + ) + OFC_RELATIONSHIPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships" + OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types" + WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +class RELATIONSHIP_TARGET_MODE: + """Open XML relationship target modes.""" + + EXTERNAL = "External" + INTERNAL = "Internal" + + +class RELATIONSHIP_TYPE: + AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" + A_F_CHUNK = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" + ) + CALC_CHAIN = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/calcChain" + ) + CERTIFICATE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/certificate" + ) + CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + CHARTSHEET = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/chartsheet" + ) + CHART_USER_SHAPES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/chartUserShapes" + ) + COMMENTS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/comments" + ) + COMMENT_AUTHORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/commentAuthors" + ) + CONNECTIONS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/connections" + ) + CONTROL = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" + ) + CORE_PROPERTIES = ( + "http://schemas.openxmlformats.org/package/2006/relationships/metada" + "ta/core-properties" + ) + CUSTOM_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/custom-properties" + ) + CUSTOM_PROPERTY = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customProperty" + ) + CUSTOM_XML = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customXml" + ) + CUSTOM_XML_PROPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/customXmlProps" + ) + DIAGRAM_COLORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramColors" + ) + DIAGRAM_DATA = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramData" + ) + DIAGRAM_LAYOUT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramLayout" + ) + DIAGRAM_QUICK_STYLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/diagramQuickStyle" + ) + DIALOGSHEET = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/dialogsheet" + ) + DRAWING = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + ) + ENDNOTES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/endnotes" + ) + EXTENDED_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/extended-properties" + ) + EXTERNAL_LINK = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/externalLink" + ) + FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" + FONT_TABLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/fontTable" + ) + FOOTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + ) + FOOTNOTES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/footnotes" + ) + GLOSSARY_DOCUMENT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/glossaryDocument" + ) + HANDOUT_MASTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/handoutMaster" + ) + HEADER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" + ) + HYPERLINK = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/hyperlink" + ) + IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + NOTES_MASTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/notesMaster" + ) + NOTES_SLIDE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/notesSlide" + ) + NUMBERING = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/numbering" + ) + OFFICE_DOCUMENT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/officeDocument" + ) + OLE_OBJECT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/oleObject" + ) + ORIGIN = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/origin" + ) + PACKAGE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" + ) + PIVOT_CACHE_DEFINITION = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/pivotCacheDefinition" + ) + PIVOT_CACHE_RECORDS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/spreadsheetml/pivotCacheRecords" + ) + PIVOT_TABLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/pivotTable" + ) + PRES_PROPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/presProps" + ) + PRINTER_SETTINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/printerSettings" + ) + QUERY_TABLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/queryTable" + ) + REVISION_HEADERS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/revisionHeaders" + ) + REVISION_LOG = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/revisionLog" + ) + SETTINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/settings" + ) + SHARED_STRINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/sharedStrings" + ) + SHEET_METADATA = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/sheetMetadata" + ) + SIGNATURE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digita" + "l-signature/signature" + ) + SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" + SLIDE_LAYOUT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideLayout" + ) + SLIDE_MASTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideMaster" + ) + SLIDE_UPDATE_INFO = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/slideUpdateInfo" + ) + STYLES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" + ) + TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + TABLE_SINGLE_CELLS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/tableSingleCells" + ) + TABLE_STYLES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/tableStyles" + ) + TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags" + THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" + THEME_OVERRIDE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/themeOverride" + ) + THUMBNAIL = ( + "http://schemas.openxmlformats.org/package/2006/relationships/metada" + "ta/thumbnail" + ) + USERNAMES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/usernames" + ) + VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video" + VIEW_PROPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/viewProps" + ) + VML_DRAWING = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/vmlDrawing" + ) + VOLATILE_DEPENDENCIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/volatileDependencies" + ) + WEB_SETTINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/webSettings" + ) + WORKSHEET_SOURCE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + "/worksheetSource" + ) + XML_MAPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" + ) diff --git a/docx/opc/coreprops.py b/src/docx/opc/coreprops.py similarity index 87% rename from docx/opc/coreprops.py rename to src/docx/opc/coreprops.py index 2d38dabd3..2fd9a75c8 100644 --- a/docx/opc/coreprops.py +++ b/src/docx/opc/coreprops.py @@ -1,20 +1,13 @@ -# encoding: utf-8 +"""Provides CoreProperties, Dublin-Core attributes of the document. -""" -The :mod:`pptx.packaging` module coheres around the concerns of reading and -writing presentations to and from a .pptx file. +These are broadly-standardized attributes like author, last-modified, etc. """ -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +class CoreProperties: + """Corresponds to part named ``/docProps/core.xml``, containing the core document + properties for this document package.""" -class CoreProperties(object): - """ - Corresponds to part named ``/docProps/core.xml``, containing the core - document properties for this document package. - """ def __init__(self, element): self._element = element diff --git a/src/docx/opc/exceptions.py b/src/docx/opc/exceptions.py new file mode 100644 index 000000000..c5583d301 --- /dev/null +++ b/src/docx/opc/exceptions.py @@ -0,0 +1,12 @@ +"""Exceptions specific to python-opc. + +The base exception class is OpcError. +""" + + +class OpcError(Exception): + """Base error class for python-opc.""" + + +class PackageNotFoundError(OpcError): + """Raised when a package cannot be found at the specified path.""" diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py new file mode 100644 index 000000000..570dcf413 --- /dev/null +++ b/src/docx/opc/oxml.py @@ -0,0 +1,243 @@ +"""Temporary stand-in for main oxml module. + +This module came across with the PackageReader transplant. Probably much will get +replaced with objects from the pptx.oxml.core and then this module will either get +deleted or only hold the package related custom element classes. +""" + +from lxml import etree + +from docx.opc.constants import NAMESPACE as NS +from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM + +# configure XML parser +element_class_lookup = etree.ElementNamespaceClassLookup() +oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False) +oxml_parser.set_element_class_lookup(element_class_lookup) + +nsmap = { + "ct": NS.OPC_CONTENT_TYPES, + "pr": NS.OPC_RELATIONSHIPS, + "r": NS.OFC_RELATIONSHIPS, +} + + +# =========================================================================== +# functions +# =========================================================================== + + +def parse_xml(text: str) -> etree._Element: # pyright: ignore[reportPrivateUsage] + """`etree.fromstring()` replacement that uses oxml parser.""" + return etree.fromstring(text, oxml_parser) + + +def qn(tag): + """Stands for "qualified name", a utility function to turn a namespace prefixed tag + name into a Clark-notation qualified tag name for lxml. + + For + example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``. + """ + prefix, tagroot = tag.split(":") + uri = nsmap[prefix] + return "{%s}%s" % (uri, tagroot) + + +def serialize_part_xml(part_elm): + """Serialize `part_elm` etree element to XML suitable for storage as an XML part. + + That is to say, no insignificant whitespace added for readability, and an + appropriate XML declaration added with UTF-8 encoding specified. + """ + return etree.tostring(part_elm, encoding="UTF-8", standalone=True) + + +def serialize_for_reading(element): + """Serialize `element` to human-readable XML suitable for tests. + + No XML declaration. + """ + return etree.tostring(element, encoding="unicode", pretty_print=True) + + +# =========================================================================== +# Custom element classes +# =========================================================================== + + +class BaseOxmlElement(etree.ElementBase): + """Base class for all custom element classes, to add standardized behavior to all + classes in one place.""" + + @property + def xml(self): + """Return XML string for this element, suitable for testing purposes. + + Pretty printed for readability and without an XML declaration at the top. + """ + return serialize_for_reading(self) + + +class CT_Default(BaseOxmlElement): + """```` element, specifying the default content type to be applied to a + part with the specified extension.""" + + @property + def content_type(self): + """String held in the ``ContentType`` attribute of this ```` + element.""" + return self.get("ContentType") + + @property + def extension(self): + """String held in the ``Extension`` attribute of this ```` element.""" + return self.get("Extension") + + @staticmethod + def new(ext, content_type): + """Return a new ```` element with attributes set to parameter + values.""" + xml = '' % nsmap["ct"] + default = parse_xml(xml) + default.set("Extension", ext) + default.set("ContentType", content_type) + return default + + +class CT_Override(BaseOxmlElement): + """```` element, specifying the content type to be applied for a part with + the specified partname.""" + + @property + def content_type(self): + """String held in the ``ContentType`` attribute of this ```` + element.""" + return self.get("ContentType") + + @staticmethod + def new(partname, content_type): + """Return a new ```` element with attributes set to parameter + values.""" + xml = '' % nsmap["ct"] + override = parse_xml(xml) + override.set("PartName", partname) + override.set("ContentType", content_type) + return override + + @property + def partname(self): + """String held in the ``PartName`` attribute of this ```` element.""" + return self.get("PartName") + + +class CT_Relationship(BaseOxmlElement): + """```` element, representing a single relationship from a source to a + target part.""" + + @staticmethod + def new(rId, reltype, target, target_mode=RTM.INTERNAL): + """Return a new ```` element.""" + xml = '' % nsmap["pr"] + relationship = parse_xml(xml) + relationship.set("Id", rId) + relationship.set("Type", reltype) + relationship.set("Target", target) + if target_mode == RTM.EXTERNAL: + relationship.set("TargetMode", RTM.EXTERNAL) + return relationship + + @property + def rId(self): + """String held in the ``Id`` attribute of this ```` element.""" + return self.get("Id") + + @property + def reltype(self): + """String held in the ``Type`` attribute of this ```` element.""" + return self.get("Type") + + @property + def target_ref(self): + """String held in the ``Target`` attribute of this ```` + element.""" + return self.get("Target") + + @property + def target_mode(self): + """String held in the ``TargetMode`` attribute of this ```` + element, either ``Internal`` or ``External``. + + Defaults to ``Internal``. + """ + return self.get("TargetMode", RTM.INTERNAL) + + +class CT_Relationships(BaseOxmlElement): + """```` element, the root element in a .rels file.""" + + def add_rel(self, rId, reltype, target, is_external=False): + """Add a child ```` element with attributes set according to + parameter values.""" + target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL + relationship = CT_Relationship.new(rId, reltype, target, target_mode) + self.append(relationship) + + @staticmethod + def new(): + """Return a new ```` element.""" + xml = '' % nsmap["pr"] + relationships = parse_xml(xml) + return relationships + + @property + def Relationship_lst(self): + """Return a list containing all the ```` child elements.""" + return self.findall(qn("pr:Relationship")) + + @property + def xml(self): + """Return XML string for this element, suitable for saving in a .rels stream, + not pretty printed and with an XML declaration at the top.""" + return serialize_part_xml(self) + + +class CT_Types(BaseOxmlElement): + """```` element, the container element for Default and Override elements in + [Content_Types].xml.""" + + def add_default(self, ext, content_type): + """Add a child ```` element with attributes set to parameter values.""" + default = CT_Default.new(ext, content_type) + self.append(default) + + def add_override(self, partname, content_type): + """Add a child ```` element with attributes set to parameter + values.""" + override = CT_Override.new(partname, content_type) + self.append(override) + + @property + def defaults(self): + return self.findall(qn("ct:Default")) + + @staticmethod + def new(): + """Return a new ```` element.""" + xml = '' % nsmap["ct"] + types = parse_xml(xml) + return types + + @property + def overrides(self): + return self.findall(qn("ct:Override")) + + +ct_namespace = element_class_lookup.get_namespace(nsmap["ct"]) +ct_namespace["Default"] = CT_Default +ct_namespace["Override"] = CT_Override +ct_namespace["Types"] = CT_Types + +pr_namespace = element_class_lookup.get_namespace(nsmap["pr"]) +pr_namespace["Relationship"] = CT_Relationship +pr_namespace["Relationships"] = CT_Relationships diff --git a/docx/opc/package.py b/src/docx/opc/package.py similarity index 60% rename from docx/opc/package.py rename to src/docx/opc/package.py index 7ba87bab5..b5bdc0e7c 100644 --- a/docx/opc/package.py +++ b/src/docx/opc/package.py @@ -1,9 +1,5 @@ -# encoding: utf-8 - """Objects that implement reading and writing OPC packages.""" -from __future__ import absolute_import, division, print_function, unicode_literals - from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.packuri import PACKAGE_URI, PackURI from docx.opc.part import PartFactory @@ -14,7 +10,7 @@ from docx.opc.shared import lazyproperty -class OpcPackage(object): +class OpcPackage: """Main API class for |python-opc|. A new instance is constructed by calling the :meth:`open` class method with a path @@ -25,9 +21,9 @@ def __init__(self): super(OpcPackage, self).__init__() def after_unmarshal(self): - """ - Entry point for any post-unmarshaling processing. May be overridden - by subclasses without forwarding call to super. + """Entry point for any post-unmarshaling processing. + + May be overridden by subclasses without forwarding call to super. """ # don't place any code here, just catch call if not overridden by # subclass @@ -35,17 +31,14 @@ def after_unmarshal(self): @property def core_properties(self): - """ - |CoreProperties| object providing read/write access to the Dublin - Core properties for this document. - """ + """|CoreProperties| object providing read/write access to the Dublin Core + properties for this document.""" return self._core_properties_part.core_properties def iter_rels(self): - """ - Generate exactly one reference to each relationship in the package by - performing a depth-first traversal of the rels graph. - """ + """Generate exactly one reference to each relationship in the package by + performing a depth-first traversal of the rels graph.""" + def walk_rels(source, visited=None): visited = [] if visited is None else visited for rel in source.rels.values(): @@ -64,11 +57,10 @@ def walk_rels(source, visited=None): yield rel def iter_parts(self): - """ - Generate exactly one reference to each of the parts in the package by - performing a depth-first traversal of the rels graph. - """ - def walk_parts(source, visited=list()): + """Generate exactly one reference to each of the parts in the package by + performing a depth-first traversal of the rels graph.""" + + def walk_parts(source, visited=[]): for rel in source.rels.values(): if rel.is_external: continue @@ -85,31 +77,30 @@ def walk_parts(source, visited=list()): yield part def load_rel(self, reltype, target, rId, is_external=False): - """ - Return newly added |_Relationship| instance of *reltype* between this - part and *target* with key *rId*. Target mode is set to - ``RTM.EXTERNAL`` if *is_external* is |True|. Intended for use during - load from a serialized package, where the rId is well known. Other - methods exist for adding a new relationship to the package during - processing. + """Return newly added |_Relationship| instance of `reltype` between this part + and `target` with key `rId`. + + Target mode is set to ``RTM.EXTERNAL`` if `is_external` is |True|. Intended for + use during load from a serialized package, where the rId is well known. Other + methods exist for adding a new relationship to the package during processing. """ return self.rels.add_relationship(reltype, target, rId, is_external) @property def main_document_part(self): - """ - Return a reference to the main document part for this package. - Examples include a document part for a WordprocessingML package, a - presentation part for a PresentationML package, or a workbook part - for a SpreadsheetML package. + """Return a reference to the main document part for this package. + + Examples include a document part for a WordprocessingML package, a presentation + part for a PresentationML package, or a workbook part for a SpreadsheetML + package. """ return self.part_related_by(RT.OFFICE_DOCUMENT) def next_partname(self, template): - """Return a |PackURI| instance representing partname matching *template*. + """Return a |PackURI| instance representing partname matching `template`. The returned part-name has the next available numeric suffix to distinguish it - from other parts of its type. *template* is a printf (%)-style template string + from other parts of its type. `template` is a printf (%)-style template string containing a single replacement item, a '%d' to be used to insert the integer portion of the partname. Example: "/word/header%d.xml" """ @@ -121,61 +112,49 @@ def next_partname(self, template): @classmethod def open(cls, pkg_file): - """ - Return an |OpcPackage| instance loaded with the contents of - *pkg_file*. - """ + """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" pkg_reader = PackageReader.from_file(pkg_file) package = cls() Unmarshaller.unmarshal(pkg_reader, package, PartFactory) return package def part_related_by(self, reltype): - """ - Return part to which this package has a relationship of *reltype*. - Raises |KeyError| if no such relationship is found and |ValueError| - if more than one such relationship is found. + """Return part to which this package has a relationship of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. """ return self.rels.part_with_reltype(reltype) @property def parts(self): - """ - Return a list containing a reference to each of the parts in this - package. - """ - return [part for part in self.iter_parts()] + """Return a list containing a reference to each of the parts in this package.""" + return list(self.iter_parts()) def relate_to(self, part, reltype): - """ - Return rId key of relationship to *part*, from the existing - relationship if there is one, otherwise a newly created one. - """ + """Return rId key of relationship to `part`, from the existing relationship if + there is one, otherwise a newly created one.""" rel = self.rels.get_or_add(reltype, part) return rel.rId @lazyproperty def rels(self): - """ - Return a reference to the |Relationships| instance holding the - collection of relationships for this package. - """ + """Return a reference to the |Relationships| instance holding the collection of + relationships for this package.""" return Relationships(PACKAGE_URI.baseURI) def save(self, pkg_file): - """ - Save this package to *pkg_file*, where *file* can be either a path to - a file (a string) or a file-like object. - """ + """Save this package to `pkg_file`, where `file` can be either a path to a file + (a string) or a file-like object.""" for part in self.parts: part.before_marshal() PackageWriter.write(pkg_file, self.rels, self.parts) @property def _core_properties_part(self): - """ - |CorePropertiesPart| object related to this package. Creates - a default core properties part if one is not present (not common). + """|CorePropertiesPart| object related to this package. + + Creates a default core properties part if one is not present (not common). """ try: return self.part_related_by(RT.CORE_PROPERTIES) @@ -185,19 +164,17 @@ def _core_properties_part(self): return core_properties_part -class Unmarshaller(object): +class Unmarshaller: """Hosts static methods for unmarshalling a package from a |PackageReader|.""" @staticmethod def unmarshal(pkg_reader, package, part_factory): + """Construct graph of parts and realized relationships based on the contents of + `pkg_reader`, delegating construction of each part to `part_factory`. + + Package relationships are added to `pkg`. """ - Construct graph of parts and realized relationships based on the - contents of *pkg_reader*, delegating construction of each part to - *part_factory*. Package relationships are added to *pkg*. - """ - parts = Unmarshaller._unmarshal_parts( - pkg_reader, package, part_factory - ) + parts = Unmarshaller._unmarshal_parts(pkg_reader, package, part_factory) Unmarshaller._unmarshal_relationships(pkg_reader, package, parts) for part in parts.values(): part.after_unmarshal() @@ -205,10 +182,11 @@ def unmarshal(pkg_reader, package, part_factory): @staticmethod def _unmarshal_parts(pkg_reader, package, part_factory): - """ - Return a dictionary of |Part| instances unmarshalled from - *pkg_reader*, keyed by partname. Side-effect is that each part in - *pkg_reader* is constructed using *part_factory*. + """Return a dictionary of |Part| instances unmarshalled from `pkg_reader`, keyed + by partname. + + Side-effect is that each part in `pkg_reader` is constructed using + `part_factory`. """ parts = {} for partname, content_type, reltype, blob in pkg_reader.iter_sparts(): @@ -219,13 +197,12 @@ def _unmarshal_parts(pkg_reader, package, part_factory): @staticmethod def _unmarshal_relationships(pkg_reader, package, parts): - """ - Add a relationship to the source object corresponding to each of the - relationships in *pkg_reader* with its target_part set to the actual - target part in *parts*. - """ + """Add a relationship to the source object corresponding to each of the + relationships in `pkg_reader` with its target_part set to the actual target part + in `parts`.""" for source_uri, srel in pkg_reader.iter_srels(): - source = package if source_uri == '/' else parts[source_uri] - target = (srel.target_ref if srel.is_external - else parts[srel.target_partname]) + source = package if source_uri == "/" else parts[source_uri] + target = ( + srel.target_ref if srel.is_external else parts[srel.target_partname] + ) source.load_rel(srel.reltype, target, srel.rId, srel.is_external) diff --git a/src/docx/opc/packuri.py b/src/docx/opc/packuri.py new file mode 100644 index 000000000..fe330d89b --- /dev/null +++ b/src/docx/opc/packuri.py @@ -0,0 +1,110 @@ +"""Provides the PackURI value type. + +Also some useful known pack URI strings such as PACKAGE_URI. +""" + +import posixpath +import re + + +class PackURI(str): + """Provides access to pack URI components such as the baseURI and the filename + slice. + + Behaves as |str| otherwise. + """ + + _filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?") + + def __new__(cls, pack_uri_str): + if pack_uri_str[0] != "/": + tmpl = "PackURI must begin with slash, got '%s'" + raise ValueError(tmpl % pack_uri_str) + return str.__new__(cls, pack_uri_str) + + @staticmethod + def from_rel_ref(baseURI, relative_ref): + """Return a |PackURI| instance containing the absolute pack URI formed by + translating `relative_ref` onto `baseURI`.""" + joined_uri = posixpath.join(baseURI, relative_ref) + abs_uri = posixpath.abspath(joined_uri) + return PackURI(abs_uri) + + @property + def baseURI(self): + """The base URI of this pack URI, the directory portion, roughly speaking. + + E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. For the package pseudo- + partname '/', baseURI is '/'. + """ + return posixpath.split(self)[0] + + @property + def ext(self): + """The extension portion of this pack URI, e.g. ``'xml'`` for + ``'/word/document.xml'``. + + Note the period is not included. + """ + # raw_ext is either empty string or starts with period, e.g. '.xml' + raw_ext = posixpath.splitext(self)[1] + return raw_ext[1:] if raw_ext.startswith(".") else raw_ext + + @property + def filename(self): + """The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for + ``'/ppt/slides/slide1.xml'``. + + For the package pseudo-partname '/', filename is ''. + """ + return posixpath.split(self)[1] + + @property + def idx(self): + """Return partname index as integer for tuple partname or None for singleton + partname, e.g. ``21`` for ``'/ppt/slides/slide21.xml'`` and |None| for + ``'/ppt/presentation.xml'``.""" + filename = self.filename + if not filename: + return None + name_part = posixpath.splitext(filename)[0] # filename w/ext removed + match = self._filename_re.match(name_part) + if match is None: + return None + if match.group(2): + return int(match.group(2)) + return None + + @property + def membername(self): + """The pack URI with the leading slash stripped off, the form used as the Zip + file membername for the package item. + + Returns '' for the package pseudo-partname '/'. + """ + return self[1:] + + def relative_ref(self, baseURI): + """Return string containing relative reference to package item from `baseURI`. + + E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would return + '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'. + """ + # workaround for posixpath bug in 2.6, doesn't generate correct + # relative path when `start` (second) parameter is root ('/') + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) + + @property + def rels_uri(self): + """The pack URI of the .rels part corresponding to the current pack URI. + + Only produces sensible output if the pack URI is a partname or the package + pseudo-partname '/'. + """ + rels_filename = "%s.rels" % self.filename + rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename) + return PackURI(rels_uri_str) + + +PACKAGE_URI = PackURI("/") +CONTENT_TYPES_URI = PackURI("/[Content_Types].xml") diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py new file mode 100644 index 000000000..ad9abf7c9 --- /dev/null +++ b/src/docx/opc/part.py @@ -0,0 +1,224 @@ +"""Open Packaging Convention (OPC) objects related to package parts.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docx.opc.oxml import serialize_part_xml +from docx.opc.packuri import PackURI +from docx.opc.rel import Relationships +from docx.opc.shared import cls_method_fn, lazyproperty +from docx.oxml.parser import parse_xml + +if TYPE_CHECKING: + from docx.package import Package + + +class Part: + """Base class for package parts. + + Provides common properties and methods, but intended to be subclassed in client code + to implement specific part behaviors. + """ + + def __init__( + self, + partname: str, + content_type: str, + blob: bytes | None = None, + package: Package | None = None, + ): + super(Part, self).__init__() + self._partname = partname + self._content_type = content_type + self._blob = blob + self._package = package + + def after_unmarshal(self): + """Entry point for post-unmarshaling processing, for example to parse the part + XML. + + May be overridden by subclasses without forwarding call to super. + """ + # don't place any code here, just catch call if not overridden by + # subclass + pass + + def before_marshal(self): + """Entry point for pre-serialization processing, for example to finalize part + naming if necessary. + + May be overridden by subclasses without forwarding call to super. + """ + # don't place any code here, just catch call if not overridden by + # subclass + pass + + @property + def blob(self): + """Contents of this package part as a sequence of bytes. + + May be text or binary. Intended to be overridden by subclasses. Default behavior + is to return load blob. + """ + return self._blob + + @property + def content_type(self): + """Content type of this part.""" + return self._content_type + + def drop_rel(self, rId: str): + """Remove the relationship identified by `rId` if its reference count is less + than 2. + + Relationships with a reference count of 0 are implicit relationships. + """ + if self._rel_ref_count(rId) < 2: + del self.rels[rId] + + @classmethod + def load(cls, partname, content_type, blob, package): + return cls(partname, content_type, blob, package) + + def load_rel(self, reltype, target, rId, is_external=False): + """Return newly added |_Relationship| instance of `reltype` between this part + and `target` with key `rId`. + + Target mode is set to ``RTM.EXTERNAL`` if `is_external` is |True|. Intended for + use during load from a serialized package, where the rId is well-known. Other + methods exist for adding a new relationship to a part when manipulating a part. + """ + return self.rels.add_relationship(reltype, target, rId, is_external) + + @property + def package(self): + """|OpcPackage| instance this part belongs to.""" + return self._package + + @property + def partname(self): + """|PackURI| instance holding partname of this part, e.g. + '/ppt/slides/slide1.xml'.""" + return self._partname + + @partname.setter + def partname(self, partname): + if not isinstance(partname, PackURI): + tmpl = "partname must be instance of PackURI, got '%s'" + raise TypeError(tmpl % type(partname).__name__) + self._partname = partname + + def part_related_by(self, reltype: str) -> Part: + """Return part to which this part has a relationship of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than + one such relationship is found. Provides ability to resolve implicitly related + part, such as Slide -> SlideLayout. + """ + return self.rels.part_with_reltype(reltype) + + def relate_to(self, target: Part, reltype: str, is_external: bool = False) -> str: + """Return rId key of relationship of `reltype` to `target`. + + The returned `rId` is from an existing relationship if there is one, otherwise a + new relationship is created. + """ + if is_external: + return self.rels.get_or_add_ext_rel(reltype, target) + else: + rel = self.rels.get_or_add(reltype, target) + return rel.rId + + @property + def related_parts(self): + """Dictionary mapping related parts by rId, so child objects can resolve + explicit relationships present in the part XML, e.g. sldIdLst to a specific + |Slide| instance.""" + return self.rels.related_parts + + @lazyproperty + def rels(self): + """|Relationships| instance holding the relationships for this part.""" + return Relationships(self._partname.baseURI) + + def target_ref(self, rId): + """Return URL contained in target ref of relationship identified by `rId`.""" + rel = self.rels[rId] + return rel.target_ref + + def _rel_ref_count(self, rId): + """Return the count of references in this part's XML to the relationship + identified by `rId`.""" + rIds = self._element.xpath("//@r:id") + return len([_rId for _rId in rIds if _rId == rId]) + + +class PartFactory: + """Provides a way for client code to specify a subclass of |Part| to be constructed + by |Unmarshaller| based on its content type and/or a custom callable. + + Setting ``PartFactory.part_class_selector`` to a callable object will cause that + object to be called with the parameters ``content_type, reltype``, once for each + part in the package. If the callable returns an object, it is used as the class for + that part. If it returns |None|, part class selection falls back to the content type + map defined in ``PartFactory.part_type_for``. If no class is returned from either of + these, the class contained in ``PartFactory.default_part_type`` is used to construct + the part, which is by default ``opc.package.Part``. + """ + + part_class_selector = None + part_type_for = {} + default_part_type = Part + + def __new__(cls, partname, content_type, reltype, blob, package): + PartClass = None + if cls.part_class_selector is not None: + part_class_selector = cls_method_fn(cls, "part_class_selector") + PartClass = part_class_selector(content_type, reltype) + if PartClass is None: + PartClass = cls._part_cls_for(content_type) + return PartClass.load(partname, content_type, blob, package) + + @classmethod + def _part_cls_for(cls, content_type): + """Return the custom part class registered for `content_type`, or the default + part class if no custom class is registered for `content_type`.""" + if content_type in cls.part_type_for: + return cls.part_type_for[content_type] + return cls.default_part_type + + +class XmlPart(Part): + """Base class for package parts containing an XML payload, which is most of them. + + Provides additional methods to the |Part| base class that take care of parsing and + reserializing the XML payload and managing relationships to other parts. + """ + + def __init__(self, partname, content_type, element, package): + super(XmlPart, self).__init__(partname, content_type, package=package) + self._element = element + + @property + def blob(self): + return serialize_part_xml(self._element) + + @property + def element(self): + """The root XML element of this XML part.""" + return self._element + + @classmethod + def load(cls, partname, content_type, blob, package): + element = parse_xml(blob) + return cls(partname, content_type, element, package) + + @property + def part(self): + """Part of the parent protocol, "children" of the document will not know the + part that contains them so must ask their parent object. + + That chain of delegation ends here for child objects. + """ + return self diff --git a/docx/opc/parts/__init__.py b/src/docx/opc/parts/__init__.py similarity index 100% rename from docx/opc/parts/__init__.py rename to src/docx/opc/parts/__init__.py diff --git a/src/docx/opc/parts/coreprops.py b/src/docx/opc/parts/coreprops.py new file mode 100644 index 000000000..6e26e1d05 --- /dev/null +++ b/src/docx/opc/parts/coreprops.py @@ -0,0 +1,39 @@ +"""Core properties part, corresponds to ``/docProps/core.xml`` part in package.""" + +from datetime import datetime + +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.coreprops import CoreProperties +from docx.opc.packuri import PackURI +from docx.opc.part import XmlPart +from docx.oxml.coreprops import CT_CoreProperties + + +class CorePropertiesPart(XmlPart): + """Corresponds to part named ``/docProps/core.xml``, containing the core document + properties for this document package.""" + + @classmethod + def default(cls, package): + """Return a new |CorePropertiesPart| object initialized with default values for + its base properties.""" + core_properties_part = cls._new(package) + core_properties = core_properties_part.core_properties + core_properties.title = "Word Document" + core_properties.last_modified_by = "python-docx" + core_properties.revision = 1 + core_properties.modified = datetime.utcnow() + return core_properties_part + + @property + def core_properties(self): + """A |CoreProperties| object providing read/write access to the core properties + contained in this core properties part.""" + return CoreProperties(self.element) + + @classmethod + def _new(cls, package): + partname = PackURI("/docProps/core.xml") + content_type = CT.OPC_CORE_PROPERTIES + coreProperties = CT_CoreProperties.new() + return CorePropertiesPart(partname, content_type, coreProperties, package) diff --git a/src/docx/opc/phys_pkg.py b/src/docx/opc/phys_pkg.py new file mode 100644 index 000000000..5ec32237c --- /dev/null +++ b/src/docx/opc/phys_pkg.py @@ -0,0 +1,119 @@ +"""Provides a general interface to a `physical` OPC package, such as a zip file.""" + +import os +from zipfile import ZIP_DEFLATED, ZipFile, is_zipfile + +from docx.opc.exceptions import PackageNotFoundError +from docx.opc.packuri import CONTENT_TYPES_URI + + +class PhysPkgReader: + """Factory for physical package reader objects.""" + + def __new__(cls, pkg_file): + # if `pkg_file` is a string, treat it as a path + if isinstance(pkg_file, str): + if os.path.isdir(pkg_file): + reader_cls = _DirPkgReader + elif is_zipfile(pkg_file): + reader_cls = _ZipPkgReader + else: + raise PackageNotFoundError("Package not found at '%s'" % pkg_file) + else: # assume it's a stream and pass it to Zip reader to sort out + reader_cls = _ZipPkgReader + + return super(PhysPkgReader, cls).__new__(reader_cls) + + +class PhysPkgWriter: + """Factory for physical package writer objects.""" + + def __new__(cls, pkg_file): + return super(PhysPkgWriter, cls).__new__(_ZipPkgWriter) + + +class _DirPkgReader(PhysPkgReader): + """Implements |PhysPkgReader| interface for an OPC package extracted into a + directory.""" + + def __init__(self, path): + """`path` is the path to a directory containing an expanded package.""" + super(_DirPkgReader, self).__init__() + self._path = os.path.abspath(path) + + def blob_for(self, pack_uri): + """Return contents of file corresponding to `pack_uri` in package directory.""" + path = os.path.join(self._path, pack_uri.membername) + with open(path, "rb") as f: + blob = f.read() + return blob + + def close(self): + """Provides interface consistency with |ZipFileSystem|, but does nothing, a + directory file system doesn't need closing.""" + pass + + @property + def content_types_xml(self): + """Return the `[Content_Types].xml` blob from the package.""" + return self.blob_for(CONTENT_TYPES_URI) + + def rels_xml_for(self, source_uri): + """Return rels item XML for source with `source_uri`, or None if the item has no + rels item.""" + try: + rels_xml = self.blob_for(source_uri.rels_uri) + except IOError: + rels_xml = None + return rels_xml + + +class _ZipPkgReader(PhysPkgReader): + """Implements |PhysPkgReader| interface for a zip file OPC package.""" + + def __init__(self, pkg_file): + super(_ZipPkgReader, self).__init__() + self._zipf = ZipFile(pkg_file, "r") + + def blob_for(self, pack_uri): + """Return blob corresponding to `pack_uri`. + + Raises |ValueError| if no matching member is present in zip archive. + """ + return self._zipf.read(pack_uri.membername) + + def close(self): + """Close the zip archive, releasing any resources it is using.""" + self._zipf.close() + + @property + def content_types_xml(self): + """Return the `[Content_Types].xml` blob from the zip package.""" + return self.blob_for(CONTENT_TYPES_URI) + + def rels_xml_for(self, source_uri): + """Return rels item XML for source with `source_uri` or None if no rels item is + present.""" + try: + rels_xml = self.blob_for(source_uri.rels_uri) + except KeyError: + rels_xml = None + return rels_xml + + +class _ZipPkgWriter(PhysPkgWriter): + """Implements |PhysPkgWriter| interface for a zip file OPC package.""" + + def __init__(self, pkg_file): + super(_ZipPkgWriter, self).__init__() + self._zipf = ZipFile(pkg_file, "w", compression=ZIP_DEFLATED) + + def close(self): + """Close the zip archive, flushing any pending physical writes and releasing any + resources it's using.""" + self._zipf.close() + + def write(self, pack_uri, blob): + """Write `blob` to this zip package with the membername corresponding to + `pack_uri`.""" + self._zipf.writestr(pack_uri.membername, blob) diff --git a/docx/opc/pkgreader.py b/src/docx/opc/pkgreader.py similarity index 57% rename from docx/opc/pkgreader.py rename to src/docx/opc/pkgreader.py index ae80b3586..f00e7b5f0 100644 --- a/docx/opc/pkgreader.py +++ b/src/docx/opc/pkgreader.py @@ -1,24 +1,16 @@ -# encoding: utf-8 +"""Low-level, read-only API to a serialized Open Packaging Convention (OPC) package.""" -""" -Provides a low-level, read-only API to a serialized Open Packaging Convention -(OPC) package. -""" +from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from docx.opc.oxml import parse_xml +from docx.opc.packuri import PACKAGE_URI, PackURI +from docx.opc.phys_pkg import PhysPkgReader +from docx.opc.shared import CaseInsensitiveDict -from __future__ import absolute_import -from .constants import RELATIONSHIP_TARGET_MODE as RTM -from .oxml import parse_xml -from .packuri import PACKAGE_URI, PackURI -from .phys_pkg import PhysPkgReader -from .shared import CaseInsensitiveDict +class PackageReader: + """Provides access to the contents of a zip-format OPC package via its + :attr:`serialized_parts` and :attr:`pkg_srels` attributes.""" - -class PackageReader(object): - """ - Provides access to the contents of a zip-format OPC package via its - :attr:`serialized_parts` and :attr:`pkg_srels` attributes. - """ def __init__(self, content_types, pkg_srels, sparts): super(PackageReader, self).__init__() self._pkg_srels = pkg_srels @@ -26,9 +18,7 @@ def __init__(self, content_types, pkg_srels, sparts): @staticmethod def from_file(pkg_file): - """ - Return a |PackageReader| instance loaded with contents of *pkg_file*. - """ + """Return a |PackageReader| instance loaded with contents of `pkg_file`.""" phys_reader = PhysPkgReader(pkg_file) content_types = _ContentTypeMap.from_xml(phys_reader.content_types_xml) pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI) @@ -39,18 +29,14 @@ def from_file(pkg_file): return PackageReader(content_types, pkg_srels, sparts) def iter_sparts(self): - """ - Generate a 4-tuple `(partname, content_type, reltype, blob)` for each - of the serialized parts in the package. - """ + """Generate a 4-tuple `(partname, content_type, reltype, blob)` for each of the + serialized parts in the package.""" for s in self._sparts: yield (s.partname, s.content_type, s.reltype, s.blob) def iter_srels(self): - """ - Generate a 2-tuple `(source_uri, srel)` for each of the relationships - in the package. - """ + """Generate a 2-tuple `(source_uri, srel)` for each of the relationships in the + package.""" for srel in self._pkg_srels: yield (PACKAGE_URI, srel) for spart in self._sparts: @@ -59,38 +45,28 @@ def iter_srels(self): @staticmethod def _load_serialized_parts(phys_reader, pkg_srels, content_types): - """ - Return a list of |_SerializedPart| instances corresponding to the - parts in *phys_reader* accessible by walking the relationship graph - starting with *pkg_srels*. - """ + """Return a list of |_SerializedPart| instances corresponding to the parts in + `phys_reader` accessible by walking the relationship graph starting with + `pkg_srels`.""" sparts = [] part_walker = PackageReader._walk_phys_parts(phys_reader, pkg_srels) for partname, blob, reltype, srels in part_walker: content_type = content_types[partname] - spart = _SerializedPart( - partname, content_type, reltype, blob, srels - ) + spart = _SerializedPart(partname, content_type, reltype, blob, srels) sparts.append(spart) return tuple(sparts) @staticmethod def _srels_for(phys_reader, source_uri): - """ - Return |_SerializedRelationships| instance populated with - relationships for source identified by *source_uri*. - """ + """Return |_SerializedRelationships| instance populated with relationships for + source identified by `source_uri`.""" rels_xml = phys_reader.rels_xml_for(source_uri) - return _SerializedRelationships.load_from_xml( - source_uri.baseURI, rels_xml) + return _SerializedRelationships.load_from_xml(source_uri.baseURI, rels_xml) @staticmethod def _walk_phys_parts(phys_reader, srels, visited_partnames=None): - """ - Generate a 4-tuple `(partname, blob, reltype, srels)` for each of the - parts in *phys_reader* by walking the relationship graph rooted at - srels. - """ + """Generate a 4-tuple `(partname, blob, reltype, srels)` for each of the parts + in `phys_reader` by walking the relationship graph rooted at srels.""" if visited_partnames is None: visited_partnames = [] for srel in srels: @@ -111,20 +87,17 @@ def _walk_phys_parts(phys_reader, srels, visited_partnames=None): yield (partname, blob, reltype, srels) -class _ContentTypeMap(object): - """ - Value type providing dictionary semantics for looking up content type by - part name, e.g. ``content_type = cti['/ppt/presentation.xml']``. - """ +class _ContentTypeMap: + """Value type providing dictionary semantics for looking up content type by part + name, e.g. ``content_type = cti['/ppt/presentation.xml']``.""" + def __init__(self): super(_ContentTypeMap, self).__init__() self._overrides = CaseInsensitiveDict() self._defaults = CaseInsensitiveDict() def __getitem__(self, partname): - """ - Return content type for part identified by *partname*. - """ + """Return content type for part identified by `partname`.""" if not isinstance(partname, PackURI): tmpl = "_ContentTypeMap key must be , got %s" raise KeyError(tmpl % type(partname)) @@ -137,10 +110,8 @@ def __getitem__(self, partname): @staticmethod def from_xml(content_types_xml): - """ - Return a new |_ContentTypeMap| instance populated with the contents - of *content_types_xml*. - """ + """Return a new |_ContentTypeMap| instance populated with the contents of + `content_types_xml`.""" types_elm = parse_xml(content_types_xml) ct_map = _ContentTypeMap() for o in types_elm.overrides: @@ -150,25 +121,23 @@ def from_xml(content_types_xml): return ct_map def _add_default(self, extension, content_type): - """ - Add the default mapping of *extension* to *content_type* to this - content type mapping. - """ + """Add the default mapping of `extension` to `content_type` to this content type + mapping.""" self._defaults[extension] = content_type def _add_override(self, partname, content_type): - """ - Add the default mapping of *partname* to *content_type* to this - content type mapping. - """ + """Add the default mapping of `partname` to `content_type` to this content type + mapping.""" self._overrides[partname] = content_type -class _SerializedPart(object): - """ - Value object for an OPC package part. Provides access to the partname, - content type, blob, and serialized relationships for the part. +class _SerializedPart: + """Value object for an OPC package part. + + Provides access to the partname, content type, blob, and serialized relationships + for the part. """ + def __init__(self, partname, content_type, reltype, blob, srels): super(_SerializedPart, self).__init__() self._partname = partname @@ -191,9 +160,7 @@ def blob(self): @property def reltype(self): - """ - The referring relationship type of this part. - """ + """The referring relationship type of this part.""" return self._reltype @property @@ -201,12 +168,13 @@ def srels(self): return self._srels -class _SerializedRelationship(object): - """ - Value object representing a serialized relationship in an OPC package. - Serialized, in this case, means any target part is referred to via its - partname rather than a direct link to an in-memory |Part| object. +class _SerializedRelationship: + """Value object representing a serialized relationship in an OPC package. + + Serialized, in this case, means any target part is referred to via its partname + rather than a direct link to an in-memory |Part| object. """ + def __init__(self, baseURI, rel_elm): super(_SerializedRelationship, self).__init__() self._baseURI = baseURI @@ -217,9 +185,7 @@ def __init__(self, baseURI, rel_elm): @property def is_external(self): - """ - True if target_mode is ``RTM.EXTERNAL`` - """ + """True if target_mode is ``RTM.EXTERNAL``""" return self._target_mode == RTM.EXTERNAL @property @@ -229,66 +195,60 @@ def reltype(self): @property def rId(self): - """ - Relationship id, like 'rId9', corresponds to the ``Id`` attribute on - the ``CT_Relationship`` element. - """ + """Relationship id, like 'rId9', corresponds to the ``Id`` attribute on the + ``CT_Relationship`` element.""" return self._rId @property def target_mode(self): - """ - String in ``TargetMode`` attribute of ``CT_Relationship`` element, - one of ``RTM.INTERNAL`` or ``RTM.EXTERNAL``. - """ + """String in ``TargetMode`` attribute of ``CT_Relationship`` element, one of + ``RTM.INTERNAL`` or ``RTM.EXTERNAL``.""" return self._target_mode @property def target_ref(self): - """ - String in ``Target`` attribute of ``CT_Relationship`` element, a - relative part reference for internal target mode or an arbitrary URI, - e.g. an HTTP URL, for external target mode. - """ + """String in ``Target`` attribute of ``CT_Relationship`` element, a relative + part reference for internal target mode or an arbitrary URI, e.g. an HTTP URL, + for external target mode.""" return self._target_ref @property def target_partname(self): - """ - |PackURI| instance containing partname targeted by this relationship. - Raises ``ValueError`` on reference if target_mode is ``'External'``. - Use :attr:`target_mode` to check before referencing. + """|PackURI| instance containing partname targeted by this relationship. + + Raises ``ValueError`` on reference if target_mode is ``'External'``. Use + :attr:`target_mode` to check before referencing. """ if self.is_external: - msg = ('target_partname attribute on Relationship is undefined w' - 'here TargetMode == "External"') + msg = ( + "target_partname attribute on Relationship is undefined w" + 'here TargetMode == "External"' + ) raise ValueError(msg) # lazy-load _target_partname attribute - if not hasattr(self, '_target_partname'): - self._target_partname = PackURI.from_rel_ref(self._baseURI, - self.target_ref) + if not hasattr(self, "_target_partname"): + self._target_partname = PackURI.from_rel_ref(self._baseURI, self.target_ref) return self._target_partname -class _SerializedRelationships(object): - """ - Read-only sequence of |_SerializedRelationship| instances corresponding - to the relationships item XML passed to constructor. - """ +class _SerializedRelationships: + """Read-only sequence of |_SerializedRelationship| instances corresponding to the + relationships item XML passed to constructor.""" + def __init__(self): super(_SerializedRelationships, self).__init__() self._srels = [] def __iter__(self): - """Support iteration, e.g. 'for x in srels:'""" + """Support iteration, e.g. 'for x in srels:'.""" return self._srels.__iter__() @staticmethod def load_from_xml(baseURI, rels_item_xml): - """ - Return |_SerializedRelationships| instance loaded with the - relationships contained in *rels_item_xml*. Returns an empty - collection if *rels_item_xml* is |None|. + """Return |_SerializedRelationships| instance loaded with the relationships + contained in `rels_item_xml`. + + Returns an empty collection if `rels_item_xml` is |None|. """ srels = _SerializedRelationships() if rels_item_xml is not None: diff --git a/src/docx/opc/pkgwriter.py b/src/docx/opc/pkgwriter.py new file mode 100644 index 000000000..75af6ac75 --- /dev/null +++ b/src/docx/opc/pkgwriter.py @@ -0,0 +1,108 @@ +"""Provides low-level, write-only API to serialized (OPC) package. + +OPC stands for Open Packaging Convention. This is e, essentially an implementation of +OpcPackage.save(). +""" + +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.oxml import CT_Types, serialize_part_xml +from docx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI +from docx.opc.phys_pkg import PhysPkgWriter +from docx.opc.shared import CaseInsensitiveDict +from docx.opc.spec import default_content_types + + +class PackageWriter: + """Writes a zip-format OPC package to `pkg_file`, where `pkg_file` can be either a + path to a zip file (a string) or a file-like object. + + Its single API method, :meth:`write`, is static, so this class is not intended to be + instantiated. + """ + + @staticmethod + def write(pkg_file, pkg_rels, parts): + """Write a physical package (.pptx file) to `pkg_file` containing `pkg_rels` and + `parts` and a content types stream based on the content types of the parts.""" + phys_writer = PhysPkgWriter(pkg_file) + PackageWriter._write_content_types_stream(phys_writer, parts) + PackageWriter._write_pkg_rels(phys_writer, pkg_rels) + PackageWriter._write_parts(phys_writer, parts) + phys_writer.close() + + @staticmethod + def _write_content_types_stream(phys_writer, parts): + """Write ``[Content_Types].xml`` part to the physical package with an + appropriate content type lookup target for each part in `parts`.""" + cti = _ContentTypesItem.from_parts(parts) + phys_writer.write(CONTENT_TYPES_URI, cti.blob) + + @staticmethod + def _write_parts(phys_writer, parts): + """Write the blob of each part in `parts` to the package, along with a rels item + for its relationships if and only if it has any.""" + for part in parts: + phys_writer.write(part.partname, part.blob) + if len(part._rels): + phys_writer.write(part.partname.rels_uri, part._rels.xml) + + @staticmethod + def _write_pkg_rels(phys_writer, pkg_rels): + """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package.""" + phys_writer.write(PACKAGE_URI.rels_uri, pkg_rels.xml) + + +class _ContentTypesItem: + """Service class that composes a content types item ([Content_Types].xml) based on a + list of parts. + + Not meant to be instantiated directly, its single interface method is xml_for(), + e.g. ``_ContentTypesItem.xml_for(parts)``. + """ + + def __init__(self): + self._defaults = CaseInsensitiveDict() + self._overrides = {} + + @property + def blob(self): + """Return XML form of this content types item, suitable for storage as + ``[Content_Types].xml`` in an OPC package.""" + return serialize_part_xml(self._element) + + @classmethod + def from_parts(cls, parts): + """Return content types XML mapping each part in `parts` to the appropriate + content type and suitable for storage as ``[Content_Types].xml`` in an OPC + package.""" + cti = cls() + cti._defaults["rels"] = CT.OPC_RELATIONSHIPS + cti._defaults["xml"] = CT.XML + for part in parts: + cti._add_content_type(part.partname, part.content_type) + return cti + + def _add_content_type(self, partname, content_type): + """Add a content type for the part with `partname` and `content_type`, using a + default or override as appropriate.""" + ext = partname.ext + if (ext.lower(), content_type) in default_content_types: + self._defaults[ext] = content_type + else: + self._overrides[partname] = content_type + + @property + def _element(self): + """Return XML form of this content types item, suitable for storage as + ``[Content_Types].xml`` in an OPC package. + + Although the sequence of elements is not strictly significant, as an aid to + testing and readability Default elements are sorted by extension and Override + elements are sorted by partname. + """ + _types_elm = CT_Types.new() + for ext in sorted(self._defaults.keys()): + _types_elm.add_default(ext, self._defaults[ext]) + for partname in sorted(self._overrides.keys()): + _types_elm.add_override(partname, self._overrides[partname]) + return _types_elm diff --git a/docx/opc/rel.py b/src/docx/opc/rel.py similarity index 55% rename from docx/opc/rel.py rename to src/docx/opc/rel.py index 7dba2af8e..efac5e06b 100644 --- a/docx/opc/rel.py +++ b/src/docx/opc/rel.py @@ -1,29 +1,24 @@ -# encoding: utf-8 +"""Relationship-related objects.""" -""" -Relationship-related objects. -""" +from __future__ import annotations -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from typing import Any, Dict -from .oxml import CT_Relationships +from docx.opc.oxml import CT_Relationships -class Relationships(dict): - """ - Collection object for |_Relationship| instances, having list semantics. - """ - def __init__(self, baseURI): +class Relationships(Dict[str, "_Relationship"]): + """Collection object for |_Relationship| instances, having list semantics.""" + + def __init__(self, baseURI: str): super(Relationships, self).__init__() self._baseURI = baseURI - self._target_parts_by_rId = {} + self._target_parts_by_rId: Dict[str, Any] = {} - def add_relationship(self, reltype, target, rId, is_external=False): - """ - Return a newly added |_Relationship| instance. - """ + def add_relationship( + self, reltype: str, target: str | Any, rId: str, is_external: bool = False + ) -> "_Relationship": + """Return a newly added |_Relationship| instance.""" rel = _Relationship(rId, reltype, target, self._baseURI, is_external) self[rId] = rel if not is_external: @@ -31,10 +26,8 @@ def add_relationship(self, reltype, target, rId, is_external=False): return rel def get_or_add(self, reltype, target_part): - """ - Return relationship of *reltype* to *target_part*, newly added if not - already present in collection. - """ + """Return relationship of `reltype` to `target_part`, newly added if not already + present in collection.""" rel = self._get_matching(reltype, target_part) if rel is None: rId = self._next_rId @@ -42,53 +35,39 @@ def get_or_add(self, reltype, target_part): return rel def get_or_add_ext_rel(self, reltype, target_ref): - """ - Return rId of external relationship of *reltype* to *target_ref*, - newly added if not already present in collection. - """ + """Return rId of external relationship of `reltype` to `target_ref`, newly added + if not already present in collection.""" rel = self._get_matching(reltype, target_ref, is_external=True) if rel is None: rId = self._next_rId - rel = self.add_relationship( - reltype, target_ref, rId, is_external=True - ) + rel = self.add_relationship(reltype, target_ref, rId, is_external=True) return rel.rId def part_with_reltype(self, reltype): - """ - Return target part of rel with matching *reltype*, raising |KeyError| - if not found and |ValueError| if more than one matching relationship - is found. - """ + """Return target part of rel with matching `reltype`, raising |KeyError| if not + found and |ValueError| if more than one matching relationship is found.""" rel = self._get_rel_of_type(reltype) return rel.target_part @property def related_parts(self): - """ - dict mapping rIds to target parts for all the internal relationships - in the collection. - """ + """Dict mapping rIds to target parts for all the internal relationships in the + collection.""" return self._target_parts_by_rId @property def xml(self): - """ - Serialize this relationship collection into XML suitable for storage - as a .rels file in an OPC package. - """ + """Serialize this relationship collection into XML suitable for storage as a + .rels file in an OPC package.""" rels_elm = CT_Relationships.new() for rel in self.values(): - rels_elm.add_rel( - rel.rId, rel.reltype, rel.target_ref, rel.is_external - ) + rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external) return rels_elm.xml def _get_matching(self, reltype, target, is_external=False): - """ - Return relationship of matching *reltype*, *target*, and - *is_external* from collection, or None if not found. - """ + """Return relationship of matching `reltype`, `target`, and `is_external` from + collection, or None if not found.""" + def matches(rel, reltype, target, is_external): if rel.reltype != reltype: return False @@ -105,10 +84,10 @@ def matches(rel, reltype, target, is_external): return None def _get_rel_of_type(self, reltype): - """ - Return single relationship of type *reltype* from the collection. - Raises |KeyError| if no matching relationship is found. Raises - |ValueError| if more than one matching relationship is found. + """Return single relationship of type `reltype` from the collection. + + Raises |KeyError| if no matching relationship is found. Raises |ValueError| if + more than one matching relationship is found. """ matching = [rel for rel in self.values() if rel.reltype == reltype] if len(matching) == 0: @@ -121,21 +100,18 @@ def _get_rel_of_type(self, reltype): @property def _next_rId(self): - """ - Next available rId in collection, starting from 'rId1' and making use - of any gaps in numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. - """ - for n in range(1, len(self)+2): - rId_candidate = 'rId%d' % n # like 'rId19' + """Next available rId in collection, starting from 'rId1' and making use of any + gaps in numbering, e.g. 'rId2' for rIds ['rId1', 'rId3'].""" + for n in range(1, len(self) + 2): + rId_candidate = "rId%d" % n # like 'rId19' if rId_candidate not in self: return rId_candidate -class _Relationship(object): - """ - Value object for relationship to part. - """ - def __init__(self, rId, reltype, target, baseURI, external=False): +class _Relationship: + """Value object for relationship to part.""" + + def __init__(self, rId: str, reltype, target, baseURI, external=False): super(_Relationship, self).__init__() self._rId = rId self._reltype = reltype @@ -158,12 +134,14 @@ def rId(self): @property def target_part(self): if self._is_external: - raise ValueError("target_part property on _Relationship is undef" - "ined when target mode is External") + raise ValueError( + "target_part property on _Relationship is undef" + "ined when target mode is External" + ) return self._target @property - def target_ref(self): + def target_ref(self) -> str: if self._is_external: return self._target else: diff --git a/src/docx/opc/shared.py b/src/docx/opc/shared.py new file mode 100644 index 000000000..1862f66db --- /dev/null +++ b/src/docx/opc/shared.py @@ -0,0 +1,45 @@ +"""Objects shared by opc modules.""" + + +class CaseInsensitiveDict(dict): + """Mapping type that behaves like dict except that it matches without respect to the + case of the key. + + E.g. cid['A'] == cid['a']. Note this is not general-purpose, just complete enough to + satisfy opc package needs. It assumes str keys, and that it is created empty; keys + passed in constructor are not accounted for + """ + + def __contains__(self, key): + return super(CaseInsensitiveDict, self).__contains__(key.lower()) + + def __getitem__(self, key): + return super(CaseInsensitiveDict, self).__getitem__(key.lower()) + + def __setitem__(self, key, value): + return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) + + +def cls_method_fn(cls: type, method_name: str): + """Return method of `cls` having `method_name`.""" + return getattr(cls, method_name) + + +def lazyproperty(f): + """@lazyprop decorator. + + Decorated method will be called only on first access to calculate a cached property + value. After that, the cached value is returned. + """ + cache_attr_name = "_%s" % f.__name__ # like '_foobar' for prop 'foobar' + docstring = f.__doc__ + + def get_prop_value(obj): + try: + return getattr(obj, cache_attr_name) + except AttributeError: + value = f(obj) + setattr(obj, cache_attr_name, value) + return value + + return property(get_prop_value, doc=docstring) diff --git a/src/docx/opc/spec.py b/src/docx/opc/spec.py new file mode 100644 index 000000000..011a4825d --- /dev/null +++ b/src/docx/opc/spec.py @@ -0,0 +1,24 @@ +"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500.""" + +from docx.opc.constants import CONTENT_TYPE as CT + +default_content_types = ( + ("bin", CT.PML_PRINTER_SETTINGS), + ("bin", CT.SML_PRINTER_SETTINGS), + ("bin", CT.WML_PRINTER_SETTINGS), + ("bmp", CT.BMP), + ("emf", CT.X_EMF), + ("fntdata", CT.X_FONTDATA), + ("gif", CT.GIF), + ("jpe", CT.JPEG), + ("jpeg", CT.JPEG), + ("jpg", CT.JPEG), + ("png", CT.PNG), + ("rels", CT.OPC_RELATIONSHIPS), + ("tif", CT.TIFF), + ("tiff", CT.TIFF), + ("wdp", CT.MS_PHOTO), + ("wmf", CT.X_WMF), + ("xlsx", CT.SML_SHEET), + ("xml", CT.XML), +) diff --git a/src/docx/oxml/__init__.py b/src/docx/oxml/__init__.py new file mode 100644 index 000000000..bf8d00962 --- /dev/null +++ b/src/docx/oxml/__init__.py @@ -0,0 +1,234 @@ +"""Initializes oxml sub-package. + +This including registering custom element classes corresponding to Open XML elements. +""" + +from __future__ import annotations + +from docx.oxml.drawing import CT_Drawing +from docx.oxml.parser import register_element_cls +from docx.oxml.shape import ( + CT_Anchor, + CT_Blip, + CT_BlipFillProperties, + CT_GraphicalObject, + CT_GraphicalObjectData, + CT_Inline, + CT_NonVisualDrawingProps, + CT_Picture, + CT_PictureNonVisual, + CT_Point2D, + CT_PositiveSize2D, + CT_ShapeProperties, + CT_Transform2D, +) +from docx.oxml.shared import CT_DecimalNumber, CT_OnOff, CT_String +from docx.oxml.text.hyperlink import CT_Hyperlink +from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak +from docx.oxml.text.run import ( + CT_R, + CT_Br, + CT_Cr, + CT_NoBreakHyphen, + CT_PTab, + CT_Text, +) + +# --------------------------------------------------------------------------- +# DrawingML-related elements + +register_element_cls("a:blip", CT_Blip) +register_element_cls("a:ext", CT_PositiveSize2D) +register_element_cls("a:graphic", CT_GraphicalObject) +register_element_cls("a:graphicData", CT_GraphicalObjectData) +register_element_cls("a:off", CT_Point2D) +register_element_cls("a:xfrm", CT_Transform2D) +register_element_cls("pic:blipFill", CT_BlipFillProperties) +register_element_cls("pic:cNvPr", CT_NonVisualDrawingProps) +register_element_cls("pic:nvPicPr", CT_PictureNonVisual) +register_element_cls("pic:pic", CT_Picture) +register_element_cls("pic:spPr", CT_ShapeProperties) +register_element_cls("w:drawing", CT_Drawing) +register_element_cls("wp:anchor", CT_Anchor) +register_element_cls("wp:docPr", CT_NonVisualDrawingProps) +register_element_cls("wp:extent", CT_PositiveSize2D) +register_element_cls("wp:inline", CT_Inline) + +# --------------------------------------------------------------------------- +# hyperlink-related elements + +register_element_cls("w:hyperlink", CT_Hyperlink) + +# --------------------------------------------------------------------------- +# text-related elements + +register_element_cls("w:br", CT_Br) +register_element_cls("w:cr", CT_Cr) +register_element_cls("w:lastRenderedPageBreak", CT_LastRenderedPageBreak) +register_element_cls("w:noBreakHyphen", CT_NoBreakHyphen) +register_element_cls("w:ptab", CT_PTab) +register_element_cls("w:r", CT_R) +register_element_cls("w:t", CT_Text) + +# --------------------------------------------------------------------------- +# header/footer-related mappings + +register_element_cls("w:evenAndOddHeaders", CT_OnOff) +register_element_cls("w:titlePg", CT_OnOff) + +# --------------------------------------------------------------------------- +# other custom element class mappings + +from .coreprops import CT_CoreProperties # noqa + +register_element_cls("cp:coreProperties", CT_CoreProperties) + +from .document import CT_Body, CT_Document # noqa + +register_element_cls("w:body", CT_Body) +register_element_cls("w:document", CT_Document) + +from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr # noqa + +register_element_cls("w:abstractNumId", CT_DecimalNumber) +register_element_cls("w:ilvl", CT_DecimalNumber) +register_element_cls("w:lvlOverride", CT_NumLvl) +register_element_cls("w:num", CT_Num) +register_element_cls("w:numId", CT_DecimalNumber) +register_element_cls("w:numPr", CT_NumPr) +register_element_cls("w:numbering", CT_Numbering) +register_element_cls("w:startOverride", CT_DecimalNumber) + +from .section import ( # noqa + CT_HdrFtr, + CT_HdrFtrRef, + CT_PageMar, + CT_PageSz, + CT_SectPr, + CT_SectType, +) + +register_element_cls("w:footerReference", CT_HdrFtrRef) +register_element_cls("w:ftr", CT_HdrFtr) +register_element_cls("w:hdr", CT_HdrFtr) +register_element_cls("w:headerReference", CT_HdrFtrRef) +register_element_cls("w:pgMar", CT_PageMar) +register_element_cls("w:pgSz", CT_PageSz) +register_element_cls("w:sectPr", CT_SectPr) +register_element_cls("w:type", CT_SectType) + +from .settings import CT_Settings # noqa + +register_element_cls("w:settings", CT_Settings) + +from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa + +register_element_cls("w:basedOn", CT_String) +register_element_cls("w:latentStyles", CT_LatentStyles) +register_element_cls("w:locked", CT_OnOff) +register_element_cls("w:lsdException", CT_LsdException) +register_element_cls("w:name", CT_String) +register_element_cls("w:next", CT_String) +register_element_cls("w:qFormat", CT_OnOff) +register_element_cls("w:semiHidden", CT_OnOff) +register_element_cls("w:style", CT_Style) +register_element_cls("w:styles", CT_Styles) +register_element_cls("w:uiPriority", CT_DecimalNumber) +register_element_cls("w:unhideWhenUsed", CT_OnOff) + +from .table import ( # noqa + CT_Height, + CT_Row, + CT_Tbl, + CT_TblGrid, + CT_TblGridCol, + CT_TblLayoutType, + CT_TblPr, + CT_TblWidth, + CT_Tc, + CT_TcPr, + CT_TrPr, + CT_VMerge, + CT_VerticalJc, +) + +register_element_cls("w:bidiVisual", CT_OnOff) +register_element_cls("w:gridCol", CT_TblGridCol) +register_element_cls("w:gridSpan", CT_DecimalNumber) +register_element_cls("w:tbl", CT_Tbl) +register_element_cls("w:tblGrid", CT_TblGrid) +register_element_cls("w:tblLayout", CT_TblLayoutType) +register_element_cls("w:tblPr", CT_TblPr) +register_element_cls("w:tblStyle", CT_String) +register_element_cls("w:tc", CT_Tc) +register_element_cls("w:tcPr", CT_TcPr) +register_element_cls("w:tcW", CT_TblWidth) +register_element_cls("w:tr", CT_Row) +register_element_cls("w:trHeight", CT_Height) +register_element_cls("w:trPr", CT_TrPr) +register_element_cls("w:vAlign", CT_VerticalJc) +register_element_cls("w:vMerge", CT_VMerge) + +from .text.font import ( # noqa + CT_Color, + CT_Fonts, + CT_Highlight, + CT_HpsMeasure, + CT_RPr, + CT_Underline, + CT_VerticalAlignRun, +) + +register_element_cls("w:b", CT_OnOff) +register_element_cls("w:bCs", CT_OnOff) +register_element_cls("w:caps", CT_OnOff) +register_element_cls("w:color", CT_Color) +register_element_cls("w:cs", CT_OnOff) +register_element_cls("w:dstrike", CT_OnOff) +register_element_cls("w:emboss", CT_OnOff) +register_element_cls("w:highlight", CT_Highlight) +register_element_cls("w:i", CT_OnOff) +register_element_cls("w:iCs", CT_OnOff) +register_element_cls("w:imprint", CT_OnOff) +register_element_cls("w:noProof", CT_OnOff) +register_element_cls("w:oMath", CT_OnOff) +register_element_cls("w:outline", CT_OnOff) +register_element_cls("w:rFonts", CT_Fonts) +register_element_cls("w:rPr", CT_RPr) +register_element_cls("w:rStyle", CT_String) +register_element_cls("w:rtl", CT_OnOff) +register_element_cls("w:shadow", CT_OnOff) +register_element_cls("w:smallCaps", CT_OnOff) +register_element_cls("w:snapToGrid", CT_OnOff) +register_element_cls("w:specVanish", CT_OnOff) +register_element_cls("w:strike", CT_OnOff) +register_element_cls("w:sz", CT_HpsMeasure) +register_element_cls("w:u", CT_Underline) +register_element_cls("w:vanish", CT_OnOff) +register_element_cls("w:vertAlign", CT_VerticalAlignRun) +register_element_cls("w:webHidden", CT_OnOff) + +from .text.paragraph import CT_P # noqa + +register_element_cls("w:p", CT_P) + +from .text.parfmt import ( # noqa + CT_Ind, + CT_Jc, + CT_PPr, + CT_Spacing, + CT_TabStop, + CT_TabStops, +) + +register_element_cls("w:ind", CT_Ind) +register_element_cls("w:jc", CT_Jc) +register_element_cls("w:keepLines", CT_OnOff) +register_element_cls("w:keepNext", CT_OnOff) +register_element_cls("w:pageBreakBefore", CT_OnOff) +register_element_cls("w:pPr", CT_PPr) +register_element_cls("w:pStyle", CT_String) +register_element_cls("w:spacing", CT_Spacing) +register_element_cls("w:tab", CT_TabStop) +register_element_cls("w:tabs", CT_TabStops) +register_element_cls("w:widowControl", CT_OnOff) diff --git a/src/docx/oxml/coreprops.py b/src/docx/oxml/coreprops.py new file mode 100644 index 000000000..2cafcd960 --- /dev/null +++ b/src/docx/oxml/coreprops.py @@ -0,0 +1,289 @@ +"""Custom element classes for core properties-related XML elements.""" + +import re +from datetime import datetime, timedelta +from typing import Any + +from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne + + +class CT_CoreProperties(BaseOxmlElement): + """`` element, the root element of the Core Properties part. + + Stored as `/docProps/core.xml`. Implements many of the Dublin Core document metadata + elements. String elements resolve to an empty string ("") if the element is not + present in the XML. String elements are limited in length to 255 unicode characters. + """ + + category = ZeroOrOne("cp:category", successors=()) + contentStatus = ZeroOrOne("cp:contentStatus", successors=()) + created = ZeroOrOne("dcterms:created", successors=()) + creator = ZeroOrOne("dc:creator", successors=()) + description = ZeroOrOne("dc:description", successors=()) + identifier = ZeroOrOne("dc:identifier", successors=()) + keywords = ZeroOrOne("cp:keywords", successors=()) + language = ZeroOrOne("dc:language", successors=()) + lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=()) + lastPrinted = ZeroOrOne("cp:lastPrinted", successors=()) + modified = ZeroOrOne("dcterms:modified", successors=()) + revision = ZeroOrOne("cp:revision", successors=()) + subject = ZeroOrOne("dc:subject", successors=()) + title = ZeroOrOne("dc:title", successors=()) + version = ZeroOrOne("cp:version", successors=()) + + _coreProperties_tmpl = "\n" % nsdecls("cp", "dc", "dcterms") + + @classmethod + def new(cls): + """Return a new `` element.""" + xml = cls._coreProperties_tmpl + coreProperties = parse_xml(xml) + return coreProperties + + @property + def author_text(self): + """The text in the `dc:creator` child element.""" + return self._text_of_element("creator") + + @author_text.setter + def author_text(self, value: str): + self._set_element_text("creator", value) + + @property + def category_text(self) -> str: + return self._text_of_element("category") + + @category_text.setter + def category_text(self, value: str): + self._set_element_text("category", value) + + @property + def comments_text(self) -> str: + return self._text_of_element("description") + + @comments_text.setter + def comments_text(self, value: str): + self._set_element_text("description", value) + + @property + def contentStatus_text(self): + return self._text_of_element("contentStatus") + + @contentStatus_text.setter + def contentStatus_text(self, value: str): + self._set_element_text("contentStatus", value) + + @property + def created_datetime(self): + return self._datetime_of_element("created") + + @created_datetime.setter + def created_datetime(self, value): + self._set_element_datetime("created", value) + + @property + def identifier_text(self): + return self._text_of_element("identifier") + + @identifier_text.setter + def identifier_text(self, value): + self._set_element_text("identifier", value) + + @property + def keywords_text(self): + return self._text_of_element("keywords") + + @keywords_text.setter + def keywords_text(self, value): + self._set_element_text("keywords", value) + + @property + def language_text(self): + return self._text_of_element("language") + + @language_text.setter + def language_text(self, value): + self._set_element_text("language", value) + + @property + def lastModifiedBy_text(self): + return self._text_of_element("lastModifiedBy") + + @lastModifiedBy_text.setter + def lastModifiedBy_text(self, value): + self._set_element_text("lastModifiedBy", value) + + @property + def lastPrinted_datetime(self): + return self._datetime_of_element("lastPrinted") + + @lastPrinted_datetime.setter + def lastPrinted_datetime(self, value): + self._set_element_datetime("lastPrinted", value) + + @property + def modified_datetime(self): + return self._datetime_of_element("modified") + + @modified_datetime.setter + def modified_datetime(self, value): + self._set_element_datetime("modified", value) + + @property + def revision_number(self): + """Integer value of revision property.""" + revision = self.revision + if revision is None: + return 0 + revision_str = revision.text + try: + revision = int(revision_str) + except ValueError: + # non-integer revision strings also resolve to 0 + revision = 0 + # as do negative integers + if revision < 0: + revision = 0 + return revision + + @revision_number.setter + def revision_number(self, value): + """Set revision property to string value of integer `value`.""" + if not isinstance(value, int) or value < 1: + tmpl = "revision property requires positive int, got '%s'" + raise ValueError(tmpl % value) + revision = self.get_or_add_revision() + revision.text = str(value) + + @property + def subject_text(self): + return self._text_of_element("subject") + + @subject_text.setter + def subject_text(self, value): + self._set_element_text("subject", value) + + @property + def title_text(self): + return self._text_of_element("title") + + @title_text.setter + def title_text(self, value): + self._set_element_text("title", value) + + @property + def version_text(self): + return self._text_of_element("version") + + @version_text.setter + def version_text(self, value): + self._set_element_text("version", value) + + def _datetime_of_element(self, property_name): + element = getattr(self, property_name) + if element is None: + return None + datetime_str = element.text + try: + return self._parse_W3CDTF_to_datetime(datetime_str) + except ValueError: + # invalid datetime strings are ignored + return None + + def _get_or_add(self, prop_name): + """Return element returned by "get_or_add_" method for `prop_name`.""" + get_or_add_method_name = "get_or_add_%s" % prop_name + get_or_add_method = getattr(self, get_or_add_method_name) + element = get_or_add_method() + return element + + @classmethod + def _offset_dt(cls, dt, offset_str): + """A |datetime| instance offset from `dt` by timezone offset in `offset_str`. + + `offset_str` is like `"-07:00"`. + """ + match = cls._offset_pattern.match(offset_str) + if match is None: + raise ValueError("'%s' is not a valid offset string" % offset_str) + sign, hours_str, minutes_str = match.groups() + sign_factor = -1 if sign == "+" else 1 + hours = int(hours_str) * sign_factor + minutes = int(minutes_str) * sign_factor + td = timedelta(hours=hours, minutes=minutes) + return dt + td + + _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)") + + @classmethod + def _parse_W3CDTF_to_datetime(cls, w3cdtf_str): + # valid W3CDTF date cases: + # yyyy e.g. "2003" + # yyyy-mm e.g. "2003-12" + # yyyy-mm-dd e.g. "2003-12-31" + # UTC timezone e.g. "2003-12-31T10:14:55Z" + # numeric timezone e.g. "2003-12-31T10:14:55-08:00" + templates = ( + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d", + "%Y-%m", + "%Y", + ) + # strptime isn't smart enough to parse literal timezone offsets like + # "-07:30", so we have to do it ourselves + parseable_part = w3cdtf_str[:19] + offset_str = w3cdtf_str[19:] + dt = None + for tmpl in templates: + try: + dt = datetime.strptime(parseable_part, tmpl) + except ValueError: + continue + if dt is None: + tmpl = "could not parse W3CDTF datetime string '%s'" + raise ValueError(tmpl % w3cdtf_str) + if len(offset_str) == 6: + return cls._offset_dt(dt, offset_str) + return dt + + def _set_element_datetime(self, prop_name, value): + """Set date/time value of child element having `prop_name` to `value`.""" + if not isinstance(value, datetime): + tmpl = "property requires object, got %s" + raise ValueError(tmpl % type(value)) + element = self._get_or_add(prop_name) + dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ") + element.text = dt_str + if prop_name in ("created", "modified"): + # These two require an explicit "xsi:type="dcterms:W3CDTF"" + # attribute. The first and last line are a hack required to add + # the xsi namespace to the root element rather than each child + # element in which it is referenced + self.set(qn("xsi:foo"), "bar") + element.set(qn("xsi:type"), "dcterms:W3CDTF") + del self.attrib[qn("xsi:foo")] + + def _set_element_text(self, prop_name: str, value: Any) -> None: + """Set string value of `name` property to `value`.""" + if not isinstance(value, str): + value = str(value) + + if len(value) > 255: + tmpl = "exceeded 255 char limit for property, got:\n\n'%s'" + raise ValueError(tmpl % value) + element = self._get_or_add(prop_name) + element.text = value + + def _text_of_element(self, property_name: str) -> str: + """The text in the element matching `property_name`. + + The empty string if the element is not present or contains no text. + """ + element = getattr(self, property_name) + if element is None: + return "" + if element.text is None: + return "" + return element.text diff --git a/src/docx/oxml/document.py b/src/docx/oxml/document.py new file mode 100644 index 000000000..c4894f601 --- /dev/null +++ b/src/docx/oxml/document.py @@ -0,0 +1,68 @@ +"""Custom element classes that correspond to the document part, e.g. .""" + +from __future__ import annotations + +from typing import List + +from docx.oxml.section import CT_SectPr +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne + + +class CT_Document(BaseOxmlElement): + """```` element, the root element of a document.xml file.""" + + body = ZeroOrOne("w:body") + + @property + def sectPr_lst(self) -> List[CT_SectPr]: + """All `w:sectPr` elements directly accessible from document element. + + Note this does not include a `sectPr` child in a paragraphs wrapped in + revision marks or other intervening layer, perhaps `w:sdt` or customXml + elements. + + `w:sectPr` elements appear in document order. The last one is always + `w:body/w:sectPr`, all preceding are `w:p/w:pPr/w:sectPr`. + """ + xpath = "./w:body/w:p/w:pPr/w:sectPr | ./w:body/w:sectPr" + return self.xpath(xpath) + + +class CT_Body(BaseOxmlElement): + """````, the container element for the main document story in + ``document.xml``.""" + + p = ZeroOrMore("w:p", successors=("w:sectPr",)) + tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",)) + sectPr = ZeroOrOne("w:sectPr", successors=()) + + def add_section_break(self): + """Return `w:sectPr` element for new section added at end of document. + + The last `w:sectPr` becomes the second-to-last, with the new `w:sectPr` being an + exact clone of the previous one, except that all header and footer references + are removed (and are therefore now "inherited" from the prior section). + + A copy of the previously-last `w:sectPr` will now appear in a new `w:p` at the + end of the document. The returned `w:sectPr` is the sentinel `w:sectPr` for the + document (and as implemented, `is` the prior sentinel `w:sectPr` with headers + and footers removed). + """ + # ---get the sectPr at file-end, which controls last section (sections[-1])--- + sentinel_sectPr = self.get_or_add_sectPr() + # ---add exact copy to new `w:p` element; that is now second-to last section--- + self.add_p().set_sectPr(sentinel_sectPr.clone()) + # ---remove any header or footer references from "new" last section--- + for hdrftr_ref in sentinel_sectPr.xpath("w:headerReference|w:footerReference"): + sentinel_sectPr.remove(hdrftr_ref) + # ---the sentinel `w:sectPr` now controls the new last section--- + return sentinel_sectPr + + def clear_content(self): + """Remove all content child elements from this element. + + Leave the element if it is present. + """ + content_elms = self[:-1] if self.sectPr is not None else self[:] + for content_elm in content_elms: + self.remove(content_elm) diff --git a/src/docx/oxml/drawing.py b/src/docx/oxml/drawing.py new file mode 100644 index 000000000..5b627f973 --- /dev/null +++ b/src/docx/oxml/drawing.py @@ -0,0 +1,11 @@ +"""Custom element-classes for DrawingML-related elements like ``. + +For legacy reasons, many DrawingML-related elements are in `docx.oxml.shape`. Expect +those to move over here as we have reason to touch them. +""" + +from docx.oxml.xmlchemy import BaseOxmlElement + + +class CT_Drawing(BaseOxmlElement): + """`` element, containing a DrawingML object like a picture or chart.""" diff --git a/src/docx/oxml/exceptions.py b/src/docx/oxml/exceptions.py new file mode 100644 index 000000000..8919239a2 --- /dev/null +++ b/src/docx/oxml/exceptions.py @@ -0,0 +1,10 @@ +"""Exceptions for oxml sub-package.""" + + +class XmlchemyError(Exception): + """Generic error class.""" + + +class InvalidXmlError(XmlchemyError): + """Raised when invalid XML is encountered, such as on attempt to access a missing + required child element.""" diff --git a/src/docx/oxml/ns.py b/src/docx/oxml/ns.py new file mode 100644 index 000000000..3238864e9 --- /dev/null +++ b/src/docx/oxml/ns.py @@ -0,0 +1,109 @@ +"""Namespace-related objects.""" + +from typing import Any, Dict + +from typing_extensions import Self + +nsmap = { + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "c": "http://schemas.openxmlformats.org/drawingml/2006/chart", + "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", + "dc": "http://purl.org/dc/elements/1.1/", + "dcmitype": "http://purl.org/dc/dcmitype/", + "dcterms": "http://purl.org/dc/terms/", + "dgm": "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "sl": "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xml": "http://www.w3.org/XML/1998/namespace", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", +} + +pfxmap = {value: key for key, value in nsmap.items()} + + +class NamespacePrefixedTag(str): + """Value object that knows the semantics of an XML tag having a namespace prefix.""" + + def __new__(cls, nstag: str, *args: Any): + return super(NamespacePrefixedTag, cls).__new__(cls, nstag) + + def __init__(self, nstag: str): + self._pfx, self._local_part = nstag.split(":") + self._ns_uri = nsmap[self._pfx] + + @property + def clark_name(self) -> str: + return "{%s}%s" % (self._ns_uri, self._local_part) + + @classmethod + def from_clark_name(cls, clark_name: str) -> Self: + nsuri, local_name = clark_name[1:].split("}") + nstag = "%s:%s" % (pfxmap[nsuri], local_name) + return cls(nstag) + + @property + def local_part(self) -> str: + """The local part of this tag. + + E.g. "foobar" is returned for tag "f:foobar". + """ + return self._local_part + + @property + def nsmap(self) -> Dict[str, str]: + """Single-member dict mapping prefix of this tag to it's namespace name. + + Example: `{"f": "http://foo/bar"}`. This is handy for passing to xpath calls + and other uses. + """ + return {self._pfx: self._ns_uri} + + @property + def nspfx(self) -> str: + """The namespace-prefix for this tag. + + For example, "f" is returned for tag "f:foobar". + """ + return self._pfx + + @property + def nsuri(self) -> str: + """The namespace URI for this tag. + + For example, "http://foo/bar" would be returned for tag "f:foobar" if the "f" + prefix maps to "http://foo/bar" in nsmap. + """ + return self._ns_uri + + +def nsdecls(*prefixes: str) -> str: + """Namespace declaration including each namespace-prefix in `prefixes`. + + Handy for adding required namespace declarations to a tree root element. + """ + return " ".join(['xmlns:%s="%s"' % (pfx, nsmap[pfx]) for pfx in prefixes]) + + +def nspfxmap(*nspfxs: str) -> Dict[str, str]: + """Subset namespace-prefix mappings specified by *nspfxs*. + + Any number of namespace prefixes can be supplied, e.g. namespaces("a", "r", "p"). + """ + return {pfx: nsmap[pfx] for pfx in nspfxs} + + +def qn(tag: str) -> str: + """Stands for "qualified name". + + This utility function converts a familiar namespace-prefixed tag name like "w:p" + into a Clark-notation qualified tag name for lxml. For example, `qn("w:p")` returns + "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p". + """ + prefix, tagroot = tag.split(":") + uri = nsmap[prefix] + return "{%s}%s" % (uri, tagroot) diff --git a/src/docx/oxml/numbering.py b/src/docx/oxml/numbering.py new file mode 100644 index 000000000..3512de655 --- /dev/null +++ b/src/docx/oxml/numbering.py @@ -0,0 +1,109 @@ +"""Custom element classes related to the numbering part.""" + +from docx.oxml.parser import OxmlElement +from docx.oxml.shared import CT_DecimalNumber +from docx.oxml.simpletypes import ST_DecimalNumber +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OneAndOnlyOne, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, +) + + +class CT_Num(BaseOxmlElement): + """```` element, which represents a concrete list definition instance, having + a required child that references an abstract numbering definition + that defines most of the formatting details.""" + + abstractNumId = OneAndOnlyOne("w:abstractNumId") + lvlOverride = ZeroOrMore("w:lvlOverride") + numId = RequiredAttribute("w:numId", ST_DecimalNumber) + + def add_lvlOverride(self, ilvl): + """Return a newly added CT_NumLvl () element having its ``ilvl`` + attribute set to `ilvl`.""" + return self._add_lvlOverride(ilvl=ilvl) + + @classmethod + def new(cls, num_id, abstractNum_id): + """Return a new ```` element having numId of `num_id` and having a + ```` child with val attribute set to `abstractNum_id`.""" + num = OxmlElement("w:num") + num.numId = num_id + abstractNumId = CT_DecimalNumber.new("w:abstractNumId", abstractNum_id) + num.append(abstractNumId) + return num + + +class CT_NumLvl(BaseOxmlElement): + """```` element, which identifies a level in a list definition to + override with settings it contains.""" + + startOverride = ZeroOrOne("w:startOverride", successors=("w:lvl",)) + ilvl = RequiredAttribute("w:ilvl", ST_DecimalNumber) + + def add_startOverride(self, val): + """Return a newly added CT_DecimalNumber element having tagname + ``w:startOverride`` and ``val`` attribute set to `val`.""" + return self._add_startOverride(val=val) + + +class CT_NumPr(BaseOxmlElement): + """A ```` element, a container for numbering properties applied to a + paragraph.""" + + ilvl = ZeroOrOne("w:ilvl", successors=("w:numId", "w:numberingChange", "w:ins")) + numId = ZeroOrOne("w:numId", successors=("w:numberingChange", "w:ins")) + + # @ilvl.setter + # def _set_ilvl(self, val): + # """ + # Get or add a child and set its ``w:val`` attribute to `val`. + # """ + # ilvl = self.get_or_add_ilvl() + # ilvl.val = val + + # @numId.setter + # def numId(self, val): + # """ + # Get or add a child and set its ``w:val`` attribute to + # `val`. + # """ + # numId = self.get_or_add_numId() + # numId.val = val + + +class CT_Numbering(BaseOxmlElement): + """```` element, the root element of a numbering part, i.e. + numbering.xml.""" + + num = ZeroOrMore("w:num", successors=("w:numIdMacAtCleanup",)) + + def add_num(self, abstractNum_id): + """Return a newly added CT_Num () element referencing the abstract + numbering definition identified by `abstractNum_id`.""" + next_num_id = self._next_numId + num = CT_Num.new(next_num_id, abstractNum_id) + return self._insert_num(num) + + def num_having_numId(self, numId): + """Return the ```` child element having ``numId`` attribute matching + `numId`.""" + xpath = './w:num[@w:numId="%d"]' % numId + try: + return self.xpath(xpath)[0] + except IndexError: + raise KeyError("no element with numId %d" % numId) + + @property + def _next_numId(self): + """The first ``numId`` unused by a ```` element, starting at 1 and + filling any gaps in numbering between existing ```` elements.""" + numId_strs = self.xpath("./w:num/@w:numId") + num_ids = [int(numId_str) for numId_str in numId_strs] + for num in range(1, len(num_ids) + 2): + if num not in num_ids: + break + return num diff --git a/src/docx/oxml/parser.py b/src/docx/oxml/parser.py new file mode 100644 index 000000000..7e6a0fb49 --- /dev/null +++ b/src/docx/oxml/parser.py @@ -0,0 +1,60 @@ +"""XML parser for python-docx.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, Type, cast + +from lxml import etree + +from docx.oxml.ns import NamespacePrefixedTag, nsmap + +if TYPE_CHECKING: + from docx.oxml.xmlchemy import BaseOxmlElement + + +# -- configure XML parser -- +element_class_lookup = etree.ElementNamespaceClassLookup() +oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False) +oxml_parser.set_element_class_lookup(element_class_lookup) + + +def parse_xml(xml: str) -> "BaseOxmlElement": + """Root lxml element obtained by parsing XML character string `xml`. + + The custom parser is used, so custom element classes are produced for elements in + `xml` that have them. + """ + return cast("BaseOxmlElement", etree.fromstring(xml, oxml_parser)) + + +def register_element_cls(tag: str, cls: Type["BaseOxmlElement"]): + """Register an lxml custom element-class to use for `tag`. + + A instance of `cls` to be constructed when the oxml parser encounters an element + with matching `tag`. `tag` is a string of the form `nspfx:tagroot`, e.g. + `'w:document'`. + """ + nspfx, tagroot = tag.split(":") + namespace = element_class_lookup.get_namespace(nsmap[nspfx]) + namespace[tagroot] = cls + + +def OxmlElement( + nsptag_str: str, + attrs: Dict[str, str] | None = None, + nsdecls: Dict[str, str] | None = None, +) -> BaseOxmlElement: + """Return a 'loose' lxml element having the tag specified by `nsptag_str`. + + The tag in `nsptag_str` must contain the standard namespace prefix, e.g. `a:tbl`. + The resulting element is an instance of the custom element class for this tag name + if one is defined. A dictionary of attribute values may be provided as `attrs`; they + are set if present. All namespaces defined in the dict `nsdecls` are declared in the + element using the key as the prefix and the value as the namespace name. If + `nsdecls` is not provided, a single namespace declaration is added based on the + prefix on `nsptag_str`. + """ + nsptag = NamespacePrefixedTag(nsptag_str) + if nsdecls is None: + nsdecls = nsptag.nsmap + return oxml_parser.makeelement(nsptag.clark_name, attrib=attrs, nsmap=nsdecls) diff --git a/src/docx/oxml/section.py b/src/docx/oxml/section.py new file mode 100644 index 000000000..d1dc33ce2 --- /dev/null +++ b/src/docx/oxml/section.py @@ -0,0 +1,545 @@ +"""Section-related custom element classes.""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Callable, Iterator, Sequence, Union, cast + +from lxml import etree +from typing_extensions import TypeAlias + +from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START +from docx.oxml.ns import nsmap +from docx.oxml.shared import CT_OnOff +from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString +from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, +) +from docx.shared import Length, lazyproperty + +BlockElement: TypeAlias = Union[CT_P, CT_Tbl] + + +class CT_HdrFtr(BaseOxmlElement): + """`w:hdr` and `w:ftr`, the root element for header and footer part respectively.""" + + p = ZeroOrMore("w:p", successors=()) + tbl = ZeroOrMore("w:tbl", successors=()) + + +class CT_HdrFtrRef(BaseOxmlElement): + """`w:headerReference` and `w:footerReference` elements.""" + + type_: WD_HEADER_FOOTER = ( + RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:type", WD_HEADER_FOOTER + ) + ) + rId: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "r:id", XsdString + ) + + +class CT_PageMar(BaseOxmlElement): + """```` element, defining page margins.""" + + top: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:top", ST_SignedTwipsMeasure + ) + right: Length | None = OptionalAttribute( # pyright: ignore + "w:right", ST_TwipsMeasure + ) + bottom: Length | None = OptionalAttribute( # pyright: ignore + "w:bottom", ST_SignedTwipsMeasure + ) + left: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:left", ST_TwipsMeasure + ) + header: Length | None = OptionalAttribute( # pyright: ignore + "w:header", ST_TwipsMeasure + ) + footer: Length | None = OptionalAttribute( # pyright: ignore + "w:footer", ST_TwipsMeasure + ) + gutter: Length | None = OptionalAttribute( # pyright: ignore + "w:gutter", ST_TwipsMeasure + ) + + +class CT_PageSz(BaseOxmlElement): + """```` element, defining page dimensions and orientation.""" + + w: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:w", ST_TwipsMeasure + ) + h: Length | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:h", ST_TwipsMeasure + ) + orient: WD_ORIENTATION = ( + OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:orient", WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT + ) + ) + + +class CT_SectPr(BaseOxmlElement): + """`w:sectPr` element, the container element for section properties.""" + + get_or_add_pgMar: Callable[[], CT_PageMar] + get_or_add_pgSz: Callable[[], CT_PageSz] + get_or_add_titlePg: Callable[[], CT_OnOff] + get_or_add_type: Callable[[], CT_SectType] + _add_footerReference: Callable[[], CT_HdrFtrRef] + _add_headerReference: Callable[[], CT_HdrFtrRef] + _remove_titlePg: Callable[[], None] + _remove_type: Callable[[], None] + + _tag_seq = ( + "w:footnotePr", + "w:endnotePr", + "w:type", + "w:pgSz", + "w:pgMar", + "w:paperSrc", + "w:pgBorders", + "w:lnNumType", + "w:pgNumType", + "w:cols", + "w:formProt", + "w:vAlign", + "w:noEndnote", + "w:titlePg", + "w:textDirection", + "w:bidi", + "w:rtlGutter", + "w:docGrid", + "w:printerSettings", + "w:sectPrChange", + ) + headerReference = ZeroOrMore("w:headerReference", successors=_tag_seq) + footerReference = ZeroOrMore("w:footerReference", successors=_tag_seq) + type: CT_SectType | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:type", successors=_tag_seq[3:] + ) + pgSz: CT_PageSz | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:pgSz", successors=_tag_seq[4:] + ) + pgMar: CT_PageMar | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:pgMar", successors=_tag_seq[5:] + ) + titlePg: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:titlePg", successors=_tag_seq[14:] + ) + del _tag_seq + + def add_footerReference(self, type_: WD_HEADER_FOOTER, rId: str) -> CT_HdrFtrRef: + """Return newly added CT_HdrFtrRef element of `type_` with `rId`. + + The element tag is `w:footerReference`. + """ + footerReference = self._add_footerReference() + footerReference.type_ = type_ + footerReference.rId = rId + return footerReference + + def add_headerReference(self, type_: WD_HEADER_FOOTER, rId: str) -> CT_HdrFtrRef: + """Return newly added CT_HdrFtrRef element of `type_` with `rId`. + + The element tag is `w:headerReference`. + """ + headerReference = self._add_headerReference() + headerReference.type_ = type_ + headerReference.rId = rId + return headerReference + + @property + def bottom_margin(self) -> Length | None: + """Value of the `w:bottom` attr of `` child element, as |Length|. + + |None| when either the element or the attribute is not present. + """ + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.bottom + + @bottom_margin.setter + def bottom_margin(self, value: int | Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.bottom = ( + value if value is None or isinstance(value, Length) else Length(value) + ) + + def clone(self) -> CT_SectPr: + """Return an exact duplicate of this ```` element tree suitable for + use in adding a section break. + + All rsid* attributes are removed from the root ```` element. + """ + cloned_sectPr = deepcopy(self) + cloned_sectPr.attrib.clear() + return cloned_sectPr + + @property + def footer(self) -> Length | None: + """Distance from bottom edge of page to bottom edge of the footer. + + This is the value of the `w:footer` attribute in the `w:pgMar` child element, + as a |Length| object, or |None| if either the element or the attribute is not + present. + """ + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.footer + + @footer.setter + def footer(self, value: int | Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.footer = ( + value if value is None or isinstance(value, Length) else Length(value) + ) + + def get_footerReference(self, type_: WD_HEADER_FOOTER) -> CT_HdrFtrRef | None: + """Return footerReference element of `type_` or None if not present.""" + path = "./w:footerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_) + footerReferences = self.xpath(path) + if not footerReferences: + return None + return footerReferences[0] + + def get_headerReference(self, type_: WD_HEADER_FOOTER) -> CT_HdrFtrRef | None: + """Return headerReference element of `type_` or None if not present.""" + matching_headerReferences = self.xpath( + "./w:headerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_) + ) + if len(matching_headerReferences) == 0: + return None + return matching_headerReferences[0] + + @property + def gutter(self) -> Length | None: + """The value of the ``w:gutter`` attribute in the ```` child element, + as a |Length| object, or |None| if either the element or the attribute is not + present.""" + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.gutter + + @gutter.setter + def gutter(self, value: int | Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.gutter = ( + value if value is None or isinstance(value, Length) else Length(value) + ) + + @property + def header(self) -> Length | None: + """Distance from top edge of page to top edge of header. + + This value comes from the `w:header` attribute on the `w:pgMar` child element. + |None| if either the element or the attribute is not present. + """ + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.header + + @header.setter + def header(self, value: int | Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.header = ( + value if value is None or isinstance(value, Length) else Length(value) + ) + + def iter_inner_content(self) -> Iterator[CT_P | CT_Tbl]: + """Generate all `w:p` and `w:tbl` elements in this section. + + Elements appear in document order. Elements shaded by nesting in a `w:ins` or + other "wrapper" element will not be included. + """ + return _SectBlockElementIterator.iter_sect_block_elements(self) + + @property + def left_margin(self) -> Length | None: + """The value of the ``w:left`` attribute in the ```` child element, as + a |Length| object, or |None| if either the element or the attribute is not + present.""" + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.left + + @left_margin.setter + def left_margin(self, value: int | Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.left = ( + value if value is None or isinstance(value, Length) else Length(value) + ) + + @property + def orientation(self) -> WD_ORIENTATION: + """`WD_ORIENTATION` member indicating page-orientation for this section. + + This is the value of the `orient` attribute on the `w:pgSz` child, or + `WD_ORIENTATION.PORTRAIT` if not present. + """ + pgSz = self.pgSz + if pgSz is None: + return WD_ORIENTATION.PORTRAIT + return pgSz.orient + + @orientation.setter + def orientation(self, value: WD_ORIENTATION | None): + pgSz = self.get_or_add_pgSz() + pgSz.orient = value if value else WD_ORIENTATION.PORTRAIT + + @property + def page_height(self) -> Length | None: + """Value in EMU of the `h` attribute of the `w:pgSz` child element. + + |None| if not present. + """ + pgSz = self.pgSz + if pgSz is None: + return None + return pgSz.h + + @page_height.setter + def page_height(self, value: Length | None): + pgSz = self.get_or_add_pgSz() + pgSz.h = value + + @property + def page_width(self) -> Length | None: + """Value in EMU of the ``w`` attribute of the ```` child element. + + |None| if not present. + """ + pgSz = self.pgSz + if pgSz is None: + return None + return pgSz.w + + @page_width.setter + def page_width(self, value: Length | None): + pgSz = self.get_or_add_pgSz() + pgSz.w = value + + @property + def preceding_sectPr(self) -> CT_SectPr | None: + """SectPr immediately preceding this one or None if this is the first.""" + # -- [1] predicate returns list of zero or one value -- + preceding_sectPrs = self.xpath("./preceding::w:sectPr[1]") + return preceding_sectPrs[0] if len(preceding_sectPrs) > 0 else None + + def remove_footerReference(self, type_: WD_HEADER_FOOTER) -> str: + """Return rId of w:footerReference child of `type_` after removing it.""" + footerReference = self.get_footerReference(type_) + if footerReference is None: + # -- should never happen, but to satisfy type-check and just in case -- + raise ValueError("CT_SectPr has no footer reference") + rId = footerReference.rId + self.remove(footerReference) + return rId + + def remove_headerReference(self, type_: WD_HEADER_FOOTER): + """Return rId of w:headerReference child of `type_` after removing it.""" + headerReference = self.get_headerReference(type_) + if headerReference is None: + # -- should never happen, but to satisfy type-check and just in case -- + raise ValueError("CT_SectPr has no header reference") + rId = headerReference.rId + self.remove(headerReference) + return rId + + @property + def right_margin(self) -> Length | None: + """The value of the ``w:right`` attribute in the ```` child element, as + a |Length| object, or |None| if either the element or the attribute is not + present.""" + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.right + + @right_margin.setter + def right_margin(self, value: Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.right = value + + @property + def start_type(self) -> WD_SECTION_START: + """The member of the ``WD_SECTION_START`` enumeration corresponding to the value + of the ``val`` attribute of the ```` child element, or + ``WD_SECTION_START.NEW_PAGE`` if not present.""" + type = self.type + if type is None or type.val is None: + return WD_SECTION_START.NEW_PAGE + return type.val + + @start_type.setter + def start_type(self, value: WD_SECTION_START | None): + if value is None or value is WD_SECTION_START.NEW_PAGE: + self._remove_type() + return + type = self.get_or_add_type() + type.val = value + + @property + def titlePg_val(self) -> bool: + """Value of `w:titlePg/@val` or |False| if `./w:titlePg` is not present.""" + titlePg = self.titlePg + if titlePg is None: + return False + return titlePg.val + + @titlePg_val.setter + def titlePg_val(self, value: bool | None): + if value in [None, False]: + self._remove_titlePg() + else: + self.get_or_add_titlePg().val = True + + @property + def top_margin(self) -> Length | None: + """The value of the ``w:top`` attribute in the ```` child element, as a + |Length| object, or |None| if either the element or the attribute is not + present.""" + pgMar = self.pgMar + if pgMar is None: + return None + return pgMar.top + + @top_margin.setter + def top_margin(self, value: Length | None): + pgMar = self.get_or_add_pgMar() + pgMar.top = value + + +class CT_SectType(BaseOxmlElement): + """```` element, defining the section start type.""" + + val: WD_SECTION_START | None = ( # pyright: ignore[reportGeneralTypeIssues] + OptionalAttribute("w:val", WD_SECTION_START) + ) + + +# == HELPERS ========================================================================= + + +class _SectBlockElementIterator: + """Generates the block-item XML elements in a section. + + A block-item element is a `CT_P` (paragraph) or a `CT_Tbl` (table). + """ + + _compiled_blocks_xpath: etree.XPath | None = None + _compiled_count_xpath: etree.XPath | None = None + + def __init__(self, sectPr: CT_SectPr): + self._sectPr = sectPr + + @classmethod + def iter_sect_block_elements(cls, sectPr: CT_SectPr) -> Iterator[BlockElement]: + """Generate each CT_P or CT_Tbl element within extents governed by `sectPr`.""" + return cls(sectPr)._iter_sect_block_elements() + + def _iter_sect_block_elements(self) -> Iterator[BlockElement]: + """Generate each CT_P or CT_Tbl element in section.""" + # -- General strategy is to get all block ( and ) elements from + # -- start of doc to and including this section, then compute the count of those + # -- elements that came from prior sections and skip that many to leave only the + # -- ones in this section. It's possible to express this "between here and + # -- there" (end of prior section and end of this one) concept in XPath, but it + # -- would be harder to follow because there are special cases (e.g. no prior + # -- section) and the boundary expressions are fairly hairy. I also believe it + # -- would be computationally more expensive than doing it this straighforward + # -- albeit (theoretically) slightly wasteful way. + + sectPr, sectPrs = self._sectPr, self._sectPrs + sectPr_idx = sectPrs.index(sectPr) + + # -- count block items belonging to prior sections -- + n_blks_to_skip = ( + 0 + if sectPr_idx == 0 + else self._count_of_blocks_in_and_above_section(sectPrs[sectPr_idx - 1]) + ) + + # -- and skip those in set of all blks from doc start to end of this section -- + for element in self._blocks_in_and_above_section(sectPr)[n_blks_to_skip:]: + yield element + + def _blocks_in_and_above_section(self, sectPr: CT_SectPr) -> Sequence[BlockElement]: + """All ps and tbls in section defined by `sectPr` and all prior sections.""" + if self._compiled_blocks_xpath is None: + self._compiled_blocks_xpath = etree.XPath( + self._blocks_in_and_above_section_xpath, + namespaces=nsmap, + regexp=False, + ) + xpath = self._compiled_blocks_xpath + # -- XPath callable results are Any (basically), so need a cast. Also the + # -- callable wants an etree._Element, which CT_SectPr is, but we haven't + # -- figured out the typing through the metaclass yet. + return cast( + Sequence[BlockElement], + xpath(sectPr), # pyright: ignore[reportGeneralTypeIssues] + ) + + @lazyproperty + def _blocks_in_and_above_section_xpath(self) -> str: + """XPath expr for ps and tbls in context of a sectPr and all prior sectPrs.""" + # -- "p_sect" is a section with sectPr located at w:p/w:pPr/w:sectPr. + # -- "body_sect" is a section with sectPr located at w:body/w:sectPr. The last + # -- section in the document is a "body_sect". All others are of the "p_sect" + # -- variety. "term" means "terminal", like the last p or tbl in the section. + # -- "pred" means "predecessor", like a preceding p or tbl in the section. + + # -- the terminal block in a p-based sect is the p the sectPr appears in -- + p_sect_term_block = "./parent::w:pPr/parent::w:p" + # -- the terminus of a body-based sect is the sectPr itself (not a block) -- + body_sect_term = "self::w:sectPr[parent::w:body]" + # -- all the ps and tbls preceding (but not including) the context node -- + pred_ps_and_tbls = "preceding-sibling::*[self::w:p | self::w:tbl]" + + # -- p_sect_term_block and body_sect_term(inus) are mutually exclusive. So the + # -- result is either the union of nodes found by the first two selectors or the + # -- nodes found by the last selector, never both. + return ( + # -- include the p containing a sectPr -- + f"{p_sect_term_block}" + # -- along with all the blocks that precede it -- + f" | {p_sect_term_block}/{pred_ps_and_tbls}" + # -- or all the preceding blocks if sectPr is body-based (last sectPr) -- + f" | {body_sect_term}/{pred_ps_and_tbls}" + ) + + def _count_of_blocks_in_and_above_section(self, sectPr: CT_SectPr) -> int: + """All ps and tbls in section defined by `sectPr` and all prior sections.""" + if self._compiled_count_xpath is None: + self._compiled_count_xpath = etree.XPath( + f"count({self._blocks_in_and_above_section_xpath})", + namespaces=nsmap, + regexp=False, + ) + xpath = self._compiled_count_xpath + # -- numeric XPath results are always float, so need an int() conversion -- + return int( + cast(float, xpath(sectPr)) # pyright: ignore[reportGeneralTypeIssues] + ) + + @lazyproperty + def _sectPrs(self) -> Sequence[CT_SectPr]: + """All w:sectPr elements in document, in document-order.""" + return self._sectPr.xpath( + "/w:document/w:body/w:p/w:pPr/w:sectPr | /w:document/w:body/w:sectPr", + ) diff --git a/src/docx/oxml/settings.py b/src/docx/oxml/settings.py new file mode 100644 index 000000000..fd39fbd99 --- /dev/null +++ b/src/docx/oxml/settings.py @@ -0,0 +1,125 @@ +"""Custom element classes related to document settings.""" + +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne + + +class CT_Settings(BaseOxmlElement): + """`w:settings` element, root element for the settings part.""" + + _tag_seq = ( + "w:writeProtection", + "w:view", + "w:zoom", + "w:removePersonalInformation", + "w:removeDateAndTime", + "w:doNotDisplayPageBoundaries", + "w:displayBackgroundShape", + "w:printPostScriptOverText", + "w:printFractionalCharacterWidth", + "w:printFormsData", + "w:embedTrueTypeFonts", + "w:embedSystemFonts", + "w:saveSubsetFonts", + "w:saveFormsData", + "w:mirrorMargins", + "w:alignBordersAndEdges", + "w:bordersDoNotSurroundHeader", + "w:bordersDoNotSurroundFooter", + "w:gutterAtTop", + "w:hideSpellingErrors", + "w:hideGrammaticalErrors", + "w:activeWritingStyle", + "w:proofState", + "w:formsDesign", + "w:attachedTemplate", + "w:linkStyles", + "w:stylePaneFormatFilter", + "w:stylePaneSortMethod", + "w:documentType", + "w:mailMerge", + "w:revisionView", + "w:trackRevisions", + "w:doNotTrackMoves", + "w:doNotTrackFormatting", + "w:documentProtection", + "w:autoFormatOverride", + "w:styleLockTheme", + "w:styleLockQFSet", + "w:defaultTabStop", + "w:autoHyphenation", + "w:consecutiveHyphenLimit", + "w:hyphenationZone", + "w:doNotHyphenateCaps", + "w:showEnvelope", + "w:summaryLength", + "w:clickAndTypeStyle", + "w:defaultTableStyle", + "w:evenAndOddHeaders", + "w:bookFoldRevPrinting", + "w:bookFoldPrinting", + "w:bookFoldPrintingSheets", + "w:drawingGridHorizontalSpacing", + "w:drawingGridVerticalSpacing", + "w:displayHorizontalDrawingGridEvery", + "w:displayVerticalDrawingGridEvery", + "w:doNotUseMarginsForDrawingGridOrigin", + "w:drawingGridHorizontalOrigin", + "w:drawingGridVerticalOrigin", + "w:doNotShadeFormData", + "w:noPunctuationKerning", + "w:characterSpacingControl", + "w:printTwoOnOne", + "w:strictFirstAndLastChars", + "w:noLineBreaksAfter", + "w:noLineBreaksBefore", + "w:savePreviewPicture", + "w:doNotValidateAgainstSchema", + "w:saveInvalidXml", + "w:ignoreMixedContent", + "w:alwaysShowPlaceholderText", + "w:doNotDemarcateInvalidXml", + "w:saveXmlDataOnly", + "w:useXSLTWhenSaving", + "w:saveThroughXslt", + "w:showXMLTags", + "w:alwaysMergeEmptyNamespace", + "w:updateFields", + "w:hdrShapeDefaults", + "w:footnotePr", + "w:endnotePr", + "w:compat", + "w:docVars", + "w:rsids", + "m:mathPr", + "w:attachedSchema", + "w:themeFontLang", + "w:clrSchemeMapping", + "w:doNotIncludeSubdocsInStats", + "w:doNotAutoCompressPictures", + "w:forceUpgrade", + "w:captions", + "w:readModeInkLockDown", + "w:smartTagType", + "sl:schemaLibrary", + "w:shapeDefaults", + "w:doNotEmbedSmartTags", + "w:decimalSymbol", + "w:listSeparator", + ) + evenAndOddHeaders = ZeroOrOne("w:evenAndOddHeaders", successors=_tag_seq[48:]) + del _tag_seq + + @property + def evenAndOddHeaders_val(self): + """Value of `w:evenAndOddHeaders/@w:val` or |None| if not present.""" + evenAndOddHeaders = self.evenAndOddHeaders + if evenAndOddHeaders is None: + return False + return evenAndOddHeaders.val + + @evenAndOddHeaders_val.setter + def evenAndOddHeaders_val(self, value): + if value in [None, False]: + self._remove_evenAndOddHeaders() + else: + self.get_or_add_evenAndOddHeaders().val = value diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py new file mode 100644 index 000000000..05c96697a --- /dev/null +++ b/src/docx/oxml/shape.py @@ -0,0 +1,284 @@ +"""Custom element classes for shape-related elements like ``.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml +from docx.oxml.simpletypes import ( + ST_Coordinate, + ST_DrawingElementId, + ST_PositiveCoordinate, + ST_RelationshipId, + XsdString, + XsdToken, +) +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OneAndOnlyOne, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, +) + +if TYPE_CHECKING: + from docx.shared import Length + + +class CT_Anchor(BaseOxmlElement): + """`` element, container for a "floating" shape.""" + + +class CT_Blip(BaseOxmlElement): + """```` element, specifies image source and adjustments such as alpha and + tint.""" + + embed = OptionalAttribute("r:embed", ST_RelationshipId) + link = OptionalAttribute("r:link", ST_RelationshipId) + + +class CT_BlipFillProperties(BaseOxmlElement): + """```` element, specifies picture properties.""" + + blip = ZeroOrOne("a:blip", successors=("a:srcRect", "a:tile", "a:stretch")) + + +class CT_GraphicalObject(BaseOxmlElement): + """```` element, container for a DrawingML object.""" + + graphicData = OneAndOnlyOne("a:graphicData") + + +class CT_GraphicalObjectData(BaseOxmlElement): + """```` element, container for the XML of a DrawingML object.""" + + pic = ZeroOrOne("pic:pic") + uri = RequiredAttribute("uri", XsdToken) + + +class CT_Inline(BaseOxmlElement): + """`` element, container for an inline shape.""" + + extent = OneAndOnlyOne("wp:extent") + docPr = OneAndOnlyOne("wp:docPr") + graphic = OneAndOnlyOne("a:graphic") + + @classmethod + def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture) -> CT_Inline: + """Return a new ```` element populated with the values passed as + parameters.""" + inline = parse_xml(cls._inline_xml()) + inline.extent.cx = cx + inline.extent.cy = cy + inline.docPr.id = shape_id + inline.docPr.name = "Picture %d" % shape_id + inline.graphic.graphicData.uri = ( + "http://schemas.openxmlformats.org/drawingml/2006/picture" + ) + inline.graphic.graphicData._insert_pic(pic) + return inline + + @classmethod + def new_pic_inline( + cls, shape_id: int, rId: str, filename: str, cx: Length, cy: Length + ) -> CT_Inline: + """Create `wp:inline` element containing a `pic:pic` element. + + The contents of the `pic:pic` element is taken from the argument values. + """ + pic_id = 0 # Word doesn't seem to use this, but does not omit it + pic = CT_Picture.new(pic_id, filename, rId, cx, cy) + inline = cls.new(cx, cy, shape_id, pic) + inline.graphic.graphicData._insert_pic(pic) + return inline + + @classmethod + def _inline_xml(cls): + return ( + "\n" + ' \n' + ' \n' + " \n" + ' \n' + " \n" + " \n" + ' \n' + " \n" + "" % nsdecls("wp", "a", "pic", "r") + ) + + +class CT_NonVisualDrawingProps(BaseOxmlElement): + """Used for ```` element, and perhaps others. + + Specifies the id and name of a DrawingML drawing. + """ + + id = RequiredAttribute("id", ST_DrawingElementId) + name = RequiredAttribute("name", XsdString) + + +class CT_NonVisualPictureProperties(BaseOxmlElement): + """```` element, specifies picture locking and resize behaviors.""" + + +class CT_Picture(BaseOxmlElement): + """```` element, a DrawingML picture.""" + + nvPicPr = OneAndOnlyOne("pic:nvPicPr") + blipFill = OneAndOnlyOne("pic:blipFill") + spPr = OneAndOnlyOne("pic:spPr") + + @classmethod + def new(cls, pic_id, filename, rId, cx, cy): + """Return a new ```` element populated with the minimal contents + required to define a viable picture element, based on the values passed as + parameters.""" + pic = parse_xml(cls._pic_xml()) + pic.nvPicPr.cNvPr.id = pic_id + pic.nvPicPr.cNvPr.name = filename + pic.blipFill.blip.embed = rId + pic.spPr.cx = cx + pic.spPr.cy = cy + return pic + + @classmethod + def _pic_xml(cls): + return ( + "\n" + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + ' \n' + ' \n' + " \n" + ' \n' + " \n" + "" % nsdecls("pic", "a", "r") + ) + + +class CT_PictureNonVisual(BaseOxmlElement): + """```` element, non-visual picture properties.""" + + cNvPr = OneAndOnlyOne("pic:cNvPr") + + +class CT_Point2D(BaseOxmlElement): + """Used for ```` element, and perhaps others. + + Specifies an x, y coordinate (point). + """ + + x = RequiredAttribute("x", ST_Coordinate) + y = RequiredAttribute("y", ST_Coordinate) + + +class CT_PositiveSize2D(BaseOxmlElement): + """Used for ```` element, and perhaps others later. + + Specifies the size of a DrawingML drawing. + """ + + cx = RequiredAttribute("cx", ST_PositiveCoordinate) + cy = RequiredAttribute("cy", ST_PositiveCoordinate) + + +class CT_PresetGeometry2D(BaseOxmlElement): + """```` element, specifies an preset autoshape geometry, such as + ``rect``.""" + + +class CT_RelativeRect(BaseOxmlElement): + """```` element, specifying picture should fill containing rectangle + shape.""" + + +class CT_ShapeProperties(BaseOxmlElement): + """```` element, specifies size and shape of picture container.""" + + xfrm = ZeroOrOne( + "a:xfrm", + successors=( + "a:custGeom", + "a:prstGeom", + "a:ln", + "a:effectLst", + "a:effectDag", + "a:scene3d", + "a:sp3d", + "a:extLst", + ), + ) + + @property + def cx(self): + """Shape width as an instance of Emu, or None if not present.""" + xfrm = self.xfrm + if xfrm is None: + return None + return xfrm.cx + + @cx.setter + def cx(self, value): + xfrm = self.get_or_add_xfrm() + xfrm.cx = value + + @property + def cy(self): + """Shape height as an instance of Emu, or None if not present.""" + xfrm = self.xfrm + if xfrm is None: + return None + return xfrm.cy + + @cy.setter + def cy(self, value): + xfrm = self.get_or_add_xfrm() + xfrm.cy = value + + +class CT_StretchInfoProperties(BaseOxmlElement): + """```` element, specifies how picture should fill its containing + shape.""" + + +class CT_Transform2D(BaseOxmlElement): + """```` element, specifies size and shape of picture container.""" + + off = ZeroOrOne("a:off", successors=("a:ext",)) + ext = ZeroOrOne("a:ext", successors=()) + + @property + def cx(self): + ext = self.ext + if ext is None: + return None + return ext.cx + + @cx.setter + def cx(self, value): + ext = self.get_or_add_ext() + ext.cx = value + + @property + def cy(self): + ext = self.ext + if ext is None: + return None + return ext.cy + + @cy.setter + def cy(self, value): + ext = self.get_or_add_ext() + ext.cy = value diff --git a/src/docx/oxml/shared.py b/src/docx/oxml/shared.py new file mode 100644 index 000000000..1774560ac --- /dev/null +++ b/src/docx/oxml/shared.py @@ -0,0 +1,55 @@ +"""Objects shared by modules in the docx.oxml subpackage.""" + +from __future__ import annotations + +from typing import cast + +from docx.oxml.ns import qn +from docx.oxml.parser import OxmlElement +from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff, ST_String +from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute + + +class CT_DecimalNumber(BaseOxmlElement): + """Used for ````, ````, ```` and several others, + containing a text representation of a decimal number (e.g. 42) in its ``val`` + attribute.""" + + val = RequiredAttribute("w:val", ST_DecimalNumber) + + @classmethod + def new(cls, nsptagname, val): + """Return a new ``CT_DecimalNumber`` element having tagname `nsptagname` and + ``val`` attribute set to `val`.""" + return OxmlElement(nsptagname, attrs={qn("w:val"): str(val)}) + + +class CT_OnOff(BaseOxmlElement): + """Used for `w:b`, `w:i` elements and others. + + Contains a bool-ish string in its `val` attribute, xsd:boolean plus "on" and + "off". Defaults to `True`, so `` for example means "bold is turned on". + """ + + val: bool = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_OnOff, default=True + ) + + +class CT_String(BaseOxmlElement): + """Used for `w:pStyle` and `w:tblStyle` elements and others. + + In those cases, it containing a style name in its `val` attribute. + """ + + val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_String + ) + + @classmethod + def new(cls, nsptagname: str, val: str): + """Return a new ``CT_String`` element with tagname `nsptagname` and ``val`` + attribute set to `val`.""" + elm = cast(CT_String, OxmlElement(nsptagname)) + elm.val = val + return elm diff --git a/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py similarity index 60% rename from docx/oxml/simpletypes.py rename to src/docx/oxml/simpletypes.py index 400a23700..4e8d91cba 100644 --- a/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -1,24 +1,28 @@ -# encoding: utf-8 +"""Simple-type classes, corresponding to ST_* schema items. +These provide validation and format translation for values stored in XML element +attributes. Naming generally corresponds to the simple type in the associated XML +schema. """ -Simple type classes, providing validation and format translation for values -stored in XML element attributes. Naming generally corresponds to the simple -type in the associated XML schema. -""" -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from docx.exceptions import InvalidXmlError +from docx.shared import Emu, Pt, RGBColor, Twips -from ..exceptions import InvalidXmlError -from ..shared import Emu, Pt, RGBColor, Twips +if TYPE_CHECKING: + from docx import types as t + from docx.shared import Length -class BaseSimpleType(object): +class BaseSimpleType: + """Base class for simple-types.""" @classmethod - def from_xml(cls, str_value): - return cls.convert_from_xml(str_value) + def from_xml(cls, xml_value: str): + return cls.convert_from_xml(xml_value) @classmethod def to_xml(cls, value): @@ -26,38 +30,32 @@ def to_xml(cls, value): str_value = cls.convert_to_xml(value) return str_value + @classmethod + def convert_from_xml(cls, str_value: str) -> t.AbstractSimpleTypeMember: + return int(str_value) + @classmethod def validate_int(cls, value): if not isinstance(value, int): - raise TypeError( - "value must be , got %s" % type(value) - ) + raise TypeError("value must be , got %s" % type(value)) @classmethod def validate_int_in_range(cls, value, min_inclusive, max_inclusive): cls.validate_int(value) if value < min_inclusive or value > max_inclusive: raise ValueError( - "value must be in range %d to %d inclusive, got %d" % - (min_inclusive, max_inclusive, value) + "value must be in range %d to %d inclusive, got %d" + % (min_inclusive, max_inclusive, value) ) @classmethod - def validate_string(cls, value): - if isinstance(value, str): - return value - try: - if isinstance(value, basestring): - return value - except NameError: # means we're on Python 3 - pass - raise TypeError( - "value must be a string, got %s" % type(value) - ) + def validate_string(cls, value: Any) -> str: + if not isinstance(value, str): + raise TypeError("value must be a string, got %s" % type(value)) + return value class BaseIntType(BaseSimpleType): - @classmethod def convert_from_xml(cls, str_value): return int(str_value) @@ -72,53 +70,45 @@ def validate(cls, value): class BaseStringType(BaseSimpleType): - @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> str: return str_value @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: str) -> str: return value @classmethod - def validate(cls, value): + def validate(cls, value: str): cls.validate_string(value) class BaseStringEnumerationType(BaseStringType): - @classmethod def validate(cls, value): cls.validate_string(value) if value not in cls._members: - raise ValueError( - "must be one of %s, got '%s'" % (cls._members, value) - ) + raise ValueError("must be one of %s, got '%s'" % (cls._members, value)) class XsdAnyUri(BaseStringType): - """ - There's a regular expression this is supposed to meet but so far thinking - spending cycles on validating wouldn't be worth it for the number of - programming errors it would catch. - """ + """There's a regular expression this is supposed to meet but so far thinking + spending cycles on validating wouldn't be worth it for the number of programming + errors it would catch.""" class XsdBoolean(BaseSimpleType): - @classmethod def convert_from_xml(cls, str_value): - if str_value not in ('1', '0', 'true', 'false'): + if str_value not in ("1", "0", "true", "false"): raise InvalidXmlError( - "value must be one of '1', '0', 'true' or 'false', got '%s'" - % str_value + "value must be one of '1', '0', 'true' or 'false', got '%s'" % str_value ) - return str_value in ('1', 'true') + return str_value in ("1", "true") @classmethod def convert_to_xml(cls, value): - return {True: '1', False: '0'}[value] + return {True: "1", False: "0"}[value] @classmethod def validate(cls, value): @@ -130,27 +120,24 @@ def validate(cls, value): class XsdId(BaseStringType): + """String that must begin with a letter or underscore and cannot contain any colons. + + Not fully validated because not used in external API. """ - String that must begin with a letter or underscore and cannot contain any - colons. Not fully validated because not used in external API. - """ + pass class XsdInt(BaseIntType): - @classmethod def validate(cls, value): cls.validate_int_in_range(value, -2147483648, 2147483647) class XsdLong(BaseIntType): - @classmethod def validate(cls, value): - cls.validate_int_in_range( - value, -9223372036854775808, 9223372036854775807 - ) + cls.validate_int_in_range(value, -9223372036854775808, 9223372036854775807) class XsdString(BaseStringType): @@ -158,62 +145,50 @@ class XsdString(BaseStringType): class XsdStringEnumeration(BaseStringEnumerationType): - """ - Set of enumerated xsd:string values. - """ + """Set of enumerated xsd:string values.""" class XsdToken(BaseStringType): - """ - xsd:string with whitespace collapsing, e.g. multiple spaces reduced to - one, leading and trailing space stripped. - """ + """Xsd:string with whitespace collapsing, e.g. multiple spaces reduced to one, + leading and trailing space stripped.""" + pass class XsdUnsignedInt(BaseIntType): - @classmethod def validate(cls, value): cls.validate_int_in_range(value, 0, 4294967295) class XsdUnsignedLong(BaseIntType): - @classmethod def validate(cls, value): cls.validate_int_in_range(value, 0, 18446744073709551615) class ST_BrClear(XsdString): - @classmethod - def validate(cls, value): + def validate(cls, value: str) -> None: cls.validate_string(value) - valid_values = ('none', 'left', 'right', 'all') + valid_values = ("none", "left", "right", "all") if value not in valid_values: - raise ValueError( - "must be one of %s, got '%s'" % (valid_values, value) - ) + raise ValueError("must be one of %s, got '%s'" % (valid_values, value)) class ST_BrType(XsdString): - @classmethod def validate(cls, value): cls.validate_string(value) - valid_values = ('page', 'column', 'textWrapping') + valid_values = ("page", "column", "textWrapping") if value not in valid_values: - raise ValueError( - "must be one of %s, got '%s'" % (valid_values, value) - ) + raise ValueError("must be one of %s, got '%s'" % (valid_values, value)) class ST_Coordinate(BaseIntType): - @classmethod def convert_from_xml(cls, str_value): - if 'i' in str_value or 'm' in str_value or 'p' in str_value: + if "i" in str_value or "m" in str_value or "p" in str_value: return ST_UniversalMeasure.convert_from_xml(str_value) return Emu(int(str_value)) @@ -223,7 +198,6 @@ def validate(cls, value): class ST_CoordinateUnqualified(XsdLong): - @classmethod def validate(cls, value): cls.validate_int_in_range(value, -27273042329600, 27273042316900) @@ -238,20 +212,17 @@ class ST_DrawingElementId(XsdUnsignedInt): class ST_HexColor(BaseStringType): - @classmethod def convert_from_xml(cls, str_value): - if str_value == 'auto': + if str_value == "auto": return ST_HexColorAuto.AUTO return RGBColor.from_string(str_value) @classmethod def convert_to_xml(cls, value): - """ - Keep alpha hex numerals all uppercase just for consistency. - """ + """Keep alpha hex numerals all uppercase just for consistency.""" # expecting 3-tuple of ints in range 0-255 - return '%02X%02X%02X' % value + return "%02X%02X%02X" % value @classmethod def validate(cls, value): @@ -264,55 +235,50 @@ def validate(cls, value): class ST_HexColorAuto(XsdStringEnumeration): - """ - Value for `w:color/[@val="auto"] attribute setting - """ - AUTO = 'auto' + """Value for `w:color/[@val="auto"] attribute setting.""" + + AUTO = "auto" _members = (AUTO,) class ST_HpsMeasure(XsdUnsignedLong): - """ - Half-point measure, e.g. 24.0 represents 12.0 points. - """ + """Half-point measure, e.g. 24.0 represents 12.0 points.""" + @classmethod - def convert_from_xml(cls, str_value): - if 'm' in str_value or 'n' in str_value or 'p' in str_value: + def convert_from_xml(cls, str_value: str) -> Length: + if "m" in str_value or "n" in str_value or "p" in str_value: return ST_UniversalMeasure.convert_from_xml(str_value) - return Pt(int(str_value)/2.0) + return Pt(int(str_value) / 2.0) @classmethod - def convert_to_xml(cls, value): + def convert_to_xml(cls, value: int | Length) -> str: emu = Emu(value) half_points = int(emu.pt * 2) return str(half_points) class ST_Merge(XsdStringEnumeration): - """ - Valid values for attribute - """ - CONTINUE = 'continue' - RESTART = 'restart' + """Valid values for attribute.""" + + CONTINUE = "continue" + RESTART = "restart" _members = (CONTINUE, RESTART) class ST_OnOff(XsdBoolean): - @classmethod def convert_from_xml(cls, str_value): - if str_value not in ('1', '0', 'true', 'false', 'on', 'off'): + if str_value not in ("1", "0", "true", "false", "on", "off"): raise InvalidXmlError( "value must be one of '1', '0', 'true', 'false', 'on', or 'o" "ff', got '%s'" % str_value ) - return str_value in ('1', 'true', 'on') + return str_value in ("1", "true", "on") class ST_PositiveCoordinate(XsdLong): - @classmethod def convert_from_xml(cls, str_value): return Emu(int(str_value)) @@ -327,10 +293,9 @@ class ST_RelationshipId(XsdString): class ST_SignedTwipsMeasure(XsdInt): - @classmethod def convert_from_xml(cls, str_value): - if 'i' in str_value or 'm' in str_value or 'p' in str_value: + if "i" in str_value or "m" in str_value or "p" in str_value: return ST_UniversalMeasure.convert_from_xml(str_value) return Twips(int(str_value)) @@ -346,34 +311,27 @@ class ST_String(XsdString): class ST_TblLayoutType(XsdString): - @classmethod def validate(cls, value): cls.validate_string(value) - valid_values = ('fixed', 'autofit') + valid_values = ("fixed", "autofit") if value not in valid_values: - raise ValueError( - "must be one of %s, got '%s'" % (valid_values, value) - ) + raise ValueError("must be one of %s, got '%s'" % (valid_values, value)) class ST_TblWidth(XsdString): - @classmethod def validate(cls, value): cls.validate_string(value) - valid_values = ('auto', 'dxa', 'nil', 'pct') + valid_values = ("auto", "dxa", "nil", "pct") if value not in valid_values: - raise ValueError( - "must be one of %s, got '%s'" % (valid_values, value) - ) + raise ValueError("must be one of %s, got '%s'" % (valid_values, value)) class ST_TwipsMeasure(XsdUnsignedLong): - @classmethod def convert_from_xml(cls, str_value): - if 'i' in str_value or 'm' in str_value or 'p' in str_value: + if "i" in str_value or "m" in str_value or "p" in str_value: return ST_UniversalMeasure.convert_from_xml(str_value) return Twips(int(str_value)) @@ -385,25 +343,26 @@ def convert_to_xml(cls, value): class ST_UniversalMeasure(BaseSimpleType): - @classmethod - def convert_from_xml(cls, str_value): + def convert_from_xml(cls, str_value: str) -> Emu: float_part, units_part = str_value[:-2], str_value[-2:] quantity = float(float_part) multiplier = { - 'mm': 36000, 'cm': 360000, 'in': 914400, 'pt': 12700, - 'pc': 152400, 'pi': 152400 + "mm": 36000, + "cm": 360000, + "in": 914400, + "pt": 12700, + "pc": 152400, + "pi": 152400, }[units_part] - emu_value = Emu(int(round(quantity * multiplier))) - return emu_value + return Emu(int(round(quantity * multiplier))) class ST_VerticalAlignRun(XsdStringEnumeration): - """ - Valid values for `w:vertAlign/@val`. - """ - BASELINE = 'baseline' - SUPERSCRIPT = 'superscript' - SUBSCRIPT = 'subscript' + """Valid values for `w:vertAlign/@val`.""" + + BASELINE = "baseline" + SUPERSCRIPT = "superscript" + SUBSCRIPT = "subscript" _members = (BASELINE, SUPERSCRIPT, SUBSCRIPT) diff --git a/src/docx/oxml/styles.py b/src/docx/oxml/styles.py new file mode 100644 index 000000000..e0a3eaeaf --- /dev/null +++ b/src/docx/oxml/styles.py @@ -0,0 +1,322 @@ +"""Custom element classes related to the styles part.""" + +from __future__ import annotations + +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff, ST_String +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, +) + + +def styleId_from_name(name): + """Return the style id corresponding to `name`, taking into account special-case + names such as 'Heading 1'.""" + return { + "caption": "Caption", + "heading 1": "Heading1", + "heading 2": "Heading2", + "heading 3": "Heading3", + "heading 4": "Heading4", + "heading 5": "Heading5", + "heading 6": "Heading6", + "heading 7": "Heading7", + "heading 8": "Heading8", + "heading 9": "Heading9", + }.get(name, name.replace(" ", "")) + + +class CT_LatentStyles(BaseOxmlElement): + """`w:latentStyles` element, defining behavior defaults for latent styles and + containing `w:lsdException` child elements that each override those defaults for a + named latent style.""" + + lsdException = ZeroOrMore("w:lsdException", successors=()) + + count = OptionalAttribute("w:count", ST_DecimalNumber) + defLockedState = OptionalAttribute("w:defLockedState", ST_OnOff) + defQFormat = OptionalAttribute("w:defQFormat", ST_OnOff) + defSemiHidden = OptionalAttribute("w:defSemiHidden", ST_OnOff) + defUIPriority = OptionalAttribute("w:defUIPriority", ST_DecimalNumber) + defUnhideWhenUsed = OptionalAttribute("w:defUnhideWhenUsed", ST_OnOff) + + def bool_prop(self, attr_name): + """Return the boolean value of the attribute having `attr_name`, or |False| if + not present.""" + value = getattr(self, attr_name) + if value is None: + return False + return value + + def get_by_name(self, name): + """Return the `w:lsdException` child having `name`, or |None| if not found.""" + found = self.xpath('w:lsdException[@w:name="%s"]' % name) + if not found: + return None + return found[0] + + def set_bool_prop(self, attr_name, value): + """Set the on/off attribute having `attr_name` to `value`.""" + setattr(self, attr_name, bool(value)) + + +class CT_LsdException(BaseOxmlElement): + """```` element, defining override visibility behaviors for a named + latent style.""" + + locked = OptionalAttribute("w:locked", ST_OnOff) + name = RequiredAttribute("w:name", ST_String) + qFormat = OptionalAttribute("w:qFormat", ST_OnOff) + semiHidden = OptionalAttribute("w:semiHidden", ST_OnOff) + uiPriority = OptionalAttribute("w:uiPriority", ST_DecimalNumber) + unhideWhenUsed = OptionalAttribute("w:unhideWhenUsed", ST_OnOff) + + def delete(self): + """Remove this `w:lsdException` element from the XML document.""" + self.getparent().remove(self) + + def on_off_prop(self, attr_name): + """Return the boolean value of the attribute having `attr_name`, or |None| if + not present.""" + return getattr(self, attr_name) + + def set_on_off_prop(self, attr_name, value): + """Set the on/off attribute having `attr_name` to `value`.""" + setattr(self, attr_name, value) + + +class CT_Style(BaseOxmlElement): + """A ```` element, representing a style definition.""" + + _tag_seq = ( + "w:name", + "w:aliases", + "w:basedOn", + "w:next", + "w:link", + "w:autoRedefine", + "w:hidden", + "w:uiPriority", + "w:semiHidden", + "w:unhideWhenUsed", + "w:qFormat", + "w:locked", + "w:personal", + "w:personalCompose", + "w:personalReply", + "w:rsid", + "w:pPr", + "w:rPr", + "w:tblPr", + "w:trPr", + "w:tcPr", + "w:tblStylePr", + ) + name = ZeroOrOne("w:name", successors=_tag_seq[1:]) + basedOn = ZeroOrOne("w:basedOn", successors=_tag_seq[3:]) + next = ZeroOrOne("w:next", successors=_tag_seq[4:]) + uiPriority = ZeroOrOne("w:uiPriority", successors=_tag_seq[8:]) + semiHidden = ZeroOrOne("w:semiHidden", successors=_tag_seq[9:]) + unhideWhenUsed = ZeroOrOne("w:unhideWhenUsed", successors=_tag_seq[10:]) + qFormat = ZeroOrOne("w:qFormat", successors=_tag_seq[11:]) + locked = ZeroOrOne("w:locked", successors=_tag_seq[12:]) + pPr = ZeroOrOne("w:pPr", successors=_tag_seq[17:]) + rPr = ZeroOrOne("w:rPr", successors=_tag_seq[18:]) + del _tag_seq + + type: WD_STYLE_TYPE | None = ( + OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:type", WD_STYLE_TYPE + ) + ) + styleId: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:styleId", ST_String + ) + default = OptionalAttribute("w:default", ST_OnOff) + customStyle = OptionalAttribute("w:customStyle", ST_OnOff) + + @property + def basedOn_val(self): + """Value of `w:basedOn/@w:val` or |None| if not present.""" + basedOn = self.basedOn + if basedOn is None: + return None + return basedOn.val + + @basedOn_val.setter + def basedOn_val(self, value): + if value is None: + self._remove_basedOn() + else: + self.get_or_add_basedOn().val = value + + @property + def base_style(self): + """Sibling CT_Style element this style is based on or |None| if no base style or + base style not found.""" + basedOn = self.basedOn + if basedOn is None: + return None + styles = self.getparent() + base_style = styles.get_by_id(basedOn.val) + if base_style is None: + return None + return base_style + + def delete(self): + """Remove this `w:style` element from its parent `w:styles` element.""" + self.getparent().remove(self) + + @property + def locked_val(self): + """Value of `w:locked/@w:val` or |False| if not present.""" + locked = self.locked + if locked is None: + return False + return locked.val + + @locked_val.setter + def locked_val(self, value): + self._remove_locked() + if bool(value) is True: + locked = self._add_locked() + locked.val = value + + @property + def name_val(self): + """Value of ```` child or |None| if not present.""" + name = self.name + if name is None: + return None + return name.val + + @name_val.setter + def name_val(self, value): + self._remove_name() + if value is not None: + name = self._add_name() + name.val = value + + @property + def next_style(self): + """Sibling CT_Style element identified by the value of `w:name/@w:val` or |None| + if no value is present or no style with that style id is found.""" + next = self.next + if next is None: + return None + styles = self.getparent() + return styles.get_by_id(next.val) # None if not found + + @property + def qFormat_val(self): + """Value of `w:qFormat/@w:val` or |False| if not present.""" + qFormat = self.qFormat + if qFormat is None: + return False + return qFormat.val + + @qFormat_val.setter + def qFormat_val(self, value): + self._remove_qFormat() + if bool(value): + self._add_qFormat() + + @property + def semiHidden_val(self): + """Value of ```` child or |False| if not present.""" + semiHidden = self.semiHidden + if semiHidden is None: + return False + return semiHidden.val + + @semiHidden_val.setter + def semiHidden_val(self, value): + self._remove_semiHidden() + if bool(value) is True: + semiHidden = self._add_semiHidden() + semiHidden.val = value + + @property + def uiPriority_val(self): + """Value of ```` child or |None| if not present.""" + uiPriority = self.uiPriority + if uiPriority is None: + return None + return uiPriority.val + + @uiPriority_val.setter + def uiPriority_val(self, value): + self._remove_uiPriority() + if value is not None: + uiPriority = self._add_uiPriority() + uiPriority.val = value + + @property + def unhideWhenUsed_val(self): + """Value of `w:unhideWhenUsed/@w:val` or |False| if not present.""" + unhideWhenUsed = self.unhideWhenUsed + if unhideWhenUsed is None: + return False + return unhideWhenUsed.val + + @unhideWhenUsed_val.setter + def unhideWhenUsed_val(self, value): + self._remove_unhideWhenUsed() + if bool(value) is True: + unhideWhenUsed = self._add_unhideWhenUsed() + unhideWhenUsed.val = value + + +class CT_Styles(BaseOxmlElement): + """```` element, the root element of a styles part, i.e. styles.xml.""" + + _tag_seq = ("w:docDefaults", "w:latentStyles", "w:style") + latentStyles = ZeroOrOne("w:latentStyles", successors=_tag_seq[2:]) + style = ZeroOrMore("w:style", successors=()) + del _tag_seq + + def add_style_of_type(self, name, style_type, builtin): + """Return a newly added `w:style` element having `name` and `style_type`. + + `w:style/@customStyle` is set based on the value of `builtin`. + """ + style = self.add_style() + style.type = style_type + style.customStyle = None if builtin else True + style.styleId = styleId_from_name(name) + style.name_val = name + return style + + def default_for(self, style_type): + """Return `w:style[@w:type="*{style_type}*][-1]` or |None| if not found.""" + default_styles_for_type = [ + s for s in self._iter_styles() if s.type == style_type and s.default + ] + if not default_styles_for_type: + return None + # spec calls for last default in document order + return default_styles_for_type[-1] + + def get_by_id(self, styleId: str) -> CT_Style | None: + """`w:style` child where @styleId = `styleId`. + + |None| if not found. + """ + xpath = f'w:style[@w:styleId="{styleId}"]' + return next(iter(self.xpath(xpath)), None) + + def get_by_name(self, name: str) -> CT_Style | None: + """`w:style` child with `w:name` grandchild having value `name`. + + |None| if not found. + """ + xpath = 'w:style[w:name/@w:val="%s"]' % name + return next(iter(self.xpath(xpath)), None) + + def _iter_styles(self): + """Generate each of the `w:style` child elements in document order.""" + return (style for style in self.xpath("w:style")) diff --git a/docx/oxml/table.py b/src/docx/oxml/table.py similarity index 50% rename from docx/oxml/table.py rename to src/docx/oxml/table.py index e55bf9126..cefc545bf 100644 --- a/docx/oxml/table.py +++ b/src/docx/oxml/table.py @@ -1,45 +1,46 @@ -# encoding: utf-8 - -"""Custom element classes for tables""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from . import parse_xml -from ..enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE -from ..exceptions import InvalidSpanError -from .ns import nsdecls, qn -from ..shared import Emu, Twips -from .simpletypes import ( - ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt +"""Custom element classes for tables.""" + +from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE +from docx.exceptions import InvalidSpanError +from docx.oxml.ns import nsdecls, qn +from docx.oxml.parser import parse_xml +from docx.oxml.simpletypes import ( + ST_Merge, + ST_TblLayoutType, + ST_TblWidth, + ST_TwipsMeasure, + XsdInt, ) -from .xmlchemy import ( - BaseOxmlElement, OneAndOnlyOne, OneOrMore, OptionalAttribute, - RequiredAttribute, ZeroOrOne, ZeroOrMore +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, ) +from docx.shared import Emu, Twips class CT_Height(BaseOxmlElement): - """ - Used for ```` to specify a row height and row height rule. - """ - val = OptionalAttribute('w:val', ST_TwipsMeasure) - hRule = OptionalAttribute('w:hRule', WD_ROW_HEIGHT_RULE) + """Used for ```` to specify a row height and row height rule.""" + + val = OptionalAttribute("w:val", ST_TwipsMeasure) + hRule = OptionalAttribute("w:hRule", WD_ROW_HEIGHT_RULE) class CT_Row(BaseOxmlElement): - """ - ```` element - """ - tblPrEx = ZeroOrOne('w:tblPrEx') # custom inserter below - trPr = ZeroOrOne('w:trPr') # custom inserter below - tc = ZeroOrMore('w:tc') + """```` element.""" + + tblPrEx = ZeroOrOne("w:tblPrEx") # custom inserter below + trPr = ZeroOrOne("w:trPr") # custom inserter below + tc = ZeroOrMore("w:tc") def tc_at_grid_col(self, idx): - """ - The ```` element appearing at grid column *idx*. Raises - |ValueError| if no ``w:tc`` element begins at that grid column. + """The ```` element appearing at grid column `idx`. + + Raises |ValueError| if no ``w:tc`` element begins at that grid column. """ grid_col = 0 for tc in self.tc_lst: @@ -47,23 +48,18 @@ def tc_at_grid_col(self, idx): return tc grid_col += tc.grid_span if grid_col > idx: - raise ValueError('no cell on grid column %d' % idx) - raise ValueError('index out of bounds') + raise ValueError("no cell on grid column %d" % idx) + raise ValueError("index out of bounds") @property def tr_idx(self): - """ - The index of this ```` element within its parent ```` - element. - """ + """The index of this ```` element within its parent ```` + element.""" return self.getparent().tr_lst.index(self) @property def trHeight_hRule(self): - """ - Return the value of `w:trPr/w:trHeight@w:hRule`, or |None| if not - present. - """ + """Return the value of `w:trPr/w:trHeight@w:hRule`, or |None| if not present.""" trPr = self.trPr if trPr is None: return None @@ -76,10 +72,7 @@ def trHeight_hRule(self, value): @property def trHeight_val(self): - """ - Return the value of `w:trPr/w:trHeight@w:val`, or |None| if not - present. - """ + """Return the value of `w:trPr/w:trHeight@w:val`, or |None| if not present.""" trPr = self.trPr if trPr is None: return None @@ -105,19 +98,17 @@ def _new_tc(self): class CT_Tbl(BaseOxmlElement): - """ - ```` element - """ - tblPr = OneAndOnlyOne('w:tblPr') - tblGrid = OneAndOnlyOne('w:tblGrid') - tr = ZeroOrMore('w:tr') + """```` element.""" + + tblPr = OneAndOnlyOne("w:tblPr") + tblGrid = OneAndOnlyOne("w:tblGrid") + tr = ZeroOrMore("w:tr") @property def bidiVisual_val(self): - """ - Value of `w:tblPr/w:bidiVisual/@w:val` or |None| if not present. - Controls whether table cells are displayed right-to-left or - left-to-right. + """Value of `w:tblPr/w:bidiVisual/@w:val` or |None| if not present. + + Controls whether table cells are displayed right-to-left or left-to-right. """ bidiVisual = self.tblPr.bidiVisual if bidiVisual is None: @@ -134,16 +125,15 @@ def bidiVisual_val(self, value): @property def col_count(self): - """ - The number of grid columns in this table. - """ + """The number of grid columns in this table.""" return len(self.tblGrid.gridCol_lst) def iter_tcs(self): - """ - Generate each of the `w:tc` elements in this table, left to right and - top to bottom. Each cell in the first row is generated, followed by - each cell in the second row, etc. + """Generate each of the `w:tc` elements in this table, left to right and top to + bottom. + + Each cell in the first row is generated, followed by each cell in the second + row, etc. """ for tr in self.tr_lst: for tc in tr.tc_lst: @@ -151,18 +141,14 @@ def iter_tcs(self): @classmethod def new_tbl(cls, rows, cols, width): - """ - Return a new `w:tbl` element having *rows* rows and *cols* columns - with *width* distributed evenly between the columns. - """ + """Return a new `w:tbl` element having `rows` rows and `cols` columns with + `width` distributed evenly between the columns.""" return parse_xml(cls._tbl_xml(rows, cols, width)) @property def tblStyle_val(self): - """ - Value of `w:tblPr/w:tblStyle/@w:val` (a table style id) or |None| if - not present. - """ + """Value of `w:tblPr/w:tblStyle/@w:val` (a table style id) or |None| if not + present.""" tblStyle = self.tblPr.tblStyle if tblStyle is None: return None @@ -170,9 +156,9 @@ def tblStyle_val(self): @tblStyle_val.setter def tblStyle_val(self, styleId): - """ - Set the value of `w:tblPr/w:tblStyle/@w:val` (a table style id) to - *styleId*. If *styleId* is None, remove the `w:tblStyle` element. + """Set the value of `w:tblPr/w:tblStyle/@w:val` (a table style id) to `styleId`. + + If `styleId` is None, remove the `w:tblStyle` element. """ tblPr = self.tblPr tblPr._remove_tblStyle() @@ -182,114 +168,118 @@ def tblStyle_val(self, styleId): @classmethod def _tbl_xml(cls, rows, cols, width): - col_width = Emu(width/cols) if cols > 0 else Emu(0) + col_width = Emu(width / cols) if cols > 0 else Emu(0) return ( - '\n' - ' \n' + "\n" + " \n" ' \n' ' \n' - ' \n' - '%s' # tblGrid - '%s' # trs - '\n' + " \n" + "%s" # tblGrid + "%s" # trs + "\n" ) % ( - nsdecls('w'), + nsdecls("w"), cls._tblGrid_xml(cols, col_width), - cls._trs_xml(rows, cols, col_width) + cls._trs_xml(rows, cols, col_width), ) @classmethod def _tblGrid_xml(cls, col_count, col_width): - xml = ' \n' + xml = " \n" for i in range(col_count): xml += ' \n' % col_width.twips - xml += ' \n' + xml += " \n" return xml @classmethod def _trs_xml(cls, row_count, col_count, col_width): - xml = '' + xml = "" for i in range(row_count): - xml += ( - ' \n' - '%s' - ' \n' - ) % cls._tcs_xml(col_count, col_width) + xml += (" \n" "%s" " \n") % cls._tcs_xml( + col_count, col_width + ) return xml @classmethod def _tcs_xml(cls, col_count, col_width): - xml = '' + xml = "" for i in range(col_count): xml += ( - ' \n' - ' \n' + " \n" + " \n" ' \n' - ' \n' - ' \n' - ' \n' + " \n" + " \n" + " \n" ) % col_width.twips return xml class CT_TblGrid(BaseOxmlElement): - """ - ```` element, child of ````, holds ```` - elements that define column count, width, etc. - """ - gridCol = ZeroOrMore('w:gridCol', successors=('w:tblGridChange',)) + """```` element, child of ````, holds ```` elements + that define column count, width, etc.""" + + gridCol = ZeroOrMore("w:gridCol", successors=("w:tblGridChange",)) class CT_TblGridCol(BaseOxmlElement): - """ - ```` element, child of ````, defines a table - column. - """ - w = OptionalAttribute('w:w', ST_TwipsMeasure) + """```` element, child of ````, defines a table column.""" + + w = OptionalAttribute("w:w", ST_TwipsMeasure) @property def gridCol_idx(self): - """ - The index of this ```` element within its parent - ```` element. - """ + """The index of this ```` element within its parent ```` + element.""" return self.getparent().gridCol_lst.index(self) class CT_TblLayoutType(BaseOxmlElement): - """ - ```` element, specifying whether column widths are fixed or - can be automatically adjusted based on content. - """ - type = OptionalAttribute('w:type', ST_TblLayoutType) + """```` element, specifying whether column widths are fixed or can be + automatically adjusted based on content.""" + + type = OptionalAttribute("w:type", ST_TblLayoutType) class CT_TblPr(BaseOxmlElement): - """ - ```` element, child of ````, holds child elements that - define table properties such as style and borders. - """ + """```` element, child of ````, holds child elements that define + table properties such as style and borders.""" + _tag_seq = ( - 'w:tblStyle', 'w:tblpPr', 'w:tblOverlap', 'w:bidiVisual', - 'w:tblStyleRowBandSize', 'w:tblStyleColBandSize', 'w:tblW', 'w:jc', - 'w:tblCellSpacing', 'w:tblInd', 'w:tblBorders', 'w:shd', - 'w:tblLayout', 'w:tblCellMar', 'w:tblLook', 'w:tblCaption', - 'w:tblDescription', 'w:tblPrChange' + "w:tblStyle", + "w:tblpPr", + "w:tblOverlap", + "w:bidiVisual", + "w:tblStyleRowBandSize", + "w:tblStyleColBandSize", + "w:tblW", + "w:jc", + "w:tblCellSpacing", + "w:tblInd", + "w:tblBorders", + "w:shd", + "w:tblLayout", + "w:tblCellMar", + "w:tblLook", + "w:tblCaption", + "w:tblDescription", + "w:tblPrChange", ) - tblStyle = ZeroOrOne('w:tblStyle', successors=_tag_seq[1:]) - bidiVisual = ZeroOrOne('w:bidiVisual', successors=_tag_seq[4:]) - jc = ZeroOrOne('w:jc', successors=_tag_seq[8:]) - tblLayout = ZeroOrOne('w:tblLayout', successors=_tag_seq[13:]) + tblStyle = ZeroOrOne("w:tblStyle", successors=_tag_seq[1:]) + bidiVisual = ZeroOrOne("w:bidiVisual", successors=_tag_seq[4:]) + jc = ZeroOrOne("w:jc", successors=_tag_seq[8:]) + tblLayout = ZeroOrOne("w:tblLayout", successors=_tag_seq[13:]) del _tag_seq @property def alignment(self): - """ - Member of :ref:`WdRowAlignment` enumeration or |None|, based on the - contents of the `w:val` attribute of `./w:jc`. |None| if no `w:jc` - element is present. + """Member of :ref:`WdRowAlignment` enumeration or |None|, based on the contents + of the `w:val` attribute of `./w:jc`. + + |None| if no `w:jc` element is present. """ jc = self.jc if jc is None: @@ -306,26 +296,22 @@ def alignment(self, value): @property def autofit(self): - """ - Return |False| if there is a ```` child with ``w:type`` - attribute set to ``'fixed'``. Otherwise return |True|. + """|False| when there is a `w:tblLayout` child with `@w:type="fixed"`. + + Otherwise |True|. """ tblLayout = self.tblLayout - if tblLayout is None: - return True - return False if tblLayout.type == 'fixed' else True + return True if tblLayout is None else tblLayout.type != "fixed" @autofit.setter def autofit(self, value): tblLayout = self.get_or_add_tblLayout() - tblLayout.type = 'autofit' if value else 'fixed' + tblLayout.type = "autofit" if value else "fixed" @property def style(self): - """ - Return the value of the ``val`` attribute of the ```` - child or |None| if not present. - """ + """Return the value of the ``val`` attribute of the ```` child or + |None| if not present.""" tblStyle = self.tblStyle if tblStyle is None: return None @@ -340,46 +326,42 @@ def style(self, value): class CT_TblWidth(BaseOxmlElement): - """ - Used for ```` and ```` elements and many others, to - specify a table-related width. - """ + """Used for ```` and ```` elements and many others, to specify a + table-related width.""" + # the type for `w` attr is actually ST_MeasurementOrPercent, but using # XsdInt for now because only dxa (twips) values are being used. It's not # entirely clear what the semantics are for other values like -01.4mm - w = RequiredAttribute('w:w', XsdInt) - type = RequiredAttribute('w:type', ST_TblWidth) + w = RequiredAttribute("w:w", XsdInt) + type = RequiredAttribute("w:type", ST_TblWidth) @property def width(self): - """ - Return the EMU length value represented by the combined ``w:w`` and - ``w:type`` attributes. - """ - if self.type != 'dxa': + """Return the EMU length value represented by the combined ``w:w`` and + ``w:type`` attributes.""" + if self.type != "dxa": return None return Twips(self.w) @width.setter def width(self, value): - self.type = 'dxa' + self.type = "dxa" self.w = Emu(value).twips class CT_Tc(BaseOxmlElement): - """`w:tc` table cell element""" + """`w:tc` table cell element.""" - tcPr = ZeroOrOne('w:tcPr') # bunches of successors, overriding insert - p = OneOrMore('w:p') - tbl = OneOrMore('w:tbl') + tcPr = ZeroOrOne("w:tcPr") # bunches of successors, overriding insert + p = OneOrMore("w:p") + tbl = OneOrMore("w:tbl") @property def bottom(self): - """ - The row index that marks the bottom extent of the vertical span of - this cell. This is one greater than the index of the bottom-most row - of the span, similar to how a slice of the cell's rows would be - specified. + """The row index that marks the bottom extent of the vertical span of this cell. + + This is one greater than the index of the bottom-most row of the span, similar + to how a slice of the cell's rows would be specified. """ if self.vMerge is not None: tc_below = self._tc_below @@ -388,12 +370,12 @@ def bottom(self): return self._tr_idx + 1 def clear_content(self): - """ - Remove all content child elements, preserving the ```` - element if present. Note that this leaves the ```` element in - an invalid state because it doesn't contain at least one block-level - element. It's up to the caller to add a ````child element as the - last content element. + """Remove all content child elements, preserving the ```` element if + present. + + Note that this leaves the ```` element in an invalid state because it + doesn't contain at least one block-level element. It's up to the caller to add a + ````child element as the last content element. """ new_children = [] tcPr = self.tcPr @@ -403,9 +385,9 @@ def clear_content(self): @property def grid_span(self): - """ - The integer number of columns this cell spans. Determined by - ./w:tcPr/w:gridSpan/@val, it defaults to 1. + """The integer number of columns this cell spans. + + Determined by ./w:tcPr/w:gridSpan/@val, it defaults to 1. """ tcPr = self.tcPr if tcPr is None: @@ -418,28 +400,22 @@ def grid_span(self, value): tcPr.grid_span = value def iter_block_items(self): - """ - Generate a reference to each of the block-level content elements in - this cell, in the order they appear. - """ - block_item_tags = (qn('w:p'), qn('w:tbl'), qn('w:sdt')) + """Generate a reference to each of the block-level content elements in this + cell, in the order they appear.""" + block_item_tags = (qn("w:p"), qn("w:tbl"), qn("w:sdt")) for child in self: if child.tag in block_item_tags: yield child @property def left(self): - """ - The grid column index at which this ```` element appears. - """ + """The grid column index at which this ```` element appears.""" return self._grid_col def merge(self, other_tc): - """ - Return the top-left ```` element of a new span formed by - merging the rectangular region defined by using this tc element and - *other_tc* as diagonal corners. - """ + """Return the top-left ```` element of a new span formed by merging the + rectangular region defined by using this tc element and `other_tc` as diagonal + corners.""" top, left, height, width = self._span_dimensions(other_tc) top_tc = self._tbl.tr_lst[top].tc_at_grid_col(left) top_tc._grow_to(width, height) @@ -447,41 +423,31 @@ def merge(self, other_tc): @classmethod def new(cls): - """ - Return a new ```` element, containing an empty paragraph as the - required EG_BlockLevelElt. - """ - return parse_xml( - '\n' - ' \n' - '' % nsdecls('w') - ) + """Return a new ```` element, containing an empty paragraph as the + required EG_BlockLevelElt.""" + return parse_xml("\n" " \n" "" % nsdecls("w")) @property def right(self): - """ - The grid column index that marks the right-side extent of the - horizontal span of this cell. This is one greater than the index of - the right-most column of the span, similar to how a slice of the - cell's columns would be specified. + """The grid column index that marks the right-side extent of the horizontal span + of this cell. + + This is one greater than the index of the right-most column of the span, similar + to how a slice of the cell's columns would be specified. """ return self._grid_col + self.grid_span @property def top(self): - """ - The top-most row index in the vertical span of this cell. - """ + """The top-most row index in the vertical span of this cell.""" if self.vMerge is None or self.vMerge == ST_Merge.RESTART: return self._tr_idx return self._tc_above.top @property def vMerge(self): - """ - The value of the ./w:tcPr/w:vMerge/@val attribute, or |None| if the - w:vMerge element is not present. - """ + """The value of the ./w:tcPr/w:vMerge/@val attribute, or |None| if the w:vMerge + element is not present.""" tcPr = self.tcPr if tcPr is None: return None @@ -494,10 +460,8 @@ def vMerge(self, value): @property def width(self): - """ - Return the EMU length value represented in the ``./w:tcPr/w:tcW`` - child element or |None| if not present. - """ + """Return the EMU length value represented in the ``./w:tcPr/w:tcW`` child + element or |None| if not present.""" tcPr = self.tcPr if tcPr is None: return None @@ -509,29 +473,25 @@ def width(self, value): tcPr.width = value def _add_width_of(self, other_tc): - """ - Add the width of *other_tc* to this cell. Does nothing if either this - tc or *other_tc* does not have a specified width. + """Add the width of `other_tc` to this cell. + + Does nothing if either this tc or `other_tc` does not have a specified width. """ if self.width and other_tc.width: self.width += other_tc.width @property def _grid_col(self): - """ - The grid column at which this cell begins. - """ + """The grid column at which this cell begins.""" tr = self._tr idx = tr.tc_lst.index(self) preceding_tcs = tr.tc_lst[:idx] return sum(tc.grid_span for tc in preceding_tcs) def _grow_to(self, width, height, top_tc=None): - """ - Grow this cell to *width* grid columns and *height* rows by expanding - horizontal spans and creating continuation cells to form vertical - spans. - """ + """Grow this cell to `width` grid columns and `height` rows by expanding + horizontal spans and creating continuation cells to form vertical spans.""" + def vMerge_val(top_tc): if top_tc is not self: return ST_Merge.CONTINUE @@ -542,22 +502,17 @@ def vMerge_val(top_tc): top_tc = self if top_tc is None else top_tc self._span_to_width(width, top_tc, vMerge_val(top_tc)) if height > 1: - self._tc_below._grow_to(width, height-1, top_tc) + self._tc_below._grow_to(width, height - 1, top_tc) def _insert_tcPr(self, tcPr): - """ - ``tcPr`` has a bunch of successors, but it comes first if it appears, - so just overriding and using insert(0, ...) rather than spelling out - successors. - """ + """``tcPr`` has a bunch of successors, but it comes first if it appears, so just + overriding and using insert(0, ...) rather than spelling out successors.""" self.insert(0, tcPr) return tcPr @property def _is_empty(self): - """ - True if this cell contains only a single empty ```` element. - """ + """True if this cell contains only a single empty ```` element.""" block_items = list(self.iter_block_items()) if len(block_items) > 1: return False @@ -567,10 +522,8 @@ def _is_empty(self): return False def _move_content_to(self, other_tc): - """ - Append the content of this cell to *other_tc*, leaving this cell with - a single empty ```` element. - """ + """Append the content of this cell to `other_tc`, leaving this cell with a + single empty ```` element.""" if other_tc is self: return if self._is_empty: @@ -587,27 +540,21 @@ def _new_tbl(self): @property def _next_tc(self): - """ - The `w:tc` element immediately following this one in this row, or - |None| if this is the last `w:tc` element in the row. - """ - following_tcs = self.xpath('./following-sibling::w:tc') + """The `w:tc` element immediately following this one in this row, or |None| if + this is the last `w:tc` element in the row.""" + following_tcs = self.xpath("./following-sibling::w:tc") return following_tcs[0] if following_tcs else None def _remove(self): - """ - Remove this `w:tc` element from the XML tree. - """ + """Remove this `w:tc` element from the XML tree.""" self.getparent().remove(self) def _remove_trailing_empty_p(self): - """ - Remove the last content element from this cell if it is an empty - ```` element. - """ + """Remove the last content element from this cell if it is an empty ```` + element.""" block_items = list(self.iter_block_items()) last_content_elm = block_items[-1] - if last_content_elm.tag != qn('w:p'): + if last_content_elm.tag != qn("w:p"): return p = last_content_elm if len(p.r_lst) > 0: @@ -615,25 +562,24 @@ def _remove_trailing_empty_p(self): self.remove(p) def _span_dimensions(self, other_tc): - """ - Return a (top, left, height, width) 4-tuple specifying the extents of - the merged cell formed by using this tc and *other_tc* as opposite - corner extents. - """ + """Return a (top, left, height, width) 4-tuple specifying the extents of the + merged cell formed by using this tc and `other_tc` as opposite corner + extents.""" + def raise_on_inverted_L(a, b): if a.top == b.top and a.bottom != b.bottom: - raise InvalidSpanError('requested span not rectangular') + raise InvalidSpanError("requested span not rectangular") if a.left == b.left and a.right != b.right: - raise InvalidSpanError('requested span not rectangular') + raise InvalidSpanError("requested span not rectangular") def raise_on_tee_shaped(a, b): top_most, other = (a, b) if a.top < b.top else (b, a) if top_most.top < other.top and top_most.bottom > other.bottom: - raise InvalidSpanError('requested span not rectangular') + raise InvalidSpanError("requested span not rectangular") left_most, other = (a, b) if a.left < b.left else (b, a) if left_most.left < other.left and left_most.right > other.right: - raise InvalidSpanError('requested span not rectangular') + raise InvalidSpanError("requested span not rectangular") raise_on_inverted_L(self, other_tc) raise_on_tee_shaped(self, other_tc) @@ -646,15 +592,15 @@ def raise_on_tee_shaped(a, b): return top, left, bottom - top, right - left def _span_to_width(self, grid_width, top_tc, vMerge): - """ - Incorporate and then remove `w:tc` elements to the right of this one - until this cell spans *grid_width*. Raises |ValueError| if - *grid_width* cannot be exactly achieved, such as when a merged cell - would drive the span width greater than *grid_width* or if not enough - grid columns are available to make this cell that wide. All content - from incorporated cells is appended to *top_tc*. The val attribute of - the vMerge element on the single remaining cell is set to *vMerge*. - If *vMerge* is |None|, the vMerge element is removed if present. + """Incorporate and then remove `w:tc` elements to the right of this one until + this cell spans `grid_width`. + + Raises |ValueError| if `grid_width` cannot be exactly achieved, such as when a + merged cell would drive the span width greater than `grid_width` or if not + enough grid columns are available to make this cell that wide. All content from + incorporated cells is appended to `top_tc`. The val attribute of the vMerge + element on the single remaining cell is set to `vMerge`. If `vMerge` is |None|, + the vMerge element is removed if present. """ self._move_content_to(top_tc) while self.grid_span < grid_width: @@ -662,20 +608,21 @@ def _span_to_width(self, grid_width, top_tc, vMerge): self.vMerge = vMerge def _swallow_next_tc(self, grid_width, top_tc): + """Extend the horizontal span of this `w:tc` element to incorporate the + following `w:tc` element in the row and then delete that following `w:tc` + element. + + Any content in the following `w:tc` element is appended to the content of + `top_tc`. The width of the following `w:tc` element is added to this one, if + present. Raises |InvalidSpanError| if the width of the resulting cell is greater + than `grid_width` or if there is no next `` element in the row. """ - Extend the horizontal span of this `w:tc` element to incorporate the - following `w:tc` element in the row and then delete that following - `w:tc` element. Any content in the following `w:tc` element is - appended to the content of *top_tc*. The width of the following - `w:tc` element is added to this one, if present. Raises - |InvalidSpanError| if the width of the resulting cell is greater than - *grid_width* or if there is no next `` element in the row. - """ + def raise_on_invalid_swallow(next_tc): if next_tc is None: - raise InvalidSpanError('not enough grid columns') + raise InvalidSpanError("not enough grid columns") if self.grid_span + next_tc.grid_span > grid_width: - raise InvalidSpanError('span is not rectangular') + raise InvalidSpanError("span is not rectangular") next_tc = self._next_tc raise_on_invalid_swallow(next_tc) @@ -686,23 +633,17 @@ def raise_on_invalid_swallow(next_tc): @property def _tbl(self): - """ - The tbl element this tc element appears in. - """ - return self.xpath('./ancestor::w:tbl[position()=1]')[0] + """The tbl element this tc element appears in.""" + return self.xpath("./ancestor::w:tbl[position()=1]")[0] @property def _tc_above(self): - """ - The `w:tc` element immediately above this one in its grid column. - """ + """The `w:tc` element immediately above this one in its grid column.""" return self._tr_above.tc_at_grid_col(self._grid_col) @property def _tc_below(self): - """ - The tc element immediately below this one in its grid column. - """ + """The tc element immediately below this one in its grid column.""" tr_below = self._tr_below if tr_below is None: return None @@ -710,65 +651,72 @@ def _tc_below(self): @property def _tr(self): - """ - The tr element this tc element appears in. - """ - return self.xpath('./ancestor::w:tr[position()=1]')[0] + """The tr element this tc element appears in.""" + return self.xpath("./ancestor::w:tr[position()=1]")[0] @property def _tr_above(self): - """ - The tr element prior in sequence to the tr this cell appears in. + """The tr element prior in sequence to the tr this cell appears in. + Raises |ValueError| if called on a cell in the top-most row. """ tr_lst = self._tbl.tr_lst tr_idx = tr_lst.index(self._tr) if tr_idx == 0: - raise ValueError('no tr above topmost tr') - return tr_lst[tr_idx-1] + raise ValueError("no tr above topmost tr") + return tr_lst[tr_idx - 1] @property def _tr_below(self): - """ - The tr element next in sequence after the tr this cell appears in, or - |None| if this cell appears in the last row. - """ + """The tr element next in sequence after the tr this cell appears in, or |None| + if this cell appears in the last row.""" tr_lst = self._tbl.tr_lst tr_idx = tr_lst.index(self._tr) try: - return tr_lst[tr_idx+1] + return tr_lst[tr_idx + 1] except IndexError: return None @property def _tr_idx(self): - """ - The row index of the tr element this tc element appears in. - """ + """The row index of the tr element this tc element appears in.""" return self._tbl.tr_lst.index(self._tr) class CT_TcPr(BaseOxmlElement): - """ - ```` element, defining table cell properties - """ + """```` element, defining table cell properties.""" + _tag_seq = ( - 'w:cnfStyle', 'w:tcW', 'w:gridSpan', 'w:hMerge', 'w:vMerge', - 'w:tcBorders', 'w:shd', 'w:noWrap', 'w:tcMar', 'w:textDirection', - 'w:tcFitText', 'w:vAlign', 'w:hideMark', 'w:headers', 'w:cellIns', - 'w:cellDel', 'w:cellMerge', 'w:tcPrChange' + "w:cnfStyle", + "w:tcW", + "w:gridSpan", + "w:hMerge", + "w:vMerge", + "w:tcBorders", + "w:shd", + "w:noWrap", + "w:tcMar", + "w:textDirection", + "w:tcFitText", + "w:vAlign", + "w:hideMark", + "w:headers", + "w:cellIns", + "w:cellDel", + "w:cellMerge", + "w:tcPrChange", ) - tcW = ZeroOrOne('w:tcW', successors=_tag_seq[2:]) - gridSpan = ZeroOrOne('w:gridSpan', successors=_tag_seq[3:]) - vMerge = ZeroOrOne('w:vMerge', successors=_tag_seq[5:]) - vAlign = ZeroOrOne('w:vAlign', successors=_tag_seq[12:]) + tcW = ZeroOrOne("w:tcW", successors=_tag_seq[2:]) + gridSpan = ZeroOrOne("w:gridSpan", successors=_tag_seq[3:]) + vMerge = ZeroOrOne("w:vMerge", successors=_tag_seq[5:]) + vAlign = ZeroOrOne("w:vAlign", successors=_tag_seq[12:]) del _tag_seq @property def grid_span(self): - """ - The integer number of columns this cell spans. Determined by - ./w:gridSpan/@val, it defaults to 1. + """The integer number of columns this cell spans. + + Determined by ./w:gridSpan/@val, it defaults to 1. """ gridSpan = self.gridSpan if gridSpan is None: @@ -785,8 +733,8 @@ def grid_span(self, value): def vAlign_val(self): """Value of `w:val` attribute on `w:vAlign` child. - Value is |None| if `w:vAlign` child is not present. The `w:val` - attribute on `w:vAlign` is required. + Value is |None| if `w:vAlign` child is not present. The `w:val` attribute on + `w:vAlign` is required. """ vAlign = self.vAlign if vAlign is None: @@ -802,10 +750,8 @@ def vAlign_val(self, value): @property def vMerge_val(self): - """ - The value of the ./w:vMerge/@val attribute, or |None| if the - w:vMerge element is not present. - """ + """The value of the ./w:vMerge/@val attribute, or |None| if the w:vMerge element + is not present.""" vMerge = self.vMerge if vMerge is None: return None @@ -819,10 +765,8 @@ def vMerge_val(self, value): @property def width(self): - """ - Return the EMU length value represented in the ```` child - element or |None| if not present or its type is not 'dxa'. - """ + """Return the EMU length value represented in the ```` child element or + |None| if not present or its type is not 'dxa'.""" tcW = self.tcW if tcW is None: return None @@ -835,23 +779,31 @@ def width(self, value): class CT_TrPr(BaseOxmlElement): - """ - ```` element, defining table row properties - """ + """```` element, defining table row properties.""" + _tag_seq = ( - 'w:cnfStyle', 'w:divId', 'w:gridBefore', 'w:gridAfter', 'w:wBefore', - 'w:wAfter', 'w:cantSplit', 'w:trHeight', 'w:tblHeader', - 'w:tblCellSpacing', 'w:jc', 'w:hidden', 'w:ins', 'w:del', - 'w:trPrChange' + "w:cnfStyle", + "w:divId", + "w:gridBefore", + "w:gridAfter", + "w:wBefore", + "w:wAfter", + "w:cantSplit", + "w:trHeight", + "w:tblHeader", + "w:tblCellSpacing", + "w:jc", + "w:hidden", + "w:ins", + "w:del", + "w:trPrChange", ) - trHeight = ZeroOrOne('w:trHeight', successors=_tag_seq[8:]) + trHeight = ZeroOrOne("w:trHeight", successors=_tag_seq[8:]) del _tag_seq @property def trHeight_hRule(self): - """ - Return the value of `w:trHeight@w:hRule`, or |None| if not present. - """ + """Return the value of `w:trHeight@w:hRule`, or |None| if not present.""" trHeight = self.trHeight if trHeight is None: return None @@ -866,9 +818,7 @@ def trHeight_hRule(self, value): @property def trHeight_val(self): - """ - Return the value of `w:trHeight@w:val`, or |None| if not present. - """ + """Return the value of `w:trHeight@w:val`, or |None| if not present.""" trHeight = self.trHeight if trHeight is None: return None @@ -884,11 +834,11 @@ def trHeight_val(self, value): class CT_VerticalJc(BaseOxmlElement): """`w:vAlign` element, specifying vertical alignment of cell.""" - val = RequiredAttribute('w:val', WD_CELL_VERTICAL_ALIGNMENT) + + val = RequiredAttribute("w:val", WD_CELL_VERTICAL_ALIGNMENT) class CT_VMerge(BaseOxmlElement): - """ - ```` element, specifying vertical merging behavior of a cell. - """ - val = OptionalAttribute('w:val', ST_Merge, default=ST_Merge.CONTINUE) + """```` element, specifying vertical merging behavior of a cell.""" + + val = OptionalAttribute("w:val", ST_Merge, default=ST_Merge.CONTINUE) diff --git a/docx/oxml/text/__init__.py b/src/docx/oxml/text/__init__.py similarity index 100% rename from docx/oxml/text/__init__.py rename to src/docx/oxml/text/__init__.py diff --git a/src/docx/oxml/text/font.py b/src/docx/oxml/text/font.py new file mode 100644 index 000000000..0e183cf65 --- /dev/null +++ b/src/docx/oxml/text/font.py @@ -0,0 +1,368 @@ +"""Custom element classes related to run properties (font).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable + +from docx.enum.dml import MSO_THEME_COLOR +from docx.enum.text import WD_COLOR_INDEX, WD_UNDERLINE +from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml +from docx.oxml.simpletypes import ( + ST_HexColor, + ST_HpsMeasure, + ST_String, + ST_VerticalAlignRun, +) +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, +) + +if TYPE_CHECKING: + from docx.oxml.shared import CT_OnOff, CT_String + from docx.shared import Length + + +class CT_Color(BaseOxmlElement): + """`w:color` element, specifying the color of a font and perhaps other objects.""" + + val = RequiredAttribute("w:val", ST_HexColor) + themeColor = OptionalAttribute("w:themeColor", MSO_THEME_COLOR) + + +class CT_Fonts(BaseOxmlElement): + """`` element. + + Specifies typeface name for the various language types. + """ + + ascii: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:ascii", ST_String + ) + hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:hAnsi", ST_String + ) + + +class CT_Highlight(BaseOxmlElement): + """`w:highlight` element, specifying font highlighting/background color.""" + + val: WD_COLOR_INDEX = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", WD_COLOR_INDEX + ) + + +class CT_HpsMeasure(BaseOxmlElement): + """Used for `` element and others, specifying font size in half-points.""" + + val: Length = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_HpsMeasure + ) + + +class CT_RPr(BaseOxmlElement): + """`` element, containing the properties for a run.""" + + get_or_add_highlight: Callable[[], CT_Highlight] + get_or_add_rFonts: Callable[[], CT_Fonts] + get_or_add_sz: Callable[[], CT_HpsMeasure] + get_or_add_vertAlign: Callable[[], CT_VerticalAlignRun] + _add_rStyle: Callable[..., CT_String] + _add_u: Callable[[], CT_Underline] + _remove_highlight: Callable[[], None] + _remove_rFonts: Callable[[], None] + _remove_rStyle: Callable[[], None] + _remove_sz: Callable[[], None] + _remove_u: Callable[[], None] + _remove_vertAlign: Callable[[], None] + + _tag_seq = ( + "w:rStyle", + "w:rFonts", + "w:b", + "w:bCs", + "w:i", + "w:iCs", + "w:caps", + "w:smallCaps", + "w:strike", + "w:dstrike", + "w:outline", + "w:shadow", + "w:emboss", + "w:imprint", + "w:noProof", + "w:snapToGrid", + "w:vanish", + "w:webHidden", + "w:color", + "w:spacing", + "w:w", + "w:kern", + "w:position", + "w:sz", + "w:szCs", + "w:highlight", + "w:u", + "w:effect", + "w:bdr", + "w:shd", + "w:fitText", + "w:vertAlign", + "w:rtl", + "w:cs", + "w:em", + "w:lang", + "w:eastAsianLayout", + "w:specVanish", + "w:oMath", + ) + rStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:rStyle", successors=_tag_seq[1:] + ) + rFonts: CT_Fonts | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:rFonts", successors=_tag_seq[2:] + ) + b: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:b", successors=_tag_seq[3:] + ) + bCs = ZeroOrOne("w:bCs", successors=_tag_seq[4:]) + i = ZeroOrOne("w:i", successors=_tag_seq[5:]) + iCs = ZeroOrOne("w:iCs", successors=_tag_seq[6:]) + caps = ZeroOrOne("w:caps", successors=_tag_seq[7:]) + smallCaps = ZeroOrOne("w:smallCaps", successors=_tag_seq[8:]) + strike = ZeroOrOne("w:strike", successors=_tag_seq[9:]) + dstrike = ZeroOrOne("w:dstrike", successors=_tag_seq[10:]) + outline = ZeroOrOne("w:outline", successors=_tag_seq[11:]) + shadow = ZeroOrOne("w:shadow", successors=_tag_seq[12:]) + emboss = ZeroOrOne("w:emboss", successors=_tag_seq[13:]) + imprint = ZeroOrOne("w:imprint", successors=_tag_seq[14:]) + noProof = ZeroOrOne("w:noProof", successors=_tag_seq[15:]) + snapToGrid = ZeroOrOne("w:snapToGrid", successors=_tag_seq[16:]) + vanish = ZeroOrOne("w:vanish", successors=_tag_seq[17:]) + webHidden = ZeroOrOne("w:webHidden", successors=_tag_seq[18:]) + color = ZeroOrOne("w:color", successors=_tag_seq[19:]) + sz: CT_HpsMeasure | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:sz", successors=_tag_seq[24:] + ) + highlight: CT_Highlight | None = ( + ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:highlight", successors=_tag_seq[26:] + ) + ) + u: CT_Underline | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:u", successors=_tag_seq[27:] + ) + vertAlign: CT_VerticalAlignRun | None = ( + ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:vertAlign", successors=_tag_seq[32:] + ) + ) + rtl = ZeroOrOne("w:rtl", successors=_tag_seq[33:]) + cs = ZeroOrOne("w:cs", successors=_tag_seq[34:]) + specVanish = ZeroOrOne("w:specVanish", successors=_tag_seq[38:]) + oMath = ZeroOrOne("w:oMath", successors=_tag_seq[39:]) + del _tag_seq + + def _new_color(self): + """Override metaclass method to set `w:color/@val` to RGB black on create.""" + return parse_xml('' % nsdecls("w")) + + @property + def highlight_val(self) -> WD_COLOR_INDEX | None: + """Value of `./w:highlight/@val`. + + Specifies font's highlight color, or `None` if the text is not highlighted. + """ + highlight = self.highlight + if highlight is None: + return None + return highlight.val + + @highlight_val.setter + def highlight_val(self, value: WD_COLOR_INDEX | None) -> None: + if value is None: + self._remove_highlight() + return + highlight = self.get_or_add_highlight() + highlight.val = value + + @property + def rFonts_ascii(self) -> str | None: + """The value of `w:rFonts/@w:ascii` or |None| if not present. + + Represents the assigned typeface name. The rFonts element also specifies other + special-case typeface names; this method handles the case where just the common + name is required. + """ + rFonts = self.rFonts + if rFonts is None: + return None + return rFonts.ascii + + @rFonts_ascii.setter + def rFonts_ascii(self, value: str | None) -> None: + if value is None: + self._remove_rFonts() + return + rFonts = self.get_or_add_rFonts() + rFonts.ascii = value + + @property + def rFonts_hAnsi(self) -> str | None: + """The value of `w:rFonts/@w:hAnsi` or |None| if not present.""" + rFonts = self.rFonts + if rFonts is None: + return None + return rFonts.hAnsi + + @rFonts_hAnsi.setter + def rFonts_hAnsi(self, value: str | None): + if value is None and self.rFonts is None: + return + rFonts = self.get_or_add_rFonts() + rFonts.hAnsi = value + + @property + def style(self) -> str | None: + """String in `./w:rStyle/@val`, or None if `w:rStyle` is not present.""" + rStyle = self.rStyle + if rStyle is None: + return None + return rStyle.val + + @style.setter + def style(self, style: str | None) -> None: + """Set `./w:rStyle/@val` to `style`, adding the `w:rStyle` element if necessary. + + If `style` is |None|, remove `w:rStyle` element if present. + """ + if style is None: + self._remove_rStyle() + elif self.rStyle is None: + self._add_rStyle(val=style) + else: + self.rStyle.val = style + + @property + def subscript(self) -> bool | None: + """|True| if `./w:vertAlign/@w:val` is "subscript". + + |False| if `w:vertAlign/@w:val` contains any other value. |None| if + `w:vertAlign` is not present. + """ + vertAlign = self.vertAlign + if vertAlign is None: + return None + if vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT: + return True + return False + + @subscript.setter + def subscript(self, value: bool | None) -> None: + if value is None: + self._remove_vertAlign() + elif bool(value) is True: + self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUBSCRIPT + # -- assert bool(value) is False -- + elif ( + self.vertAlign is not None + and self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT + ): + self._remove_vertAlign() + + @property + def superscript(self) -> bool | None: + """|True| if `w:vertAlign/@w:val` is 'superscript'. + + |False| if `w:vertAlign/@w:val` contains any other value. |None| if + `w:vertAlign` is not present. + """ + vertAlign = self.vertAlign + if vertAlign is None: + return None + if vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT: + return True + return False + + @superscript.setter + def superscript(self, value: bool | None): + if value is None: + self._remove_vertAlign() + elif bool(value) is True: + self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUPERSCRIPT + # -- assert bool(value) is False -- + elif ( + self.vertAlign is not None + and self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT + ): + self._remove_vertAlign() + + @property + def sz_val(self) -> Length | None: + """The value of `w:sz/@w:val` or |None| if not present.""" + sz = self.sz + if sz is None: + return None + return sz.val + + @sz_val.setter + def sz_val(self, value: Length | None): + if value is None: + self._remove_sz() + return + sz = self.get_or_add_sz() + sz.val = value + + @property + def u_val(self) -> WD_UNDERLINE | None: + """Value of `w:u/@val`, or None if not present. + + Values `WD_UNDERLINE.SINGLE` and `WD_UNDERLINE.NONE` are mapped to `True` and + `False` respectively. + """ + u = self.u + if u is None: + return None + return u.val + + @u_val.setter + def u_val(self, value: WD_UNDERLINE | None): + self._remove_u() + if value is not None: + self._add_u().val = value + + def _get_bool_val(self, name: str) -> bool | None: + """Value of boolean child with `name`, e.g. "w:b", "w:i", and "w:smallCaps".""" + element = getattr(self, name) + if element is None: + return None + return element.val + + def _set_bool_val(self, name: str, value: bool | None): + if value is None: + getattr(self, "_remove_%s" % name)() + return + element = getattr(self, "get_or_add_%s" % name)() + element.val = value + + +class CT_Underline(BaseOxmlElement): + """`` element, specifying the underlining style for a run.""" + + val: WD_UNDERLINE | None = ( + OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", WD_UNDERLINE + ) + ) + + +class CT_VerticalAlignRun(BaseOxmlElement): + """`` element, specifying subscript or superscript.""" + + val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:val", ST_VerticalAlignRun + ) diff --git a/src/docx/oxml/text/hyperlink.py b/src/docx/oxml/text/hyperlink.py new file mode 100644 index 000000000..76733457b --- /dev/null +++ b/src/docx/oxml/text/hyperlink.py @@ -0,0 +1,41 @@ +"""Custom element classes related to hyperlinks (CT_Hyperlink).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from docx.oxml.simpletypes import ST_OnOff, XsdString +from docx.oxml.text.run import CT_R +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, +) + +if TYPE_CHECKING: + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak + + +class CT_Hyperlink(BaseOxmlElement): + """`` element, containing the text and address for a hyperlink.""" + + r_lst: List[CT_R] + + rId = RequiredAttribute("r:id", XsdString) + history = OptionalAttribute("w:history", ST_OnOff, default=True) + + r = ZeroOrMore("w:r") + + @property + def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]: + """All `w:lastRenderedPageBreak` descendants of this hyperlink.""" + return self.xpath("./w:r/w:lastRenderedPageBreak") + + @property # pyright: ignore[reportIncompatibleVariableOverride] + def text(self) -> str: + """The textual content of this hyperlink. + + `CT_Hyperlink` stores the hyperlink-text as one or more `w:r` children. + """ + return "".join(r.text for r in self.xpath("w:r")) diff --git a/src/docx/oxml/text/pagebreak.py b/src/docx/oxml/text/pagebreak.py new file mode 100644 index 000000000..943f9b6c2 --- /dev/null +++ b/src/docx/oxml/text/pagebreak.py @@ -0,0 +1,284 @@ +"""Custom element class for rendered page-break (CT_LastRenderedPageBreak).""" + +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from docx.oxml.xmlchemy import BaseOxmlElement +from docx.shared import lazyproperty + +if TYPE_CHECKING: + from docx.oxml.text.hyperlink import CT_Hyperlink + from docx.oxml.text.paragraph import CT_P + + +class CT_LastRenderedPageBreak(BaseOxmlElement): + """`` element, indicating page break inserted by renderer. + + A rendered page-break is one inserted by the renderer when it runs out of room on a + page. It is an empty element (no attrs or children) and is a child of CT_R, peer to + CT_Text. + + NOTE: this complex-type name does not exist in the schema, where + `w:lastRenderedPageBreak` maps to `CT_Empty`. This name was added to give it + distinguished behavior. CT_Empty is used for many elements. + """ + + @property + def following_fragment_p(self) -> CT_P: + """A "loose" `CT_P` containing only the paragraph content before this break. + + Raises `ValueError` if this `w:lastRenderedPageBreak` is not the first rendered + page-break in its paragraph. + + The returned `CT_P` is a "clone" (deepcopy) of the `w:p` ancestor of this + page-break with this `w:lastRenderedPageBreak` element and all content preceding + it removed. + + NOTE: this `w:p` can itself contain one or more `w:renderedPageBreak` elements + (when the paragraph contained more than one). While this is rare, the caller + should treat this paragraph the same as other paragraphs and split it if + necessary in a folloing step or recursion. + """ + if not self == self._first_lrpb_in_p(self._enclosing_p): + raise ValueError("only defined on first rendered page-break in paragraph") + + # -- splitting approach is different when break is inside a hyperlink -- + return ( + self._following_frag_in_hlink + if self._is_in_hyperlink + else self._following_frag_in_run + ) + + @property + def follows_all_content(self) -> bool: + """True when this page-break element is the last "content" in the paragraph. + + This is very uncommon case and may only occur in contrived or cases where the + XML is edited by hand, but it is not precluded by the spec. + """ + # -- a page-break inside a hyperlink never meets these criteria (for our + # -- purposes at least) because it is considered "atomic" and always associated + # -- with the page it starts on. + if self._is_in_hyperlink: + return False + + return bool( + # -- XPath will match zero-or-one w:lastRenderedPageBreak element -- + self._enclosing_p.xpath( + # -- in first run of paragraph -- + f"(./w:r)[last()]" + # -- all page-breaks -- + f"/w:lastRenderedPageBreak" + # -- that are not preceded by any content-bearing elements -- + f"[not(following-sibling::*[{self._run_inner_content_xpath}])]" + ) + ) + + @property + def precedes_all_content(self) -> bool: + """True when a `w:lastRenderedPageBreak` precedes all paragraph content. + + This is a common case; it occurs whenever the page breaks on an even paragraph + boundary. + """ + # -- a page-break inside a hyperlink never meets these criteria because there + # -- is always part of the hyperlink text before the page-break. + if self._is_in_hyperlink: + return False + + return bool( + # -- XPath will match zero-or-one w:lastRenderedPageBreak element -- + self._enclosing_p.xpath( + # -- in first run of paragraph -- + f"./w:r[1]" + # -- all page-breaks -- + f"/w:lastRenderedPageBreak" + # -- that are not preceded by any content-bearing elements -- + f"[not(preceding-sibling::*[{self._run_inner_content_xpath}])]" + ) + ) + + @property + def preceding_fragment_p(self) -> CT_P: + """A "loose" `CT_P` containing only the paragraph content before this break. + + Raises `ValueError` if this `w:lastRenderedPageBreak` is not the first rendered + paragraph in its paragraph. + + The returned `CT_P` is a "clone" (deepcopy) of the `w:p` ancestor of this + page-break with this `w:lastRenderedPageBreak` element and all its following + siblings removed. + """ + if not self == self._first_lrpb_in_p(self._enclosing_p): + raise ValueError("only defined on first rendered page-break in paragraph") + + # -- splitting approach is different when break is inside a hyperlink -- + return ( + self._preceding_frag_in_hlink + if self._is_in_hyperlink + else self._preceding_frag_in_run + ) + + def _enclosing_hyperlink(self, lrpb: CT_LastRenderedPageBreak) -> CT_Hyperlink: + """The `w:hyperlink` grandparent of this `w:lastRenderedPageBreak`. + + Raises `IndexError` when this page-break has a `w:p` grandparent, so only call + when `._is_in_hyperlink` is True. + """ + return lrpb.xpath("./parent::w:r/parent::w:hyperlink")[0] + + @property + def _enclosing_p(self) -> CT_P: + """The `w:p` element parent or grandparent of this `w:lastRenderedPageBreak`.""" + return self.xpath("./ancestor::w:p[1]")[0] + + def _first_lrpb_in_p(self, p: CT_P) -> CT_LastRenderedPageBreak: + """The first `w:lastRenderedPageBreak` element in `p`. + + Raises `ValueError` if there are no rendered page-breaks in `p`. + """ + lrpbs = p.xpath( + "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak" + ) + if not lrpbs: + raise ValueError("no rendered page-breaks in paragraph element") + return lrpbs[0] + + @lazyproperty + def _following_frag_in_hlink(self) -> CT_P: + """Following CT_P fragment when break occurs within a hyperlink. + + Note this is a *partial-function* and raises when `lrpb` is not inside a + hyperlink. + """ + if not self._is_in_hyperlink: + raise ValueError("only defined on a rendered page-break in a hyperlink") + + # -- work on a clone `w:p` so our mutations don't persist -- + p = copy.deepcopy(self._enclosing_p) + + # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) -- + lrpb = self._first_lrpb_in_p(p) + + # -- locate `w:hyperlink` in which this `w:lastRenderedPageBreak` is found -- + hyperlink = lrpb._enclosing_hyperlink(lrpb) + + # -- delete all w:p inner-content preceding the hyperlink -- + for e in hyperlink.xpath("./preceding-sibling::*[not(self::w:pPr)]"): + p.remove(e) + + # -- remove the whole hyperlink, it belongs to the preceding-fragment-p -- + hyperlink.getparent().remove(hyperlink) + + # -- that's it, return the remaining fragment of `w:p` clone -- + return p + + @lazyproperty + def _following_frag_in_run(self) -> CT_P: + """following CT_P fragment when break does not occur in a hyperlink. + + Note this is a *partial-function* and raises when `lrpb` is inside a hyperlink. + """ + if self._is_in_hyperlink: + raise ValueError("only defined on a rendered page-break not in a hyperlink") + + # -- work on a clone `w:p` so our mutations don't persist -- + p = copy.deepcopy(self._enclosing_p) + + # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) -- + lrpb = self._first_lrpb_in_p(p) + + # -- locate `w:r` in which this `w:lastRenderedPageBreak` is found -- + enclosing_r = lrpb.xpath("./parent::w:r")[0] + + # -- delete all w:p inner-content preceding that run (but not w:pPr) -- + for e in enclosing_r.xpath("./preceding-sibling::*[not(self::w:pPr)]"): + p.remove(e) + + # -- then remove all run inner-content preceding this lrpb in its run (but not + # -- the `w:rPr`) and also remove the page-break itself + for e in lrpb.xpath("./preceding-sibling::*[not(self::w:rPr)]"): + enclosing_r.remove(e) + enclosing_r.remove(lrpb) + + return p + + @lazyproperty + def _is_in_hyperlink(self) -> bool: + """True when this page-break is embedded in a hyperlink run.""" + return bool(self.xpath("./parent::w:r/parent::w:hyperlink")) + + @lazyproperty + def _preceding_frag_in_hlink(self) -> CT_P: + """Preceding CT_P fragment when break occurs within a hyperlink. + + Note this is a *partial-function* and raises when `lrpb` is not inside a + hyperlink. + """ + if not self._is_in_hyperlink: + raise ValueError("only defined on a rendered page-break in a hyperlink") + + # -- work on a clone `w:p` so our mutations don't persist -- + p = copy.deepcopy(self._enclosing_p) + + # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) -- + lrpb = self._first_lrpb_in_p(p) + + # -- locate `w:hyperlink` in which this `w:lastRenderedPageBreak` is found -- + hyperlink = lrpb._enclosing_hyperlink(lrpb) + + # -- delete all w:p inner-content following the hyperlink -- + for e in hyperlink.xpath("./following-sibling::*"): + p.remove(e) + + # -- remove this page-break from inside the hyperlink -- + lrpb.getparent().remove(lrpb) + + # -- that's it, the entire hyperlink goes into the preceding fragment so + # -- the hyperlink is not "split". + return p + + @lazyproperty + def _preceding_frag_in_run(self) -> CT_P: + """Preceding CT_P fragment when break does not occur in a hyperlink. + + Note this is a *partial-function* and raises when `lrpb` is inside a hyperlink. + """ + if self._is_in_hyperlink: + raise ValueError("only defined on a rendered page-break not in a hyperlink") + + # -- work on a clone `w:p` so our mutations don't persist -- + p = copy.deepcopy(self._enclosing_p) + + # -- get this `w:lastRenderedPageBreak` in the cloned `w:p` (not self) -- + lrpb = self._first_lrpb_in_p(p) + + # -- locate `w:r` in which this `w:lastRenderedPageBreak` is found -- + enclosing_r = lrpb.xpath("./parent::w:r")[0] + + # -- delete all `w:p` inner-content following that run -- + for e in enclosing_r.xpath("./following-sibling::*"): + p.remove(e) + + # -- then delete all `w:r` inner-content following this lrpb in its run and + # -- also remove the page-break itself + for e in lrpb.xpath("./following-sibling::*"): + enclosing_r.remove(e) + enclosing_r.remove(lrpb) + + return p + + @lazyproperty + def _run_inner_content_xpath(self) -> str: + """XPath fragment matching any run inner-content elements.""" + return ( + "self::w:br" + " | self::w:cr" + " | self::w:drawing" + " | self::w:noBreakHyphen" + " | self::w:ptab" + " | self::w:t" + " | self::w:tab" + ) diff --git a/src/docx/oxml/text/paragraph.py b/src/docx/oxml/text/paragraph.py new file mode 100644 index 000000000..f771dd74f --- /dev/null +++ b/src/docx/oxml/text/paragraph.py @@ -0,0 +1,106 @@ +# pyright: reportPrivateUsage=false + +"""Custom element classes related to paragraphs (CT_P).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, List, cast + +from docx.oxml.parser import OxmlElement +from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne + +if TYPE_CHECKING: + from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + from docx.oxml.section import CT_SectPr + from docx.oxml.text.hyperlink import CT_Hyperlink + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak + from docx.oxml.text.parfmt import CT_PPr + from docx.oxml.text.run import CT_R + + +class CT_P(BaseOxmlElement): + """`` element, containing the properties and text for a paragraph.""" + + add_r: Callable[[], CT_R] + get_or_add_pPr: Callable[[], CT_PPr] + hyperlink_lst: List[CT_Hyperlink] + r_lst: List[CT_R] + + pPr: CT_PPr | None = ZeroOrOne("w:pPr") # pyright: ignore[reportGeneralTypeIssues] + hyperlink = ZeroOrMore("w:hyperlink") + r = ZeroOrMore("w:r") + + def add_p_before(self) -> CT_P: + """Return a new `` element inserted directly prior to this one.""" + new_p = cast(CT_P, OxmlElement("w:p")) + self.addprevious(new_p) + return new_p + + @property + def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None: + """The value of the `` grandchild element or |None| if not present.""" + pPr = self.pPr + if pPr is None: + return None + return pPr.jc_val + + @alignment.setter + def alignment(self, value: WD_PARAGRAPH_ALIGNMENT): + pPr = self.get_or_add_pPr() + pPr.jc_val = value + + def clear_content(self): + """Remove all child elements, except the `` element if present.""" + for child in self.xpath("./*[not(self::w:pPr)]"): + self.remove(child) + + @property + def inner_content_elements(self) -> List[CT_R | CT_Hyperlink]: + """Run and hyperlink children of the `w:p` element, in document order.""" + return self.xpath("./w:r | ./w:hyperlink") + + @property + def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]: + """All `w:lastRenderedPageBreak` descendants of this paragraph. + + Rendered page-breaks commonly occur in a run but can also occur in a run inside + a hyperlink. This returns both. + """ + return self.xpath( + "./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak" + ) + + def set_sectPr(self, sectPr: CT_SectPr): + """Unconditionally replace or add `sectPr` as grandchild in correct sequence.""" + pPr = self.get_or_add_pPr() + pPr._remove_sectPr() + pPr._insert_sectPr(sectPr) + + @property + def style(self) -> str | None: + """String contained in `w:val` attribute of `./w:pPr/w:pStyle` grandchild. + + |None| if not present. + """ + pPr = self.pPr + if pPr is None: + return None + return pPr.style + + @style.setter + def style(self, style: str | None): + pPr = self.get_or_add_pPr() + pPr.style = style + + @property # pyright: ignore[reportIncompatibleVariableOverride] + def text(self): + """The textual content of this paragraph. + + Inner-content child elements like `w:r` and `w:hyperlink` are translated to + their text equivalent. + """ + return "".join(e.text for e in self.xpath("w:r | w:hyperlink")) + + def _insert_pPr(self, pPr: CT_PPr) -> CT_PPr: + self.insert(0, pPr) + return pPr diff --git a/src/docx/oxml/text/parfmt.py b/src/docx/oxml/text/parfmt.py new file mode 100644 index 000000000..49ea01003 --- /dev/null +++ b/src/docx/oxml/text/parfmt.py @@ -0,0 +1,368 @@ +"""Custom element classes related to paragraph properties (CT_PPr).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable + +from docx.enum.text import ( + WD_ALIGN_PARAGRAPH, + WD_LINE_SPACING, + WD_TAB_ALIGNMENT, + WD_TAB_LEADER, +) +from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure +from docx.oxml.xmlchemy import ( + BaseOxmlElement, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrOne, +) +from docx.shared import Length + +if TYPE_CHECKING: + from docx.oxml.section import CT_SectPr + from docx.oxml.shared import CT_String + + +class CT_Ind(BaseOxmlElement): + """```` element, specifying paragraph indentation.""" + + left = OptionalAttribute("w:left", ST_SignedTwipsMeasure) + right = OptionalAttribute("w:right", ST_SignedTwipsMeasure) + firstLine = OptionalAttribute("w:firstLine", ST_TwipsMeasure) + hanging = OptionalAttribute("w:hanging", ST_TwipsMeasure) + + +class CT_Jc(BaseOxmlElement): + """```` element, specifying paragraph justification.""" + + val = RequiredAttribute("w:val", WD_ALIGN_PARAGRAPH) + + +class CT_PPr(BaseOxmlElement): + """```` element, containing the properties for a paragraph.""" + + get_or_add_pStyle: Callable[[], CT_String] + _insert_sectPr: Callable[[CT_SectPr], None] + _remove_pStyle: Callable[[], None] + _remove_sectPr: Callable[[], None] + + _tag_seq = ( + "w:pStyle", + "w:keepNext", + "w:keepLines", + "w:pageBreakBefore", + "w:framePr", + "w:widowControl", + "w:numPr", + "w:suppressLineNumbers", + "w:pBdr", + "w:shd", + "w:tabs", + "w:suppressAutoHyphens", + "w:kinsoku", + "w:wordWrap", + "w:overflowPunct", + "w:topLinePunct", + "w:autoSpaceDE", + "w:autoSpaceDN", + "w:bidi", + "w:adjustRightInd", + "w:snapToGrid", + "w:spacing", + "w:ind", + "w:contextualSpacing", + "w:mirrorIndents", + "w:suppressOverlap", + "w:jc", + "w:textDirection", + "w:textAlignment", + "w:textboxTightWrap", + "w:outlineLvl", + "w:divId", + "w:cnfStyle", + "w:rPr", + "w:sectPr", + "w:pPrChange", + ) + pStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues] + "w:pStyle", successors=_tag_seq[1:] + ) + keepNext = ZeroOrOne("w:keepNext", successors=_tag_seq[2:]) + keepLines = ZeroOrOne("w:keepLines", successors=_tag_seq[3:]) + pageBreakBefore = ZeroOrOne("w:pageBreakBefore", successors=_tag_seq[4:]) + widowControl = ZeroOrOne("w:widowControl", successors=_tag_seq[6:]) + numPr = ZeroOrOne("w:numPr", successors=_tag_seq[7:]) + tabs = ZeroOrOne("w:tabs", successors=_tag_seq[11:]) + spacing = ZeroOrOne("w:spacing", successors=_tag_seq[22:]) + ind = ZeroOrOne("w:ind", successors=_tag_seq[23:]) + jc = ZeroOrOne("w:jc", successors=_tag_seq[27:]) + sectPr = ZeroOrOne("w:sectPr", successors=_tag_seq[35:]) + del _tag_seq + + @property + def first_line_indent(self): + """A |Length| value calculated from the values of `w:ind/@w:firstLine` and + `w:ind/@w:hanging`. + + Returns |None| if the `w:ind` child is not present. + """ + ind = self.ind + if ind is None: + return None + hanging = ind.hanging + if hanging is not None: + return Length(-hanging) + firstLine = ind.firstLine + if firstLine is None: + return None + return firstLine + + @first_line_indent.setter + def first_line_indent(self, value): + if self.ind is None and value is None: + return + ind = self.get_or_add_ind() + ind.firstLine = ind.hanging = None + if value is None: + return + elif value < 0: + ind.hanging = -value + else: + ind.firstLine = value + + @property + def ind_left(self): + """The value of `w:ind/@w:left` or |None| if not present.""" + ind = self.ind + if ind is None: + return None + return ind.left + + @ind_left.setter + def ind_left(self, value): + if value is None and self.ind is None: + return + ind = self.get_or_add_ind() + ind.left = value + + @property + def ind_right(self): + """The value of `w:ind/@w:right` or |None| if not present.""" + ind = self.ind + if ind is None: + return None + return ind.right + + @ind_right.setter + def ind_right(self, value): + if value is None and self.ind is None: + return + ind = self.get_or_add_ind() + ind.right = value + + @property + def jc_val(self) -> WD_ALIGN_PARAGRAPH | None: + """Value of the `` child element or |None| if not present.""" + return self.jc.val if self.jc is not None else None + + @jc_val.setter + def jc_val(self, value): + if value is None: + self._remove_jc() + return + self.get_or_add_jc().val = value + + @property + def keepLines_val(self): + """The value of `keepLines/@val` or |None| if not present.""" + keepLines = self.keepLines + if keepLines is None: + return None + return keepLines.val + + @keepLines_val.setter + def keepLines_val(self, value): + if value is None: + self._remove_keepLines() + else: + self.get_or_add_keepLines().val = value + + @property + def keepNext_val(self): + """The value of `keepNext/@val` or |None| if not present.""" + keepNext = self.keepNext + if keepNext is None: + return None + return keepNext.val + + @keepNext_val.setter + def keepNext_val(self, value): + if value is None: + self._remove_keepNext() + else: + self.get_or_add_keepNext().val = value + + @property + def pageBreakBefore_val(self): + """The value of `pageBreakBefore/@val` or |None| if not present.""" + pageBreakBefore = self.pageBreakBefore + if pageBreakBefore is None: + return None + return pageBreakBefore.val + + @pageBreakBefore_val.setter + def pageBreakBefore_val(self, value): + if value is None: + self._remove_pageBreakBefore() + else: + self.get_or_add_pageBreakBefore().val = value + + @property + def spacing_after(self): + """The value of `w:spacing/@w:after` or |None| if not present.""" + spacing = self.spacing + if spacing is None: + return None + return spacing.after + + @spacing_after.setter + def spacing_after(self, value): + if value is None and self.spacing is None: + return + self.get_or_add_spacing().after = value + + @property + def spacing_before(self): + """The value of `w:spacing/@w:before` or |None| if not present.""" + spacing = self.spacing + if spacing is None: + return None + return spacing.before + + @spacing_before.setter + def spacing_before(self, value): + if value is None and self.spacing is None: + return + self.get_or_add_spacing().before = value + + @property + def spacing_line(self): + """The value of `w:spacing/@w:line` or |None| if not present.""" + spacing = self.spacing + if spacing is None: + return None + return spacing.line + + @spacing_line.setter + def spacing_line(self, value): + if value is None and self.spacing is None: + return + self.get_or_add_spacing().line = value + + @property + def spacing_lineRule(self): + """The value of `w:spacing/@w:lineRule` as a member of the :ref:`WdLineSpacing` + enumeration. + + Only the `MULTIPLE`, `EXACTLY`, and `AT_LEAST` members are used. It is the + responsibility of the client to calculate the use of `SINGLE`, `DOUBLE`, and + `MULTIPLE` based on the value of `w:spacing/@w:line` if that behavior is + desired. + """ + spacing = self.spacing + if spacing is None: + return None + lineRule = spacing.lineRule + if lineRule is None and spacing.line is not None: + return WD_LINE_SPACING.MULTIPLE + return lineRule + + @spacing_lineRule.setter + def spacing_lineRule(self, value): + if value is None and self.spacing is None: + return + self.get_or_add_spacing().lineRule = value + + @property + def style(self) -> str | None: + """String contained in `./w:pStyle/@val`, or None if child is not present.""" + pStyle = self.pStyle + if pStyle is None: + return None + return pStyle.val + + @style.setter + def style(self, style: str | None): + """Set `./w:pStyle/@val` `style`, adding a new element if necessary. + + If `style` is |None|, remove `./w:pStyle` when present. + """ + if style is None: + self._remove_pStyle() + return + pStyle = self.get_or_add_pStyle() + pStyle.val = style + + @property + def widowControl_val(self): + """The value of `widowControl/@val` or |None| if not present.""" + widowControl = self.widowControl + if widowControl is None: + return None + return widowControl.val + + @widowControl_val.setter + def widowControl_val(self, value): + if value is None: + self._remove_widowControl() + else: + self.get_or_add_widowControl().val = value + + +class CT_Spacing(BaseOxmlElement): + """```` element, specifying paragraph spacing attributes such as space + before and line spacing.""" + + after = OptionalAttribute("w:after", ST_TwipsMeasure) + before = OptionalAttribute("w:before", ST_TwipsMeasure) + line = OptionalAttribute("w:line", ST_SignedTwipsMeasure) + lineRule = OptionalAttribute("w:lineRule", WD_LINE_SPACING) + + +class CT_TabStop(BaseOxmlElement): + """`` element, representing an individual tab stop. + + Overloaded to use for a tab-character in a run, which also uses the w:tab tag but + only needs a __str__ method. + """ + + val = RequiredAttribute("w:val", WD_TAB_ALIGNMENT) + leader = OptionalAttribute("w:leader", WD_TAB_LEADER, default=WD_TAB_LEADER.SPACES) + pos = RequiredAttribute("w:pos", ST_SignedTwipsMeasure) + + def __str__(self) -> str: + """Text equivalent of a `w:tab` element appearing in a run. + + Allows text of run inner-content to be accessed consistently across all text + inner-content. + """ + return "\t" + + +class CT_TabStops(BaseOxmlElement): + """```` element, container for a sorted sequence of tab stops.""" + + tab = OneOrMore("w:tab", successors=()) + + def insert_tab_in_order(self, pos, align, leader): + """Insert a newly created `w:tab` child element in `pos` order.""" + new_tab = self._new_tab() + new_tab.pos, new_tab.val, new_tab.leader = pos, align, leader + for tab in self.tab_lst: + if new_tab.pos < tab.pos: + tab.addprevious(new_tab) + return new_tab + self.append(new_tab) + return new_tab diff --git a/src/docx/oxml/text/run.py b/src/docx/oxml/text/run.py new file mode 100644 index 000000000..f17d33845 --- /dev/null +++ b/src/docx/oxml/text/run.py @@ -0,0 +1,279 @@ +"""Custom element classes related to text runs (CT_R).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Iterator, List + +from docx.oxml.drawing import CT_Drawing +from docx.oxml.ns import qn +from docx.oxml.simpletypes import ST_BrClear, ST_BrType +from docx.oxml.text.font import CT_RPr +from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne +from docx.shared import TextAccumulator + +if TYPE_CHECKING: + from docx.oxml.shape import CT_Anchor, CT_Inline + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak + from docx.oxml.text.parfmt import CT_TabStop + +# ------------------------------------------------------------------------------------ +# Run-level elements + + +class CT_R(BaseOxmlElement): + """`` element, containing the properties and text for a run.""" + + add_br: Callable[[], CT_Br] + add_tab: Callable[[], CT_TabStop] + get_or_add_rPr: Callable[[], CT_RPr] + _add_drawing: Callable[[], CT_Drawing] + _add_t: Callable[..., CT_Text] + + rPr: CT_RPr | None = ZeroOrOne("w:rPr") # pyright: ignore[reportGeneralTypeIssues] + br = ZeroOrMore("w:br") + cr = ZeroOrMore("w:cr") + drawing = ZeroOrMore("w:drawing") + t = ZeroOrMore("w:t") + tab = ZeroOrMore("w:tab") + + def add_t(self, text: str) -> CT_Text: + """Return a newly added `` element containing `text`.""" + t = self._add_t(text=text) + if len(text.strip()) < len(text): + t.set(qn("xml:space"), "preserve") + return t + + def add_drawing(self, inline_or_anchor: CT_Inline | CT_Anchor) -> CT_Drawing: + """Return newly appended `CT_Drawing` (`w:drawing`) child element. + + The `w:drawing` element has `inline_or_anchor` as its child. + """ + drawing = self._add_drawing() + drawing.append(inline_or_anchor) + return drawing + + def clear_content(self) -> None: + """Remove all child elements except a `w:rPr` element if present.""" + # -- remove all run inner-content except a `w:rPr` when present. -- + for e in self.xpath("./*[not(self::w:rPr)]"): + self.remove(e) + + @property + def inner_content_items(self) -> List[str | CT_Drawing | CT_LastRenderedPageBreak]: + """Text of run, possibly punctuated by `w:lastRenderedPageBreak` elements.""" + from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak + + accum = TextAccumulator() + + def iter_items() -> Iterator[str | CT_Drawing | CT_LastRenderedPageBreak]: + for e in self.xpath( + "w:br" + " | w:cr" + " | w:drawing" + " | w:lastRenderedPageBreak" + " | w:noBreakHyphen" + " | w:ptab" + " | w:t" + " | w:tab" + ): + if isinstance(e, (CT_Drawing, CT_LastRenderedPageBreak)): + yield from accum.pop() + yield e + else: + accum.push(str(e)) + + # -- don't forget the "tail" string -- + yield from accum.pop() + + return list(iter_items()) + + @property + def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]: + """All `w:lastRenderedPageBreaks` descendants of this run.""" + return self.xpath("./w:lastRenderedPageBreak") + + @property + def style(self) -> str | None: + """String contained in `w:val` attribute of `w:rStyle` grandchild. + + |None| if that element is not present. + """ + rPr = self.rPr + if rPr is None: + return None + return rPr.style + + @style.setter + def style(self, style: str | None): + """Set character style of this `w:r` element to `style`. + + If `style` is None, remove the style element. + """ + rPr = self.get_or_add_rPr() + rPr.style = style + + @property + def text(self) -> str: + """The textual content of this run. + + Inner-content child elements like `w:tab` are translated to their text + equivalent. + """ + return "".join( + str(e) + for e in self.xpath("w:br | w:cr | w:noBreakHyphen | w:ptab | w:t | w:tab") + ) + + @text.setter # pyright: ignore[reportIncompatibleVariableOverride] + def text(self, text: str): + self.clear_content() + _RunContentAppender.append_to_run_from_text(self, text) + + def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr: + self.insert(0, rPr) + return rPr + + +# ------------------------------------------------------------------------------------ +# Run inner-content elements + + +class CT_Br(BaseOxmlElement): + """`` element, indicating a line, page, or column break in a run.""" + + type: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:type", ST_BrType, default="textWrapping" + ) + clear: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues] + "w:clear", ST_BrClear + ) + + def __str__(self) -> str: + """Text equivalent of this element. Actual value depends on break type. + + A line break is translated as "\n". Column and page breaks produce the empty + string (""). + + This allows the text of run inner-content to be accessed in a consistent way + for all run inner-context text elements. + """ + return "\n" if self.type == "textWrapping" else "" + + +class CT_Cr(BaseOxmlElement): + """`` element, representing a carriage-return (0x0D) character within a run. + + In Word, this represents a "soft carriage-return" in the sense that it does not end + the paragraph the way pressing Enter (aka. Return) on the keyboard does. Here the + text equivalent is considered to be newline ("\n") since in plain-text that's the + closest Python equivalent. + + NOTE: this complex-type name does not exist in the schema, where `w:tab` maps to + `CT_Empty`. This name was added to give it distinguished behavior. CT_Empty is used + for many elements. + """ + + def __str__(self) -> str: + """Text equivalent of this element, a single newline ("\n").""" + return "\n" + + +class CT_NoBreakHyphen(BaseOxmlElement): + """`` element, a hyphen ineligible for a line-wrap position. + + This maps to a plain-text dash ("-"). + + NOTE: this complex-type name does not exist in the schema, where `w:noBreakHyphen` + maps to `CT_Empty`. This name was added to give it behavior distinguished from the + many other elements represented in the schema by CT_Empty. + """ + + def __str__(self) -> str: + """Text equivalent of this element, a single dash character ("-").""" + return "-" + + +class CT_PTab(BaseOxmlElement): + """`` element, representing an absolute-position tab character within a run. + + This character advances the rendering position to the specified position regardless + of any tab-stops, perhaps for layout of a table-of-contents (TOC) or similar. + """ + + def __str__(self) -> str: + """Text equivalent of this element, a single tab ("\t") character. + + This allows the text of run inner-content to be accessed in a consistent way + for all run inner-context text elements. + """ + return "\t" + + +# -- CT_Tab functionality is provided by CT_TabStop which also uses `w:tab` tag. That +# -- element class provides the __str__() method for this empty element, unconditionally +# -- returning "\t". + + +class CT_Text(BaseOxmlElement): + """`` element, containing a sequence of characters within a run.""" + + def __str__(self) -> str: + """Text contained in this element, the empty string if it has no content. + + This property allows this run inner-content element to be queried for its text + the same way as other run-content elements are. In particular, this never + returns None, as etree._Element does when there is no content. + """ + return self.text or "" + + +# ------------------------------------------------------------------------------------ +# Utility + + +class _RunContentAppender: + """Translates a Python string into run content elements appended in a `w:r` element. + + Contiguous sequences of regular characters are appended in a single `` element. + Each tab character ('\t') causes a `` element to be appended. Likewise a + newline or carriage return character ('\n', '\r') causes a `` element to be + appended. + """ + + def __init__(self, r: CT_R): + self._r = r + self._bfr: List[str] = [] + + @classmethod + def append_to_run_from_text(cls, r: CT_R, text: str): + """Append inner-content elements for `text` to `r` element.""" + appender = cls(r) + appender.add_text(text) + + def add_text(self, text: str): + """Append inner-content elements for `text` to the `w:r` element.""" + for char in text: + self.add_char(char) + self.flush() + + def add_char(self, char: str): + """Process next character of input through finite state maching (FSM). + + There are two possible states, buffer pending and not pending, but those are + hidden behind the `.flush()` method which must be called at the end of text to + ensure any pending `` element is written. + """ + if char == "\t": + self.flush() + self._r.add_tab() + elif char in "\r\n": + self.flush() + self._r.add_br() + else: + self._bfr.append(char) + + def flush(self): + text = "".join(self._bfr) + if text: + self._r.add_t(text) + self._bfr.clear() diff --git a/docx/oxml/xmlchemy.py b/src/docx/oxml/xmlchemy.py similarity index 50% rename from docx/oxml/xmlchemy.py rename to src/docx/oxml/xmlchemy.py index 46dbf462b..2ea985abc 100644 --- a/docx/oxml/xmlchemy.py +++ b/src/docx/oxml/xmlchemy.py @@ -1,37 +1,35 @@ -# encoding: utf-8 +"""Enabling declarative definition of lxml custom element classes.""" -""" -Provides a wrapper around lxml that enables declarative definition of custom -element classes. -""" +from __future__ import annotations -from __future__ import absolute_import +import re +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Type, TypeVar from lxml import etree +from lxml.etree import ElementBase -import re - -from docx.compat import Unicode -from docx.oxml import OxmlElement from docx.oxml.exceptions import InvalidXmlError from docx.oxml.ns import NamespacePrefixedTag, nsmap, qn from docx.shared import lazyproperty +if TYPE_CHECKING: + from docx import types as t + from docx.enum.base import BaseXmlEnum + from docx.oxml.simpletypes import BaseSimpleType + def serialize_for_reading(element): + """Serialize `element` to human-readable XML suitable for tests. + + No XML declaration. """ - Serialize *element* to human-readable XML suitable for tests. No XML - declaration. - """ - xml = etree.tostring(element, encoding='unicode', pretty_print=True) + xml = etree.tostring(element, encoding="unicode", pretty_print=True) return XmlString(xml) -class XmlString(Unicode): - """ - Provides string comparison override suitable for serialized XML that is - useful for tests. - """ +class XmlString(str): + """Provides string comparison override suitable for serialized XML that is useful + for tests.""" # ' text' # | | || | @@ -39,7 +37,7 @@ class XmlString(Unicode): # front attrs | text # close - _xml_elm_line_patt = re.compile(r'( *)([^<]*)?$') + _xml_elm_line_patt = re.compile(r"( *)([^<]*)?$") def __eq__(self, other): lines = self.splitlines() @@ -55,19 +53,17 @@ def __ne__(self, other): return not self.__eq__(other) def _attr_seq(self, attrs): - """ - Return a sequence of attribute strings parsed from *attrs*. Each - attribute string is stripped of whitespace on both ends. + """Return a sequence of attribute strings parsed from `attrs`. + + Each attribute string is stripped of whitespace on both ends. """ attrs = attrs.strip() attr_lst = attrs.split() return sorted(attr_lst) def _eq_elm_strs(self, line, line_2): - """ - Return True if the element in *line_2* is XML equivalent to the - element in *line*. - """ + """Return True if the element in `line_2` is XML equivalent to the element in + `line`.""" front, attrs, close, text = self._parse_line(line) front_2, attrs_2, close_2, text_2 = self._parse_line(line_2) if front != front_2: @@ -82,190 +78,213 @@ def _eq_elm_strs(self, line, line_2): @classmethod def _parse_line(cls, line): - """ - Return front, attrs, close, text 4-tuple result of parsing XML element - string *line*. - """ + """Return front, attrs, close, text 4-tuple result of parsing XML element string + `line`.""" match = cls._xml_elm_line_patt.match(line) front, attrs, close, text = [match.group(n) for n in range(1, 5)] return front, attrs, close, text +_T = TypeVar("_T") + + class MetaOxmlElement(type): - """ - Metaclass for BaseOxmlElement - """ - def __init__(cls, clsname, bases, clsdict): + """Metaclass for BaseOxmlElement.""" + + def __new__( + cls: Type[_T], clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any] + ) -> _T: + bases = (*bases, etree.ElementBase) + return super().__new__(cls, clsname, bases, namespace) + + def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]): dispatchable = ( - OneAndOnlyOne, OneOrMore, OptionalAttribute, RequiredAttribute, - ZeroOrMore, ZeroOrOne, ZeroOrOneChoice + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, + ZeroOrOne, + ZeroOrOneChoice, ) - for key, value in clsdict.items(): + for key, value in namespace.items(): if isinstance(value, dispatchable): value.populate_class_members(cls, key) -class BaseAttribute(object): - """ - Base class for OptionalAttribute and RequiredAttribute, providing common - methods. +class BaseAttribute: + """Base class for OptionalAttribute and RequiredAttribute. + + Provides common methods. """ - def __init__(self, attr_name, simple_type): + + def __init__( + self, attr_name: str, simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType] + ): super(BaseAttribute, self).__init__() self._attr_name = attr_name self._simple_type = simple_type - def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members( + self, element_cls: Type[BaseOxmlElement], prop_name: str + ) -> None: + """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name self._add_attr_property() def _add_attr_property(self): - """ - Add a read/write ``{prop_name}`` property to the element class that - returns the interpreted value of this attribute on access and changes - the attribute value to its ST_* counterpart on assignment. - """ + """Add a read/write ``{prop_name}`` property to the element class that returns + the interpreted value of this attribute on access and changes the attribute + value to its ST_* counterpart on assignment.""" property_ = property(self._getter, self._setter, None) # assign unconditionally to overwrite element name definition setattr(self._element_cls, self._prop_name, property_) @property def _clark_name(self): - if ':' in self._attr_name: + if ":" in self._attr_name: return qn(self._attr_name) return self._attr_name + @property + def _getter(self) -> Callable[[BaseOxmlElement], t.AbstractSimpleTypeMember]: + ... + + @property + def _setter( + self, + ) -> Callable[[BaseOxmlElement, t.AbstractSimpleTypeMember | None], None]: + ... + class OptionalAttribute(BaseAttribute): + """Defines an optional attribute on a custom element class. + + An optional attribute returns a default value when not present for reading. When + assigned |None|, the attribute is removed, but still returns the default value when + one is specified. """ - Defines an optional attribute on a custom element class. An optional - attribute returns a default value when not present for reading. When - assigned |None|, the attribute is removed. - """ - def __init__(self, attr_name, simple_type, default=None): + + def __init__( + self, + attr_name: str, + simple_type: Type[BaseXmlEnum] | Type[BaseSimpleType], + default: BaseXmlEnum | BaseSimpleType | None = None, + ): super(OptionalAttribute, self).__init__(attr_name, simple_type) self._default = default @property - def _getter(self): - """ - Return a function object suitable for the "get" side of the attribute - property descriptor. - """ - def get_attr_value(obj): + def _docstring(self): + """String to use as `__doc__` attribute of attribute property.""" + return ( + f"{self._simple_type.__name__} type-converted value of" + f" ``{self._attr_name}`` attribute, or |None| (or specified default" + f" value) if not present. Assigning the default value causes the" + f" attribute to be removed from the element." + ) + + @property + def _getter( + self, + ) -> Callable[[BaseOxmlElement], str | bool | t.AbstractSimpleTypeMember]: + """Function suitable for `__get__()` method on attribute property descriptor.""" + + def get_attr_value( + obj: BaseOxmlElement, + ) -> str | bool | t.AbstractSimpleTypeMember: attr_str_value = obj.get(self._clark_name) if attr_str_value is None: return self._default return self._simple_type.from_xml(attr_str_value) + get_attr_value.__doc__ = self._docstring return get_attr_value @property - def _docstring(self): - """ - Return the string to use as the ``__doc__`` attribute of the property - for this attribute. - """ - return ( - '%s type-converted value of ``%s`` attribute, or |None| (or spec' - 'ified default value) if not present. Assigning the default valu' - 'e causes the attribute to be removed from the element.' % - (self._simple_type.__name__, self._attr_name) - ) + def _setter(self) -> Callable[[BaseOxmlElement, t.AbstractSimpleTypeMember], None]: + """Function suitable for `__set__()` method on attribute property descriptor.""" - @property - def _setter(self): - """ - Return a function object suitable for the "set" side of the attribute - property descriptor. - """ - def set_attr_value(obj, value): + def set_attr_value( + obj: BaseOxmlElement, value: t.AbstractSimpleTypeMember | None + ): if value is None or value == self._default: if self._clark_name in obj.attrib: del obj.attrib[self._clark_name] return str_value = self._simple_type.to_xml(value) obj.set(self._clark_name, str_value) + return set_attr_value class RequiredAttribute(BaseAttribute): + """Defines a required attribute on a custom element class. + + A required attribute is assumed to be present for reading, so does not have a + default value; its actual value is always used. If missing on read, an + |InvalidXmlError| is raised. It also does not remove the attribute if |None| is + assigned. Assigning |None| raises |TypeError| or |ValueError|, depending on the + simple type of the attribute. """ - Defines a required attribute on a custom element class. A required - attribute is assumed to be present for reading, so does not have - a default value; its actual value is always used. If missing on read, - an |InvalidXmlError| is raised. It also does not remove the attribute if - |None| is assigned. Assigning |None| raises |TypeError| or |ValueError|, - depending on the simple type of the attribute. - """ + + @property + def _docstring(self): + """Return the string to use as the ``__doc__`` attribute of the property for + this attribute.""" + return "%s type-converted value of ``%s`` attribute." % ( + self._simple_type.__name__, + self._attr_name, + ) + @property def _getter(self): - """ - Return a function object suitable for the "get" side of the attribute - property descriptor. - """ + """Return a function object suitable for the "get" side of the attribute + property descriptor.""" + def get_attr_value(obj): attr_str_value = obj.get(self._clark_name) if attr_str_value is None: raise InvalidXmlError( - "required '%s' attribute not present on element %s" % - (self._attr_name, obj.tag) + "required '%s' attribute not present on element %s" + % (self._attr_name, obj.tag) ) return self._simple_type.from_xml(attr_str_value) + get_attr_value.__doc__ = self._docstring return get_attr_value - @property - def _docstring(self): - """ - Return the string to use as the ``__doc__`` attribute of the property - for this attribute. - """ - return ( - '%s type-converted value of ``%s`` attribute.' % - (self._simple_type.__name__, self._attr_name) - ) - @property def _setter(self): - """ - Return a function object suitable for the "set" side of the attribute - property descriptor. - """ + """Return a function object suitable for the "set" side of the attribute + property descriptor.""" + def set_attr_value(obj, value): str_value = self._simple_type.to_xml(value) obj.set(self._clark_name, str_value) + return set_attr_value -class _BaseChildElement(object): - """ - Base class for the child element classes corresponding to varying - cardinalities, such as ZeroOrOne and ZeroOrMore. - """ +class _BaseChildElement: + """Base class for the child element classes corresponding to varying cardinalities, + such as ZeroOrOne and ZeroOrMore.""" + def __init__(self, nsptagname, successors=()): super(_BaseChildElement, self).__init__() self._nsptagname = nsptagname self._successors = successors def populate_class_members(self, element_cls, prop_name): - """ - Baseline behavior for adding the appropriate methods to - *element_cls*. - """ + """Baseline behavior for adding the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._prop_name = prop_name def _add_adder(self): - """ - Add an ``_add_x()`` method to the element class for this child - element. - """ + """Add an ``_add_x()`` method to the element class for this child element.""" + def _add_child(obj, **attrs): new_method = getattr(obj, self._new_method_name) child = new_method() @@ -276,160 +295,147 @@ def _add_child(obj, **attrs): return child _add_child.__doc__ = ( - 'Add a new ``<%s>`` child element unconditionally, inserted in t' - 'he correct sequence.' % self._nsptagname + "Add a new ``<%s>`` child element unconditionally, inserted in t" + "he correct sequence." % self._nsptagname ) self._add_to_class(self._add_method_name, _add_child) def _add_creator(self): - """ - Add a ``_new_{prop_name}()`` method to the element class that creates - a new, empty element of the correct type, having no attributes. - """ + """Add a ``_new_{prop_name}()`` method to the element class that creates a new, + empty element of the correct type, having no attributes.""" creator = self._creator creator.__doc__ = ( 'Return a "loose", newly created ``<%s>`` element having no attri' - 'butes, text, or children.' % self._nsptagname + "butes, text, or children." % self._nsptagname ) self._add_to_class(self._new_method_name, creator) def _add_getter(self): - """ - Add a read-only ``{prop_name}`` property to the element class for - this child element. - """ + """Add a read-only ``{prop_name}`` property to the element class for this child + element.""" property_ = property(self._getter, None, None) # assign unconditionally to overwrite element name definition setattr(self._element_cls, self._prop_name, property_) def _add_inserter(self): - """ - Add an ``_insert_x()`` method to the element class for this child - element. - """ + """Add an ``_insert_x()`` method to the element class for this child element.""" + def _insert_child(obj, child): obj.insert_element_before(child, *self._successors) return child _insert_child.__doc__ = ( - 'Return the passed ``<%s>`` element after inserting it as a chil' - 'd in the correct sequence.' % self._nsptagname + "Return the passed ``<%s>`` element after inserting it as a chil" + "d in the correct sequence." % self._nsptagname ) self._add_to_class(self._insert_method_name, _insert_child) def _add_list_getter(self): - """ - Add a read-only ``{prop_name}_lst`` property to the element class to - retrieve a list of child elements matching this type. - """ - prop_name = '%s_lst' % self._prop_name + """Add a read-only ``{prop_name}_lst`` property to the element class to retrieve + a list of child elements matching this type.""" + prop_name = "%s_lst" % self._prop_name property_ = property(self._list_getter, None, None) setattr(self._element_cls, prop_name, property_) @lazyproperty def _add_method_name(self): - return '_add_%s' % self._prop_name + return "_add_%s" % self._prop_name def _add_public_adder(self): - """ - Add a public ``add_x()`` method to the parent element class. - """ + """Add a public ``add_x()`` method to the parent element class.""" + def add_child(obj): private_add_method = getattr(obj, self._add_method_name) child = private_add_method() return child add_child.__doc__ = ( - 'Add a new ``<%s>`` child element unconditionally, inserted in t' - 'he correct sequence.' % self._nsptagname + "Add a new ``<%s>`` child element unconditionally, inserted in t" + "he correct sequence." % self._nsptagname ) self._add_to_class(self._public_add_method_name, add_child) def _add_to_class(self, name, method): - """ - Add *method* to the target class as *name*, unless *name* is already - defined on the class. - """ + """Add `method` to the target class as `name`, unless `name` is already defined + on the class.""" if hasattr(self._element_cls, name): return setattr(self._element_cls, name, method) @property def _creator(self): - """ - Return a function object that creates a new, empty element of the - right type, having no attributes. - """ + """Callable that creates an empty element of the right type, with no attrs.""" + from docx.oxml.parser import OxmlElement + def new_child_element(obj): return OxmlElement(self._nsptagname) + return new_child_element @property def _getter(self): + """Return a function object suitable for the "get" side of the property + descriptor. + + This default getter returns the child element with matching tag name or |None| + if not present. """ - Return a function object suitable for the "get" side of the property - descriptor. This default getter returns the child element with - matching tag name or |None| if not present. - """ + def get_child_element(obj): return obj.find(qn(self._nsptagname)) + get_child_element.__doc__ = ( - '``<%s>`` child element or |None| if not present.' - % self._nsptagname + "``<%s>`` child element or |None| if not present." % self._nsptagname ) return get_child_element @lazyproperty def _insert_method_name(self): - return '_insert_%s' % self._prop_name + return "_insert_%s" % self._prop_name @property def _list_getter(self): - """ - Return a function object suitable for the "get" side of a list - property descriptor. - """ + """Return a function object suitable for the "get" side of a list property + descriptor.""" + def get_child_element_list(obj): return obj.findall(qn(self._nsptagname)) + get_child_element_list.__doc__ = ( - 'A list containing each of the ``<%s>`` child elements, in the o' - 'rder they appear.' % self._nsptagname + "A list containing each of the ``<%s>`` child elements, in the o" + "rder they appear." % self._nsptagname ) return get_child_element_list @lazyproperty def _public_add_method_name(self): + """add_childElement() is public API for a repeating element, allowing new + elements to be added to the sequence. + + May be overridden to provide a friendlier API to clients having domain + appropriate parameter names for required attributes. """ - add_childElement() is public API for a repeating element, allowing - new elements to be added to the sequence. May be overridden to - provide a friendlier API to clients having domain appropriate - parameter names for required attributes. - """ - return 'add_%s' % self._prop_name + return "add_%s" % self._prop_name @lazyproperty def _remove_method_name(self): - return '_remove_%s' % self._prop_name + return "_remove_%s" % self._prop_name @lazyproperty def _new_method_name(self): - return '_new_%s' % self._prop_name + return "_new_%s" % self._prop_name class Choice(_BaseChildElement): - """ - Defines a child element belonging to a group, only one of which may - appear as a child. - """ + """Defines a child element belonging to a group, only one of which may appear as a + child.""" + @property def nsptagname(self): return self._nsptagname - def populate_class_members( - self, element_cls, group_prop_name, successors): - """ - Add the appropriate methods to *element_cls*. - """ + def populate_class_members(self, element_cls, group_prop_name, successors): + """Add the appropriate methods to `element_cls`.""" self._element_cls = element_cls self._group_prop_name = group_prop_name self._successors = successors @@ -441,100 +447,76 @@ def populate_class_members( self._add_get_or_change_to_method() def _add_get_or_change_to_method(self): - """ - Add a ``get_or_change_to_x()`` method to the element class for this - child element. - """ + """Add a ``get_or_change_to_x()`` method to the element class for this child + element.""" + def get_or_change_to_child(obj): child = getattr(obj, self._prop_name) if child is not None: return child - remove_group_method = getattr( - obj, self._remove_group_method_name - ) + remove_group_method = getattr(obj, self._remove_group_method_name) remove_group_method() add_method = getattr(obj, self._add_method_name) child = add_method() return child get_or_change_to_child.__doc__ = ( - 'Return the ``<%s>`` child, replacing any other group element if' - ' found.' + "Return the ``<%s>`` child, replacing any other group element if" " found." ) % self._nsptagname - self._add_to_class( - self._get_or_change_to_method_name, get_or_change_to_child - ) + self._add_to_class(self._get_or_change_to_method_name, get_or_change_to_child) @property def _prop_name(self): - """ - Calculate property name from tag name, e.g. a:schemeClr -> schemeClr. - """ - if ':' in self._nsptagname: - start = self._nsptagname.index(':') + 1 - else: - start = 0 + """Property name computed from tag name, e.g. a:schemeClr -> schemeClr.""" + start = self._nsptagname.index(":") + 1 if ":" in self._nsptagname else 0 return self._nsptagname[start:] @lazyproperty def _get_or_change_to_method_name(self): - return 'get_or_change_to_%s' % self._prop_name + return "get_or_change_to_%s" % self._prop_name @lazyproperty def _remove_group_method_name(self): - return '_remove_%s' % self._group_prop_name + return "_remove_%s" % self._group_prop_name class OneAndOnlyOne(_BaseChildElement): - """ - Defines a required child element for MetaOxmlElement. - """ + """Defines a required child element for MetaOxmlElement.""" + def __init__(self, nsptagname): super(OneAndOnlyOne, self).__init__(nsptagname, None) def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ - super(OneAndOnlyOne, self).populate_class_members( - element_cls, prop_name - ) + """Add the appropriate methods to `element_cls`.""" + super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name) self._add_getter() @property def _getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. - """ + """Return a function object suitable for the "get" side of the property + descriptor.""" + def get_child_element(obj): child = obj.find(qn(self._nsptagname)) if child is None: raise InvalidXmlError( - "required ``<%s>`` child element not present" % - self._nsptagname + "required ``<%s>`` child element not present" % self._nsptagname ) return child get_child_element.__doc__ = ( - 'Required ``<%s>`` child element.' - % self._nsptagname + "Required ``<%s>`` child element." % self._nsptagname ) return get_child_element class OneOrMore(_BaseChildElement): - """ - Defines a repeating child element for MetaOxmlElement that must appear at - least once. - """ + """Defines a repeating child element for MetaOxmlElement that must appear at least + once.""" + def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ - super(OneOrMore, self).populate_class_members( - element_cls, prop_name - ) + """Add the appropriate methods to `element_cls`.""" + super(OneOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() self._add_inserter() @@ -544,16 +526,11 @@ def populate_class_members(self, element_cls, prop_name): class ZeroOrMore(_BaseChildElement): - """ - Defines an optional repeating child element for MetaOxmlElement. - """ + """Defines an optional repeating child element for MetaOxmlElement.""" + def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ - super(ZeroOrMore, self).populate_class_members( - element_cls, prop_name - ) + """Add the appropriate methods to `element_cls`.""" + super(ZeroOrMore, self).populate_class_members(element_cls, prop_name) self._add_list_getter() self._add_creator() self._add_inserter() @@ -563,13 +540,10 @@ def populate_class_members(self, element_cls, prop_name): class ZeroOrOne(_BaseChildElement): - """ - Defines an optional child element for MetaOxmlElement. - """ + """Defines an optional child element for MetaOxmlElement.""" + def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ + """Add the appropriate methods to `element_cls`.""" super(ZeroOrOne, self).populate_class_members(element_cls, prop_name) self._add_getter() self._add_creator() @@ -579,54 +553,48 @@ def populate_class_members(self, element_cls, prop_name): self._add_remover() def _add_get_or_adder(self): - """ - Add a ``get_or_add_x()`` method to the element class for this - child element. - """ + """Add a ``get_or_add_x()`` method to the element class for this child + element.""" + def get_or_add_child(obj): child = getattr(obj, self._prop_name) if child is None: add_method = getattr(obj, self._add_method_name) child = add_method() return child + get_or_add_child.__doc__ = ( - 'Return the ``<%s>`` child element, newly added if not present.' + "Return the ``<%s>`` child element, newly added if not present." ) % self._nsptagname self._add_to_class(self._get_or_add_method_name, get_or_add_child) def _add_remover(self): - """ - Add a ``_remove_x()`` method to the element class for this child - element. - """ + """Add a ``_remove_x()`` method to the element class for this child element.""" + def _remove_child(obj): obj.remove_all(self._nsptagname) + _remove_child.__doc__ = ( - 'Remove all ``<%s>`` child elements.' + "Remove all ``<%s>`` child elements." ) % self._nsptagname self._add_to_class(self._remove_method_name, _remove_child) @lazyproperty def _get_or_add_method_name(self): - return 'get_or_add_%s' % self._prop_name + return "get_or_add_%s" % self._prop_name class ZeroOrOneChoice(_BaseChildElement): - """ - Correspondes to an ``EG_*`` element group where at most one of its - members may appear as a child. - """ + """Correspondes to an ``EG_*`` element group where at most one of its members may + appear as a child.""" + def __init__(self, choices, successors=()): self._choices = choices self._successors = successors def populate_class_members(self, element_cls, prop_name): - """ - Add the appropriate methods to *element_cls*. - """ - super(ZeroOrOneChoice, self).populate_class_members( - element_cls, prop_name - ) + """Add the appropriate methods to `element_cls`.""" + super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name) self._add_choice_getter() for choice in self._choices: choice.populate_class_members( @@ -635,85 +603,85 @@ def populate_class_members(self, element_cls, prop_name): self._add_group_remover() def _add_choice_getter(self): - """ - Add a read-only ``{prop_name}`` property to the element class that - returns the present member of this group, or |None| if none are - present. - """ + """Add a read-only ``{prop_name}`` property to the element class that returns + the present member of this group, or |None| if none are present.""" property_ = property(self._choice_getter, None, None) # assign unconditionally to overwrite element name definition setattr(self._element_cls, self._prop_name, property_) def _add_group_remover(self): - """ - Add a ``_remove_eg_x()`` method to the element class for this choice - group. - """ + """Add a ``_remove_eg_x()`` method to the element class for this choice + group.""" + def _remove_choice_group(obj): for tagname in self._member_nsptagnames: obj.remove_all(tagname) _remove_choice_group.__doc__ = ( - 'Remove the current choice group child element if present.' - ) - self._add_to_class( - self._remove_choice_group_method_name, _remove_choice_group + "Remove the current choice group child element if present." ) + self._add_to_class(self._remove_choice_group_method_name, _remove_choice_group) @property def _choice_getter(self): - """ - Return a function object suitable for the "get" side of the property - descriptor. - """ + """Return a function object suitable for the "get" side of the property + descriptor.""" + def get_group_member_element(obj): return obj.first_child_found_in(*self._member_nsptagnames) + get_group_member_element.__doc__ = ( - 'Return the child element belonging to this element group, or ' - '|None| if no member child is present.' + "Return the child element belonging to this element group, or " + "|None| if no member child is present." ) return get_group_member_element @lazyproperty def _member_nsptagnames(self): - """ - Sequence of namespace-prefixed tagnames, one for each of the member - elements of this choice group. - """ + """Sequence of namespace-prefixed tagnames, one for each of the member elements + of this choice group.""" return [choice.nsptagname for choice in self._choices] @lazyproperty def _remove_choice_group_method_name(self): - return '_remove_%s' % self._prop_name + return "_remove_%s" % self._prop_name -class _OxmlElementBase(etree.ElementBase): - """ - Effective base class for all custom element classes, to add standardized - behavior to all classes in one place. Actual inheritance is from - BaseOxmlElement below, needed to manage Python 2-3 metaclass declaration - compatibility. +class BaseOxmlElement(metaclass=MetaOxmlElement): + """Effective base class for all custom element classes. + + Adds standardized behavior to all classes in one place. """ - __metaclass__ = MetaOxmlElement + addprevious: Callable[[BaseOxmlElement], None] + attrib: Dict[str, str] + append: Callable[[BaseOxmlElement], None] + find: Callable[[str], ElementBase | None] + findall: Callable[[str], List[ElementBase]] + get: Callable[[str], str | None] + getparent: Callable[[], BaseOxmlElement] + insert: Callable[[int, BaseOxmlElement], None] + remove: Callable[[BaseOxmlElement], None] + set: Callable[[str, str], None] + tag: str + text: str | None def __repr__(self): return "<%s '<%s>' at 0x%0x>" % ( - self.__class__.__name__, self._nsptag, id(self) + self.__class__.__name__, + self._nsptag, + id(self), ) - def first_child_found_in(self, *tagnames): - """ - Return the first child found with tag in *tagnames*, or None if - not found. - """ + def first_child_found_in(self, *tagnames: str) -> ElementBase | None: + """First child with tag in `tagnames`, or None if not found.""" for tagname in tagnames: child = self.find(qn(tagname)) if child is not None: return child return None - def insert_element_before(self, elm, *tagnames): + def insert_element_before(self, elm: ElementBase, *tagnames: str): successor = self.first_child_found_in(*tagnames) if successor is not None: successor.addprevious(elm) @@ -721,39 +689,28 @@ def insert_element_before(self, elm, *tagnames): self.append(elm) return elm - def remove_all(self, *tagnames): - """ - Remove all child elements whose tagname (e.g. 'a:p') appears in - *tagnames*. - """ + def remove_all(self, *tagnames: str) -> None: + """Remove child elements with tagname (e.g. "a:p") in `tagnames`.""" for tagname in tagnames: matching = self.findall(qn(tagname)) for child in matching: self.remove(child) @property - def xml(self): - """ - Return XML string for this element, suitable for testing purposes. - Pretty printed for readability and without an XML declaration at the - top. + def xml(self) -> str: + """XML string for this element, suitable for testing purposes. + + Pretty printed for readability and without an XML declaration at the top. """ return serialize_for_reading(self) - def xpath(self, xpath_str): - """ - Override of ``lxml`` _Element.xpath() method to provide standard Open - XML namespace mapping (``nsmap``) in centralized location. + def xpath(self, xpath_str: str) -> Any: + """Override of `lxml` _Element.xpath() method. + + Provides standard Open XML namespace mapping (`nsmap`) in centralized location. """ - return super(BaseOxmlElement, self).xpath( - xpath_str, namespaces=nsmap - ) + return super().xpath(xpath_str, namespaces=nsmap) @property - def _nsptag(self): + def _nsptag(self) -> str: return NamespacePrefixedTag.from_clark_name(self.tag) - - -BaseOxmlElement = MetaOxmlElement( - 'BaseOxmlElement', (etree.ElementBase,), dict(_OxmlElementBase.__dict__) -) diff --git a/docx/package.py b/src/docx/package.py similarity index 69% rename from docx/package.py rename to src/docx/package.py index 9f5ccc667..12a166bf3 100644 --- a/docx/package.py +++ b/src/docx/package.py @@ -1,8 +1,8 @@ -# encoding: utf-8 +"""WordprocessingML Package class and related objects.""" -"""WordprocessingML Package class and related objects""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import IO from docx.image.image import Image from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -13,7 +13,7 @@ class Package(OpcPackage): - """Customizations specific to a WordprocessingML package""" + """Customizations specific to a WordprocessingML package.""" def after_unmarshal(self): """Called by loading code after all parts and relationships have been loaded. @@ -22,8 +22,8 @@ def after_unmarshal(self): """ self._gather_image_parts() - def get_or_add_image_part(self, image_descriptor): - """Return |ImagePart| containing image specified by *image_descriptor*. + def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: + """Return |ImagePart| containing image specified by `image_descriptor`. The image-part is newly created if a matching one is not already present in the collection. @@ -31,7 +31,7 @@ def get_or_add_image_part(self, image_descriptor): return self.image_parts.get_or_add_image_part(image_descriptor) @lazyproperty - def image_parts(self): + def image_parts(self) -> ImageParts: """|ImageParts| collection object for this package.""" return ImageParts() @@ -47,8 +47,8 @@ def _gather_image_parts(self): self.image_parts.append(rel.target_part) -class ImageParts(object): - """Collection of |ImagePart| objects corresponding to images in the package""" +class ImageParts: + """Collection of |ImagePart| objects corresponding to images in the package.""" def __init__(self): self._image_parts = [] @@ -65,8 +65,8 @@ def __len__(self): def append(self, item): self._image_parts.append(item) - def get_or_add_image_part(self, image_descriptor): - """Return |ImagePart| object containing image identified by *image_descriptor*. + def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: + """Return |ImagePart| object containing image identified by `image_descriptor`. The image-part is newly created if a matching one is not present in the collection. @@ -78,36 +78,34 @@ def get_or_add_image_part(self, image_descriptor): return self._add_image_part(image) def _add_image_part(self, image): - """ - Return an |ImagePart| instance newly created from image and appended - to the collection. - """ + """Return an |ImagePart| instance newly created from image and appended to the + collection.""" partname = self._next_image_partname(image.ext) image_part = ImagePart.from_image(image, partname) self.append(image_part) return image_part def _get_by_sha1(self, sha1): - """ - Return the image part in this collection having a SHA1 hash matching - *sha1*, or |None| if not found. - """ + """Return the image part in this collection having a SHA1 hash matching `sha1`, + or |None| if not found.""" for image_part in self._image_parts: if image_part.sha1 == sha1: return image_part return None def _next_image_partname(self, ext): + """The next available image partname, starting from ``/word/media/image1.{ext}`` + where unused numbers are reused. + + The partname is unique by number, without regard to the extension. `ext` does + not include the leading period. """ - The next available image partname, starting from - ``/word/media/image1.{ext}`` where unused numbers are reused. The - partname is unique by number, without regard to the extension. *ext* - does not include the leading period. - """ + def image_partname(n): - return PackURI('/word/media/image%d.%s' % (n, ext)) + return PackURI("/word/media/image%d.%s" % (n, ext)) + used_numbers = [image_part.partname.idx for image_part in self] - for n in range(1, len(self)+1): + for n in range(1, len(self) + 1): if n not in used_numbers: return image_partname(n) - return image_partname(len(self)+1) + return image_partname(len(self) + 1) diff --git a/docx/parts/__init__.py b/src/docx/parts/__init__.py similarity index 100% rename from docx/parts/__init__.py rename to src/docx/parts/__init__.py diff --git a/docx/parts/document.py b/src/docx/parts/document.py similarity index 52% rename from docx/parts/document.py rename to src/docx/parts/document.py index 59d0b7a71..a157764b9 100644 --- a/docx/parts/document.py +++ b/src/docx/parts/document.py @@ -1,21 +1,25 @@ -# encoding: utf-8 +"""|DocumentPart| and closely related objects.""" -"""|DocumentPart| and closely related objects""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import TYPE_CHECKING, cast from docx.document import Document +from docx.enum.style import WD_STYLE_TYPE from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.parts.hdrftr import FooterPart, HeaderPart from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart -from docx.parts.story import BaseStoryPart +from docx.parts.story import StoryPart from docx.parts.styles import StylesPart from docx.shape import InlineShapes from docx.shared import lazyproperty +if TYPE_CHECKING: + from docx.styles.style import BaseStyle -class DocumentPart(BaseStoryPart): + +class DocumentPart(StoryPart): """Main document part of a WordprocessingML (WML) package, aka a .docx file. Acts as broker to other parts such as image, core properties, and style parts. It @@ -38,63 +42,56 @@ def add_header_part(self): @property def core_properties(self): - """ - A |CoreProperties| object providing read/write access to the core - properties of this document. - """ + """A |CoreProperties| object providing read/write access to the core properties + of this document.""" return self.package.core_properties @property def document(self): - """ - A |Document| object providing access to the content of this document. - """ + """A |Document| object providing access to the content of this document.""" return Document(self._element, self) - def drop_header_part(self, rId): - """Remove related header part identified by *rId*.""" + def drop_header_part(self, rId: str) -> None: + """Remove related header part identified by `rId`.""" self.drop_rel(rId) - def footer_part(self, rId): - """Return |FooterPart| related by *rId*.""" + def footer_part(self, rId: str): + """Return |FooterPart| related by `rId`.""" return self.related_parts[rId] - def get_style(self, style_id, style_type): - """ - Return the style in this document matching *style_id*. Returns the - default style for *style_type* if *style_id* is |None| or does not - match a defined style of *style_type*. + def get_style(self, style_id: str | None, style_type: WD_STYLE_TYPE) -> BaseStyle: + """Return the style in this document matching `style_id`. + + Returns the default style for `style_type` if `style_id` is |None| or does not + match a defined style of `style_type`. """ return self.styles.get_by_id(style_id, style_type) def get_style_id(self, style_or_name, style_type): - """ - Return the style_id (|str|) of the style of *style_type* matching - *style_or_name*. Returns |None| if the style resolves to the default - style for *style_type* or if *style_or_name* is itself |None|. Raises - if *style_or_name* is a style of the wrong type or names a style not - present in the document. + """Return the style_id (|str|) of the style of `style_type` matching + `style_or_name`. + + Returns |None| if the style resolves to the default style for `style_type` or if + `style_or_name` is itself |None|. Raises if `style_or_name` is a style of the + wrong type or names a style not present in the document. """ return self.styles.get_style_id(style_or_name, style_type) - def header_part(self, rId): - """Return |HeaderPart| related by *rId*.""" + def header_part(self, rId: str): + """Return |HeaderPart| related by `rId`.""" return self.related_parts[rId] @lazyproperty def inline_shapes(self): - """ - The |InlineShapes| instance containing the inline shapes in the - document. - """ + """The |InlineShapes| instance containing the inline shapes in the document.""" return InlineShapes(self._element.body, self) @lazyproperty def numbering_part(self): - """ - A |NumberingPart| object providing access to the numbering - definitions for this document. Creates an empty numbering part if one - is not present. + """A |NumberingPart| object providing access to the numbering definitions for + this document. + + Creates an empty numbering part if one is not present. """ try: return self.part_related_by(RT.NUMBERING) @@ -104,34 +101,28 @@ def numbering_part(self): return numbering_part def save(self, path_or_stream): - """ - Save this document to *path_or_stream*, which can be either a path to - a filesystem location (a string) or a file-like object. - """ + """Save this document to `path_or_stream`, which can be either a path to a + filesystem location (a string) or a file-like object.""" self.package.save(path_or_stream) @property def settings(self): - """ - A |Settings| object providing access to the settings in the settings - part of this document. - """ + """A |Settings| object providing access to the settings in the settings part of + this document.""" return self._settings_part.settings @property def styles(self): - """ - A |Styles| object providing access to the styles in the styles part - of this document. - """ + """A |Styles| object providing access to the styles in the styles part of this + document.""" return self._styles_part.styles @property def _settings_part(self): - """ - A |SettingsPart| object providing access to the document-level - settings for this document. Creates a default settings part if one is - not present. + """A |SettingsPart| object providing access to the document-level settings for + this document. + + Creates a default settings part if one is not present. """ try: return self.part_related_by(RT.SETTINGS) @@ -141,14 +132,16 @@ def _settings_part(self): return settings_part @property - def _styles_part(self): - """ - Instance of |StylesPart| for this document. Creates an empty styles - part if one is not present. + def _styles_part(self) -> StylesPart: + """Instance of |StylesPart| for this document. + + Creates an empty styles part if one is not present. """ try: - return self.part_related_by(RT.STYLES) + return cast(StylesPart, self.part_related_by(RT.STYLES)) except KeyError: - styles_part = StylesPart.default(self.package) + package = self.package + assert package is not None + styles_part = StylesPart.default(package) self.relate_to(styles_part, RT.STYLES) return styles_part diff --git a/docx/parts/hdrftr.py b/src/docx/parts/hdrftr.py similarity index 70% rename from docx/parts/hdrftr.py rename to src/docx/parts/hdrftr.py index 549805b2a..46821d780 100644 --- a/docx/parts/hdrftr.py +++ b/src/docx/parts/hdrftr.py @@ -1,17 +1,13 @@ -# encoding: utf-8 - -"""Header and footer part objects""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Header and footer part objects.""" import os from docx.opc.constants import CONTENT_TYPE as CT -from docx.oxml import parse_xml -from docx.parts.story import BaseStoryPart +from docx.oxml.parser import parse_xml +from docx.parts.story import StoryPart -class FooterPart(BaseStoryPart): +class FooterPart(StoryPart): """Definition of a section footer.""" @classmethod @@ -26,14 +22,14 @@ def new(cls, package): def _default_footer_xml(cls): """Return bytes containing XML for a default footer part.""" path = os.path.join( - os.path.split(__file__)[0], '..', 'templates', 'default-footer.xml' + os.path.split(__file__)[0], "..", "templates", "default-footer.xml" ) - with open(path, 'rb') as f: + with open(path, "rb") as f: xml_bytes = f.read() return xml_bytes -class HeaderPart(BaseStoryPart): +class HeaderPart(StoryPart): """Definition of a section header.""" @classmethod @@ -48,8 +44,8 @@ def new(cls, package): def _default_header_xml(cls): """Return bytes containing XML for a default header part.""" path = os.path.join( - os.path.split(__file__)[0], '..', 'templates', 'default-header.xml' + os.path.split(__file__)[0], "..", "templates", "default-header.xml" ) - with open(path, 'rb') as f: + with open(path, "rb") as f: xml_bytes = f.read() return xml_bytes diff --git a/src/docx/parts/image.py b/src/docx/parts/image.py new file mode 100644 index 000000000..e4580df74 --- /dev/null +++ b/src/docx/parts/image.py @@ -0,0 +1,75 @@ +"""The proxy class for an image part, and related objects.""" + +from __future__ import annotations + +import hashlib + +from docx.image.image import Image +from docx.opc.part import Part +from docx.shared import Emu, Inches + + +class ImagePart(Part): + """An image part. + + Corresponds to the target part of a relationship with type RELATIONSHIP_TYPE.IMAGE. + """ + + def __init__( + self, partname: str, content_type: str, blob: bytes, image: Image | None = None + ): + super(ImagePart, self).__init__(partname, content_type, blob) + self._image = image + + @property + def default_cx(self): + """Native width of this image, calculated from its width in pixels and + horizontal dots per inch (dpi).""" + px_width = self.image.px_width + horz_dpi = self.image.horz_dpi + width_in_inches = px_width / horz_dpi + return Inches(width_in_inches) + + @property + def default_cy(self): + """Native height of this image, calculated from its height in pixels and + vertical dots per inch (dpi).""" + px_height = self.image.px_height + horz_dpi = self.image.horz_dpi + height_in_emu = 914400 * px_height / horz_dpi + return Emu(height_in_emu) + + @property + def filename(self): + """Filename from which this image part was originally created. + + A generic name, e.g. 'image.png', is substituted if no name is available, for + example when the image was loaded from an unnamed stream. In that case a default + extension is applied based on the detected MIME type of the image. + """ + if self._image is not None: + return self._image.filename + return "image.%s" % self.partname.ext + + @classmethod + def from_image(cls, image, partname): + """Return an |ImagePart| instance newly created from `image` and assigned + `partname`.""" + return ImagePart(partname, image.content_type, image.blob, image) + + @property + def image(self) -> Image: + if self._image is None: + self._image = Image.from_blob(self.blob) + return self._image + + @classmethod + def load(cls, partname, content_type, blob, package): + """Called by ``docx.opc.package.PartFactory`` to load an image part from a + package being opened by ``Document(...)`` call.""" + return cls(partname, content_type, blob) + + @property + def sha1(self): + """SHA1 hash digest of the blob of this image part.""" + return hashlib.sha1(self._blob).hexdigest() diff --git a/src/docx/parts/numbering.py b/src/docx/parts/numbering.py new file mode 100644 index 000000000..54a430c1b --- /dev/null +++ b/src/docx/parts/numbering.py @@ -0,0 +1,33 @@ +"""|NumberingPart| and closely related objects.""" + +from ..opc.part import XmlPart +from ..shared import lazyproperty + + +class NumberingPart(XmlPart): + """Proxy for the numbering.xml part containing numbering definitions for a document + or glossary.""" + + @classmethod + def new(cls): + """Return newly created empty numbering part, containing only the root + ```` element.""" + raise NotImplementedError + + @lazyproperty + def numbering_definitions(self): + """The |_NumberingDefinitions| instance containing the numbering definitions + ( element proxies) for this numbering part.""" + return _NumberingDefinitions(self._element) + + +class _NumberingDefinitions: + """Collection of |_NumberingDefinition| instances corresponding to the ```` + elements in a numbering part.""" + + def __init__(self, numbering_elm): + super(_NumberingDefinitions, self).__init__() + self._numbering = numbering_elm + + def __len__(self): + return len(self._numbering.num_lst) diff --git a/src/docx/parts/settings.py b/src/docx/parts/settings.py new file mode 100644 index 000000000..d83c9d5ca --- /dev/null +++ b/src/docx/parts/settings.py @@ -0,0 +1,38 @@ +"""|SettingsPart| and closely related objects.""" + +import os + +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.opc.part import XmlPart +from docx.oxml.parser import parse_xml +from docx.settings import Settings + + +class SettingsPart(XmlPart): + """Document-level settings part of a WordprocessingML (WML) package.""" + + @classmethod + def default(cls, package): + """Return a newly created settings part, containing a default `w:settings` + element tree.""" + partname = PackURI("/word/settings.xml") + content_type = CT.WML_SETTINGS + element = parse_xml(cls._default_settings_xml()) + return cls(partname, content_type, element, package) + + @property + def settings(self): + """A |Settings| proxy object for the `w:settings` element in this part, + containing the document-level settings for this document.""" + return Settings(self.element) + + @classmethod + def _default_settings_xml(cls): + """Return a bytestream containing XML for a default settings part.""" + path = os.path.join( + os.path.split(__file__)[0], "..", "templates", "default-settings.xml" + ) + with open(path, "rb") as f: + xml_bytes = f.read() + return xml_bytes diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py new file mode 100644 index 000000000..b5c8ac882 --- /dev/null +++ b/src/docx/parts/story.py @@ -0,0 +1,95 @@ +"""|StoryPart| and related objects.""" + +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, Tuple + +from docx.opc.constants import RELATIONSHIP_TYPE as RT +from docx.opc.part import XmlPart +from docx.oxml.shape import CT_Inline +from docx.shared import Length, lazyproperty + +if TYPE_CHECKING: + from docx.enum.style import WD_STYLE_TYPE + from docx.image.image import Image + from docx.parts.document import DocumentPart + from docx.styles.style import BaseStyle + + +class StoryPart(XmlPart): + """Base class for story parts. + + A story part is one that can contain textual content, such as the document-part and + header or footer parts. These all share content behaviors like `.paragraphs`, + `.add_paragraph()`, `.add_table()` etc. + """ + + def get_or_add_image(self, image_descriptor: str | IO[bytes]) -> Tuple[str, Image]: + """Return (rId, image) pair for image identified by `image_descriptor`. + + `rId` is the str key (often like "rId7") for the relationship between this story + part and the image part, reused if already present, newly created if not. + `image` is an |Image| instance providing access to the properties of the image, + such as dimensions and image type. + """ + package = self._package + assert package is not None + image_part = package.get_or_add_image_part(image_descriptor) + rId = self.relate_to(image_part, RT.IMAGE) + return rId, image_part.image + + def get_style(self, style_id: str | None, style_type: WD_STYLE_TYPE) -> BaseStyle: + """Return the style in this document matching `style_id`. + + Returns the default style for `style_type` if `style_id` is |None| or does not + match a defined style of `style_type`. + """ + return self._document_part.get_style(style_id, style_type) + + def get_style_id( + self, style_or_name: BaseStyle | str | None, style_type: WD_STYLE_TYPE + ) -> str | None: + """Return str style_id for `style_or_name` of `style_type`. + + Returns |None| if the style resolves to the default style for `style_type` or if + `style_or_name` is itself |None|. Raises if `style_or_name` is a style of the + wrong type or names a style not present in the document. + """ + return self._document_part.get_style_id(style_or_name, style_type) + + def new_pic_inline( + self, + image_descriptor: str | IO[bytes], + width: Length | None = None, + height: Length | None = None, + ) -> CT_Inline: + """Return a newly-created `w:inline` element. + + The element contains the image specified by `image_descriptor` and is scaled + based on the values of `width` and `height`. + """ + rId, image = self.get_or_add_image(image_descriptor) + cx, cy = image.scaled_dimensions(width, height) + shape_id, filename = self.next_id, image.filename + return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy) + + @property + def next_id(self) -> int: + """Next available positive integer id value in this story XML document. + + The value is determined by incrementing the maximum existing id value. Gaps in + the existing id sequence are not filled. The id attribute value is unique in the + document, without regard to the element type it appears on. + """ + id_str_lst = self._element.xpath("//@id") + used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()] + if not used_ids: + return 1 + return max(used_ids) + 1 + + @lazyproperty + def _document_part(self) -> DocumentPart: + """|DocumentPart| object for this package.""" + package = self.package + assert package is not None + return package.main_document_part diff --git a/src/docx/parts/styles.py b/src/docx/parts/styles.py new file mode 100644 index 000000000..dffa762ef --- /dev/null +++ b/src/docx/parts/styles.py @@ -0,0 +1,44 @@ +"""Provides StylesPart and related objects.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.packuri import PackURI +from docx.opc.part import XmlPart +from docx.oxml.parser import parse_xml +from docx.styles.styles import Styles + +if TYPE_CHECKING: + from docx.opc.package import OpcPackage + + +class StylesPart(XmlPart): + """Proxy for the styles.xml part containing style definitions for a document or + glossary.""" + + @classmethod + def default(cls, package: OpcPackage) -> StylesPart: + """Return a newly created styles part, containing a default set of elements.""" + partname = PackURI("/word/styles.xml") + content_type = CT.WML_STYLES + element = parse_xml(cls._default_styles_xml()) + return cls(partname, content_type, element, package) + + @property + def styles(self): + """The |_Styles| instance containing the styles ( element proxies) for + this styles part.""" + return Styles(self.element) + + @classmethod + def _default_styles_xml(cls): + """Return a bytestream containing XML for a default styles part.""" + path = os.path.join( + os.path.split(__file__)[0], "..", "templates", "default-styles.xml" + ) + with open(path, "rb") as f: + xml_bytes = f.read() + return xml_bytes diff --git a/docx/section.py b/src/docx/section.py similarity index 64% rename from docx/section.py rename to src/docx/section.py index 32ceec7da..f72b60867 100644 --- a/docx/section.py +++ b/src/docx/section.py @@ -1,67 +1,52 @@ -# encoding: utf-8 +"""The |Section| object and related proxy classes.""" -"""The |Section| object and related proxy classes""" +from __future__ import annotations -from __future__ import absolute_import, division, print_function, unicode_literals +from typing import TYPE_CHECKING, Iterator, List, Sequence, overload from docx.blkcntnr import BlockItemContainer -from docx.compat import Sequence from docx.enum.section import WD_HEADER_FOOTER +from docx.oxml.text.paragraph import CT_P +from docx.parts.hdrftr import FooterPart, HeaderPart from docx.shared import lazyproperty +from docx.table import Table +from docx.text.paragraph import Paragraph - -class Sections(Sequence): - """Sequence of |Section| objects corresponding to the sections in the document. - - Supports ``len()``, iteration, and indexed access. - """ - - def __init__(self, document_elm, document_part): - super(Sections, self).__init__() - self._document_elm = document_elm - self._document_part = document_part - - def __getitem__(self, key): - if isinstance(key, slice): - return [ - Section(sectPr, self._document_part) - for sectPr in self._document_elm.sectPr_lst[key] - ] - return Section(self._document_elm.sectPr_lst[key], self._document_part) - - def __iter__(self): - for sectPr in self._document_elm.sectPr_lst: - yield Section(sectPr, self._document_part) - - def __len__(self): - return len(self._document_elm.sectPr_lst) +if TYPE_CHECKING: + from docx.enum.section import WD_ORIENTATION, WD_SECTION_START + from docx.oxml.document import CT_Document + from docx.oxml.section import CT_SectPr + from docx.parts.document import DocumentPart + from docx.parts.story import StoryPart + from docx.shared import Length -class Section(object): +class Section: """Document section, providing access to section and page setup settings. Also provides access to headers and footers. """ - def __init__(self, sectPr, document_part): + def __init__(self, sectPr: CT_SectPr, document_part: DocumentPart): super(Section, self).__init__() self._sectPr = sectPr self._document_part = document_part @property - def bottom_margin(self): - """ - |Length| object representing the bottom margin for all pages in this - section in English Metric Units. + def bottom_margin(self) -> Length | None: + """Read/write. Bottom margin for pages in this section, in EMU. + + `None` when no bottom margin has been specified. Assigning |None| removes any + bottom-margin setting. """ return self._sectPr.bottom_margin @bottom_margin.setter - def bottom_margin(self, value): + def bottom_margin(self, value: int | Length | None): self._sectPr.bottom_margin = value @property - def different_first_page_header_footer(self): + def different_first_page_header_footer(self) -> bool: """True if this section displays a distinct first-page header and footer. Read/write. The definition of the first-page header and footer are accessed @@ -70,11 +55,11 @@ def different_first_page_header_footer(self): return self._sectPr.titlePg_val @different_first_page_header_footer.setter - def different_first_page_header_footer(self, value): + def different_first_page_header_footer(self, value: bool): self._sectPr.titlePg_val = value @property - def even_page_footer(self): + def even_page_footer(self) -> _Footer: """|_Footer| object defining footer content for even pages. The content of this footer definition is ignored unless the document setting @@ -83,7 +68,7 @@ def even_page_footer(self): return _Footer(self._sectPr, self._document_part, WD_HEADER_FOOTER.EVEN_PAGE) @property - def even_page_header(self): + def even_page_header(self) -> _Header: """|_Header| object defining header content for even pages. The content of this header definition is ignored unless the document setting @@ -92,7 +77,7 @@ def even_page_header(self): return _Header(self._sectPr, self._document_part, WD_HEADER_FOOTER.EVEN_PAGE) @property - def first_page_footer(self): + def first_page_footer(self) -> _Footer: """|_Footer| object defining footer content for the first page of this section. The content of this footer definition is ignored unless the property @@ -101,7 +86,7 @@ def first_page_footer(self): return _Footer(self._sectPr, self._document_part, WD_HEADER_FOOTER.FIRST_PAGE) @property - def first_page_header(self): + def first_page_header(self) -> _Header: """|_Header| object defining header content for the first page of this section. The content of this header definition is ignored unless the property @@ -110,7 +95,7 @@ def first_page_header(self): return _Header(self._sectPr, self._document_part, WD_HEADER_FOOTER.FIRST_PAGE) @lazyproperty - def footer(self): + def footer(self) -> _Footer: """|_Footer| object representing default page footer for this section. The default footer is used for odd-numbered pages when separate odd/even footers @@ -119,34 +104,36 @@ def footer(self): return _Footer(self._sectPr, self._document_part, WD_HEADER_FOOTER.PRIMARY) @property - def footer_distance(self): - """ - |Length| object representing the distance from the bottom edge of the - page to the bottom edge of the footer. |None| if no setting is present - in the XML. + def footer_distance(self) -> Length | None: + """Distance from bottom edge of page to bottom edge of the footer. + + Read/write. |None| if no setting is present in the XML. """ return self._sectPr.footer @footer_distance.setter - def footer_distance(self, value): + def footer_distance(self, value: int | Length | None): self._sectPr.footer = value @property - def gutter(self): - """ - |Length| object representing the page gutter size in English Metric - Units for all pages in this section. The page gutter is extra spacing - added to the *inner* margin to ensure even margins after page - binding. + def gutter(self) -> Length | None: + """|Length| object representing page gutter size in English Metric Units. + + Read/write. The page gutter is extra spacing added to the `inner` margin to + ensure even margins after page binding. Generally only used in book-bound + documents with double-sided and facing pages. + + This setting applies to all pages in this section. + """ return self._sectPr.gutter @gutter.setter - def gutter(self, value): + def gutter(self, value: int | Length | None): self._sectPr.gutter = value @lazyproperty - def header(self): + def header(self) -> _Header: """|_Header| object representing default page header for this section. The default header is used for odd-numbered pages when separate odd/even headers @@ -155,120 +142,171 @@ def header(self): return _Header(self._sectPr, self._document_part, WD_HEADER_FOOTER.PRIMARY) @property - def header_distance(self): - """ - |Length| object representing the distance from the top edge of the - page to the top edge of the header. |None| if no setting is present - in the XML. + def header_distance(self) -> Length | None: + """Distance from top edge of page to top edge of header. + + Read/write. |None| if no setting is present in the XML. Assigning |None| causes + default value to be used. """ return self._sectPr.header @header_distance.setter - def header_distance(self, value): + def header_distance(self, value: int | Length | None): self._sectPr.header = value - @property - def left_margin(self): - """ - |Length| object representing the left margin for all pages in this - section in English Metric Units. + def iter_inner_content(self) -> Iterator[Paragraph | Table]: + """Generate each Paragraph or Table object in this `section`. + + Items appear in document order. """ + for element in self._sectPr.iter_inner_content(): + yield ( + Paragraph(element, self) # pyright: ignore[reportGeneralTypeIssues] + if isinstance(element, CT_P) + else Table(element, self) + ) + + @property + def left_margin(self) -> Length | None: + """|Length| object representing the left margin for all pages in this section in + English Metric Units.""" return self._sectPr.left_margin @left_margin.setter - def left_margin(self, value): + def left_margin(self, value: int | Length | None): self._sectPr.left_margin = value @property - def orientation(self): - """ - Member of the :ref:`WdOrientation` enumeration specifying the page - orientation for this section, one of ``WD_ORIENT.PORTRAIT`` or - ``WD_ORIENT.LANDSCAPE``. + def orientation(self) -> WD_ORIENTATION: + """:ref:`WdOrientation` member specifying page orientation for this section. + + One of ``WD_ORIENT.PORTRAIT`` or ``WD_ORIENT.LANDSCAPE``. """ return self._sectPr.orientation @orientation.setter - def orientation(self, value): + def orientation(self, value: WD_ORIENTATION | None): self._sectPr.orientation = value @property - def page_height(self): - """ - Total page height used for this section, inclusive of all edge spacing - values such as margins. Page orientation is taken into account, so - for example, its expected value would be ``Inches(8.5)`` for - letter-sized paper when orientation is landscape. + def page_height(self) -> Length | None: + """Total page height used for this section. + + This value is inclusive of all edge spacing values such as margins. + + Page orientation is taken into account, so for example, its expected value + would be ``Inches(8.5)`` for letter-sized paper when orientation is landscape. """ return self._sectPr.page_height @page_height.setter - def page_height(self, value): + def page_height(self, value: Length | None): self._sectPr.page_height = value @property - def page_width(self): - """ - Total page width used for this section, inclusive of all edge spacing - values such as margins. Page orientation is taken into account, so - for example, its expected value would be ``Inches(11)`` for - letter-sized paper when orientation is landscape. + def page_width(self) -> Length | None: + """Total page width used for this section. + + This value is like "paper size" and includes all edge spacing values such as + margins. + + Page orientation is taken into account, so for example, its expected value + would be ``Inches(11)`` for letter-sized paper when orientation is landscape. """ return self._sectPr.page_width @page_width.setter - def page_width(self, value): + def page_width(self, value: Length | None): self._sectPr.page_width = value @property - def right_margin(self): - """ - |Length| object representing the right margin for all pages in this - section in English Metric Units. - """ + def part(self) -> StoryPart: + return self._document_part + + @property + def right_margin(self) -> Length | None: + """|Length| object representing the right margin for all pages in this section + in English Metric Units.""" return self._sectPr.right_margin @right_margin.setter - def right_margin(self, value): + def right_margin(self, value: Length | None): self._sectPr.right_margin = value @property - def start_type(self): - """ - The member of the :ref:`WdSectionStart` enumeration corresponding to - the initial break behavior of this section, e.g. - ``WD_SECTION.ODD_PAGE`` if the section should begin on the next odd - page. + def start_type(self) -> WD_SECTION_START: + """Type of page-break (if any) inserted at the start of this section. + + For exmple, ``WD_SECTION_START.ODD_PAGE`` if the section should begin on the + next odd page, possibly inserting two page-breaks instead of one. """ return self._sectPr.start_type @start_type.setter - def start_type(self, value): + def start_type(self, value: WD_SECTION_START | None): self._sectPr.start_type = value @property - def top_margin(self): - """ - |Length| object representing the top margin for all pages in this - section in English Metric Units. - """ + def top_margin(self) -> Length | None: + """|Length| object representing the top margin for all pages in this section in + English Metric Units.""" return self._sectPr.top_margin @top_margin.setter - def top_margin(self, value): + def top_margin(self, value: Length | None): self._sectPr.top_margin = value -class _BaseHeaderFooter(BlockItemContainer): - """Base class for header and footer classes""" +class Sections(Sequence[Section]): + """Sequence of |Section| objects corresponding to the sections in the document. + + Supports ``len()``, iteration, and indexed access. + """ + + def __init__(self, document_elm: CT_Document, document_part: DocumentPart): + super(Sections, self).__init__() + self._document_elm = document_elm + self._document_part = document_part + + @overload + def __getitem__(self, key: int) -> Section: + ... - def __init__(self, sectPr, document_part, header_footer_index): + @overload + def __getitem__(self, key: slice) -> List[Section]: + ... + + def __getitem__(self, key: int | slice) -> Section | List[Section]: + if isinstance(key, slice): + return [ + Section(sectPr, self._document_part) + for sectPr in self._document_elm.sectPr_lst[key] + ] + return Section(self._document_elm.sectPr_lst[key], self._document_part) + + def __iter__(self) -> Iterator[Section]: + for sectPr in self._document_elm.sectPr_lst: + yield Section(sectPr, self._document_part) + + def __len__(self) -> int: + return len(self._document_elm.sectPr_lst) + + +class _BaseHeaderFooter(BlockItemContainer): + """Base class for header and footer classes.""" + + def __init__( + self, + sectPr: CT_SectPr, + document_part: DocumentPart, + header_footer_index: WD_HEADER_FOOTER, + ): self._sectPr = sectPr self._document_part = document_part self._hdrftr_index = header_footer_index @property - def is_linked_to_previous(self): + def is_linked_to_previous(self) -> bool: """``True`` if this header/footer uses the definition from the prior section. ``False`` if this header/footer has an explicit definition. @@ -282,7 +320,7 @@ def is_linked_to_previous(self): return not self._has_definition @is_linked_to_previous.setter - def is_linked_to_previous(self, value): + def is_linked_to_previous(self, value: bool) -> None: new_state = bool(value) # ---do nothing when value is not being changed--- if new_state == self.is_linked_to_previous: @@ -293,7 +331,7 @@ def is_linked_to_previous(self, value): self._add_definition() @property - def part(self): + def part(self) -> HeaderPart | FooterPart: """The |HeaderPart| or |FooterPart| for this header/footer. This overrides `BlockItemContainer.part` and is required to support image @@ -303,16 +341,16 @@ def part(self): # ---not an interface property, even though public return self._get_or_add_definition() - def _add_definition(self): + def _add_definition(self) -> HeaderPart | FooterPart: """Return newly-added header/footer part.""" raise NotImplementedError("must be implemented by each subclass") @property - def _definition(self): + def _definition(self) -> HeaderPart | FooterPart: """|HeaderPart| or |FooterPart| object containing header/footer content.""" raise NotImplementedError("must be implemented by each subclass") - def _drop_definition(self): + def _drop_definition(self) -> None: """Remove header/footer part containing the definition of this header/footer.""" raise NotImplementedError("must be implemented by each subclass") @@ -321,7 +359,7 @@ def _element(self): """`w:hdr` or `w:ftr` element, root of header/footer part.""" return self._get_or_add_definition().element - def _get_or_add_definition(self): + def _get_or_add_definition(self) -> HeaderPart | FooterPart: """Return HeaderPart or FooterPart object for this section. If this header/footer inherits its content, the part for the prior header/footer @@ -342,12 +380,12 @@ def _get_or_add_definition(self): return self._add_definition() @property - def _has_definition(self): + def _has_definition(self) -> bool: """True if this header/footer has a related part containing its definition.""" raise NotImplementedError("must be implemented by each subclass") @property - def _prior_headerfooter(self): + def _prior_headerfooter(self) -> _Header | _Footer | None: """|_Header| or |_Footer| proxy on prior sectPr element. Returns None if this is first section. @@ -365,7 +403,7 @@ class _Footer(_BaseHeaderFooter): leave an empty paragraph above the newly added one. """ - def _add_definition(self): + def _add_definition(self) -> FooterPart: """Return newly-added footer part.""" footer_part, rId = self._document_part.add_footer_part() self._sectPr.add_footerReference(self._hdrftr_index, rId) @@ -375,6 +413,8 @@ def _add_definition(self): def _definition(self): """|FooterPart| object containing content of this footer.""" footerReference = self._sectPr.get_footerReference(self._hdrftr_index) + # -- currently this is never called when `._has_definition` evaluates False -- + assert footerReference is not None return self._document_part.footer_part(footerReference.rId) def _drop_definition(self): @@ -383,10 +423,10 @@ def _drop_definition(self): self._document_part.drop_rel(rId) @property - def _has_definition(self): + def _has_definition(self) -> bool: """True if a footer is defined for this section.""" footerReference = self._sectPr.get_footerReference(self._hdrftr_index) - return False if footerReference is None else True + return footerReference is not None @property def _prior_headerfooter(self): @@ -419,6 +459,8 @@ def _add_definition(self): def _definition(self): """|HeaderPart| object containing content of this header.""" headerReference = self._sectPr.get_headerReference(self._hdrftr_index) + # -- currently this is never called when `._has_definition` evaluates False -- + assert headerReference is not None return self._document_part.header_part(headerReference.rId) def _drop_definition(self): @@ -427,10 +469,10 @@ def _drop_definition(self): self._document_part.drop_header_part(rId) @property - def _has_definition(self): + def _has_definition(self) -> bool: """True if a header is explicitly defined for this section.""" headerReference = self._sectPr.get_headerReference(self._hdrftr_index) - return False if headerReference is None else True + return headerReference is not None @property def _prior_headerfooter(self): diff --git a/docx/settings.py b/src/docx/settings.py similarity index 75% rename from docx/settings.py rename to src/docx/settings.py index 502c9d4db..78f816e87 100644 --- a/docx/settings.py +++ b/src/docx/settings.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Settings object, providing access to document-level settings""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Settings object, providing access to document-level settings.""" from docx.shared import ElementProxy @@ -13,8 +9,6 @@ class Settings(ElementProxy): Accessed using the :attr:`.Document.settings` property. """ - __slots__ = () - @property def odd_and_even_pages_header_footer(self): """True if this document has distinct odd and even page headers and footers. diff --git a/docx/shape.py b/src/docx/shape.py similarity index 63% rename from docx/shape.py rename to src/docx/shape.py index e4f885d73..b91ecbf64 100644 --- a/docx/shape.py +++ b/src/docx/shape.py @@ -1,32 +1,23 @@ -# encoding: utf-8 +"""Objects related to shapes. +A shape is a visual object that appears on the drawing layer of a document. """ -Objects related to shapes, visual objects that appear on the drawing layer of -a document. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) -from .enum.shape import WD_INLINE_SHAPE -from .oxml.ns import nsmap -from .shared import Parented +from docx.enum.shape import WD_INLINE_SHAPE +from docx.oxml.ns import nsmap +from docx.shared import Parented class InlineShapes(Parented): - """ - Sequence of |InlineShape| instances, supporting len(), iteration, and - indexed access. - """ + """Sequence of |InlineShape| instances, supporting len(), iteration, and indexed + access.""" + def __init__(self, body_elm, parent): super(InlineShapes, self).__init__(parent) self._body = body_elm def __getitem__(self, idx): - """ - Provide indexed access, e.g. 'inline_shapes[idx]' - """ + """Provide indexed access, e.g. 'inline_shapes[idx]'.""" try: inline = self._inline_lst[idx] except IndexError: @@ -43,24 +34,23 @@ def __len__(self): @property def _inline_lst(self): body = self._body - xpath = '//w:p/w:r/w:drawing/wp:inline' + xpath = "//w:p/w:r/w:drawing/wp:inline" return body.xpath(xpath) -class InlineShape(object): - """ - Proxy for an ```` element, representing the container for an - inline graphical object. - """ +class InlineShape: + """Proxy for an ```` element, representing the container for an inline + graphical object.""" + def __init__(self, inline): super(InlineShape, self).__init__() self._inline = inline @property def height(self): - """ - Read/write. The display height of this inline shape as an |Emu| - instance. + """Read/write. + + The display height of this inline shape as an |Emu| instance. """ return self._inline.extent.cy @@ -71,29 +61,29 @@ def height(self, cy): @property def type(self): - """ - The type of this inline shape as a member of + """The type of this inline shape as a member of ``docx.enum.shape.WD_INLINE_SHAPE``, e.g. ``LINKED_PICTURE``. + Read-only. """ graphicData = self._inline.graphic.graphicData uri = graphicData.uri - if uri == nsmap['pic']: + if uri == nsmap["pic"]: blip = graphicData.pic.blipFill.blip if blip.link is not None: return WD_INLINE_SHAPE.LINKED_PICTURE return WD_INLINE_SHAPE.PICTURE - if uri == nsmap['c']: + if uri == nsmap["c"]: return WD_INLINE_SHAPE.CHART - if uri == nsmap['dgm']: + if uri == nsmap["dgm"]: return WD_INLINE_SHAPE.SMART_ART return WD_INLINE_SHAPE.NOT_IMPLEMENTED @property def width(self): - """ - Read/write. The display width of this inline shape as an |Emu| - instance. + """Read/write. + + The display width of this inline shape as an |Emu| instance. """ return self._inline.extent.cx diff --git a/src/docx/shared.py b/src/docx/shared.py new file mode 100644 index 000000000..304fce4a4 --- /dev/null +++ b/src/docx/shared.py @@ -0,0 +1,366 @@ +"""Objects shared by docx modules.""" + +from __future__ import annotations + +import functools +from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, List, TypeVar, cast + +if TYPE_CHECKING: + from docx.oxml.xmlchemy import BaseOxmlElement + from docx.parts.story import StoryPart + + +class Length(int): + """Base class for length constructor classes Inches, Cm, Mm, Px, and Emu. + + Behaves as an int count of English Metric Units, 914,400 to the inch, 36,000 to the + mm. Provides convenience unit conversion methods in the form of read-only + properties. Immutable. + """ + + _EMUS_PER_INCH = 914400 + _EMUS_PER_CM = 360000 + _EMUS_PER_MM = 36000 + _EMUS_PER_PT = 12700 + _EMUS_PER_TWIP = 635 + + def __new__(cls, emu): + return int.__new__(cls, emu) + + @property + def cm(self): + """The equivalent length expressed in centimeters (float).""" + return self / float(self._EMUS_PER_CM) + + @property + def emu(self): + """The equivalent length expressed in English Metric Units (int).""" + return self + + @property + def inches(self): + """The equivalent length expressed in inches (float).""" + return self / float(self._EMUS_PER_INCH) + + @property + def mm(self): + """The equivalent length expressed in millimeters (float).""" + return self / float(self._EMUS_PER_MM) + + @property + def pt(self): + """Floating point length in points.""" + return self / float(self._EMUS_PER_PT) + + @property + def twips(self): + """The equivalent length expressed in twips (int).""" + return int(round(self / float(self._EMUS_PER_TWIP))) + + +class Inches(Length): + """Convenience constructor for length in inches, e.g. ``width = Inches(0.5)``.""" + + def __new__(cls, inches): + emu = int(inches * Length._EMUS_PER_INCH) + return Length.__new__(cls, emu) + + +class Cm(Length): + """Convenience constructor for length in centimeters, e.g. ``height = Cm(12)``.""" + + def __new__(cls, cm): + emu = int(cm * Length._EMUS_PER_CM) + return Length.__new__(cls, emu) + + +class Emu(Length): + """Convenience constructor for length in English Metric Units, e.g. ``width = + Emu(457200)``.""" + + def __new__(cls, emu): + return Length.__new__(cls, int(emu)) + + +class Mm(Length): + """Convenience constructor for length in millimeters, e.g. ``width = Mm(240.5)``.""" + + def __new__(cls, mm): + emu = int(mm * Length._EMUS_PER_MM) + return Length.__new__(cls, emu) + + +class Pt(Length): + """Convenience value class for specifying a length in points.""" + + def __new__(cls, points): + emu = int(points * Length._EMUS_PER_PT) + return Length.__new__(cls, emu) + + +class Twips(Length): + """Convenience constructor for length in twips, e.g. ``width = Twips(42)``. + + A twip is a twentieth of a point, 635 EMU. + """ + + def __new__(cls, twips): + emu = int(twips * Length._EMUS_PER_TWIP) + return Length.__new__(cls, emu) + + +class RGBColor(tuple): + """Immutable value object defining a particular RGB color.""" + + def __new__(cls, r, g, b): + msg = "RGBColor() takes three integer values 0-255" + for val in (r, g, b): + if not isinstance(val, int) or val < 0 or val > 255: + raise ValueError(msg) + return super(RGBColor, cls).__new__(cls, (r, g, b)) + + def __repr__(self): + return "RGBColor(0x%02x, 0x%02x, 0x%02x)" % self + + def __str__(self): + """Return a hex string rgb value, like '3C2F80'.""" + return "%02X%02X%02X" % self + + @classmethod + def from_string(cls, rgb_hex_str): + """Return a new instance from an RGB color hex string like ``'3C2F80'``.""" + r = int(rgb_hex_str[:2], 16) + g = int(rgb_hex_str[2:4], 16) + b = int(rgb_hex_str[4:], 16) + return cls(r, g, b) + + +T = TypeVar("T") + + +class lazyproperty(Generic[T]): + """Decorator like @property, but evaluated only on first access. + + Like @property, this can only be used to decorate methods having only a `self` + parameter, and is accessed like an attribute on an instance, i.e. trailing + parentheses are not used. Unlike @property, the decorated method is only evaluated + on first access; the resulting value is cached and that same value returned on + second and later access without re-evaluation of the method. + + Like @property, this class produces a *data descriptor* object, which is stored in + the __dict__ of the *class* under the name of the decorated method ('fget' + nominally). The cached value is stored in the __dict__ of the *instance* under that + same name. + + Because it is a data descriptor (as opposed to a *non-data descriptor*), its + `__get__()` method is executed on each access of the decorated attribute; the + __dict__ item of the same name is "shadowed" by the descriptor. + + While this may represent a performance improvement over a property, its greater + benefit may be its other characteristics. One common use is to construct + collaborator objects, removing that "real work" from the constructor, while still + only executing once. It also de-couples client code from any sequencing + considerations; if it's accessed from more than one location, it's assured it will + be ready whenever needed. + + Loosely based on: https://stackoverflow.com/a/6849299/1902513. + + A lazyproperty is read-only. There is no counterpart to the optional "setter" (or + deleter) behavior of an @property. This is critically important to maintaining its + immutability and idempotence guarantees. Attempting to assign to a lazyproperty + raises AttributeError unconditionally. + + The parameter names in the methods below correspond to this usage example:: + + class Obj(object) + + @lazyproperty + def fget(self): + return 'some result' + + obj = Obj() + + Not suitable for wrapping a function (as opposed to a method) because it is not + callable.""" + + def __init__(self, fget: Callable[..., T]) -> None: + """*fget* is the decorated method (a "getter" function). + + A lazyproperty is read-only, so there is only an *fget* function (a regular + @property can also have an fset and fdel function). This name was chosen for + consistency with Python's `property` class which uses this name for the + corresponding parameter. + """ + # --- maintain a reference to the wrapped getter method + self._fget = fget + # --- and store the name of that decorated method + self._name = fget.__name__ + # --- adopt fget's __name__, __doc__, and other attributes + functools.update_wrapper(self, fget) # pyright: ignore + + def __get__(self, obj: Any, type: Any = None) -> T: + """Called on each access of 'fget' attribute on class or instance. + + *self* is this instance of a lazyproperty descriptor "wrapping" the property + method it decorates (`fget`, nominally). + + *obj* is the "host" object instance when the attribute is accessed from an + object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on + the class, e.g. `Obj.fget`. + + *type* is the class hosting the decorated getter method (`fget`) on both class + and instance attribute access. + """ + # --- when accessed on class, e.g. Obj.fget, just return this descriptor + # --- instance (patched above to look like fget). + if obj is None: + return self # type: ignore + + # --- when accessed on instance, start by checking instance __dict__ for + # --- item with key matching the wrapped function's name + value = obj.__dict__.get(self._name) + if value is None: + # --- on first access, the __dict__ item will be absent. Evaluate fget() + # --- and store that value in the (otherwise unused) host-object + # --- __dict__ value of same name ('fget' nominally) + value = self._fget(obj) + obj.__dict__[self._name] = value + return cast(T, value) + + def __set__(self, obj: Any, value: Any) -> None: + """Raises unconditionally, to preserve read-only behavior. + + This decorator is intended to implement immutable (and idempotent) object + attributes. For that reason, assignment to this property must be explicitly + prevented. + + If this __set__ method was not present, this descriptor would become a + *non-data descriptor*. That would be nice because the cached value would be + accessed directly once set (__dict__ attrs have precedence over non-data + descriptors on instance attribute lookup). The problem is, there would be + nothing to stop assignment to the cached value, which would overwrite the result + of `fget()` and break both the immutability and idempotence guarantees of this + decorator. + + The performance with this __set__() method in place was roughly 0.4 usec per + access when measured on a 2.8GHz development machine; so quite snappy and + probably not a rich target for optimization efforts. + """ + raise AttributeError("can't set attribute") + + +def write_only_property(f): + """@write_only_property decorator. + + Creates a property (descriptor attribute) that accepts assignment, but not getattr + (use in an expression). + """ + docstring = f.__doc__ + + return property(fset=f, doc=docstring) + + +class ElementProxy: + """Base class for lxml element proxy classes. + + An element proxy class is one whose primary responsibilities are fulfilled by + manipulating the attributes and child elements of an XML element. They are the most + common type of class in python-docx other than custom element (oxml) classes. + """ + + def __init__(self, element: BaseOxmlElement, parent: Any | None = None): + self._element = element + self._parent = parent + + def __eq__(self, other): + """Return |True| if this proxy object refers to the same oxml element as does + `other`. + + ElementProxy objects are value objects and should maintain no mutable local + state. Equality for proxy objects is defined as referring to the same XML + element, whether or not they are the same proxy object instance. + """ + if not isinstance(other, ElementProxy): + return False + return self._element is other._element + + def __ne__(self, other): + if not isinstance(other, ElementProxy): + return True + return self._element is not other._element + + @property + def element(self): + """The lxml element proxied by this object.""" + return self._element + + @property + def part(self): + """The package part containing this object.""" + return self._parent.part + + +class Parented: + """Provides common services for document elements that occur below a part but may + occasionally require an ancestor object to provide a service, such as add or drop a + relationship. + + Provides ``self._parent`` attribute to subclasses. + """ + + def __init__(self, parent): + self._parent = parent + + @property + def part(self): + """The package part containing this object.""" + return self._parent.part + + +class StoryChild: + """A document element within a story part. + + Story parts include DocumentPart and Header/FooterPart and can contain block items + (paragraphs and tables). Items from the block-item subtree occasionally require an + ancestor object to provide access to part-level or package-level items like styles + or images or to add or drop a relationship. + + Provides `self._parent` attribute to subclasses. + """ + + def __init__(self, parent: StoryChild): + self._parent = parent + + @property + def part(self) -> StoryPart: + """The package part containing this object.""" + return self._parent.part + + +class TextAccumulator: + """Accepts `str` fragments and joins them together, in order, on `.pop(). + + Handy when text in a stream is broken up arbitrarily and you want to join it back + together within certain bounds. The optional `separator` argument determines how + the text fragments are punctuated, defaulting to the empty string. + """ + + def __init__(self, separator: str = ""): + self._separator = separator + self._texts: List[str] = [] + + def push(self, text: str) -> None: + """Add a text fragment to the accumulator.""" + self._texts.append(text) + + def pop(self) -> Iterator[str]: + """Generate sero-or-one str from those accumulated. + + Using `yield from accum.pop()` in a generator setting avoids producing an empty + string when no text is in the accumulator. + """ + if not self._texts: + return + text = self._separator.join(self._texts) + self._texts.clear() + yield text diff --git a/src/docx/styles/__init__.py b/src/docx/styles/__init__.py new file mode 100644 index 000000000..6358baf33 --- /dev/null +++ b/src/docx/styles/__init__.py @@ -0,0 +1,40 @@ +"""Sub-package module for docx.styles sub-package.""" + +from __future__ import annotations + +from typing import Dict + + +class BabelFish: + """Translates special-case style names from UI name (e.g. Heading 1) to + internal/styles.xml name (e.g. heading 1) and back.""" + + style_aliases = ( + ("Caption", "caption"), + ("Footer", "footer"), + ("Header", "header"), + ("Heading 1", "heading 1"), + ("Heading 2", "heading 2"), + ("Heading 3", "heading 3"), + ("Heading 4", "heading 4"), + ("Heading 5", "heading 5"), + ("Heading 6", "heading 6"), + ("Heading 7", "heading 7"), + ("Heading 8", "heading 8"), + ("Heading 9", "heading 9"), + ) + + internal_style_names: Dict[str, str] = dict(style_aliases) + ui_style_names = {item[1]: item[0] for item in style_aliases} + + @classmethod + def ui2internal(cls, ui_style_name: str) -> str: + """Return the internal style name corresponding to `ui_style_name`, such as + 'heading 1' for 'Heading 1'.""" + return cls.internal_style_names.get(ui_style_name, ui_style_name) + + @classmethod + def internal2ui(cls, internal_style_name: str) -> str: + """Return the user interface style name corresponding to `internal_style_name`, + such as 'Heading 1' for 'heading 1'.""" + return cls.ui_style_names.get(internal_style_name, internal_style_name) diff --git a/src/docx/styles/latent.py b/src/docx/styles/latent.py new file mode 100644 index 000000000..c9db62f82 --- /dev/null +++ b/src/docx/styles/latent.py @@ -0,0 +1,198 @@ +"""Latent style-related objects.""" + +from docx.shared import ElementProxy +from docx.styles import BabelFish + + +class LatentStyles(ElementProxy): + """Provides access to the default behaviors for latent styles in this document and + to the collection of |_LatentStyle| objects that define overrides of those defaults + for a particular named latent style.""" + + def __getitem__(self, key): + """Enables dictionary-style access to a latent style by name.""" + style_name = BabelFish.ui2internal(key) + lsdException = self._element.get_by_name(style_name) + if lsdException is None: + raise KeyError("no latent style with name '%s'" % key) + return _LatentStyle(lsdException) + + def __iter__(self): + return (_LatentStyle(ls) for ls in self._element.lsdException_lst) + + def __len__(self): + return len(self._element.lsdException_lst) + + def add_latent_style(self, name): + """Return a newly added |_LatentStyle| object to override the inherited defaults + defined in this latent styles object for the built-in style having `name`.""" + lsdException = self._element.add_lsdException() + lsdException.name = BabelFish.ui2internal(name) + return _LatentStyle(lsdException) + + @property + def default_priority(self): + """Integer between 0 and 99 inclusive specifying the default sort order for + latent styles in style lists and the style gallery. + + |None| if no value is assigned, which causes Word to use the default value 99. + """ + return self._element.defUIPriority + + @default_priority.setter + def default_priority(self, value): + self._element.defUIPriority = value + + @property + def default_to_hidden(self): + """Boolean specifying whether the default behavior for latent styles is to be + hidden. + + A hidden style does not appear in the recommended list or in the style gallery. + """ + return self._element.bool_prop("defSemiHidden") + + @default_to_hidden.setter + def default_to_hidden(self, value): + self._element.set_bool_prop("defSemiHidden", value) + + @property + def default_to_locked(self): + """Boolean specifying whether the default behavior for latent styles is to be + locked. + + A locked style does not appear in the styles panel or the style gallery and + cannot be applied to document content. This behavior is only active when + formatting protection is turned on for the document (via the Developer menu). + """ + return self._element.bool_prop("defLockedState") + + @default_to_locked.setter + def default_to_locked(self, value): + self._element.set_bool_prop("defLockedState", value) + + @property + def default_to_quick_style(self): + """Boolean specifying whether the default behavior for latent styles is to + appear in the style gallery when not hidden.""" + return self._element.bool_prop("defQFormat") + + @default_to_quick_style.setter + def default_to_quick_style(self, value): + self._element.set_bool_prop("defQFormat", value) + + @property + def default_to_unhide_when_used(self): + """Boolean specifying whether the default behavior for latent styles is to be + unhidden when first applied to content.""" + return self._element.bool_prop("defUnhideWhenUsed") + + @default_to_unhide_when_used.setter + def default_to_unhide_when_used(self, value): + self._element.set_bool_prop("defUnhideWhenUsed", value) + + @property + def load_count(self): + """Integer specifying the number of built-in styles to initialize to the + defaults specified in this |LatentStyles| object. + + |None| if there is no setting in the XML (very uncommon). The default Word 2011 + template sets this value to 276, accounting for the built-in styles in Word + 2010. + """ + return self._element.count + + @load_count.setter + def load_count(self, value): + self._element.count = value + + +class _LatentStyle(ElementProxy): + """Proxy for an `w:lsdException` element, which specifies display behaviors for a + built-in style when no definition for that style is stored yet in the `styles.xml` + part. + + The values in this element override the defaults specified in the parent + `w:latentStyles` element. + """ + + def delete(self): + """Remove this latent style definition such that the defaults defined in the + containing |LatentStyles| object provide the effective value for each of its + attributes. + + Attempting to access any attributes on this object after calling this method + will raise |AttributeError|. + """ + self._element.delete() + self._element = None + + @property + def hidden(self): + """Tri-state value specifying whether this latent style should appear in the + recommended list. + + |None| indicates the effective value is inherited from the parent + ```` element. + """ + return self._element.on_off_prop("semiHidden") + + @hidden.setter + def hidden(self, value): + self._element.set_on_off_prop("semiHidden", value) + + @property + def locked(self): + """Tri-state value specifying whether this latent styles is locked. + + A locked style does not appear in the styles panel or the style gallery and + cannot be applied to document content. This behavior is only active when + formatting protection is turned on for the document (via the Developer menu). + """ + return self._element.on_off_prop("locked") + + @locked.setter + def locked(self, value): + self._element.set_on_off_prop("locked", value) + + @property + def name(self): + """The name of the built-in style this exception applies to.""" + return BabelFish.internal2ui(self._element.name) + + @property + def priority(self): + """The integer sort key for this latent style in the Word UI.""" + return self._element.uiPriority + + @priority.setter + def priority(self, value): + self._element.uiPriority = value + + @property + def quick_style(self): + """Tri-state value specifying whether this latent style should appear in the + Word styles gallery when not hidden. + + |None| indicates the effective value should be inherited from the default values + in its parent |LatentStyles| object. + """ + return self._element.on_off_prop("qFormat") + + @quick_style.setter + def quick_style(self, value): + self._element.set_on_off_prop("qFormat", value) + + @property + def unhide_when_used(self): + """Tri-state value specifying whether this style should have its :attr:`hidden` + attribute set |False| the next time the style is applied to content. + + |None| indicates the effective value should be inherited from the default + specified by its parent |LatentStyles| object. + """ + return self._element.on_off_prop("unhideWhenUsed") + + @unhide_when_used.setter + def unhide_when_used(self, value): + self._element.set_on_off_prop("unhideWhenUsed", value) diff --git a/src/docx/styles/style.py b/src/docx/styles/style.py new file mode 100644 index 000000000..aa175ea80 --- /dev/null +++ b/src/docx/styles/style.py @@ -0,0 +1,254 @@ +"""Style object hierarchy.""" + +from __future__ import annotations + +from typing import Type + +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.styles import CT_Style +from docx.shared import ElementProxy +from docx.styles import BabelFish +from docx.text.font import Font +from docx.text.parfmt import ParagraphFormat + + +def StyleFactory(style_elm: CT_Style) -> BaseStyle: + """Return `Style` object of appropriate |BaseStyle| subclass for `style_elm`.""" + style_cls: Type[BaseStyle] = { + WD_STYLE_TYPE.PARAGRAPH: ParagraphStyle, + WD_STYLE_TYPE.CHARACTER: CharacterStyle, + WD_STYLE_TYPE.TABLE: _TableStyle, + WD_STYLE_TYPE.LIST: _NumberingStyle, + }[style_elm.type] + + return style_cls(style_elm) + + +class BaseStyle(ElementProxy): + """Base class for the various types of style object, paragraph, character, table, + and numbering. + + These properties and methods are inherited by all style objects. + """ + + def __init__(self, style_elm: CT_Style): + super().__init__(style_elm) + self._style_elm = style_elm + + @property + def builtin(self): + """Read-only. + + |True| if this style is a built-in style. |False| indicates it is a custom + (user-defined) style. Note this value is based on the presence of a + `customStyle` attribute in the XML, not on specific knowledge of which styles + are built into Word. + """ + return not self._element.customStyle + + def delete(self): + """Remove this style definition from the document. + + Note that calling this method does not remove or change the style applied to any + document content. Content items having the deleted style will be rendered using + the default style, as is any content with a style not defined in the document. + """ + self._element.delete() + self._element = None + + @property + def hidden(self): + """|True| if display of this style in the style gallery and list of recommended + styles is suppressed. + + |False| otherwise. In order to be shown in the style gallery, this value must be + |False| and :attr:`.quick_style` must be |True|. + """ + return self._element.semiHidden_val + + @hidden.setter + def hidden(self, value): + self._element.semiHidden_val = value + + @property + def locked(self): + """Read/write Boolean. + + |True| if this style is locked. A locked style does not appear in the styles + panel or the style gallery and cannot be applied to document content. This + behavior is only active when formatting protection is turned on for the document + (via the Developer menu). + """ + return self._element.locked_val + + @locked.setter + def locked(self, value): + self._element.locked_val = value + + @property + def name(self): + """The UI name of this style.""" + name = self._element.name_val + if name is None: + return None + return BabelFish.internal2ui(name) + + @name.setter + def name(self, value): + self._element.name_val = value + + @property + def priority(self): + """The integer sort key governing display sequence of this style in the Word UI. + + |None| indicates no setting is defined, causing Word to use the default value of + 0. Style name is used as a secondary sort key to resolve ordering of styles + having the same priority value. + """ + return self._element.uiPriority_val + + @priority.setter + def priority(self, value): + self._element.uiPriority_val = value + + @property + def quick_style(self): + """|True| if this style should be displayed in the style gallery when + :attr:`.hidden` is |False|. + + Read/write Boolean. + """ + return self._element.qFormat_val + + @quick_style.setter + def quick_style(self, value): + self._element.qFormat_val = value + + @property + def style_id(self) -> str: + """The unique key name (string) for this style. + + This value is subject to rewriting by Word and should generally not be changed + unless you are familiar with the internals involved. + """ + return self._style_elm.styleId + + @style_id.setter + def style_id(self, value): + self._element.styleId = value + + @property + def type(self): + """Member of :ref:`WdStyleType` corresponding to the type of this style, e.g. + ``WD_STYLE_TYPE.PARAGRAPH``.""" + type = self._style_elm.type + if type is None: + return WD_STYLE_TYPE.PARAGRAPH + return type + + @property + def unhide_when_used(self): + """|True| if an application should make this style visible the next time it is + applied to content. + + False otherwise. Note that |docx| does not automatically unhide a style having + |True| for this attribute when it is applied to content. + """ + return self._element.unhideWhenUsed_val + + @unhide_when_used.setter + def unhide_when_used(self, value): + self._element.unhideWhenUsed_val = value + + +class CharacterStyle(BaseStyle): + """A character style. + + A character style is applied to a |Run| object and primarily provides character- + level formatting via the |Font| object in its :attr:`.font` property. + """ + + @property + def base_style(self): + """Style object this style inherits from or |None| if this style is not based on + another style.""" + base_style = self._element.base_style + if base_style is None: + return None + return StyleFactory(base_style) + + @base_style.setter + def base_style(self, style): + style_id = style.style_id if style is not None else None + self._element.basedOn_val = style_id + + @property + def font(self): + """The |Font| object providing access to the character formatting properties for + this style, such as font name and size.""" + return Font(self._element) + + +# -- just in case someone uses the old name in an extension function -- +_CharacterStyle = CharacterStyle + + +class ParagraphStyle(CharacterStyle): + """A paragraph style. + + A paragraph style provides both character formatting and paragraph formatting such + as indentation and line-spacing. + """ + + def __repr__(self): + return "_ParagraphStyle('%s') id: %s" % (self.name, id(self)) + + @property + def next_paragraph_style(self): + """|_ParagraphStyle| object representing the style to be applied automatically + to a new paragraph inserted after a paragraph of this style. + + Returns self if no next paragraph style is defined. Assigning |None| or `self` + removes the setting such that new paragraphs are created using this same style. + """ + next_style_elm = self._element.next_style + if next_style_elm is None: + return self + if next_style_elm.type != WD_STYLE_TYPE.PARAGRAPH: + return self + return StyleFactory(next_style_elm) + + @next_paragraph_style.setter + def next_paragraph_style(self, style): + if style is None or style.style_id == self.style_id: + self._element._remove_next() + else: + self._element.get_or_add_next().val = style.style_id + + @property + def paragraph_format(self): + """The |ParagraphFormat| object providing access to the paragraph formatting + properties for this style such as indentation.""" + return ParagraphFormat(self._element) + + +# -- just in case someone uses the old name in an extension function -- +_ParagraphStyle = ParagraphStyle + + +class _TableStyle(ParagraphStyle): + """A table style. + + A table style provides character and paragraph formatting for its contents as well + as special table formatting properties. + """ + + def __repr__(self): + return "_TableStyle('%s') id: %s" % (self.name, id(self)) + + +class _NumberingStyle(BaseStyle): + """A numbering style. + + Not yet implemented. + """ diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py new file mode 100644 index 000000000..98a56e520 --- /dev/null +++ b/src/docx/styles/styles.py @@ -0,0 +1,145 @@ +"""Styles object, container for all objects in the styles part.""" + +from __future__ import annotations + +from warnings import warn + +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.styles import CT_Styles +from docx.shared import ElementProxy +from docx.styles import BabelFish +from docx.styles.latent import LatentStyles +from docx.styles.style import BaseStyle, StyleFactory + + +class Styles(ElementProxy): + """Provides access to the styles defined in a document. + + Accessed using the :attr:`.Document.styles` property. Supports ``len()``, iteration, + and dictionary-style access by style name. + """ + + def __init__(self, styles: CT_Styles): + super().__init__(styles) + self._element = styles + + def __contains__(self, name): + """Enables `in` operator on style name.""" + internal_name = BabelFish.ui2internal(name) + return any(style.name_val == internal_name for style in self._element.style_lst) + + def __getitem__(self, key: str): + """Enables dictionary-style access by UI name. + + Lookup by style id is deprecated, triggers a warning, and will be removed in a + near-future release. + """ + style_elm = self._element.get_by_name(BabelFish.ui2internal(key)) + if style_elm is not None: + return StyleFactory(style_elm) + + style_elm = self._element.get_by_id(key) + if style_elm is not None: + msg = ( + "style lookup by style_id is deprecated. Use style name as " + "key instead." + ) + warn(msg, UserWarning, stacklevel=2) + return StyleFactory(style_elm) + + raise KeyError("no style with name '%s'" % key) + + def __iter__(self): + return (StyleFactory(style) for style in self._element.style_lst) + + def __len__(self): + return len(self._element.style_lst) + + def add_style(self, name, style_type, builtin=False): + """Return a newly added style object of `style_type` and identified by `name`. + + A builtin style can be defined by passing True for the optional `builtin` + argument. + """ + style_name = BabelFish.ui2internal(name) + if style_name in self: + raise ValueError("document already contains style '%s'" % name) + style = self._element.add_style_of_type(style_name, style_type, builtin) + return StyleFactory(style) + + def default(self, style_type: WD_STYLE_TYPE): + """Return the default style for `style_type` or |None| if no default is defined + for that type (not common).""" + style = self._element.default_for(style_type) + if style is None: + return None + return StyleFactory(style) + + def get_by_id(self, style_id: str | None, style_type: WD_STYLE_TYPE): + """Return the style of `style_type` matching `style_id`. + + Returns the default for `style_type` if `style_id` is not found or is |None|, or + if the style having `style_id` is not of `style_type`. + """ + if style_id is None: + return self.default(style_type) + return self._get_by_id(style_id, style_type) + + def get_style_id(self, style_or_name, style_type): + """Return the id of the style corresponding to `style_or_name`, or |None| if + `style_or_name` is |None|. + + If `style_or_name` is not a style object, the style is looked up using + `style_or_name` as a style name, raising |ValueError| if no style with that name + is defined. Raises |ValueError| if the target style is not of `style_type`. + """ + if style_or_name is None: + return None + elif isinstance(style_or_name, BaseStyle): + return self._get_style_id_from_style(style_or_name, style_type) + else: + return self._get_style_id_from_name(style_or_name, style_type) + + @property + def latent_styles(self): + """A |LatentStyles| object providing access to the default behaviors for latent + styles and the collection of |_LatentStyle| objects that define overrides of + those defaults for a particular named latent style.""" + return LatentStyles(self._element.get_or_add_latentStyles()) + + def _get_by_id(self, style_id: str | None, style_type: WD_STYLE_TYPE): + """Return the style of `style_type` matching `style_id`. + + Returns the default for `style_type` if `style_id` is not found or if the style + having `style_id` is not of `style_type`. + """ + style = self._element.get_by_id(style_id) if style_id else None + if style is None or style.type != style_type: + return self.default(style_type) + return StyleFactory(style) + + def _get_style_id_from_name( + self, style_name: str, style_type: WD_STYLE_TYPE + ) -> str | None: + """Return the id of the style of `style_type` corresponding to `style_name`. + + Returns |None| if that style is the default style for `style_type`. Raises + |ValueError| if the named style is not found in the document or does not match + `style_type`. + """ + return self._get_style_id_from_style(self[style_name], style_type) + + def _get_style_id_from_style( + self, style: BaseStyle, style_type: WD_STYLE_TYPE + ) -> str | None: + """Id of `style`, or |None| if it is the default style of `style_type`. + + Raises |ValueError| if style is not of `style_type`. + """ + if style.type != style_type: + raise ValueError( + "assigned style is type %s, need type %s" % (style.type, style_type) + ) + if style == self.default(style_type): + return None + return style.style_id diff --git a/docx/table.py b/src/docx/table.py similarity index 50% rename from docx/table.py rename to src/docx/table.py index b3bc090fb..13cc5b7bf 100644 --- a/docx/table.py +++ b/src/docx/table.py @@ -1,30 +1,24 @@ -# encoding: utf-8 +"""The |Table| object and related proxy classes.""" -""" -The |Table| object and related proxy classes. -""" +from __future__ import annotations -from __future__ import absolute_import, print_function, unicode_literals +from typing import List, Tuple, overload -from .blkcntnr import BlockItemContainer -from .enum.style import WD_STYLE_TYPE -from .oxml.simpletypes import ST_Merge -from .shared import Inches, lazyproperty, Parented +from docx.blkcntnr import BlockItemContainer +from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.simpletypes import ST_Merge +from docx.shared import Inches, Parented, lazyproperty class Table(Parented): - """ - Proxy class for a WordprocessingML ```` element. - """ + """Proxy class for a WordprocessingML ```` element.""" + def __init__(self, tbl, parent): super(Table, self).__init__(parent) self._element = self._tbl = tbl def add_column(self, width): - """ - Return a |_Column| object of *width*, newly added rightmost to the - table. - """ + """Return a |_Column| object of `width`, newly added rightmost to the table.""" tblGrid = self._tbl.tblGrid gridCol = tblGrid.add_gridCol() gridCol.w = width @@ -34,9 +28,7 @@ def add_column(self, width): return _Column(gridCol, self) def add_row(self): - """ - Return a |_Row| instance, newly added bottom-most to the table. - """ + """Return a |_Row| instance, newly added bottom-most to the table.""" tbl = self._tbl tr = tbl.add_tr() for gridCol in tbl.tblGrid.gridCol_lst: @@ -46,11 +38,11 @@ def add_row(self): @property def alignment(self): - """ - Read/write. A member of :ref:`WdRowAlignment` or None, specifying the - positioning of this table between the page margins. |None| if no - setting is specified, causing the effective value to be inherited - from the style hierarchy. + """Read/write. + + A member of :ref:`WdRowAlignment` or None, specifying the positioning of this + table between the page margins. |None| if no setting is specified, causing the + effective value to be inherited from the style hierarchy. """ return self._tblPr.alignment @@ -60,11 +52,11 @@ def alignment(self, value): @property def autofit(self): - """ - |True| if column widths can be automatically adjusted to improve the - fit of cell contents. |False| if table layout is fixed. Column widths - are adjusted in either case if total column width exceeds page width. - Read/write boolean. + """|True| if column widths can be automatically adjusted to improve the fit of + cell contents. + + |False| if table layout is fixed. Column widths are adjusted in either case if + total column width exceeds page width. Read/write boolean. """ return self._tblPr.autofit @@ -73,84 +65,70 @@ def autofit(self, value): self._tblPr.autofit = value def cell(self, row_idx, col_idx): - """ - Return |_Cell| instance correponding to table cell at *row_idx*, - *col_idx* intersection, where (0, 0) is the top, left-most cell. - """ + """Return |_Cell| instance correponding to table cell at `row_idx`, `col_idx` + intersection, where (0, 0) is the top, left-most cell.""" cell_idx = col_idx + (row_idx * self._column_count) return self._cells[cell_idx] def column_cells(self, column_idx): - """ - Sequence of cells in the column at *column_idx* in this table. - """ + """Sequence of cells in the column at `column_idx` in this table.""" cells = self._cells idxs = range(column_idx, len(cells), self._column_count) return [cells[idx] for idx in idxs] @lazyproperty def columns(self): - """ - |_Columns| instance representing the sequence of columns in this - table. - """ + """|_Columns| instance representing the sequence of columns in this table.""" return _Columns(self._tbl, self) def row_cells(self, row_idx): - """ - Sequence of cells in the row at *row_idx* in this table. - """ + """Sequence of cells in the row at `row_idx` in this table.""" column_count = self._column_count start = row_idx * column_count end = start + column_count return self._cells[start:end] @lazyproperty - def rows(self): - """ - |_Rows| instance containing the sequence of rows in this table. - """ + def rows(self) -> _Rows: + """|_Rows| instance containing the sequence of rows in this table.""" return _Rows(self._tbl, self) @property def style(self): - """ - Read/write. A |_TableStyle| object representing the style applied to - this table. The default table style for the document (often `Normal - Table`) is returned if the table has no directly-applied style. - Assigning |None| to this property removes any directly-applied table - style causing it to inherit the default table style of the document. - Note that the style name of a table style differs slightly from that - displayed in the user interface; a hyphen, if it appears, must be - removed. For example, `Light Shading - Accent 1` becomes `Light - Shading Accent 1`. + """Read/write. A |_TableStyle| object representing the style applied to this + table. The default table style for the document (often `Normal Table`) is + returned if the table has no directly-applied style. Assigning |None| to this + property removes any directly-applied table style causing it to inherit the + default table style of the document. Note that the style name of a table style + differs slightly from that. + + displayed in the user interface; a hyphen, if it appears, must be removed. For + example, `Light Shading - Accent 1` becomes `Light Shading Accent 1`. """ style_id = self._tbl.tblStyle_val return self.part.get_style(style_id, WD_STYLE_TYPE.TABLE) @style.setter def style(self, style_or_name): - style_id = self.part.get_style_id( - style_or_name, WD_STYLE_TYPE.TABLE - ) + style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.TABLE) self._tbl.tblStyle_val = style_id @property def table(self): - """ - Provide child objects with reference to the |Table| object they - belong to, without them having to know their direct parent is - a |Table| object. This is the terminus of a series of `parent._table` - calls from an arbitrary child through its ancestors. + """Provide child objects with reference to the |Table| object they belong to, + without them having to know their direct parent is a |Table| object. + + This is the terminus of a series of `parent._table` calls from an arbitrary + child through its ancestors. """ return self @property def table_direction(self): - """ - A member of :ref:`WdTableDirection` indicating the direction in which - the table cells are ordered, e.g. `WD_TABLE_DIRECTION.LTR`. |None| - indicates the value is inherited from the style hierarchy. + """A member of :ref:`WdTableDirection` indicating the direction in which the + table cells are ordered, e.g. `WD_TABLE_DIRECTION.LTR`. + + |None| indicates the value is inherited from the style hierarchy. """ return self._element.bidiVisual_val @@ -160,10 +138,10 @@ def table_direction(self, value): @property def _cells(self): - """ - A sequence of |_Cell| objects, one for each cell of the layout grid. - If the table contains a span, one or more |_Cell| object references - are repeated. + """A sequence of |_Cell| objects, one for each cell of the layout grid. + + If the table contains a span, one or more |_Cell| object references are + repeated. """ col_count = self._column_count cells = [] @@ -179,9 +157,7 @@ def _cells(self): @property def _column_count(self): - """ - The number of grid columns in this table. - """ + """The number of grid columns in this table.""" return self._tbl.col_count @property @@ -190,32 +166,31 @@ def _tblPr(self): class _Cell(BlockItemContainer): - """Table cell""" + """Table cell.""" def __init__(self, tc, parent): super(_Cell, self).__init__(tc, parent) self._tc = self._element = tc - def add_paragraph(self, text='', style=None): - """ - Return a paragraph newly added to the end of the content in this - cell. If present, *text* is added to the paragraph in a single run. - If specified, the paragraph style *style* is applied. If *style* is - not specified or is |None|, the result is as though the 'Normal' - style was applied. Note that the formatting of text in a cell can be - influenced by the table style. *text* can contain tab (``\\t``) - characters, which are converted to the appropriate XML form for - a tab. *text* can also include newline (``\\n``) or carriage return - (``\\r``) characters, each of which is converted to a line break. + def add_paragraph(self, text="", style=None): + """Return a paragraph newly added to the end of the content in this cell. + + If present, `text` is added to the paragraph in a single run. If specified, the + paragraph style `style` is applied. If `style` is not specified or is |None|, + the result is as though the 'Normal' style was applied. Note that the formatting + of text in a cell can be influenced by the table style. `text` can contain tab + (``\\t``) characters, which are converted to the appropriate XML form for a tab. + `text` can also include newline (``\\n``) or carriage return (``\\r``) + characters, each of which is converted to a line break. """ return super(_Cell, self).add_paragraph(text, style) def add_table(self, rows, cols): - """ - Return a table newly added to this cell after any existing cell - content, having *rows* rows and *cols* columns. An empty paragraph is - added after the table because Word requires a paragraph element as - the last element in every cell. + """Return a table newly added to this cell after any existing cell content, + having `rows` rows and `cols` columns. + + An empty paragraph is added after the table because Word requires a paragraph + element as the last element in every cell. """ width = self.width if self.width is not None else Inches(1) table = super(_Cell, self).add_table(rows, cols, width) @@ -223,10 +198,10 @@ def add_table(self, rows, cols): return table def merge(self, other_cell): - """ - Return a merged cell created by spanning the rectangular region - having this cell and *other_cell* as diagonal corners. Raises - |InvalidSpanError| if the cells do not define a rectangular region. + """Return a merged cell created by spanning the rectangular region having this + cell and `other_cell` as diagonal corners. + + Raises |InvalidSpanError| if the cells do not define a rectangular region. """ tc, tc_2 = self._tc, other_cell._tc merged_tc = tc.merge(tc_2) @@ -234,34 +209,36 @@ def merge(self, other_cell): @property def paragraphs(self): - """ - List of paragraphs in the cell. A table cell is required to contain - at least one block-level element and end with a paragraph. By - default, a new cell contains a single paragraph. Read-only + """List of paragraphs in the cell. + + A table cell is required to contain at least one block-level element and end + with a paragraph. By default, a new cell contains a single paragraph. Read-only """ return super(_Cell, self).paragraphs @property def tables(self): - """ - List of tables in the cell, in the order they appear. Read-only. + """List of tables in the cell, in the order they appear. + + Read-only. """ return super(_Cell, self).tables @property - def text(self): - """ - The entire contents of this cell as a string of text. Assigning - a string to this property replaces all existing content with a single + def text(self) -> str: + """The entire contents of this cell as a string of text. + + Assigning a string to this property replaces all existing content with a single paragraph containing the assigned text in a single run. """ - return '\n'.join(p.text for p in self.paragraphs) + return "\n".join(p.text for p in self.paragraphs) @text.setter def text(self, text): - """ - Write-only. Set entire contents of cell to the string *text*. Any - existing content or revisions are replaced. + """Write-only. + + Set entire contents of cell to the string `text`. Any existing content or + revisions are replaced. """ tc = self._tc tc.clear_content() @@ -273,9 +250,9 @@ def text(self, text): def vertical_alignment(self): """Member of :ref:`WdCellVerticalAlignment` or None. - A value of |None| indicates vertical alignment for this cell is - inherited. Assigning |None| causes any explicitly defined vertical - alignment to be removed, restoring inheritance. + A value of |None| indicates vertical alignment for this cell is inherited. + Assigning |None| causes any explicitly defined vertical alignment to be removed, + restoring inheritance. """ tcPr = self._element.tcPr if tcPr is None: @@ -289,9 +266,7 @@ def vertical_alignment(self, value): @property def width(self): - """ - The width of this cell in EMU, or |None| if no explicit width is set. - """ + """The width of this cell in EMU, or |None| if no explicit width is set.""" return self._tc.width @width.setter @@ -300,33 +275,25 @@ def width(self, value): class _Column(Parented): - """ - Table column - """ + """Table column.""" + def __init__(self, gridCol, parent): super(_Column, self).__init__(parent) self._gridCol = gridCol @property def cells(self): - """ - Sequence of |_Cell| instances corresponding to cells in this column. - """ + """Sequence of |_Cell| instances corresponding to cells in this column.""" return tuple(self.table.column_cells(self._index)) @property def table(self): - """ - Reference to the |Table| object this column belongs to. - """ + """Reference to the |Table| object this column belongs to.""" return self._parent.table @property def width(self): - """ - The width of this column in EMU, or |None| if no explicit width is - set. - """ + """The width of this column in EMU, or |None| if no explicit width is set.""" return self._gridCol.w @width.setter @@ -335,25 +302,22 @@ def width(self, value): @property def _index(self): - """ - Index of this column in its table, starting from zero. - """ + """Index of this column in its table, starting from zero.""" return self._gridCol.gridCol_idx class _Columns(Parented): - """ - Sequence of |_Column| instances corresponding to the columns in a table. + """Sequence of |_Column| instances corresponding to the columns in a table. + Supports ``len()``, iteration and indexed access. """ + def __init__(self, tbl, parent): super(_Columns, self).__init__(parent) self._tbl = tbl def __getitem__(self, idx): - """ - Provide indexed access, e.g. 'columns[0]' - """ + """Provide indexed access, e.g. 'columns[0]'.""" try: gridCol = self._gridCol_lst[idx] except IndexError: @@ -370,42 +334,33 @@ def __len__(self): @property def table(self): - """ - Reference to the |Table| object this column collection belongs to. - """ + """Reference to the |Table| object this column collection belongs to.""" return self._parent.table @property def _gridCol_lst(self): - """ - Sequence containing ```` elements for this table, each - representing a table column. - """ + """Sequence containing ```` elements for this table, each + representing a table column.""" tblGrid = self._tbl.tblGrid return tblGrid.gridCol_lst class _Row(Parented): - """ - Table row - """ + """Table row.""" + def __init__(self, tr, parent): super(_Row, self).__init__(parent) self._tr = self._element = tr @property - def cells(self): - """ - Sequence of |_Cell| instances corresponding to cells in this row. - """ + def cells(self) -> Tuple[_Cell]: + """Sequence of |_Cell| instances corresponding to cells in this row.""" return tuple(self.table.row_cells(self._index)) @property def height(self): - """ - Return a |Length| object representing the height of this cell, or - |None| if no explicit height is set. - """ + """Return a |Length| object representing the height of this cell, or |None| if + no explicit height is set.""" return self._tr.trHeight_val @height.setter @@ -414,11 +369,8 @@ def height(self, value): @property def height_rule(self): - """ - Return the height rule of this cell as a member of the - :ref:`WdRowHeightRule` enumeration, or |None| if no explicit - height_rule is set. - """ + """Return the height rule of this cell as a member of the :ref:`WdRowHeightRule` + enumeration, or |None| if no explicit height_rule is set.""" return self._tr.trHeight_hRule @height_rule.setter @@ -427,32 +379,35 @@ def height_rule(self, value): @property def table(self): - """ - Reference to the |Table| object this row belongs to. - """ + """Reference to the |Table| object this row belongs to.""" return self._parent.table @property def _index(self): - """ - Index of this row in its table, starting from zero. - """ + """Index of this row in its table, starting from zero.""" return self._tr.tr_idx class _Rows(Parented): - """ - Sequence of |_Row| objects corresponding to the rows in a table. + """Sequence of |_Row| objects corresponding to the rows in a table. + Supports ``len()``, iteration, indexed access, and slicing. """ + def __init__(self, tbl, parent): super(_Rows, self).__init__(parent) self._tbl = tbl - def __getitem__(self, idx): - """ - Provide indexed access, (e.g. 'rows[0]') - """ + @overload + def __getitem__(self, idx: int) -> _Row: + ... + + @overload + def __getitem__(self, idx: slice) -> List[_Row]: + ... + + def __getitem__(self, idx: int | slice) -> _Row | List[_Row]: + """Provide indexed access, (e.g. `rows[0]` or `rows[1:3]`)""" return list(self)[idx] def __iter__(self): @@ -463,7 +418,5 @@ def __len__(self): @property def table(self): - """ - Reference to the |Table| object this row collection belongs to. - """ + """Reference to the |Table| object this row collection belongs to.""" return self._parent.table diff --git a/docx/templates/default-docx-template/[Content_Types].xml b/src/docx/templates/default-docx-template/[Content_Types].xml similarity index 100% rename from docx/templates/default-docx-template/[Content_Types].xml rename to src/docx/templates/default-docx-template/[Content_Types].xml diff --git a/docx/templates/default-docx-template/_rels/.rels b/src/docx/templates/default-docx-template/_rels/.rels similarity index 100% rename from docx/templates/default-docx-template/_rels/.rels rename to src/docx/templates/default-docx-template/_rels/.rels diff --git a/docx/templates/default-docx-template/customXml/_rels/item1.xml.rels b/src/docx/templates/default-docx-template/customXml/_rels/item1.xml.rels similarity index 100% rename from docx/templates/default-docx-template/customXml/_rels/item1.xml.rels rename to src/docx/templates/default-docx-template/customXml/_rels/item1.xml.rels diff --git a/docx/templates/default-docx-template/customXml/item1.xml b/src/docx/templates/default-docx-template/customXml/item1.xml similarity index 100% rename from docx/templates/default-docx-template/customXml/item1.xml rename to src/docx/templates/default-docx-template/customXml/item1.xml diff --git a/docx/templates/default-docx-template/customXml/itemProps1.xml b/src/docx/templates/default-docx-template/customXml/itemProps1.xml similarity index 100% rename from docx/templates/default-docx-template/customXml/itemProps1.xml rename to src/docx/templates/default-docx-template/customXml/itemProps1.xml diff --git a/docx/templates/default-docx-template/docProps/app.xml b/src/docx/templates/default-docx-template/docProps/app.xml similarity index 100% rename from docx/templates/default-docx-template/docProps/app.xml rename to src/docx/templates/default-docx-template/docProps/app.xml diff --git a/docx/templates/default-docx-template/docProps/core.xml b/src/docx/templates/default-docx-template/docProps/core.xml similarity index 100% rename from docx/templates/default-docx-template/docProps/core.xml rename to src/docx/templates/default-docx-template/docProps/core.xml diff --git a/docx/templates/default-docx-template/docProps/thumbnail.jpeg b/src/docx/templates/default-docx-template/docProps/thumbnail.jpeg similarity index 100% rename from docx/templates/default-docx-template/docProps/thumbnail.jpeg rename to src/docx/templates/default-docx-template/docProps/thumbnail.jpeg diff --git a/docx/templates/default-docx-template/word/_rels/document.xml.rels b/src/docx/templates/default-docx-template/word/_rels/document.xml.rels similarity index 100% rename from docx/templates/default-docx-template/word/_rels/document.xml.rels rename to src/docx/templates/default-docx-template/word/_rels/document.xml.rels diff --git a/docx/templates/default-docx-template/word/document.xml b/src/docx/templates/default-docx-template/word/document.xml similarity index 100% rename from docx/templates/default-docx-template/word/document.xml rename to src/docx/templates/default-docx-template/word/document.xml diff --git a/docx/templates/default-docx-template/word/fontTable.xml b/src/docx/templates/default-docx-template/word/fontTable.xml similarity index 100% rename from docx/templates/default-docx-template/word/fontTable.xml rename to src/docx/templates/default-docx-template/word/fontTable.xml diff --git a/docx/templates/default-docx-template/word/numbering.xml b/src/docx/templates/default-docx-template/word/numbering.xml similarity index 100% rename from docx/templates/default-docx-template/word/numbering.xml rename to src/docx/templates/default-docx-template/word/numbering.xml diff --git a/docx/templates/default-docx-template/word/settings.xml b/src/docx/templates/default-docx-template/word/settings.xml similarity index 100% rename from docx/templates/default-docx-template/word/settings.xml rename to src/docx/templates/default-docx-template/word/settings.xml diff --git a/docx/templates/default-docx-template/word/styles.xml b/src/docx/templates/default-docx-template/word/styles.xml similarity index 100% rename from docx/templates/default-docx-template/word/styles.xml rename to src/docx/templates/default-docx-template/word/styles.xml diff --git a/docx/templates/default-docx-template/word/stylesWithEffects.xml b/src/docx/templates/default-docx-template/word/stylesWithEffects.xml similarity index 100% rename from docx/templates/default-docx-template/word/stylesWithEffects.xml rename to src/docx/templates/default-docx-template/word/stylesWithEffects.xml diff --git a/docx/templates/default-docx-template/word/theme/theme1.xml b/src/docx/templates/default-docx-template/word/theme/theme1.xml similarity index 100% rename from docx/templates/default-docx-template/word/theme/theme1.xml rename to src/docx/templates/default-docx-template/word/theme/theme1.xml diff --git a/docx/templates/default-docx-template/word/webSettings.xml b/src/docx/templates/default-docx-template/word/webSettings.xml similarity index 100% rename from docx/templates/default-docx-template/word/webSettings.xml rename to src/docx/templates/default-docx-template/word/webSettings.xml diff --git a/docx/templates/default-footer.xml b/src/docx/templates/default-footer.xml similarity index 100% rename from docx/templates/default-footer.xml rename to src/docx/templates/default-footer.xml diff --git a/docx/templates/default-header.xml b/src/docx/templates/default-header.xml similarity index 100% rename from docx/templates/default-header.xml rename to src/docx/templates/default-header.xml diff --git a/docx/templates/default-settings.xml b/src/docx/templates/default-settings.xml similarity index 100% rename from docx/templates/default-settings.xml rename to src/docx/templates/default-settings.xml diff --git a/docx/templates/default-styles.xml b/src/docx/templates/default-styles.xml similarity index 100% rename from docx/templates/default-styles.xml rename to src/docx/templates/default-styles.xml diff --git a/docx/templates/default.docx b/src/docx/templates/default.docx similarity index 100% rename from docx/templates/default.docx rename to src/docx/templates/default.docx diff --git a/docx/text/__init__.py b/src/docx/text/__init__.py similarity index 100% rename from docx/text/__init__.py rename to src/docx/text/__init__.py diff --git a/src/docx/text/font.py b/src/docx/text/font.py new file mode 100644 index 000000000..acd60795b --- /dev/null +++ b/src/docx/text/font.py @@ -0,0 +1,432 @@ +"""Font-related proxy objects.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from docx.dml.color import ColorFormat +from docx.enum.text import WD_UNDERLINE +from docx.shared import ElementProxy, Emu + +if TYPE_CHECKING: + from docx.enum.text import WD_COLOR_INDEX + from docx.oxml.text.run import CT_R + from docx.shared import Length + + +class Font(ElementProxy): + """Proxy object for parent of a `` element and providing access to + character properties such as font name, font size, bold, and subscript.""" + + def __init__(self, r: CT_R, parent: Any | None = None): + super().__init__(r, parent) + self._element = r + self._r = r + + @property + def all_caps(self) -> bool | None: + """Read/write. + + Causes text in this font to appear in capital letters. + """ + return self._get_bool_prop("caps") + + @all_caps.setter + def all_caps(self, value: bool | None) -> None: + self._set_bool_prop("caps", value) + + @property + def bold(self) -> bool | None: + """Read/write. + + Causes text in this font to appear in bold. + """ + return self._get_bool_prop("b") + + @bold.setter + def bold(self, value: bool | None) -> None: + self._set_bool_prop("b", value) + + @property + def color(self): + """A |ColorFormat| object providing a way to get and set the text color for this + font.""" + return ColorFormat(self._element) + + @property + def complex_script(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the characters in the run to be treated as complex script + regardless of their Unicode values. + """ + return self._get_bool_prop("cs") + + @complex_script.setter + def complex_script(self, value: bool | None) -> None: + self._set_bool_prop("cs", value) + + @property + def cs_bold(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the complex script characters in the run to be displayed in + bold typeface. + """ + return self._get_bool_prop("bCs") + + @cs_bold.setter + def cs_bold(self, value: bool | None) -> None: + self._set_bool_prop("bCs", value) + + @property + def cs_italic(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the complex script characters in the run to be displayed in + italic typeface. + """ + return self._get_bool_prop("iCs") + + @cs_italic.setter + def cs_italic(self, value: bool | None) -> None: + self._set_bool_prop("iCs", value) + + @property + def double_strike(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text in the run to appear with double strikethrough. + """ + return self._get_bool_prop("dstrike") + + @double_strike.setter + def double_strike(self, value: bool | None) -> None: + self._set_bool_prop("dstrike", value) + + @property + def emboss(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text in the run to appear as if raised off the page in + relief. + """ + return self._get_bool_prop("emboss") + + @emboss.setter + def emboss(self, value: bool | None) -> None: + self._set_bool_prop("emboss", value) + + @property + def hidden(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text in the run to be hidden from display, unless + applications settings force hidden text to be shown. + """ + return self._get_bool_prop("vanish") + + @hidden.setter + def hidden(self, value: bool | None) -> None: + self._set_bool_prop("vanish", value) + + @property + def highlight_color(self) -> WD_COLOR_INDEX | None: + """Color of highlighing applied or |None| if not highlighted.""" + rPr = self._element.rPr + if rPr is None: + return None + return rPr.highlight_val + + @highlight_color.setter + def highlight_color(self, value: WD_COLOR_INDEX | None): + rPr = self._element.get_or_add_rPr() + rPr.highlight_val = value + + @property + def italic(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text of the run to appear in italics. |None| indicates + the effective value is inherited from the style hierarchy. + """ + return self._get_bool_prop("i") + + @italic.setter + def italic(self, value: bool | None) -> None: + self._set_bool_prop("i", value) + + @property + def imprint(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text in the run to appear as if pressed into the page. + """ + return self._get_bool_prop("imprint") + + @imprint.setter + def imprint(self, value: bool | None) -> None: + self._set_bool_prop("imprint", value) + + @property + def math(self) -> bool | None: + """Read/write tri-state value. + + When |True|, specifies this run contains WML that should be handled as though it + was Office Open XML Math. + """ + return self._get_bool_prop("oMath") + + @math.setter + def math(self, value: bool | None) -> None: + self._set_bool_prop("oMath", value) + + @property + def name(self) -> str | None: + """The typeface name for this |Font|. + + Causes the text it controls to appear in the named font, if a matching font is + found. |None| indicates the typeface is inherited from the style hierarchy. + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.rFonts_ascii + + @name.setter + def name(self, value: str | None) -> None: + rPr = self._element.get_or_add_rPr() + rPr.rFonts_ascii = value + rPr.rFonts_hAnsi = value + + @property + def no_proof(self) -> bool | None: + """Read/write tri-state value. + + When |True|, specifies that the contents of this run should not report any + errors when the document is scanned for spelling and grammar. + """ + return self._get_bool_prop("noProof") + + @no_proof.setter + def no_proof(self, value: bool | None) -> None: + self._set_bool_prop("noProof", value) + + @property + def outline(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the characters in the run to appear as if they have an + outline, by drawing a one pixel wide border around the inside and outside + borders of each character glyph. + """ + return self._get_bool_prop("outline") + + @outline.setter + def outline(self, value: bool | None) -> None: + self._set_bool_prop("outline", value) + + @property + def rtl(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the text in the run to have right-to-left characteristics. + """ + return self._get_bool_prop("rtl") + + @rtl.setter + def rtl(self, value: bool | None) -> None: + self._set_bool_prop("rtl", value) + + @property + def shadow(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the text in the run to appear as if each character has a + shadow. + """ + return self._get_bool_prop("shadow") + + @shadow.setter + def shadow(self, value: bool | None) -> None: + self._set_bool_prop("shadow", value) + + @property + def size(self) -> Length | None: + """Font height in English Metric Units (EMU). + + |None| indicates the font size should be inherited from the style hierarchy. + |Length| is a subclass of |int| having properties for convenient conversion into + points or other length units. The :class:`docx.shared.Pt` class allows + convenient specification of point values:: + + >>> font.size = Pt(24) + >>> font.size + 304800 + >>> font.size.pt + 24.0 + + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.sz_val + + @size.setter + def size(self, emu: int | Length | None) -> None: + rPr = self._element.get_or_add_rPr() + rPr.sz_val = None if emu is None else Emu(emu) + + @property + def small_caps(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the lowercase characters in the run to appear as capital + letters two points smaller than the font size specified for the run. + """ + return self._get_bool_prop("smallCaps") + + @small_caps.setter + def small_caps(self, value: bool | None) -> None: + self._set_bool_prop("smallCaps", value) + + @property + def snap_to_grid(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the run to use the document grid characters per line settings + defined in the docGrid element when laying out the characters in this run. + """ + return self._get_bool_prop("snapToGrid") + + @snap_to_grid.setter + def snap_to_grid(self, value: bool | None) -> None: + self._set_bool_prop("snapToGrid", value) + + @property + def spec_vanish(self) -> bool | None: + """Read/write tri-state value. + + When |True|, specifies that the given run shall always behave as if it is + hidden, even when hidden text is being displayed in the current document. The + property has a very narrow, specialized use related to the table of contents. + Consult the spec (§17.3.2.36) for more details. + """ + return self._get_bool_prop("specVanish") + + @spec_vanish.setter + def spec_vanish(self, value: bool | None) -> None: + self._set_bool_prop("specVanish", value) + + @property + def strike(self) -> bool | None: + """Read/write tri-state value. + + When |True| causes the text in the run to appear with a single horizontal line + through the center of the line. + """ + return self._get_bool_prop("strike") + + @strike.setter + def strike(self, value: bool | None) -> None: + self._set_bool_prop("strike", value) + + @property + def subscript(self) -> bool | None: + """Boolean indicating whether the characters in this |Font| appear as subscript. + + |None| indicates the subscript/subscript value is inherited from the style + hierarchy. + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.subscript + + @subscript.setter + def subscript(self, value: bool | None) -> None: + rPr = self._element.get_or_add_rPr() + rPr.subscript = value + + @property + def superscript(self) -> bool | None: + """Boolean indicating whether the characters in this |Font| appear as + superscript. + + |None| indicates the subscript/superscript value is inherited from the style + hierarchy. + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.superscript + + @superscript.setter + def superscript(self, value: bool | None) -> None: + rPr = self._element.get_or_add_rPr() + rPr.superscript = value + + @property + def underline(self) -> bool | WD_UNDERLINE | None: + """The underline style for this |Font|. + + The value is one of |None|, |True|, |False|, or a member of :ref:`WdUnderline`. + + |None| indicates the font inherits its underline value from the style hierarchy. + |False| indicates no underline. |True| indicates single underline. The values + from :ref:`WdUnderline` are used to specify other outline styles such as double, + wavy, and dotted. + """ + rPr = self._element.rPr + if rPr is None: + return None + val = rPr.u_val + return ( + None + if val == WD_UNDERLINE.INHERITED + else True + if val == WD_UNDERLINE.SINGLE + else False + if val == WD_UNDERLINE.NONE + else val + ) + + @underline.setter + def underline(self, value: bool | WD_UNDERLINE | None) -> None: + rPr = self._element.get_or_add_rPr() + # -- works fine without these two mappings, but only because True == 1 and + # -- False == 0, which happen to match the mapping for WD_UNDERLINE.SINGLE + # -- and .NONE respectively. + val = ( + WD_UNDERLINE.SINGLE + if value is True + else WD_UNDERLINE.NONE + if value is False + else value + ) + rPr.u_val = val + + @property + def web_hidden(self) -> bool | None: + """Read/write tri-state value. + + When |True|, specifies that the contents of this run shall be hidden when the + document is displayed in web page view. + """ + return self._get_bool_prop("webHidden") + + @web_hidden.setter + def web_hidden(self, value: bool | None) -> None: + self._set_bool_prop("webHidden", value) + + def _get_bool_prop(self, name: str) -> bool | None: + """Return the value of boolean child of `w:rPr` having `name`.""" + rPr = self._element.rPr + if rPr is None: + return None + return rPr._get_bool_val(name) # pyright: ignore[reportPrivateUsage] + + def _set_bool_prop(self, name: str, value: bool | None): + """Assign `value` to the boolean child `name` of `w:rPr`.""" + rPr = self._element.get_or_add_rPr() + rPr._set_bool_val(name, value) # pyright: ignore[reportPrivateUsage] diff --git a/src/docx/text/hyperlink.py b/src/docx/text/hyperlink.py new file mode 100644 index 000000000..6082a53d1 --- /dev/null +++ b/src/docx/text/hyperlink.py @@ -0,0 +1,72 @@ +"""Hyperlink-related proxy objects for python-docx, Hyperlink in particular. + +A hyperlink occurs in a paragraph, at the same level as a Run, and a hyperlink itself +contains runs, which is where the visible text of the hyperlink is stored. So it's kind +of in-between, less than a paragraph and more than a run. So it gets its own module. +""" + +from __future__ import annotations + +from typing import List + +from docx import types as t +from docx.oxml.text.hyperlink import CT_Hyperlink +from docx.shared import Parented +from docx.text.run import Run + + +class Hyperlink(Parented): + """Proxy object wrapping a `` element. + + A hyperlink occurs as a child of a paragraph, at the same level as a Run. A + hyperlink itself contains runs, which is where the visible text of the hyperlink is + stored. + """ + + def __init__(self, hyperlink: CT_Hyperlink, parent: t.StoryChild): + super().__init__(parent) + self._parent = parent + self._hyperlink = self._element = hyperlink + + @property + def address(self) -> str: + """The "URL" of the hyperlink (but not necessarily a web link). + + While commonly a web link like "https://google.com" the hyperlink address can + take a variety of forms including "internal links" to bookmarked locations + within the document. + """ + return self._parent.part.rels[self._hyperlink.rId].target_ref + + @property + def contains_page_break(self) -> bool: + """True when the text of this hyperlink is broken across page boundaries. + + This is not uncommon and can happen for example when the hyperlink text is + multiple words and occurs in the last line of a page. Theoretically, a hyperlink + can contain more than one page break but that would be extremely uncommon in + practice. Still, this value should be understood to mean that "one-or-more" + rendered page breaks are present. + """ + return bool(self._hyperlink.lastRenderedPageBreaks) + + @property + def runs(self) -> List[Run]: + """List of |Run| instances in this hyperlink. + + Together these define the visible text of the hyperlink. The text of a hyperlink + is typically contained in a single run will be broken into multiple runs if for + example part of the hyperlink is bold or the text was changed after the document + was saved. + """ + return [Run(r, self) for r in self._hyperlink.r_lst] + + @property + def text(self) -> str: + """String formed by concatenating the text of each run in the hyperlink. + + Tabs and line breaks in the XML are mapped to ``\\t`` and ``\\n`` characters + respectively. Note that rendered page-breaks can occur within a hyperlink but + they are not reflected in this text. + """ + return self._hyperlink.text diff --git a/src/docx/text/pagebreak.py b/src/docx/text/pagebreak.py new file mode 100644 index 000000000..f3c16bc5c --- /dev/null +++ b/src/docx/text/pagebreak.py @@ -0,0 +1,102 @@ +"""Proxy objects related to rendered page-breaks.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docx import types as t +from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak +from docx.shared import Parented + +if TYPE_CHECKING: + from docx.text.paragraph import Paragraph + + +class RenderedPageBreak(Parented): + """A page-break inserted by Word during page-layout for print or display purposes. + + This usually does not correspond to a "hard" page-break inserted by the document + author, rather just that Word ran out of room on one page and needed to start + another. The position of these can change depending on the printer and page-size, as + well as margins, etc. They also will change in response to edits, but not until Word + loads and saves the document. + + Note these are never inserted by `python-docx` because it has no rendering function. + These are generally only useful for text-extraction of existing documents when + `python-docx` is being used solely as a document "reader". + + NOTE: a rendered page-break can occur within a hyperlink; consider a multi-word + hyperlink like "excellent Wikipedia article on LLMs" that happens to fall close to + the end of the last line on a page such that the page breaks between "Wikipedia" and + "article". In such a "page-breaks-in-hyperlink" case, THESE METHODS WILL "MOVE" THE + PAGE-BREAK to occur after the hyperlink, such that the entire hyperlink appears in + the paragraph returned by `.preceding_paragraph_fragment`. While this places the + "tail" text of the hyperlink on the "wrong" page, it avoids having two hyperlinks + each with a fragment of the actual text and pointing to the same address. + """ + + def __init__( + self, lastRenderedPageBreak: CT_LastRenderedPageBreak, parent: t.StoryChild + ): + super().__init__(parent) + self._element = lastRenderedPageBreak + self._lastRenderedPageBreak = lastRenderedPageBreak + + @property + def preceding_paragraph_fragment(self) -> Paragraph | None: + """A "loose" paragraph containing the content preceding this page-break. + + Compare `.following_paragraph_fragment` as these two are intended to be used + together. + + This value is `None` when no content precedes this page-break. This case is + common and occurs whenever a page breaks on an even paragraph boundary. + Returning `None` for this case avoids "inserting" a non-existent paragraph into + the content stream. Note that content can include DrawingML items like images or + charts. + + Note the returned paragraph *is divorced from the document body*. Any changes + made to it will not be reflected in the document. It is intended to provide a + familiar container (`Paragraph`) to interrogate for the content preceding this + page-break in the paragraph in which it occured. + + Contains the entire hyperlink when this break occurs within a hyperlink. + """ + if self._lastRenderedPageBreak.precedes_all_content: + return None + + from docx.text.paragraph import Paragraph + + return Paragraph(self._lastRenderedPageBreak.preceding_fragment_p, self._parent) + + @property + def following_paragraph_fragment(self) -> Paragraph | None: + """A "loose" paragraph containing the content following this page-break. + + HAS POTENTIALLY SURPRISING BEHAVIORS so read carefully to be sure this is what + you want. This is primarily targeted toward text-extraction use-cases for which + precisely associating text with the page it occurs on is important. + + Compare `.preceding_paragraph_fragment` as these two are intended to be used + together. + + This value is `None` when no content follows this page-break. This case is + unlikely to occur in practice because Word places even-paragraph-boundary + page-breaks on the paragraph *following* the page-break. Still, it is possible + and must be checked for. Returning `None` for this case avoids "inserting" an + extra, non-existent paragraph into the content stream. Note that content can + include DrawingML items like images or charts, not just text. + + The returned paragraph *is divorced from the document body*. Any changes made to + it will not be reflected in the document. It is intended to provide a container + (`Paragraph`) with familiar properties and methods that can be used to + characterize the paragraph content following a mid-paragraph page-break. + + Contains no portion of the hyperlink when this break occurs within a hyperlink. + """ + if self._lastRenderedPageBreak.follows_all_content: + return None + + from docx.text.paragraph import Paragraph + + return Paragraph(self._lastRenderedPageBreak.following_fragment_p, self._parent) diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py new file mode 100644 index 000000000..2425f1d6e --- /dev/null +++ b/src/docx/text/paragraph.py @@ -0,0 +1,175 @@ +"""Paragraph-related proxy types.""" + +from __future__ import annotations + +from typing import Iterator, List, cast + +from typing_extensions import Self + +from docx.enum.style import WD_STYLE_TYPE +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +from docx.oxml.text.paragraph import CT_P +from docx.oxml.text.run import CT_R +from docx.shared import StoryChild +from docx.styles.style import CharacterStyle, ParagraphStyle +from docx.text.hyperlink import Hyperlink +from docx.text.pagebreak import RenderedPageBreak +from docx.text.parfmt import ParagraphFormat +from docx.text.run import Run + + +class Paragraph(StoryChild): + """Proxy object wrapping a `` element.""" + + def __init__(self, p: CT_P, parent: StoryChild): + super(Paragraph, self).__init__(parent) + self._p = self._element = p + + def add_run( + self, text: str | None = None, style: str | CharacterStyle | None = None + ) -> Run: + """Append run containing `text` and having character-style `style`. + + `text` can contain tab (``\\t``) characters, which are converted to the + appropriate XML form for a tab. `text` can also include newline (``\\n``) or + carriage return (``\\r``) characters, each of which is converted to a line + break. When `text` is `None`, the new run is empty. + """ + r = self._p.add_r() + run = Run(r, self) + if text: + run.text = text + if style: + run.style = style + return run + + @property + def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None: + """A member of the :ref:`WdParagraphAlignment` enumeration specifying the + justification setting for this paragraph. + + A value of |None| indicates the paragraph has no directly-applied alignment + value and will inherit its alignment value from its style hierarchy. Assigning + |None| to this property removes any directly-applied alignment value. + """ + return self._p.alignment + + @alignment.setter + def alignment(self, value: WD_PARAGRAPH_ALIGNMENT): + self._p.alignment = value + + def clear(self): + """Return this same paragraph after removing all its content. + + Paragraph-level formatting, such as style, is preserved. + """ + self._p.clear_content() + return self + + @property + def contains_page_break(self) -> bool: + """`True` when one or more rendered page-breaks occur in this paragraph.""" + return bool(self._p.lastRenderedPageBreaks) + + @property + def hyperlinks(self) -> List[Hyperlink]: + """A |Hyperlink| instance for each hyperlink in this paragraph.""" + return [Hyperlink(hyperlink, self) for hyperlink in self._p.hyperlink_lst] + + def insert_paragraph_before( + self, text: str | None = None, style: str | ParagraphStyle | None = None + ) -> Self: + """Return a newly created paragraph, inserted directly before this paragraph. + + If `text` is supplied, the new paragraph contains that text in a single run. If + `style` is provided, that style is assigned to the new paragraph. + """ + paragraph = self._insert_paragraph_before() + if text: + paragraph.add_run(text) + if style is not None: + paragraph.style = style + return paragraph + + def iter_inner_content(self) -> Iterator[Run | Hyperlink]: + """Generate the runs and hyperlinks in this paragraph, in the order they appear. + + The content in a paragraph consists of both runs and hyperlinks. This method + allows accessing each of those separately, in document order, for when the + precise position of the hyperlink within the paragraph text is important. Note + that a hyperlink itself contains runs. + """ + for r_or_hlink in self._p.inner_content_elements: + yield ( + Run(r_or_hlink, self) + if isinstance(r_or_hlink, CT_R) + else Hyperlink(r_or_hlink, self) + ) + + @property + def paragraph_format(self): + """The |ParagraphFormat| object providing access to the formatting properties + for this paragraph, such as line spacing and indentation.""" + return ParagraphFormat(self._element) + + @property + def rendered_page_breaks(self) -> List[RenderedPageBreak]: + """All rendered page-breaks in this paragraph. + + Most often an empty list, sometimes contains one page-break, but can contain + more than one is rare or contrived cases. + """ + return [ + RenderedPageBreak(lrpb, self) for lrpb in self._p.lastRenderedPageBreaks + ] + + @property + def runs(self) -> List[Run]: + """Sequence of |Run| instances corresponding to the elements in this + paragraph.""" + return [Run(r, self) for r in self._p.r_lst] + + @property + def style(self) -> ParagraphStyle | None: + """Read/Write. + + |_ParagraphStyle| object representing the style assigned to this paragraph. If + no explicit style is assigned to this paragraph, its value is the default + paragraph style for the document. A paragraph style name can be assigned in lieu + of a paragraph style object. Assigning |None| removes any applied style, making + its effective value the default paragraph style for the document. + """ + style_id = self._p.style + style = self.part.get_style(style_id, WD_STYLE_TYPE.PARAGRAPH) + return cast(ParagraphStyle, style) + + @style.setter + def style(self, style_or_name: str | ParagraphStyle | None): + style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.PARAGRAPH) + self._p.style = style_id + + @property + def text(self) -> str: + """The textual content of this paragraph. + + The text includes the visible-text portion of any hyperlinks in the paragraph. + Tabs and line breaks in the XML are mapped to ``\\t`` and ``\\n`` characters + respectively. + + Assigning text to this property causes all existing paragraph content to be + replaced with a single run containing the assigned text. A ``\\t`` character in + the text is mapped to a ```` element and each ``\\n`` or ``\\r`` + character is mapped to a line break. Paragraph-level formatting, such as style, + is preserved. All run-level formatting, such as bold or italic, is removed. + """ + return self._p.text + + @text.setter + def text(self, text: str | None): + self.clear() + self.add_run(text) + + def _insert_paragraph_before(self): + """Return a newly created paragraph, inserted directly before this paragraph.""" + p = self._p.add_p_before() + return Paragraph(p, self._parent) diff --git a/docx/text/parfmt.py b/src/docx/text/parfmt.py similarity index 52% rename from docx/text/parfmt.py rename to src/docx/text/parfmt.py index 37206729c..ea374373b 100644 --- a/docx/text/parfmt.py +++ b/src/docx/text/parfmt.py @@ -1,33 +1,21 @@ -# encoding: utf-8 +"""Paragraph-related proxy types.""" -""" -Paragraph-related proxy types. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) - -from ..enum.text import WD_LINE_SPACING -from ..shared import ElementProxy, Emu, lazyproperty, Length, Pt, Twips -from .tabstops import TabStops +from docx.enum.text import WD_LINE_SPACING +from docx.shared import ElementProxy, Emu, Length, Pt, Twips, lazyproperty +from docx.text.tabstops import TabStops class ParagraphFormat(ElementProxy): - """ - Provides access to paragraph formatting such as justification, - indentation, line spacing, space before and after, and widow/orphan - control. - """ - - __slots__ = ('_tab_stops',) + """Provides access to paragraph formatting such as justification, indentation, line + spacing, space before and after, and widow/orphan control.""" @property def alignment(self): - """ - A member of the :ref:`WdParagraphAlignment` enumeration specifying - the justification setting for this paragraph. A value of |None| - indicates paragraph alignment is inherited from the style hierarchy. + """A member of the :ref:`WdParagraphAlignment` enumeration specifying the + justification setting for this paragraph. + + A value of |None| indicates paragraph alignment is inherited from the style + hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -41,12 +29,12 @@ def alignment(self, value): @property def first_line_indent(self): - """ - |Length| value specifying the relative difference in indentation for - the first line of the paragraph. A positive value causes the first - line to be indented. A negative value produces a hanging indent. - |None| indicates first line indentation is inherited from the style - hierarchy. + """|Length| value specifying the relative difference in indentation for the + first line of the paragraph. + + A positive value causes the first line to be indented. A negative value produces + a hanging indent. |None| indicates first line indentation is inherited from the + style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -60,10 +48,10 @@ def first_line_indent(self, value): @property def keep_together(self): - """ - |True| if the paragraph should be kept "in one piece" and not broken - across a page boundary when the document is rendered. |None| - indicates its effective value is inherited from the style hierarchy. + """|True| if the paragraph should be kept "in one piece" and not broken across a + page boundary when the document is rendered. + + |None| indicates its effective value is inherited from the style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -76,12 +64,12 @@ def keep_together(self, value): @property def keep_with_next(self): - """ - |True| if the paragraph should be kept on the same page as the - subsequent paragraph when the document is rendered. For example, this - property could be used to keep a section heading on the same page as - its first paragraph. |None| indicates its effective value is - inherited from the style hierarchy. + """|True| if the paragraph should be kept on the same page as the subsequent + paragraph when the document is rendered. + + For example, this property could be used to keep a section heading on the same + page as its first paragraph. |None| indicates its effective value is inherited + from the style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -94,11 +82,12 @@ def keep_with_next(self, value): @property def left_indent(self): - """ - |Length| value specifying the space between the left margin and the - left side of the paragraph. |None| indicates the left indent value is - inherited from the style hierarchy. Use an |Inches| value object as - a convenient way to apply indentation in units of inches. + """|Length| value specifying the space between the left margin and the left side + of the paragraph. + + |None| indicates the left indent value is inherited from the style hierarchy. + Use an |Inches| value object as a convenient way to apply indentation in units + of inches. """ pPr = self._element.pPr if pPr is None: @@ -112,15 +101,15 @@ def left_indent(self, value): @property def line_spacing(self): - """ - |float| or |Length| value specifying the space between baselines in - successive lines of the paragraph. A value of |None| indicates line - spacing is inherited from the style hierarchy. A float value, e.g. - ``2.0`` or ``1.75``, indicates spacing is applied in multiples of - line heights. A |Length| value such as ``Pt(12)`` indicates spacing - is a fixed height. The |Pt| value class is a convenient way to apply - line spacing in units of points. Assigning |None| resets line spacing - to inherit from the style hierarchy. + """|float| or |Length| value specifying the space between baselines in + successive lines of the paragraph. + + A value of |None| indicates line spacing is inherited from the style hierarchy. + A float value, e.g. ``2.0`` or ``1.75``, indicates spacing is applied in + multiples of line heights. A |Length| value such as ``Pt(12)`` indicates spacing + is a fixed height. The |Pt| value class is a convenient way to apply line + spacing in units of points. Assigning |None| resets line spacing to inherit from + the style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -143,19 +132,17 @@ def line_spacing(self, value): @property def line_spacing_rule(self): - """ - A member of the :ref:`WdLineSpacing` enumeration indicating how the - value of :attr:`line_spacing` should be interpreted. Assigning any of - the :ref:`WdLineSpacing` members :attr:`SINGLE`, :attr:`DOUBLE`, or - :attr:`ONE_POINT_FIVE` will cause the value of :attr:`line_spacing` - to be updated to produce the corresponding line spacing. + """A member of the :ref:`WdLineSpacing` enumeration indicating how the value of + :attr:`line_spacing` should be interpreted. + + Assigning any of the :ref:`WdLineSpacing` members :attr:`SINGLE`, + :attr:`DOUBLE`, or :attr:`ONE_POINT_FIVE` will cause the value of + :attr:`line_spacing` to be updated to produce the corresponding line spacing. """ pPr = self._element.pPr if pPr is None: return None - return self._line_spacing_rule( - pPr.spacing_line, pPr.spacing_lineRule - ) + return self._line_spacing_rule(pPr.spacing_line, pPr.spacing_lineRule) @line_spacing_rule.setter def line_spacing_rule(self, value): @@ -174,10 +161,10 @@ def line_spacing_rule(self, value): @property def page_break_before(self): - """ - |True| if the paragraph should appear at the top of the page - following the prior paragraph. |None| indicates its effective value - is inherited from the style hierarchy. + """|True| if the paragraph should appear at the top of the page following the + prior paragraph. + + |None| indicates its effective value is inherited from the style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -190,11 +177,12 @@ def page_break_before(self, value): @property def right_indent(self): - """ - |Length| value specifying the space between the right margin and the - right side of the paragraph. |None| indicates the right indent value - is inherited from the style hierarchy. Use a |Cm| value object as - a convenient way to apply indentation in units of centimeters. + """|Length| value specifying the space between the right margin and the right + side of the paragraph. + + |None| indicates the right indent value is inherited from the style hierarchy. + Use a |Cm| value object as a convenient way to apply indentation in units of + centimeters. """ pPr = self._element.pPr if pPr is None: @@ -208,13 +196,12 @@ def right_indent(self, value): @property def space_after(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the subsequent paragraph. |None| indicates this value - is inherited from the style hierarchy. |Length| objects provide - convenience properties, such as :attr:`~.Length.pt` and - :attr:`~.Length.inches`, that allow easy conversion to various length - units. + """|Length| value specifying the spacing to appear between this paragraph and + the subsequent paragraph. + + |None| indicates this value is inherited from the style hierarchy. |Length| + objects provide convenience properties, such as :attr:`~.Length.pt` and + :attr:`~.Length.inches`, that allow easy conversion to various length units. """ pPr = self._element.pPr if pPr is None: @@ -227,13 +214,12 @@ def space_after(self, value): @property def space_before(self): - """ - |Length| value specifying the spacing to appear between this - paragraph and the prior paragraph. |None| indicates this value is - inherited from the style hierarchy. |Length| objects provide - convenience properties, such as :attr:`~.Length.pt` and - :attr:`~.Length.cm`, that allow easy conversion to various length - units. + """|Length| value specifying the spacing to appear between this paragraph and + the prior paragraph. + + |None| indicates this value is inherited from the style hierarchy. |Length| + objects provide convenience properties, such as :attr:`~.Length.pt` and + :attr:`~.Length.cm`, that allow easy conversion to various length units. """ pPr = self._element.pPr if pPr is None: @@ -246,20 +232,17 @@ def space_before(self, value): @lazyproperty def tab_stops(self): - """ - |TabStops| object providing access to the tab stops defined for this - paragraph format. - """ + """|TabStops| object providing access to the tab stops defined for this + paragraph format.""" pPr = self._element.get_or_add_pPr() return TabStops(pPr) @property def widow_control(self): - """ - |True| if the first and last lines in the paragraph remain on the - same page as the rest of the paragraph when Word repaginates the - document. |None| indicates its effective value is inherited from the - style hierarchy. + """|True| if the first and last lines in the paragraph remain on the same page + as the rest of the paragraph when Word repaginates the document. + + |None| indicates its effective value is inherited from the style hierarchy. """ pPr = self._element.pPr if pPr is None: @@ -272,12 +255,12 @@ def widow_control(self, value): @staticmethod def _line_spacing(spacing_line, spacing_lineRule): - """ - Return the line spacing value calculated from the combination of - *spacing_line* and *spacing_lineRule*. Returns a |float| number of - lines when *spacing_lineRule* is ``WD_LINE_SPACING.MULTIPLE``, - otherwise a |Length| object of absolute line height is returned. - Returns |None| when *spacing_line* is |None|. + """Return the line spacing value calculated from the combination of + `spacing_line` and `spacing_lineRule`. + + Returns a |float| number of lines when `spacing_lineRule` is + ``WD_LINE_SPACING.MULTIPLE``, otherwise a |Length| object of absolute line + height is returned. Returns |None| when `spacing_line` is |None|. """ if spacing_line is None: return None @@ -287,11 +270,11 @@ def _line_spacing(spacing_line, spacing_lineRule): @staticmethod def _line_spacing_rule(line, lineRule): - """ - Return the line spacing rule value calculated from the combination of - *line* and *lineRule*. Returns special members of the - :ref:`WdLineSpacing` enumeration when line spacing is single, double, - or 1.5 lines. + """Return the line spacing rule value calculated from the combination of `line` + and `lineRule`. + + Returns special members of the :ref:`WdLineSpacing` enumeration when line + spacing is single, double, or 1.5 lines. """ if lineRule == WD_LINE_SPACING.MULTIPLE: if line == Twips(240): diff --git a/src/docx/text/run.py b/src/docx/text/run.py new file mode 100644 index 000000000..ec0e6c757 --- /dev/null +++ b/src/docx/text/run.py @@ -0,0 +1,248 @@ +"""Run-related proxy objects for python-docx, Run in particular.""" + +from __future__ import annotations + +from typing import IO, TYPE_CHECKING, Iterator, cast + +from docx.drawing import Drawing +from docx.enum.style import WD_STYLE_TYPE +from docx.enum.text import WD_BREAK +from docx.oxml.drawing import CT_Drawing +from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak +from docx.shape import InlineShape +from docx.shared import StoryChild +from docx.styles.style import CharacterStyle +from docx.text.font import Font +from docx.text.pagebreak import RenderedPageBreak + +if TYPE_CHECKING: + from docx.enum.text import WD_UNDERLINE + from docx.oxml.text.run import CT_R, CT_Text + from docx.shared import Length + + +class Run(StoryChild): + """Proxy object wrapping `` element. + + Several of the properties on Run take a tri-state value, |True|, |False|, or |None|. + |True| and |False| correspond to on and off respectively. |None| indicates the + property is not specified directly on the run and its effective value is taken from + the style hierarchy. + """ + + def __init__(self, r: CT_R, parent: StoryChild): + super().__init__(parent) + self._r = self._element = self.element = r + + def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): + """Add a break element of `break_type` to this run. + + `break_type` can take the values `WD_BREAK.LINE`, `WD_BREAK.PAGE`, and + `WD_BREAK.COLUMN` where `WD_BREAK` is imported from `docx.enum.text`. + `break_type` defaults to `WD_BREAK.LINE`. + """ + type_, clear = { + WD_BREAK.LINE: (None, None), + WD_BREAK.PAGE: ("page", None), + WD_BREAK.COLUMN: ("column", None), + WD_BREAK.LINE_CLEAR_LEFT: ("textWrapping", "left"), + WD_BREAK.LINE_CLEAR_RIGHT: ("textWrapping", "right"), + WD_BREAK.LINE_CLEAR_ALL: ("textWrapping", "all"), + }[break_type] + br = self._r.add_br() + if type_ is not None: + br.type = type_ + if clear is not None: + br.clear = clear + + def add_picture( + self, + image_path_or_stream: str | IO[bytes], + width: Length | None = None, + height: Length | None = None, + ) -> InlineShape: + """Return |InlineShape| containing image identified by `image_path_or_stream`. + + The picture is added to the end of this run. + + `image_path_or_stream` can be a path (a string) or a file-like object containing + a binary image. + + If neither width nor height is specified, the picture appears at + its native size. If only one is specified, it is used to compute a scaling + factor that is then applied to the unspecified dimension, preserving the aspect + ratio of the image. The native size of the picture is calculated using the dots- + per-inch (dpi) value specified in the image file, defaulting to 72 dpi if no + value is specified, as is often the case. + """ + inline = self.part.new_pic_inline(image_path_or_stream, width, height) + self._r.add_drawing(inline) + return InlineShape(inline) + + def add_tab(self) -> None: + """Add a ```` element at the end of the run, which Word interprets as a + tab character.""" + self._r.add_tab() + + def add_text(self, text: str): + """Returns a newly appended |_Text| object (corresponding to a new ```` + child element) to the run, containing `text`. + + Compare with the possibly more friendly approach of assigning text to the + :attr:`Run.text` property. + """ + t = self._r.add_t(text) + return _Text(t) + + @property + def bold(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text of the run to appear in bold face. When |False|, + the text unconditionally appears non-bold. When |None| the bold setting for this + run is inherited from the style hierarchy. + """ + return self.font.bold + + @bold.setter + def bold(self, value: bool | None): + self.font.bold = value + + def clear(self): + """Return reference to this run after removing all its content. + + All run formatting is preserved. + """ + self._r.clear_content() + return self + + @property + def contains_page_break(self) -> bool: + """`True` when one or more rendered page-breaks occur in this run. + + Note that "hard" page-breaks inserted by the author are not included. A hard + page-break gives rise to a rendered page-break in the right position so if those + were included that page-break would be "double-counted". + + It would be very rare for multiple rendered page-breaks to occur in a single + run, but it is possible. + """ + return bool(self._r.lastRenderedPageBreaks) + + @property + def font(self) -> Font: + """The |Font| object providing access to the character formatting properties for + this run, such as font name and size.""" + return Font(self._element) + + @property + def italic(self) -> bool | None: + """Read/write tri-state value. + + When |True|, causes the text of the run to appear in italics. When |False|, the + text unconditionally appears non-italic. When |None| the italic setting for this + run is inherited from the style hierarchy. + """ + return self.font.italic + + @italic.setter + def italic(self, value: bool | None): + self.font.italic = value + + def iter_inner_content(self) -> Iterator[str | Drawing | RenderedPageBreak]: + """Generate the content-items in this run in the order they appear. + + NOTE: only content-types currently supported by `python-docx` are generated. In + this version, that is text and rendered page-breaks. Drawing is included but + currently only provides access to its XML element (CT_Drawing) on its + `._drawing` attribute. `Drawing` attributes and methods may be expanded in + future releases. + + There are a number of element-types that can appear inside a run, but most of + those (w:br, w:cr, w:noBreakHyphen, w:t, w:tab) have a clear plain-text + equivalent. Any contiguous range of such elements is generated as a single + `str`. Rendered page-break and drawing elements are generated individually. Any + other elements are ignored. + """ + for item in self._r.inner_content_items: + if isinstance(item, str): + yield item + elif isinstance(item, CT_LastRenderedPageBreak): + yield RenderedPageBreak(item, self) + elif isinstance( # pyright: ignore[reportUnnecessaryIsInstance] + item, CT_Drawing + ): + yield Drawing(item, self) + + @property + def style(self) -> CharacterStyle: + """Read/write. + + A |CharacterStyle| object representing the character style applied to this run. + The default character style for the document (often `Default Character Font`) is + returned if the run has no directly-applied character style. Setting this + property to |None| removes any directly-applied character style. + """ + style_id = self._r.style + return cast( + CharacterStyle, self.part.get_style(style_id, WD_STYLE_TYPE.CHARACTER) + ) + + @style.setter + def style(self, style_or_name: str | CharacterStyle | None): + style_id = self.part.get_style_id(style_or_name, WD_STYLE_TYPE.CHARACTER) + self._r.style = style_id + + @property + def text(self) -> str: + """String formed by concatenating the text equivalent of each run. + + Each `` element adds the text characters it contains. A `` element + adds a `\\t` character. A `` or `` element each add a `\\n` + character. Note that a `` element can indicate a page break or column + break as well as a line break. Only line-break `` elements translate to + a `\\n` character. Others are ignored. All other content child elements, such as + ``, are ignored. + + Assigning text to this property has the reverse effect, translating each `\\t` + character to a `` element and each `\\n` or `\\r` character to a + `` element. Any existing run content is replaced. Run formatting is + preserved. + """ + return self._r.text + + @text.setter + def text(self, text: str): + self._r.text = text + + @property + def underline(self) -> bool | WD_UNDERLINE | None: + """The underline style for this |Run|. + + Value is one of |None|, |True|, |False|, or a member of :ref:`WdUnderline`. + + A value of |None| indicates the run has no directly-applied underline value and + so will inherit the underline value of its containing paragraph. Assigning + |None| to this property removes any directly-applied underline value. + + A value of |False| indicates a directly-applied setting of no underline, + overriding any inherited value. + + A value of |True| indicates single underline. + + The values from :ref:`WdUnderline` are used to specify other outline styles such + as double, wavy, and dotted. + """ + return self.font.underline + + @underline.setter + def underline(self, value: bool): + self.font.underline = value + + +class _Text: + """Proxy object wrapping `` element.""" + + def __init__(self, t_elm: CT_Text): + super(_Text, self).__init__() + self._t = t_elm diff --git a/src/docx/text/tabstops.py b/src/docx/text/tabstops.py new file mode 100644 index 000000000..824085d2b --- /dev/null +++ b/src/docx/text/tabstops.py @@ -0,0 +1,125 @@ +"""Tabstop-related proxy types.""" + +from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER +from docx.shared import ElementProxy + + +class TabStops(ElementProxy): + """A sequence of |TabStop| objects providing access to the tab stops of a paragraph + or paragraph style. + + Supports iteration, indexed access, del, and len(). It is accesed using the + :attr:`~.ParagraphFormat.tab_stops` property of ParagraphFormat; it is not intended + to be constructed directly. + """ + + def __init__(self, element): + super(TabStops, self).__init__(element, None) + self._pPr = element + + def __delitem__(self, idx): + """Remove the tab at offset `idx` in this sequence.""" + tabs = self._pPr.tabs + try: + tabs.remove(tabs[idx]) + except (AttributeError, IndexError): + raise IndexError("tab index out of range") + + if len(tabs) == 0: + self._pPr.remove(tabs) + + def __getitem__(self, idx): + """Enables list-style access by index.""" + tabs = self._pPr.tabs + if tabs is None: + raise IndexError("TabStops object is empty") + tab = tabs.tab_lst[idx] + return TabStop(tab) + + def __iter__(self): + """Generate a TabStop object for each of the w:tab elements, in XML document + order.""" + tabs = self._pPr.tabs + if tabs is not None: + for tab in tabs.tab_lst: + yield TabStop(tab) + + def __len__(self): + tabs = self._pPr.tabs + if tabs is None: + return 0 + return len(tabs.tab_lst) + + def add_tab_stop( + self, position, alignment=WD_TAB_ALIGNMENT.LEFT, leader=WD_TAB_LEADER.SPACES + ): + """Add a new tab stop at `position`, a |Length| object specifying the location + of the tab stop relative to the paragraph edge. + + A negative `position` value is valid and appears in hanging indentation. Tab + alignment defaults to left, but may be specified by passing a member of the + :ref:`WdTabAlignment` enumeration as `alignment`. An optional leader character + can be specified by passing a member of the :ref:`WdTabLeader` enumeration as + `leader`. + """ + tabs = self._pPr.get_or_add_tabs() + tab = tabs.insert_tab_in_order(position, alignment, leader) + return TabStop(tab) + + def clear_all(self): + """Remove all custom tab stops.""" + self._pPr._remove_tabs() + + +class TabStop(ElementProxy): + """An individual tab stop applying to a paragraph or style. + + Accessed using list semantics on its containing |TabStops| object. + """ + + def __init__(self, element): + super(TabStop, self).__init__(element, None) + self._tab = element + + @property + def alignment(self): + """A member of :ref:`WdTabAlignment` specifying the alignment setting for this + tab stop. + + Read/write. + """ + return self._tab.val + + @alignment.setter + def alignment(self, value): + self._tab.val = value + + @property + def leader(self): + """A member of :ref:`WdTabLeader` specifying a repeating character used as a + "leader", filling in the space spanned by this tab. + + Assigning |None| produces the same result as assigning `WD_TAB_LEADER.SPACES`. + Read/write. + """ + return self._tab.leader + + @leader.setter + def leader(self, value): + self._tab.leader = value + + @property + def position(self): + """A |Length| object representing the distance of this tab stop from the inside + edge of the paragraph. + + May be positive or negative. Read/write. + """ + return self._tab.pos + + @position.setter + def position(self, value): + tab = self._tab + tabs = tab.getparent() + self._tab = tabs.insert_tab_in_order(value, tab.val, tab.leader) + tabs.remove(tab) diff --git a/src/docx/types.py b/src/docx/types.py new file mode 100644 index 000000000..6097f740c --- /dev/null +++ b/src/docx/types.py @@ -0,0 +1,19 @@ +"""Abstract types used by `python-docx`.""" + +from __future__ import annotations + +from typing_extensions import Protocol + +from docx.parts.story import StoryPart + + +class StoryChild(Protocol): + """An object that can fulfill the `parent` role in a `Parented` class. + + This type is for objects that have a story part like document or header as their + root part. + """ + + @property + def part(self) -> StoryPart: + ... diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..6503efb3b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +"""pytest fixtures that are shared across test modules.""" + +import pytest + +from docx import types as t +from docx.parts.story import StoryPart + + +@pytest.fixture +def fake_parent() -> t.StoryChild: + class StoryChild: + @property + def part(self) -> StoryPart: + raise NotImplementedError + + return StoryChild() diff --git a/tests/dml/test_color.py b/tests/dml/test_color.py index 32091daba..ea848e7d6 100644 --- a/tests/dml/test_color.py +++ b/tests/dml/test_color.py @@ -1,24 +1,15 @@ -# encoding: utf-8 +"""Test suite for docx.dml.color module.""" -""" -Test suite for docx.dml.color module. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +import pytest -from docx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR from docx.dml.color import ColorFormat +from docx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR from docx.shared import RGBColor from ..unitutil.cxml import element, xml -import pytest - - -class DescribeColorFormat(object): +class DescribeColorFormat: def it_knows_its_color_type(self, type_fixture): color_format, expected_value = type_fixture assert color_format.type == expected_value @@ -43,86 +34,109 @@ def it_can_change_its_theme_color(self, theme_color_set_fixture): # fixtures --------------------------------------------- - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:color{w:val=auto}', None), - ('w:r/w:rPr/w:color{w:val=4224FF}', '4224ff'), - ('w:r/w:rPr/w:color{w:val=auto,w:themeColor=accent1}', None), - ('w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}', 'f00ba9'), - ]) + @pytest.fixture( + params=[ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:color{w:val=auto}", None), + ("w:r/w:rPr/w:color{w:val=4224FF}", "4224ff"), + ("w:r/w:rPr/w:color{w:val=auto,w:themeColor=accent1}", None), + ("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}", "f00ba9"), + ] + ) def rgb_get_fixture(self, request): r_cxml, rgb = request.param color_format = ColorFormat(element(r_cxml)) expected_value = None if rgb is None else RGBColor.from_string(rgb) return color_format, expected_value - @pytest.fixture(params=[ - ('w:r', RGBColor(10, 20, 30), 'w:r/w:rPr/w:color{w:val=0A141E}'), - ('w:r/w:rPr', RGBColor(1, 2, 3), 'w:r/w:rPr/w:color{w:val=010203}'), - ('w:r/w:rPr/w:color{w:val=123abc}', RGBColor(42, 24, 99), - 'w:r/w:rPr/w:color{w:val=2A1863}'), - ('w:r/w:rPr/w:color{w:val=auto}', RGBColor(16, 17, 18), - 'w:r/w:rPr/w:color{w:val=101112}'), - ('w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}', - RGBColor(24, 42, 99), 'w:r/w:rPr/w:color{w:val=182A63}'), - ('w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}', - None, 'w:r/w:rPr'), - ('w:r', None, 'w:r'), - ]) + @pytest.fixture( + params=[ + ("w:r", RGBColor(10, 20, 30), "w:r/w:rPr/w:color{w:val=0A141E}"), + ("w:r/w:rPr", RGBColor(1, 2, 3), "w:r/w:rPr/w:color{w:val=010203}"), + ( + "w:r/w:rPr/w:color{w:val=123abc}", + RGBColor(42, 24, 99), + "w:r/w:rPr/w:color{w:val=2A1863}", + ), + ( + "w:r/w:rPr/w:color{w:val=auto}", + RGBColor(16, 17, 18), + "w:r/w:rPr/w:color{w:val=101112}", + ), + ( + "w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}", + RGBColor(24, 42, 99), + "w:r/w:rPr/w:color{w:val=182A63}", + ), + ("w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}", None, "w:r/w:rPr"), + ("w:r", None, "w:r"), + ] + ) def rgb_set_fixture(self, request): r_cxml, new_value, expected_cxml = request.param color_format = ColorFormat(element(r_cxml)) expected_xml = xml(expected_cxml) return color_format, new_value, expected_xml - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:color{w:val=auto}', None), - ('w:r/w:rPr/w:color{w:val=4224FF}', None), - ('w:r/w:rPr/w:color{w:themeColor=accent1}', 'ACCENT_1'), - ('w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=dark1}', 'DARK_1'), - ]) + @pytest.fixture( + params=[ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:color{w:val=auto}", None), + ("w:r/w:rPr/w:color{w:val=4224FF}", None), + ("w:r/w:rPr/w:color{w:themeColor=accent1}", "ACCENT_1"), + ("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=dark1}", "DARK_1"), + ] + ) def theme_color_get_fixture(self, request): r_cxml, value = request.param color_format = ColorFormat(element(r_cxml)) - expected_value = ( - None if value is None else getattr(MSO_THEME_COLOR, value) - ) + expected_value = None if value is None else getattr(MSO_THEME_COLOR, value) return color_format, expected_value - @pytest.fixture(params=[ - ('w:r', 'ACCENT_1', - 'w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}'), - ('w:r/w:rPr', 'ACCENT_2', - 'w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent2}'), - ('w:r/w:rPr/w:color{w:val=101112}', 'ACCENT_3', - 'w:r/w:rPr/w:color{w:val=101112,w:themeColor=accent3}'), - ('w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}', 'LIGHT_2', - 'w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=light2}'), - ('w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}', None, - 'w:r/w:rPr'), - ('w:r', None, 'w:r'), - ]) + @pytest.fixture( + params=[ + ("w:r", "ACCENT_1", "w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}"), + ( + "w:r/w:rPr", + "ACCENT_2", + "w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent2}", + ), + ( + "w:r/w:rPr/w:color{w:val=101112}", + "ACCENT_3", + "w:r/w:rPr/w:color{w:val=101112,w:themeColor=accent3}", + ), + ( + "w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}", + "LIGHT_2", + "w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=light2}", + ), + ("w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}", None, "w:r/w:rPr"), + ("w:r", None, "w:r"), + ] + ) def theme_color_set_fixture(self, request): r_cxml, member, expected_cxml = request.param color_format = ColorFormat(element(r_cxml)) - new_value = ( - None if member is None else getattr(MSO_THEME_COLOR, member) - ) + new_value = None if member is None else getattr(MSO_THEME_COLOR, member) expected_xml = xml(expected_cxml) return color_format, new_value, expected_xml - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:color{w:val=auto}', MSO_COLOR_TYPE.AUTO), - ('w:r/w:rPr/w:color{w:val=4224FF}', MSO_COLOR_TYPE.RGB), - ('w:r/w:rPr/w:color{w:themeColor=dark1}', MSO_COLOR_TYPE.THEME), - ('w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}', - MSO_COLOR_TYPE.THEME), - ]) + @pytest.fixture( + params=[ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:color{w:val=auto}", MSO_COLOR_TYPE.AUTO), + ("w:r/w:rPr/w:color{w:val=4224FF}", MSO_COLOR_TYPE.RGB), + ("w:r/w:rPr/w:color{w:themeColor=dark1}", MSO_COLOR_TYPE.THEME), + ( + "w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}", + MSO_COLOR_TYPE.THEME, + ), + ] + ) def type_fixture(self, request): r_cxml, expected_value = request.param color_format = ColorFormat(element(r_cxml)) diff --git a/tests/image/test_bmp.py b/tests/image/test_bmp.py index 37961369a..15b322b66 100644 --- a/tests/image/test_bmp.py +++ b/tests/image/test_bmp.py @@ -1,29 +1,23 @@ -# encoding: utf-8 +"""Test suite for docx.image.bmp module.""" -""" -Test suite for docx.image.bmp module -""" - -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO -from docx.image.constants import MIME_TYPE from docx.image.bmp import Bmp +from docx.image.constants import MIME_TYPE from ..unitutil.mock import ANY, initializer_mock -class DescribeBmp(object): - +class DescribeBmp: def it_can_construct_from_a_bmp_stream(self, Bmp__init__): cx, cy, horz_dpi, vert_dpi = 26, 43, 200, 96 bytes_ = ( - b'fillerfillerfiller\x1A\x00\x00\x00\x2B\x00\x00\x00' - b'fillerfiller\xB8\x1E\x00\x00\x00\x00\x00\x00' + b"fillerfillerfiller\x1A\x00\x00\x00\x2B\x00\x00\x00" + b"fillerfiller\xB8\x1E\x00\x00\x00\x00\x00\x00" ) - stream = BytesIO(bytes_) + stream = io.BytesIO(bytes_) bmp = Bmp.from_stream(stream) @@ -36,7 +30,7 @@ def it_knows_its_content_type(self): def it_knows_its_default_ext(self): bmp = Bmp(None, None, None, None) - assert bmp.default_ext == 'bmp' + assert bmp.default_ext == "bmp" # fixtures ------------------------------------------------------- diff --git a/tests/image/test_gif.py b/tests/image/test_gif.py index 707419f5c..a533da04d 100644 --- a/tests/image/test_gif.py +++ b/tests/image/test_gif.py @@ -1,24 +1,20 @@ -# encoding: utf-8 +"""Unit test suite for docx.image.gif module.""" -"""Unit test suite for docx.image.gif module""" - -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO from docx.image.constants import MIME_TYPE from docx.image.gif import Gif from ..unitutil.mock import ANY, initializer_mock -class DescribeGif(object): - +class DescribeGif: def it_can_construct_from_a_gif_stream(self, Gif__init__): cx, cy = 42, 24 - bytes_ = b'filler\x2A\x00\x18\x00' - stream = BytesIO(bytes_) + bytes_ = b"filler\x2A\x00\x18\x00" + stream = io.BytesIO(bytes_) gif = Gif.from_stream(stream) @@ -31,7 +27,7 @@ def it_knows_its_content_type(self): def it_knows_its_default_ext(self): gif = Gif(None, None, None, None) - assert gif.default_ext == 'gif' + assert gif.default_ext == "gif" # fixture components --------------------------------------------- diff --git a/tests/image/test_helpers.py b/tests/image/test_helpers.py index 6091f4883..9192564dc 100644 --- a/tests/image/test_helpers.py +++ b/tests/image/test_helpers.py @@ -1,25 +1,18 @@ -# encoding: utf-8 +"""Test suite for docx.image.helpers module.""" -""" -Test suite for docx.image.helpers module -""" - -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO from docx.image.exceptions import UnexpectedEndOfFileError from docx.image.helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader -class DescribeStreamReader(object): - - def it_can_read_a_string_of_specified_len_at_offset( - self, read_str_fixture): +class DescribeStreamReader: + def it_can_read_a_string_of_specified_len_at_offset(self, read_str_fixture): stream_rdr, expected_string = read_str_fixture s = stream_rdr.read_str(6, 2) - assert s == 'foobar' + assert s == "foobar" def it_raises_on_unexpected_EOF(self, read_str_fixture): stream_rdr = read_str_fixture[0] @@ -33,19 +26,21 @@ def it_can_read_a_long(self, read_long_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (BIG_ENDIAN, b'\xBE\x00\x00\x00\x2A\xEF', 1, 42), - (LITTLE_ENDIAN, b'\xBE\xEF\x2A\x00\x00\x00', 2, 42), - ]) + @pytest.fixture( + params=[ + (BIG_ENDIAN, b"\xBE\x00\x00\x00\x2A\xEF", 1, 42), + (LITTLE_ENDIAN, b"\xBE\xEF\x2A\x00\x00\x00", 2, 42), + ] + ) def read_long_fixture(self, request): byte_order, bytes_, offset, expected_int = request.param - stream = BytesIO(bytes_) + stream = io.BytesIO(bytes_) stream_rdr = StreamReader(stream, byte_order) return stream_rdr, offset, expected_int @pytest.fixture def read_str_fixture(self): - stream = BytesIO(b'\x01\x02foobar\x03\x04') + stream = io.BytesIO(b"\x01\x02foobar\x03\x04") stream_rdr = StreamReader(stream, BIG_ENDIAN) - expected_string = 'foobar' + expected_string = "foobar" return stream_rdr, expected_string diff --git a/tests/image/test_image.py b/tests/image/test_image.py index 07f9d0666..bd5ed0903 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -1,12 +1,9 @@ -# encoding: utf-8 - """Unit test suite for docx.image package""" -from __future__ import absolute_import, print_function, unicode_literals +import io import pytest -from docx.compat import BytesIO from docx.image.bmp import Bmp from docx.image.exceptions import UnrecognizedImageError from docx.image.gif import Gif @@ -29,8 +26,7 @@ ) -class DescribeImage(object): - +class DescribeImage: def it_can_construct_from_an_image_blob( self, blob_, BytesIO_, _from_stream_, stream_, image_ ): @@ -41,9 +37,7 @@ def it_can_construct_from_an_image_blob( assert image is image_ def it_can_construct_from_an_image_path(self, from_path_fixture): - image_path, _from_stream_, stream_, blob, filename, image_ = ( - from_path_fixture - ) + image_path, _from_stream_, stream_, blob, filename, image_ = from_path_fixture image = Image.from_file(image_path) _from_stream_.assert_called_once_with(stream_, blob, filename) assert image is image_ @@ -66,7 +60,7 @@ def it_can_construct_from_an_image_stream(self, from_stream_fixture): assert isinstance(image, Image) def it_provides_access_to_the_image_blob(self): - blob = b'foobar' + blob = b"foobar" image = Image(blob, None, None) assert image.blob == blob @@ -103,25 +97,23 @@ def it_can_scale_its_dimensions(self, scale_fixture): assert isinstance(scaled_height, Length) def it_knows_the_image_filename(self): - filename = 'foobar.png' + filename = "foobar.png" image = Image(None, filename, None) assert image.filename == filename def it_knows_the_image_filename_extension(self): - image = Image(None, 'foobar.png', None) - assert image.ext == 'png' + image = Image(None, "foobar.png", None) + assert image.ext == "png" def it_knows_the_sha1_of_its_image(self): - blob = b'fO0Bar' + blob = b"fO0Bar" image = Image(blob, None, None) - assert image.sha1 == '4921e7002ddfba690a937d54bda226a7b8bdeb68' + assert image.sha1 == "4921e7002ddfba690a937d54bda226a7b8bdeb68" def it_correctly_characterizes_known_images(self, known_image_fixture): image_path, characteristics = known_image_fixture - ext, content_type, px_width, px_height, horz_dpi, vert_dpi = ( - characteristics - ) - with open(test_file(image_path), 'rb') as stream: + ext, content_type, px_width, px_height, horz_dpi, vert_dpi = characteristics + with open(test_file(image_path), "rb") as stream: image = Image.from_file(stream) assert image.content_type == content_type assert image.ext == ext @@ -134,7 +126,7 @@ def it_correctly_characterizes_known_images(self, known_image_fixture): @pytest.fixture def content_type_fixture(self, image_header_): - content_type = 'image/foobar' + content_type = "image/foobar" image_header_.content_type = content_type return image_header_, content_type @@ -154,54 +146,61 @@ def dpi_fixture(self, image_header_): @pytest.fixture def from_filelike_fixture(self, _from_stream_, image_): - image_path = test_file('python-icon.png') - with open(image_path, 'rb') as f: + image_path = test_file("python-icon.png") + with open(image_path, "rb") as f: blob = f.read() - image_stream = BytesIO(blob) + image_stream = io.BytesIO(blob) return image_stream, _from_stream_, blob, image_ @pytest.fixture def from_path_fixture(self, _from_stream_, BytesIO_, stream_, image_): - filename = 'python-icon.png' + filename = "python-icon.png" image_path = test_file(filename) - with open(image_path, 'rb') as f: + with open(image_path, "rb") as f: blob = f.read() return image_path, _from_stream_, stream_, blob, filename, image_ - @pytest.fixture(params=['foobar.png', None]) + @pytest.fixture(params=["foobar.png", None]) def from_stream_fixture( - self, request, stream_, blob_, _ImageHeaderFactory_, - image_header_, Image__init_): + self, request, stream_, blob_, _ImageHeaderFactory_, image_header_, Image__init_ + ): filename_in = request.param - filename_out = 'image.png' if filename_in is None else filename_in + filename_out = "image.png" if filename_in is None else filename_in return ( - stream_, blob_, filename_in, _ImageHeaderFactory_, image_header_, - Image__init_, filename_out + stream_, + blob_, + filename_in, + _ImageHeaderFactory_, + image_header_, + Image__init_, + filename_out, ) @pytest.fixture(params=[0, 1, 2, 3, 4, 5, 6, 7, 8]) def known_image_fixture(self, request): cases = ( - ('python.bmp', ('bmp', CT.BMP, 211, 71, 96, 96)), - ('sonic.gif', ('gif', CT.GIF, 290, 360, 72, 72)), - ('python-icon.jpeg', ('jpg', CT.JPEG, 204, 204, 72, 72)), - ('300-dpi.jpg', ('jpg', CT.JPEG, 1504, 1936, 300, 300)), - ('monty-truth.png', ('png', CT.PNG, 150, 214, 72, 72)), - ('150-dpi.png', ('png', CT.PNG, 901, 1350, 150, 150)), - ('300-dpi.png', ('png', CT.PNG, 860, 579, 300, 300)), - ('72-dpi.tiff', ('tiff', CT.TIFF, 48, 48, 72, 72)), - ('300-dpi.TIF', ('tiff', CT.TIFF, 2464, 3248, 300, 300)), + ("python.bmp", ("bmp", CT.BMP, 211, 71, 96, 96)), + ("sonic.gif", ("gif", CT.GIF, 290, 360, 72, 72)), + ("python-icon.jpeg", ("jpg", CT.JPEG, 204, 204, 72, 72)), + ("300-dpi.jpg", ("jpg", CT.JPEG, 1504, 1936, 300, 300)), + ("monty-truth.png", ("png", CT.PNG, 150, 214, 72, 72)), + ("150-dpi.png", ("png", CT.PNG, 901, 1350, 150, 150)), + ("300-dpi.png", ("png", CT.PNG, 860, 579, 300, 300)), + ("72-dpi.tiff", ("tiff", CT.TIFF, 48, 48, 72, 72)), + ("300-dpi.TIF", ("tiff", CT.TIFF, 2464, 3248, 300, 300)), # ('CVS_LOGO.WMF', ('wmf', CT.X_WMF, 149, 59, 72, 72)), ) image_filename, characteristics = cases[request.param] return image_filename, characteristics - @pytest.fixture(params=[ - (None, None, 1000, 2000), - (100, None, 100, 200), - (None, 500, 250, 500), - (1500, 1500, 1500, 1500), - ]) + @pytest.fixture( + params=[ + (None, None, 1000, 2000), + (100, None, 100, 200), + (None, 500, 250, 500), + (1500, 1500, 1500, 1500), + ] + ) def scale_fixture(self, request, width_prop_, height_prop_): width, height, scaled_width, scaled_height = request.param width_prop_.return_value = Emu(1000) @@ -224,9 +223,7 @@ def blob_(self, request): @pytest.fixture def BytesIO_(self, request, stream_): - return class_mock( - request, 'docx.image.image.BytesIO', return_value=stream_ - ) + return class_mock(request, "docx.image.image.io.BytesIO", return_value=stream_) @pytest.fixture def filename_(self, request): @@ -235,12 +232,12 @@ def filename_(self, request): @pytest.fixture def _from_stream_(self, request, image_): return method_mock( - request, Image, '_from_stream', autospec=False, return_value=image_ + request, Image, "_from_stream", autospec=False, return_value=image_ ) @pytest.fixture def height_prop_(self, request): - return property_mock(request, Image, 'height') + return property_mock(request, Image, "height") @pytest.fixture def image_(self, request): @@ -249,13 +246,12 @@ def image_(self, request): @pytest.fixture def _ImageHeaderFactory_(self, request, image_header_): return function_mock( - request, 'docx.image.image._ImageHeaderFactory', - return_value=image_header_ + request, "docx.image.image._ImageHeaderFactory", return_value=image_header_ ) @pytest.fixture def image_header_(self, request): - return instance_mock(request, BaseImageHeader, default_ext='png') + return instance_mock(request, BaseImageHeader, default_ext="png") @pytest.fixture def Image__init_(self, request): @@ -263,49 +259,48 @@ def Image__init_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) @pytest.fixture def width_prop_(self, request): - return property_mock(request, Image, 'width') + return property_mock(request, Image, "width") -class Describe_ImageHeaderFactory(object): - - def it_constructs_the_right_class_for_a_given_image_stream( - self, call_fixture): +class Describe_ImageHeaderFactory: + def it_constructs_the_right_class_for_a_given_image_stream(self, call_fixture): stream, expected_class = call_fixture image_header = _ImageHeaderFactory(stream) assert isinstance(image_header, expected_class) def it_raises_on_unrecognized_image_stream(self): - stream = BytesIO(b'foobar 666 not an image stream') + stream = io.BytesIO(b"foobar 666 not an image stream") with pytest.raises(UnrecognizedImageError): _ImageHeaderFactory(stream) # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('python-icon.png', Png), - ('python-icon.jpeg', Jfif), - ('exif-420-dpi.jpg', Exif), - ('sonic.gif', Gif), - ('72-dpi.tiff', Tiff), - ('little-endian.tif', Tiff), - ('python.bmp', Bmp), - ]) + @pytest.fixture( + params=[ + ("python-icon.png", Png), + ("python-icon.jpeg", Jfif), + ("exif-420-dpi.jpg", Exif), + ("sonic.gif", Gif), + ("72-dpi.tiff", Tiff), + ("little-endian.tif", Tiff), + ("python.bmp", Bmp), + ] + ) def call_fixture(self, request): image_filename, expected_class = request.param image_path = test_file(image_filename) - with open(image_path, 'rb') as f: + with open(image_path, "rb") as f: blob = f.read() - image_stream = BytesIO(blob) + image_stream = io.BytesIO(blob) image_stream.seek(666) return image_stream, expected_class -class DescribeBaseImageHeader(object): - +class DescribeBaseImageHeader: def it_defines_content_type_as_an_abstract_property(self): base_image_header = BaseImageHeader(None, None, None, None) with pytest.raises(NotImplementedError): diff --git a/tests/image/test_jpeg.py b/tests/image/test_jpeg.py index cbbc869bb..a558e1d4e 100644 --- a/tests/image/test_jpeg.py +++ b/tests/image/test_jpeg.py @@ -1,21 +1,18 @@ -# encoding: utf-8 - """Unit test suite for docx.image.jpeg module""" -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE from docx.image.helpers import BIG_ENDIAN, StreamReader from docx.image.jpeg import ( - _App0Marker, - _App1Marker, Exif, Jfif, - _JfifMarkers, Jpeg, + _App0Marker, + _App1Marker, + _JfifMarkers, _Marker, _MarkerFactory, _MarkerFinder, @@ -34,23 +31,19 @@ ) -class DescribeJpeg(object): - +class DescribeJpeg: def it_knows_its_content_type(self): jpeg = Jpeg(None, None, None, None) assert jpeg.content_type == MIME_TYPE.JPEG def it_knows_its_default_ext(self): jpeg = Jpeg(None, None, None, None) - assert jpeg.default_ext == 'jpg' - - class DescribeExif(object): + assert jpeg.default_ext == "jpg" + class DescribeExif: def it_can_construct_from_an_exif_stream(self, from_exif_fixture): # fixture ---------------------- - stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = ( - from_exif_fixture - ) + stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = from_exif_fixture # exercise --------------------- exif = Exif.from_stream(stream_) # verify ----------------------- @@ -61,12 +54,9 @@ def it_can_construct_from_an_exif_stream(self, from_exif_fixture): assert exif.horz_dpi == horz_dpi assert exif.vert_dpi == vert_dpi - class DescribeJfif(object): - + class DescribeJfif: def it_can_construct_from_a_jfif_stream(self, from_jfif_fixture): - stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = ( - from_jfif_fixture - ) + stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = from_jfif_fixture jfif = Jfif.from_stream(stream_) _JfifMarkers_.from_stream.assert_called_once_with(stream_) assert isinstance(jfif, Jfif) @@ -85,9 +75,7 @@ def from_exif_fixture(self, stream_, _JfifMarkers_, jfif_markers_): jfif_markers_.sof.px_height = px_height jfif_markers_.app1.horz_dpi = horz_dpi jfif_markers_.app1.vert_dpi = vert_dpi - return ( - stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi - ) + return (stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi) @pytest.fixture def from_jfif_fixture(self, stream_, _JfifMarkers_, jfif_markers_): @@ -97,13 +85,11 @@ def from_jfif_fixture(self, stream_, _JfifMarkers_, jfif_markers_): jfif_markers_.sof.px_height = px_height jfif_markers_.app0.horz_dpi = horz_dpi jfif_markers_.app0.vert_dpi = vert_dpi - return ( - stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi - ) + return (stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi) @pytest.fixture def _JfifMarkers_(self, request, jfif_markers_): - _JfifMarkers_ = class_mock(request, 'docx.image.jpeg._JfifMarkers') + _JfifMarkers_ = class_mock(request, "docx.image.jpeg._JfifMarkers") _JfifMarkers_.from_stream.return_value = jfif_markers_ return _JfifMarkers_ @@ -113,13 +99,12 @@ def jfif_markers_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) -class Describe_JfifMarkers(object): - +class Describe_JfifMarkers: def it_can_construct_from_a_jfif_stream( - self, stream_, _MarkerParser_, _JfifMarkers__init_, soi_, app0_, sof_, sos_ + self, stream_, _MarkerParser_, _JfifMarkers__init_, soi_, app0_, sof_, sos_ ): marker_lst = [soi_, app0_, sof_, sos_] @@ -163,15 +148,11 @@ def it_raises_if_it_cant_find_the_SOF_marker(self, no_sof_fixture): @pytest.fixture def app0_(self, request): - return instance_mock( - request, _App0Marker, marker_code=JPEG_MARKER_CODE.APP0 - ) + return instance_mock(request, _App0Marker, marker_code=JPEG_MARKER_CODE.APP0) @pytest.fixture def app1_(self, request): - return instance_mock( - request, _App1Marker, marker_code=JPEG_MARKER_CODE.APP1 - ) + return instance_mock(request, _App1Marker, marker_code=JPEG_MARKER_CODE.APP1) @pytest.fixture def app0_fixture(self, soi_, app0_, eoi_): @@ -187,9 +168,7 @@ def app1_fixture(self, soi_, app1_, eoi_): @pytest.fixture def eoi_(self, request): - return instance_mock( - request, _SofMarker, marker_code=JPEG_MARKER_CODE.EOI - ) + return instance_mock(request, _SofMarker, marker_code=JPEG_MARKER_CODE.EOI) @pytest.fixture def _JfifMarkers__init_(self, request): @@ -203,7 +182,7 @@ def marker_parser_(self, request, markers_all_): @pytest.fixture def _MarkerParser_(self, request, marker_parser_): - _MarkerParser_ = class_mock(request, 'docx.image.jpeg._MarkerParser') + _MarkerParser_ = class_mock(request, "docx.image.jpeg._MarkerParser") _MarkerParser_.from_stream.return_value = marker_parser_ return _MarkerParser_ @@ -228,9 +207,7 @@ def no_sof_fixture(self, soi_, eoi_): @pytest.fixture def sof_(self, request): - return instance_mock( - request, _SofMarker, marker_code=JPEG_MARKER_CODE.SOF0 - ) + return instance_mock(request, _SofMarker, marker_code=JPEG_MARKER_CODE.SOF0) @pytest.fixture def sof_fixture(self, soi_, sof_, eoi_): @@ -240,23 +217,18 @@ def sof_fixture(self, soi_, sof_, eoi_): @pytest.fixture def soi_(self, request): - return instance_mock( - request, _Marker, marker_code=JPEG_MARKER_CODE.SOI - ) + return instance_mock(request, _Marker, marker_code=JPEG_MARKER_CODE.SOI) @pytest.fixture def sos_(self, request): - return instance_mock( - request, _Marker, marker_code=JPEG_MARKER_CODE.SOS - ) + return instance_mock(request, _Marker, marker_code=JPEG_MARKER_CODE.SOS) @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) -class Describe_Marker(object): - +class Describe_Marker: def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture): stream, marker_code, offset, _Marker__init_, length = from_stream_fixture @@ -267,14 +239,16 @@ def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (JPEG_MARKER_CODE.SOI, 2, 0), - (JPEG_MARKER_CODE.APP0, 4, 16), - ]) + @pytest.fixture( + params=[ + (JPEG_MARKER_CODE.SOI, 2, 0), + (JPEG_MARKER_CODE.APP0, 4, 16), + ] + ) def from_stream_fixture(self, request, _Marker__init_): marker_code, offset, length = request.param - bytes_ = b'\xFF\xD8\xFF\xE0\x00\x10' - stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"\xFF\xD8\xFF\xE0\x00\x10" + stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) return stream_reader, marker_code, offset, _Marker__init_, length @pytest.fixture @@ -282,13 +256,12 @@ def _Marker__init_(self, request): return initializer_mock(request, _Marker) -class Describe_App0Marker(object): - +class Describe_App0Marker: def it_can_construct_from_a_stream_and_offset(self, _App0Marker__init_): - bytes_ = b'\x00\x10JFIF\x00\x01\x01\x01\x00\x2A\x00\x18' + bytes_ = b"\x00\x10JFIF\x00\x01\x01\x01\x00\x2A\x00\x18" marker_code, offset, length = JPEG_MARKER_CODE.APP0, 0, 16 density_units, x_density, y_density = 1, 42, 24 - stream = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) app0_marker = _App0Marker.from_stream(stream, marker_code, offset) @@ -299,9 +272,7 @@ def it_can_construct_from_a_stream_and_offset(self, _App0Marker__init_): def it_knows_the_image_dpi(self, dpi_fixture): density_units, x_density, y_density, horz_dpi, vert_dpi = dpi_fixture - app0 = _App0Marker( - None, None, None, density_units, x_density, y_density - ) + app0 = _App0Marker(None, None, None, density_units, x_density, y_density) assert app0.horz_dpi == horz_dpi assert app0.vert_dpi == vert_dpi @@ -311,27 +282,26 @@ def it_knows_the_image_dpi(self, dpi_fixture): def _App0Marker__init_(self, request): return initializer_mock(request, _App0Marker) - @pytest.fixture(params=[ - (0, 100, 200, 72, 72), - (1, 100, 200, 100, 200), - (2, 100, 200, 254, 508), - ]) + @pytest.fixture( + params=[ + (0, 100, 200, 72, 72), + (1, 100, 200, 100, 200), + (2, 100, 200, 254, 508), + ] + ) def dpi_fixture(self, request): - density_units, x_density, y_density, horz_dpi, vert_dpi = ( - request.param - ) + density_units, x_density, y_density, horz_dpi, vert_dpi = request.param return density_units, x_density, y_density, horz_dpi, vert_dpi -class Describe_App1Marker(object): - +class Describe_App1Marker: def it_can_construct_from_a_stream_and_offset( self, _App1Marker__init_, _tiff_from_exif_segment_ ): - bytes_ = b'\x00\x42Exif\x00\x00' + bytes_ = b"\x00\x42Exif\x00\x00" marker_code, offset, length = JPEG_MARKER_CODE.APP1, 0, 66 horz_dpi, vert_dpi = 42, 24 - stream = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) app1_marker = _App1Marker.from_stream(stream, marker_code, offset) @@ -342,9 +312,9 @@ def it_can_construct_from_a_stream_and_offset( assert isinstance(app1_marker, _App1Marker) def it_can_construct_from_non_Exif_APP1_segment(self, _App1Marker__init_): - bytes_ = b'\x00\x42Foobar' + bytes_ = b"\x00\x42Foobar" marker_code, offset, length = JPEG_MARKER_CODE.APP1, 0, 66 - stream = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) app1_marker = _App1Marker.from_stream(stream, marker_code, offset) @@ -353,8 +323,7 @@ def it_can_construct_from_non_Exif_APP1_segment(self, _App1Marker__init_): ) assert isinstance(app1_marker, _App1Marker) - def it_gets_a_tiff_from_its_Exif_segment_to_help_construct( - self, get_tiff_fixture): + def it_gets_a_tiff_from_its_Exif_segment_to_help_construct(self, get_tiff_fixture): stream, offset, length = get_tiff_fixture[:3] BytesIO_, segment_bytes, substream_ = get_tiff_fixture[3:6] Tiff_, tiff_ = get_tiff_fixture[6:] @@ -376,28 +345,31 @@ def _App1Marker__init_(self, request): return initializer_mock(request, _App1Marker) @pytest.fixture - def BytesIO_(self, request, substream_): - return class_mock( - request, 'docx.image.jpeg.BytesIO', return_value=substream_ + def get_tiff_fixture(self, request, substream_, Tiff_, tiff_): + bytes_ = b"xfillerxMM\x00*\x00\x00\x00\x42" + stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) + BytesIO_ = class_mock( + request, "docx.image.jpeg.io.BytesIO", return_value=substream_ ) - - @pytest.fixture - def get_tiff_fixture(self, request, BytesIO_, substream_, Tiff_, tiff_): - bytes_ = b'xfillerxMM\x00*\x00\x00\x00\x42' - stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) offset, segment_length, segment_bytes = 0, 16, bytes_[8:] return ( - stream_reader, offset, segment_length, BytesIO_, segment_bytes, - substream_, Tiff_, tiff_ + stream_reader, + offset, + segment_length, + BytesIO_, + segment_bytes, + substream_, + Tiff_, + tiff_, ) @pytest.fixture def substream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) @pytest.fixture def Tiff_(self, request, tiff_): - Tiff_ = class_mock(request, 'docx.image.jpeg.Tiff') + Tiff_ = class_mock(request, "docx.image.jpeg.Tiff") Tiff_.from_stream.return_value = tiff_ return Tiff_ @@ -408,18 +380,20 @@ def tiff_(self, request): @pytest.fixture def _tiff_from_exif_segment_(self, request, tiff_): return method_mock( - request, _App1Marker, '_tiff_from_exif_segment', autospec=False, - return_value=tiff_ + request, + _App1Marker, + "_tiff_from_exif_segment", + autospec=False, + return_value=tiff_, ) -class Describe_SofMarker(object): - +class Describe_SofMarker: def it_can_construct_from_a_stream_and_offset(self, request, _SofMarker__init_): - bytes_ = b'\x00\x11\x00\x00\x2A\x00\x18' + bytes_ = b"\x00\x11\x00\x00\x2A\x00\x18" marker_code, offset, length = JPEG_MARKER_CODE.SOF0, 0, 17 px_width, px_height = 24, 42 - stream = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) sof_marker = _SofMarker.from_stream(stream, marker_code, offset) @@ -440,28 +414,34 @@ def _SofMarker__init_(self, request): return initializer_mock(request, _SofMarker) -class Describe_MarkerFactory(object): - +class Describe_MarkerFactory: def it_constructs_the_appropriate_marker_object(self, call_fixture): marker_code, stream_, offset_, marker_cls_ = call_fixture marker = _MarkerFactory(marker_code, stream_, offset_) - marker_cls_.from_stream.assert_called_once_with( - stream_, marker_code, offset_ - ) + marker_cls_.from_stream.assert_called_once_with(stream_, marker_code, offset_) assert marker is marker_cls_.from_stream.return_value # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - JPEG_MARKER_CODE.APP0, - JPEG_MARKER_CODE.APP1, - JPEG_MARKER_CODE.SOF0, - JPEG_MARKER_CODE.SOF7, - JPEG_MARKER_CODE.SOS, - ]) + @pytest.fixture( + params=[ + JPEG_MARKER_CODE.APP0, + JPEG_MARKER_CODE.APP1, + JPEG_MARKER_CODE.SOF0, + JPEG_MARKER_CODE.SOF7, + JPEG_MARKER_CODE.SOS, + ] + ) def call_fixture( - self, request, stream_, offset_, _App0Marker_, _App1Marker_, - _SofMarker_, _Marker_): + self, + request, + stream_, + offset_, + _App0Marker_, + _App1Marker_, + _SofMarker_, + _Marker_, + ): marker_code = request.param if marker_code == JPEG_MARKER_CODE.APP0: marker_cls_ = _App0Marker_ @@ -475,15 +455,15 @@ def call_fixture( @pytest.fixture def _App0Marker_(self, request): - return class_mock(request, 'docx.image.jpeg._App0Marker') + return class_mock(request, "docx.image.jpeg._App0Marker") @pytest.fixture def _App1Marker_(self, request): - return class_mock(request, 'docx.image.jpeg._App1Marker') + return class_mock(request, "docx.image.jpeg._App1Marker") @pytest.fixture def _Marker_(self, request): - return class_mock(request, 'docx.image.jpeg._Marker') + return class_mock(request, "docx.image.jpeg._Marker") @pytest.fixture def offset_(self, request): @@ -491,15 +471,14 @@ def offset_(self, request): @pytest.fixture def _SofMarker_(self, request): - return class_mock(request, 'docx.image.jpeg._SofMarker') + return class_mock(request, "docx.image.jpeg._SofMarker") @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) -class Describe_MarkerFinder(object): - +class Describe_MarkerFinder: def it_can_construct_from_a_stream(self, stream_, _MarkerFinder__init_): marker_finder = _MarkerFinder.from_stream(stream_) @@ -517,30 +496,31 @@ def it_can_find_the_next_marker_after_a_given_offset(self, next_fixture): def _MarkerFinder__init_(self, request): return initializer_mock(request, _MarkerFinder) - @pytest.fixture(params=[ - (0, JPEG_MARKER_CODE.SOI, 2), - (1, JPEG_MARKER_CODE.APP0, 4), - (2, JPEG_MARKER_CODE.APP0, 4), - (3, JPEG_MARKER_CODE.EOI, 12), - (4, JPEG_MARKER_CODE.EOI, 12), - (6, JPEG_MARKER_CODE.EOI, 12), - (8, JPEG_MARKER_CODE.EOI, 12), - ]) + @pytest.fixture( + params=[ + (0, JPEG_MARKER_CODE.SOI, 2), + (1, JPEG_MARKER_CODE.APP0, 4), + (2, JPEG_MARKER_CODE.APP0, 4), + (3, JPEG_MARKER_CODE.EOI, 12), + (4, JPEG_MARKER_CODE.EOI, 12), + (6, JPEG_MARKER_CODE.EOI, 12), + (8, JPEG_MARKER_CODE.EOI, 12), + ] + ) def next_fixture(self, request): start, marker_code, segment_offset = request.param - bytes_ = b'\xFF\xD8\xFF\xE0\x00\x01\xFF\x00\xFF\xFF\xFF\xD9' - stream_reader = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"\xFF\xD8\xFF\xE0\x00\x01\xFF\x00\xFF\xFF\xFF\xD9" + stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) marker_finder = _MarkerFinder(stream_reader) expected_code_and_offset = (marker_code, segment_offset) return marker_finder, start, expected_code_and_offset @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) - + return instance_mock(request, io.BytesIO) -class Describe_MarkerParser(object): +class Describe_MarkerParser: def it_can_construct_from_a_jfif_stream( self, stream_, StreamReader_, _MarkerParser__init_, stream_reader_ ): @@ -550,16 +530,20 @@ def it_can_construct_from_a_jfif_stream( _MarkerParser__init_.assert_called_once_with(ANY, stream_reader_) assert isinstance(marker_parser, _MarkerParser) - def it_can_iterate_over_the_jfif_markers_in_its_stream( - self, iter_markers_fixture): - (marker_parser, stream_, _MarkerFinder_, marker_finder_, - _MarkerFactory_, marker_codes, offsets, - marker_lst) = iter_markers_fixture - markers = [marker for marker in marker_parser.iter_markers()] + def it_can_iterate_over_the_jfif_markers_in_its_stream(self, iter_markers_fixture): + ( + marker_parser, + stream_, + _MarkerFinder_, + marker_finder_, + _MarkerFactory_, + marker_codes, + offsets, + marker_lst, + ) = iter_markers_fixture + markers = list(marker_parser.iter_markers()) _MarkerFinder_.from_stream.assert_called_once_with(stream_) - assert marker_finder_.next.call_args_list == [ - call(0), call(2), call(20) - ] + assert marker_finder_.next.call_args_list == [call(0), call(2), call(20)] assert _MarkerFactory_.call_args_list == [ call(marker_codes[0], stream_, offsets[0]), call(marker_codes[1], stream_, offsets[1]), @@ -579,34 +563,48 @@ def eoi_(self, request): @pytest.fixture def iter_markers_fixture( - self, stream_reader_, _MarkerFinder_, marker_finder_, - _MarkerFactory_, soi_, app0_, eoi_): + self, + stream_reader_, + _MarkerFinder_, + marker_finder_, + _MarkerFactory_, + soi_, + app0_, + eoi_, + ): marker_parser = _MarkerParser(stream_reader_) offsets = [2, 4, 22] marker_lst = [soi_, app0_, eoi_] marker_finder_.next.side_effect = [ - (JPEG_MARKER_CODE.SOI, offsets[0]), + (JPEG_MARKER_CODE.SOI, offsets[0]), (JPEG_MARKER_CODE.APP0, offsets[1]), - (JPEG_MARKER_CODE.EOI, offsets[2]), + (JPEG_MARKER_CODE.EOI, offsets[2]), ] marker_codes = [ - JPEG_MARKER_CODE.SOI, JPEG_MARKER_CODE.APP0, JPEG_MARKER_CODE.EOI + JPEG_MARKER_CODE.SOI, + JPEG_MARKER_CODE.APP0, + JPEG_MARKER_CODE.EOI, ] return ( - marker_parser, stream_reader_, _MarkerFinder_, marker_finder_, - _MarkerFactory_, marker_codes, offsets, marker_lst + marker_parser, + stream_reader_, + _MarkerFinder_, + marker_finder_, + _MarkerFactory_, + marker_codes, + offsets, + marker_lst, ) @pytest.fixture def _MarkerFactory_(self, request, soi_, app0_, eoi_): return class_mock( - request, 'docx.image.jpeg._MarkerFactory', - side_effect=[soi_, app0_, eoi_] + request, "docx.image.jpeg._MarkerFactory", side_effect=[soi_, app0_, eoi_] ) @pytest.fixture def _MarkerFinder_(self, request, marker_finder_): - _MarkerFinder_ = class_mock(request, 'docx.image.jpeg._MarkerFinder') + _MarkerFinder_ = class_mock(request, "docx.image.jpeg._MarkerFinder") _MarkerFinder_.from_stream.return_value = marker_finder_ return _MarkerFinder_ @@ -624,13 +622,12 @@ def soi_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) @pytest.fixture def StreamReader_(self, request, stream_reader_): return class_mock( - request, 'docx.image.jpeg.StreamReader', - return_value=stream_reader_ + request, "docx.image.jpeg.StreamReader", return_value=stream_reader_ ) @pytest.fixture diff --git a/tests/image/test_png.py b/tests/image/test_png.py index 21ed66fa3..61e7fdbed 100644 --- a/tests/image/test_png.py +++ b/tests/image/test_png.py @@ -1,18 +1,21 @@ -# encoding: utf-8 +"""Unit test suite for docx.image.png module.""" -"""Unit test suite for docx.image.png module""" - -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO from docx.image.constants import MIME_TYPE, PNG_CHUNK_TYPE from docx.image.exceptions import InvalidImageStreamError from docx.image.helpers import BIG_ENDIAN, StreamReader from docx.image.png import ( - _Chunk, _Chunks, _ChunkFactory, _ChunkParser, _IHDRChunk, _pHYsChunk, - Png, _PngParser + Png, + _Chunk, + _ChunkFactory, + _ChunkParser, + _Chunks, + _IHDRChunk, + _pHYsChunk, + _PngParser, ) from ..unitutil.mock import ( @@ -26,10 +29,9 @@ ) -class DescribePng(object): - +class DescribePng: def it_can_construct_from_a_png_stream( - self, stream_, _PngParser_, png_parser_, Png__init__ + self, stream_, _PngParser_, png_parser_, Png__init__ ): px_width, px_height, horz_dpi, vert_dpi = 42, 24, 36, 63 png_parser_.px_width = px_width @@ -51,7 +53,7 @@ def it_knows_its_content_type(self): def it_knows_its_default_ext(self): png = Png(None, None, None, None) - assert png.default_ext == 'png' + assert png.default_ext == "png" # fixtures ------------------------------------------------------- @@ -61,7 +63,7 @@ def Png__init__(self, request): @pytest.fixture def _PngParser_(self, request, png_parser_): - _PngParser_ = class_mock(request, 'docx.image.png._PngParser') + _PngParser_ = class_mock(request, "docx.image.png._PngParser") _PngParser_.parse.return_value = png_parser_ return _PngParser_ @@ -71,11 +73,10 @@ def png_parser_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) -class Describe_PngParser(object): - +class Describe_PngParser: def it_can_parse_the_headers_of_a_PNG_stream( self, stream_, _Chunks_, _PngParser__init_, chunks_ ): @@ -104,7 +105,7 @@ def it_defaults_image_dpi_to_72(self, no_dpi_fixture): @pytest.fixture def _Chunks_(self, request, chunks_): - _Chunks_ = class_mock(request, 'docx.image.png._Chunks') + _Chunks_ = class_mock(request, "docx.image.png._Chunks") _Chunks_.from_stream.return_value = chunks_ return _Chunks_ @@ -130,9 +131,7 @@ def dpi_fixture(self, chunks_): png_parser = _PngParser(chunks_) return png_parser, horz_dpi, vert_dpi - @pytest.fixture(params=[ - (-1, -1), (0, 1000), (None, 1000), (1, 0), (1, None) - ]) + @pytest.fixture(params=[(-1, -1), (0, 1000), (None, 1000), (1, 0), (1, None)]) def no_dpi_fixture(self, request, chunks_): """ Scenarios are: 1) no pHYs chunk in PNG stream, 2) units specifier @@ -154,11 +153,10 @@ def _PngParser__init_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) - + return instance_mock(request, io.BytesIO) -class Describe_Chunks(object): +class Describe_Chunks: def it_can_construct_from_a_stream( self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_ ): @@ -174,7 +172,7 @@ def it_can_construct_from_a_stream( def it_provides_access_to_the_IHDR_chunk(self, IHDR_fixture): chunks, IHDR_chunk_ = IHDR_fixture - assert chunks.IHDR == IHDR_chunk_ + assert IHDR_chunk_ == chunks.IHDR def it_provides_access_to_the_pHYs_chunk(self, pHYs_fixture): chunks, expected_chunk = pHYs_fixture @@ -189,7 +187,7 @@ def it_raises_if_theres_no_IHDR_chunk(self, no_IHDR_fixture): @pytest.fixture def _ChunkParser_(self, request, chunk_parser_): - _ChunkParser_ = class_mock(request, 'docx.image.png._ChunkParser') + _ChunkParser_ = class_mock(request, "docx.image.png._ChunkParser") _ChunkParser_.from_stream.return_value = chunk_parser_ return _ChunkParser_ @@ -209,9 +207,7 @@ def IHDR_fixture(self, IHDR_chunk_, pHYs_chunk_): @pytest.fixture def IHDR_chunk_(self, request): - return instance_mock( - request, _IHDRChunk, type_name=PNG_CHUNK_TYPE.IHDR - ) + return instance_mock(request, _IHDRChunk, type_name=PNG_CHUNK_TYPE.IHDR) @pytest.fixture def no_IHDR_fixture(self, pHYs_chunk_): @@ -221,9 +217,7 @@ def no_IHDR_fixture(self, pHYs_chunk_): @pytest.fixture def pHYs_chunk_(self, request): - return instance_mock( - request, _pHYsChunk, type_name=PNG_CHUNK_TYPE.pHYs - ) + return instance_mock(request, _pHYsChunk, type_name=PNG_CHUNK_TYPE.pHYs) @pytest.fixture(params=[True, False]) def pHYs_fixture(self, request, IHDR_chunk_, pHYs_chunk_): @@ -237,11 +231,10 @@ def pHYs_fixture(self, request, IHDR_chunk_, pHYs_chunk_): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) -class Describe_ChunkParser(object): - +class Describe_ChunkParser: def it_can_construct_from_a_stream( self, stream_, StreamReader_, stream_rdr_, _ChunkParser__init_ ): @@ -258,7 +251,7 @@ def it_can_iterate_over_the_chunks_in_its_png_stream( chunk_lst = [chunk_, chunk_2_] chunk_parser = _ChunkParser(stream_rdr_) - chunks = [chunk for chunk in chunk_parser.iter_chunks()] + chunks = list(chunk_parser.iter_chunks()) _iter_chunk_offsets_.assert_called_once_with(chunk_parser) assert _ChunkFactory_.call_args_list == [ @@ -267,10 +260,9 @@ def it_can_iterate_over_the_chunks_in_its_png_stream( ] assert chunks == chunk_lst - def it_iterates_over_the_chunk_offsets_to_help_parse( - self, iter_offsets_fixture): + def it_iterates_over_the_chunk_offsets_to_help_parse(self, iter_offsets_fixture): chunk_parser, expected_chunk_offsets = iter_offsets_fixture - chunk_offsets = [co for co in chunk_parser._iter_chunk_offsets()] + chunk_offsets = list(chunk_parser._iter_chunk_offsets()) assert chunk_offsets == expected_chunk_offsets # fixtures ------------------------------------------------------- @@ -286,8 +278,7 @@ def chunk_2_(self, request): @pytest.fixture def _ChunkFactory_(self, request, chunk_lst_): return function_mock( - request, 'docx.image.png._ChunkFactory', - side_effect=chunk_lst_ + request, "docx.image.png._ChunkFactory", side_effect=chunk_lst_ ) @pytest.fixture @@ -305,14 +296,16 @@ def _iter_chunk_offsets_(self, request): (PNG_CHUNK_TYPE.pHYs, 4), ) return method_mock( - request, _ChunkParser, '_iter_chunk_offsets', - return_value=iter(chunk_offsets) + request, + _ChunkParser, + "_iter_chunk_offsets", + return_value=iter(chunk_offsets), ) @pytest.fixture def iter_offsets_fixture(self): - bytes_ = b'-filler-\x00\x00\x00\x00IHDRxxxx\x00\x00\x00\x00IEND' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"-filler-\x00\x00\x00\x00IHDRxxxx\x00\x00\x00\x00IEND" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) chunk_parser = _ChunkParser(stream_rdr) expected_chunk_offsets = [ (PNG_CHUNK_TYPE.IHDR, 16), @@ -323,37 +316,35 @@ def iter_offsets_fixture(self): @pytest.fixture def StreamReader_(self, request, stream_rdr_): return class_mock( - request, 'docx.image.png.StreamReader', return_value=stream_rdr_ + request, "docx.image.png.StreamReader", return_value=stream_rdr_ ) @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) @pytest.fixture def stream_rdr_(self, request): return instance_mock(request, StreamReader) -class Describe_ChunkFactory(object): - +class Describe_ChunkFactory: def it_constructs_the_appropriate_Chunk_subclass(self, call_fixture): chunk_type, stream_rdr_, offset, chunk_cls_ = call_fixture chunk = _ChunkFactory(chunk_type, stream_rdr_, offset) - chunk_cls_.from_offset.assert_called_once_with( - chunk_type, stream_rdr_, offset - ) + chunk_cls_.from_offset.assert_called_once_with(chunk_type, stream_rdr_, offset) assert isinstance(chunk, _Chunk) # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - PNG_CHUNK_TYPE.IHDR, - PNG_CHUNK_TYPE.pHYs, - PNG_CHUNK_TYPE.IEND, - ]) - def call_fixture( - self, request, _IHDRChunk_, _pHYsChunk_, _Chunk_, stream_rdr_): + @pytest.fixture( + params=[ + PNG_CHUNK_TYPE.IHDR, + PNG_CHUNK_TYPE.pHYs, + PNG_CHUNK_TYPE.IEND, + ] + ) + def call_fixture(self, request, _IHDRChunk_, _pHYsChunk_, _Chunk_, stream_rdr_): chunk_type = request.param chunk_cls_ = { PNG_CHUNK_TYPE.IHDR: _IHDRChunk_, @@ -365,7 +356,7 @@ def call_fixture( @pytest.fixture def _Chunk_(self, request, chunk_): - _Chunk_ = class_mock(request, 'docx.image.png._Chunk') + _Chunk_ = class_mock(request, "docx.image.png._Chunk") _Chunk_.from_offset.return_value = chunk_ return _Chunk_ @@ -375,7 +366,7 @@ def chunk_(self, request): @pytest.fixture def _IHDRChunk_(self, request, ihdr_chunk_): - _IHDRChunk_ = class_mock(request, 'docx.image.png._IHDRChunk') + _IHDRChunk_ = class_mock(request, "docx.image.png._IHDRChunk") _IHDRChunk_.from_offset.return_value = ihdr_chunk_ return _IHDRChunk_ @@ -385,7 +376,7 @@ def ihdr_chunk_(self, request): @pytest.fixture def _pHYsChunk_(self, request, phys_chunk_): - _pHYsChunk_ = class_mock(request, 'docx.image.png._pHYsChunk') + _pHYsChunk_ = class_mock(request, "docx.image.png._pHYsChunk") _pHYsChunk_.from_offset.return_value = phys_chunk_ return _pHYsChunk_ @@ -398,17 +389,15 @@ def stream_rdr_(self, request): return instance_mock(request, StreamReader) -class Describe_Chunk(object): - +class Describe_Chunk: def it_can_construct_from_a_stream_and_offset(self): - chunk_type = 'fOOB' + chunk_type = "fOOB" chunk = _Chunk.from_offset(chunk_type, None, None) assert isinstance(chunk, _Chunk) assert chunk.type_name == chunk_type -class Describe_IHDRChunk(object): - +class Describe_IHDRChunk: def it_can_construct_from_a_stream_and_offset(self, from_offset_fixture): stream_rdr, offset, px_width, px_height = from_offset_fixture ihdr_chunk = _IHDRChunk.from_offset(None, stream_rdr, offset) @@ -420,14 +409,13 @@ def it_can_construct_from_a_stream_and_offset(self, from_offset_fixture): @pytest.fixture def from_offset_fixture(self): - bytes_ = b'\x00\x00\x00\x2A\x00\x00\x00\x18' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x18" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) offset, px_width, px_height = 0, 42, 24 return stream_rdr, offset, px_width, px_height -class Describe_pHYsChunk(object): - +class Describe_pHYsChunk: def it_can_construct_from_a_stream_and_offset(self, from_offset_fixture): stream_rdr, offset = from_offset_fixture[:2] horz_px_per_unit, vert_px_per_unit = from_offset_fixture[2:4] @@ -442,12 +430,7 @@ def it_can_construct_from_a_stream_and_offset(self, from_offset_fixture): @pytest.fixture def from_offset_fixture(self): - bytes_ = b'\x00\x00\x00\x2A\x00\x00\x00\x18\x01' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) - offset, horz_px_per_unit, vert_px_per_unit, units_specifier = ( - 0, 42, 24, 1 - ) - return ( - stream_rdr, offset, horz_px_per_unit, vert_px_per_unit, - units_specifier - ) + bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x18\x01" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) + offset, horz_px_per_unit, vert_px_per_unit, units_specifier = (0, 42, 24, 1) + return (stream_rdr, offset, horz_px_per_unit, vert_px_per_unit, units_specifier) diff --git a/tests/image/test_tiff.py b/tests/image/test_tiff.py index 277df5f21..b7f37afe5 100644 --- a/tests/image/test_tiff.py +++ b/tests/image/test_tiff.py @@ -1,15 +1,13 @@ -# encoding: utf-8 - """Unit test suite for docx.image.tiff module""" -from __future__ import absolute_import, print_function +import io import pytest -from docx.compat import BytesIO from docx.image.constants import MIME_TYPE, TIFF_TAG from docx.image.helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader from docx.image.tiff import ( + Tiff, _AsciiIfdEntry, _IfdEntries, _IfdEntry, @@ -18,7 +16,6 @@ _LongIfdEntry, _RationalIfdEntry, _ShortIfdEntry, - Tiff, _TiffParser, ) @@ -34,8 +31,7 @@ ) -class DescribeTiff(object): - +class DescribeTiff: def it_can_construct_from_a_tiff_stream( self, stream_, _TiffParser_, tiff_parser_, Tiff__init_ ): @@ -60,7 +56,7 @@ def it_knows_its_content_type(self): def it_knows_its_default_ext(self): tiff = Tiff(None, None, None, None) - assert tiff.default_ext == 'tiff' + assert tiff.default_ext == "tiff" # fixtures ------------------------------------------------------- @@ -70,7 +66,7 @@ def Tiff__init_(self, request): @pytest.fixture def _TiffParser_(self, request, tiff_parser_): - _TiffParser_ = class_mock(request, 'docx.image.tiff._TiffParser') + _TiffParser_ = class_mock(request, "docx.image.tiff._TiffParser") _TiffParser_.parse.return_value = tiff_parser_ return _TiffParser_ @@ -80,11 +76,10 @@ def tiff_parser_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) - + return instance_mock(request, io.BytesIO) -class Describe_TiffParser(object): +class Describe_TiffParser: def it_can_parse_the_properties_from_a_tiff_stream( self, stream_, @@ -98,9 +93,7 @@ def it_can_parse_the_properties_from_a_tiff_stream( tiff_parser = _TiffParser.parse(stream_) _make_stream_reader_.assert_called_once_with(stream_) - _IfdEntries_.from_stream.assert_called_once_with( - stream_rdr_, ifd0_offset_ - ) + _IfdEntries_.from_stream.assert_called_once_with(stream_rdr_, ifd0_offset_) _TiffParser__init_.assert_called_once_with(ANY, ifd_entries_) assert isinstance(tiff_parser, _TiffParser) @@ -113,7 +106,7 @@ def it_makes_a_stream_reader_to_help_parse(self, mk_stream_rdr_fixture): def it_knows_image_width_and_height_after_parsing(self): px_width, px_height = 42, 24 entries = { - TIFF_TAG.IMAGE_WIDTH: px_width, + TIFF_TAG.IMAGE_WIDTH: px_width, TIFF_TAG.IMAGE_LENGTH: px_height, } ifd_entries = _IfdEntries(entries) @@ -128,13 +121,15 @@ def it_knows_the_horz_and_vert_dpi_after_parsing(self, dpi_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (1, 150, 240, 72, 72), - (2, 42, 24, 42, 24), - (3, 100, 200, 254, 508), - (2, None, None, 72, 72), - (None, 96, 100, 96, 100), - ]) + @pytest.fixture( + params=[ + (1, 150, 240, 72, 72), + (2, 42, 24, 42, 24), + (3, 100, 200, 254, 508), + (2, None, None, 72, 72), + (None, 96, 100, 96, 100), + ] + ) def dpi_fixture(self, request): resolution_unit, x_resolution, y_resolution = request.param[:3] expected_horz_dpi, expected_vert_dpi = request.param[3:] @@ -152,7 +147,7 @@ def dpi_fixture(self, request): @pytest.fixture def _IfdEntries_(self, request, ifd_entries_): - _IfdEntries_ = class_mock(request, 'docx.image.tiff._IfdEntries') + _IfdEntries_ = class_mock(request, "docx.image.tiff._IfdEntries") _IfdEntries_.from_stream.return_value = ifd_entries_ return _IfdEntries_ @@ -169,28 +164,30 @@ def _make_stream_reader_(self, request, stream_rdr_): return method_mock( request, _TiffParser, - '_make_stream_reader', + "_make_stream_reader", autospec=False, - return_value=stream_rdr_ + return_value=stream_rdr_, ) - @pytest.fixture(params=[ - (b'MM\x00*', BIG_ENDIAN), - (b'II*\x00', LITTLE_ENDIAN), - ]) + @pytest.fixture( + params=[ + (b"MM\x00*", BIG_ENDIAN), + (b"II*\x00", LITTLE_ENDIAN), + ] + ) def mk_stream_rdr_fixture(self, request, StreamReader_, stream_rdr_): bytes_, endian = request.param - stream = BytesIO(bytes_) + stream = io.BytesIO(bytes_) return stream, StreamReader_, endian, stream_rdr_ @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) + return instance_mock(request, io.BytesIO) @pytest.fixture def StreamReader_(self, request, stream_rdr_): return class_mock( - request, 'docx.image.tiff.StreamReader', return_value=stream_rdr_ + request, "docx.image.tiff.StreamReader", return_value=stream_rdr_ ) @pytest.fixture @@ -204,8 +201,7 @@ def _TiffParser__init_(self, request): return initializer_mock(request, _TiffParser) -class Describe_IfdEntries(object): - +class Describe_IfdEntries: def it_can_construct_from_a_stream_and_offset( self, stream_, @@ -226,7 +222,7 @@ def it_can_construct_from_a_stream_and_offset( assert isinstance(ifd_entries, _IfdEntries) def it_has_basic_mapping_semantics(self): - key, value = 1, 'foobar' + key, value = 1, "foobar" entries = {key: value} ifd_entries = _IfdEntries(entries) assert key in ifd_entries @@ -249,7 +245,7 @@ def _IfdEntries__init_(self, request): @pytest.fixture def _IfdParser_(self, request, ifd_parser_): return class_mock( - request, 'docx.image.tiff._IfdParser', return_value=ifd_parser_ + request, "docx.image.tiff._IfdParser", return_value=ifd_parser_ ) @pytest.fixture @@ -262,16 +258,19 @@ def offset_(self, request): @pytest.fixture def stream_(self, request): - return instance_mock(request, BytesIO) - - -class Describe_IfdParser(object): - - def it_can_iterate_through_the_directory_entries_in_an_IFD( - self, iter_fixture): - (ifd_parser, _IfdEntryFactory_, stream_rdr, offsets, - expected_entries) = iter_fixture - entries = [e for e in ifd_parser.iter_entries()] + return instance_mock(request, io.BytesIO) + + +class Describe_IfdParser: + def it_can_iterate_through_the_directory_entries_in_an_IFD(self, iter_fixture): + ( + ifd_parser, + _IfdEntryFactory_, + stream_rdr, + offsets, + expected_entries, + ) = iter_fixture + entries = list(ifd_parser.iter_entries()) assert _IfdEntryFactory_.call_args_list == [ call(stream_rdr, offsets[0]), call(stream_rdr, offsets[1]), @@ -291,24 +290,21 @@ def ifd_entry_2_(self, request): @pytest.fixture def _IfdEntryFactory_(self, request, ifd_entry_, ifd_entry_2_): return function_mock( - request, 'docx.image.tiff._IfdEntryFactory', - side_effect=[ifd_entry_, ifd_entry_2_] + request, + "docx.image.tiff._IfdEntryFactory", + side_effect=[ifd_entry_, ifd_entry_2_], ) @pytest.fixture def iter_fixture(self, _IfdEntryFactory_, ifd_entry_, ifd_entry_2_): - stream_rdr = StreamReader(BytesIO(b'\x00\x02'), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(b"\x00\x02"), BIG_ENDIAN) offsets = [2, 14] ifd_parser = _IfdParser(stream_rdr, offset=0) expected_entries = [ifd_entry_, ifd_entry_2_] - return ( - ifd_parser, _IfdEntryFactory_, stream_rdr, offsets, - expected_entries - ) - + return (ifd_parser, _IfdEntryFactory_, stream_rdr, offsets, expected_entries) -class Describe_IfdEntryFactory(object): +class Describe_IfdEntryFactory: def it_constructs_the_right_class_for_a_given_ifd_entry(self, fixture): stream_rdr, offset, entry_cls_, ifd_entry_ = fixture ifd_entry = _IfdEntryFactory(stream_rdr, offset) @@ -317,27 +313,36 @@ def it_constructs_the_right_class_for_a_given_ifd_entry(self, fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (b'\x66\x66\x00\x01', 'BYTE'), - (b'\x66\x66\x00\x02', 'ASCII'), - (b'\x66\x66\x00\x03', 'SHORT'), - (b'\x66\x66\x00\x04', 'LONG'), - (b'\x66\x66\x00\x05', 'RATIONAL'), - (b'\x66\x66\x00\x06', 'CUSTOM'), - ]) + @pytest.fixture( + params=[ + (b"\x66\x66\x00\x01", "BYTE"), + (b"\x66\x66\x00\x02", "ASCII"), + (b"\x66\x66\x00\x03", "SHORT"), + (b"\x66\x66\x00\x04", "LONG"), + (b"\x66\x66\x00\x05", "RATIONAL"), + (b"\x66\x66\x00\x06", "CUSTOM"), + ] + ) def fixture( - self, request, ifd_entry_, _IfdEntry_, _AsciiIfdEntry_, - _ShortIfdEntry_, _LongIfdEntry_, _RationalIfdEntry_): + self, + request, + ifd_entry_, + _IfdEntry_, + _AsciiIfdEntry_, + _ShortIfdEntry_, + _LongIfdEntry_, + _RationalIfdEntry_, + ): bytes_, entry_type = request.param entry_cls_ = { - 'BYTE': _IfdEntry_, - 'ASCII': _AsciiIfdEntry_, - 'SHORT': _ShortIfdEntry_, - 'LONG': _LongIfdEntry_, - 'RATIONAL': _RationalIfdEntry_, - 'CUSTOM': _IfdEntry_, + "BYTE": _IfdEntry_, + "ASCII": _AsciiIfdEntry_, + "SHORT": _ShortIfdEntry_, + "LONG": _LongIfdEntry_, + "RATIONAL": _RationalIfdEntry_, + "CUSTOM": _IfdEntry_, }[entry_type] - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) offset = 0 return stream_rdr, offset, entry_cls_, ifd_entry_ @@ -347,35 +352,31 @@ def ifd_entry_(self, request): @pytest.fixture def _IfdEntry_(self, request, ifd_entry_): - _IfdEntry_ = class_mock(request, 'docx.image.tiff._IfdEntry') + _IfdEntry_ = class_mock(request, "docx.image.tiff._IfdEntry") _IfdEntry_.from_stream.return_value = ifd_entry_ return _IfdEntry_ @pytest.fixture def _AsciiIfdEntry_(self, request, ifd_entry_): - _AsciiIfdEntry_ = class_mock( - request, 'docx.image.tiff._AsciiIfdEntry') + _AsciiIfdEntry_ = class_mock(request, "docx.image.tiff._AsciiIfdEntry") _AsciiIfdEntry_.from_stream.return_value = ifd_entry_ return _AsciiIfdEntry_ @pytest.fixture def _ShortIfdEntry_(self, request, ifd_entry_): - _ShortIfdEntry_ = class_mock( - request, 'docx.image.tiff._ShortIfdEntry') + _ShortIfdEntry_ = class_mock(request, "docx.image.tiff._ShortIfdEntry") _ShortIfdEntry_.from_stream.return_value = ifd_entry_ return _ShortIfdEntry_ @pytest.fixture def _LongIfdEntry_(self, request, ifd_entry_): - _LongIfdEntry_ = class_mock( - request, 'docx.image.tiff._LongIfdEntry') + _LongIfdEntry_ = class_mock(request, "docx.image.tiff._LongIfdEntry") _LongIfdEntry_.from_stream.return_value = ifd_entry_ return _LongIfdEntry_ @pytest.fixture def _RationalIfdEntry_(self, request, ifd_entry_): - _RationalIfdEntry_ = class_mock( - request, 'docx.image.tiff._RationalIfdEntry') + _RationalIfdEntry_ = class_mock(request, "docx.image.tiff._RationalIfdEntry") _RationalIfdEntry_.from_stream.return_value = ifd_entry_ return _RationalIfdEntry_ @@ -384,13 +385,12 @@ def offset_(self, request): return instance_mock(request, int) -class Describe_IfdEntry(object): - +class Describe_IfdEntry: def it_can_construct_from_a_stream_and_offset( self, _parse_value_, _IfdEntry__init_, value_ ): - bytes_ = b'\x00\x01\x66\x66\x00\x00\x00\x02\x00\x00\x00\x03' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"\x00\x01\x66\x66\x00\x00\x00\x02\x00\x00\x00\x03" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) offset, tag_code, value_count, value_offset = 0, 1, 2, 3 _parse_value_.return_value = value_ @@ -415,44 +415,40 @@ def _IfdEntry__init_(self, request): @pytest.fixture def _parse_value_(self, request): - return method_mock(request, _IfdEntry, '_parse_value', autospec=False) + return method_mock(request, _IfdEntry, "_parse_value", autospec=False) @pytest.fixture def value_(self, request): return loose_mock(request) -class Describe_AsciiIfdEntry(object): - +class Describe_AsciiIfdEntry: def it_can_parse_an_ascii_string_IFD_entry(self): - bytes_ = b'foobar\x00' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"foobar\x00" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _AsciiIfdEntry._parse_value(stream_rdr, None, 7, 0) - assert val == 'foobar' + assert val == "foobar" -class Describe_ShortIfdEntry(object): - +class Describe_ShortIfdEntry: def it_can_parse_a_short_int_IFD_entry(self): - bytes_ = b'foobaroo\x00\x2A' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"foobaroo\x00\x2A" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _ShortIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 -class Describe_LongIfdEntry(object): - +class Describe_LongIfdEntry: def it_can_parse_a_long_int_IFD_entry(self): - bytes_ = b'foobaroo\x00\x00\x00\x2A' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"foobaroo\x00\x00\x00\x2A" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _LongIfdEntry._parse_value(stream_rdr, 0, 1, None) assert val == 42 -class Describe_RationalIfdEntry(object): - +class Describe_RationalIfdEntry: def it_can_parse_a_rational_IFD_entry(self): - bytes_ = b'\x00\x00\x00\x2A\x00\x00\x00\x54' - stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN) + bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x54" + stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN) val = _RationalIfdEntry._parse_value(stream_rdr, None, 1, 0) assert val == 0.5 diff --git a/tests/opc/parts/test_coreprops.py b/tests/opc/parts/test_coreprops.py index 921a885d3..1db650353 100644 --- a/tests/opc/parts/test_coreprops.py +++ b/tests/opc/parts/test_coreprops.py @@ -1,12 +1,4 @@ -# encoding: utf-8 - -""" -Unit test suite for the docx.opc.parts.coreprops module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Unit test suite for the docx.opc.parts.coreprops module.""" from datetime import datetime, timedelta @@ -19,8 +11,7 @@ from ...unitutil.mock import class_mock, instance_mock -class DescribeCorePropertiesPart(object): - +class DescribeCorePropertiesPart: def it_provides_access_to_its_core_props_object(self, coreprops_fixture): core_properties_part, CoreProperties_ = coreprops_fixture core_properties = core_properties_part.core_properties @@ -31,8 +22,8 @@ def it_can_create_a_default_core_properties_part(self): core_properties_part = CorePropertiesPart.default(None) assert isinstance(core_properties_part, CorePropertiesPart) core_properties = core_properties_part.core_properties - assert core_properties.title == 'Word Document' - assert core_properties.last_modified_by == 'python-docx' + assert core_properties.title == "Word Document" + assert core_properties.last_modified_by == "python-docx" assert core_properties.revision == 1 delta = datetime.utcnow() - core_properties.modified max_expected_delta = timedelta(seconds=2) @@ -49,7 +40,7 @@ def coreprops_fixture(self, element_, CoreProperties_): @pytest.fixture def CoreProperties_(self, request): - return class_mock(request, 'docx.opc.parts.coreprops.CoreProperties') + return class_mock(request, "docx.opc.parts.coreprops.CoreProperties") @pytest.fixture def element_(self, request): diff --git a/tests/opc/test_coreprops.py b/tests/opc/test_coreprops.py index 47195f597..2978ad5ae 100644 --- a/tests/opc/test_coreprops.py +++ b/tests/opc/test_coreprops.py @@ -1,23 +1,14 @@ -# encoding: utf-8 +"""Unit test suite for the docx.opc.coreprops module.""" -""" -Unit test suite for the docx.opc.coreprops module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from datetime import datetime import pytest -from datetime import datetime - from docx.opc.coreprops import CoreProperties -from docx.oxml import parse_xml - +from docx.oxml.parser import parse_xml -class DescribeCoreProperties(object): +class DescribeCoreProperties: def it_knows_the_string_property_values(self, text_prop_get_fixture): core_properties, prop_name, expected_value = text_prop_get_fixture actual_value = getattr(core_properties, prop_name) @@ -34,9 +25,7 @@ def it_knows_the_date_property_values(self, date_prop_get_fixture): assert actual_datetime == expected_datetime def it_can_change_the_date_property_values(self, date_prop_set_fixture): - core_properties, prop_name, value, expected_xml = ( - date_prop_set_fixture - ) + core_properties, prop_name, value, expected_xml = date_prop_set_fixture setattr(core_properties, prop_name, value) assert core_properties._element.xml == expected_xml @@ -51,23 +40,42 @@ def it_can_change_the_revision_number(self, revision_set_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('created', datetime(2012, 11, 17, 16, 37, 40)), - ('last_printed', datetime(2014, 6, 4, 4, 28)), - ('modified', None), - ]) + @pytest.fixture( + params=[ + ("created", datetime(2012, 11, 17, 16, 37, 40)), + ("last_printed", datetime(2014, 6, 4, 4, 28)), + ("modified", None), + ] + ) def date_prop_get_fixture(self, request, core_properties): prop_name, expected_datetime = request.param return core_properties, prop_name, expected_datetime - @pytest.fixture(params=[ - ('created', 'dcterms:created', datetime(2001, 2, 3, 4, 5), - '2001-02-03T04:05:00Z', ' xsi:type="dcterms:W3CDTF"'), - ('last_printed', 'cp:lastPrinted', datetime(2014, 6, 4, 4), - '2014-06-04T04:00:00Z', ''), - ('modified', 'dcterms:modified', datetime(2005, 4, 3, 2, 1), - '2005-04-03T02:01:00Z', ' xsi:type="dcterms:W3CDTF"'), - ]) + @pytest.fixture( + params=[ + ( + "created", + "dcterms:created", + datetime(2001, 2, 3, 4, 5), + "2001-02-03T04:05:00Z", + ' xsi:type="dcterms:W3CDTF"', + ), + ( + "last_printed", + "cp:lastPrinted", + datetime(2014, 6, 4, 4), + "2014-06-04T04:00:00Z", + "", + ), + ( + "modified", + "dcterms:modified", + datetime(2005, 4, 3, 2, 1), + "2005-04-03T02:01:00Z", + ' xsi:type="dcterms:W3CDTF"', + ), + ] + ) def date_prop_set_fixture(self, request): prop_name, tagname, value, str_val, attrs = request.param coreProperties = self.coreProperties(None, None) @@ -75,56 +83,62 @@ def date_prop_set_fixture(self, request): expected_xml = self.coreProperties(tagname, str_val, attrs) return core_properties, prop_name, value, expected_xml - @pytest.fixture(params=[ - ('42', 42), (None, 0), ('foobar', 0), ('-17', 0), ('32.7', 0) - ]) + @pytest.fixture( + params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)] + ) def revision_get_fixture(self, request): str_val, expected_revision = request.param - tagname = '' if str_val is None else 'cp:revision' + tagname = "" if str_val is None else "cp:revision" coreProperties = self.coreProperties(tagname, str_val) core_properties = CoreProperties(parse_xml(coreProperties)) return core_properties, expected_revision - @pytest.fixture(params=[ - (42, '42'), - ]) + @pytest.fixture( + params=[ + (42, "42"), + ] + ) def revision_set_fixture(self, request): value, str_val = request.param coreProperties = self.coreProperties(None, None) core_properties = CoreProperties(parse_xml(coreProperties)) - expected_xml = self.coreProperties('cp:revision', str_val) + expected_xml = self.coreProperties("cp:revision", str_val) return core_properties, value, expected_xml - @pytest.fixture(params=[ - ('author', 'python-docx'), - ('category', ''), - ('comments', ''), - ('content_status', 'DRAFT'), - ('identifier', 'GXS 10.2.1ab'), - ('keywords', 'foo bar baz'), - ('language', 'US-EN'), - ('last_modified_by', 'Steve Canny'), - ('subject', 'Spam'), - ('title', 'Word Document'), - ('version', '1.2.88'), - ]) + @pytest.fixture( + params=[ + ("author", "python-docx"), + ("category", ""), + ("comments", ""), + ("content_status", "DRAFT"), + ("identifier", "GXS 10.2.1ab"), + ("keywords", "foo bar baz"), + ("language", "US-EN"), + ("last_modified_by", "Steve Canny"), + ("subject", "Spam"), + ("title", "Word Document"), + ("version", "1.2.88"), + ] + ) def text_prop_get_fixture(self, request, core_properties): prop_name, expected_value = request.param return core_properties, prop_name, expected_value - @pytest.fixture(params=[ - ('author', 'dc:creator', 'scanny'), - ('category', 'cp:category', 'silly stories'), - ('comments', 'dc:description', 'Bar foo to you'), - ('content_status', 'cp:contentStatus', 'FINAL'), - ('identifier', 'dc:identifier', 'GT 5.2.xab'), - ('keywords', 'cp:keywords', 'dog cat moo'), - ('language', 'dc:language', 'GB-EN'), - ('last_modified_by', 'cp:lastModifiedBy', 'Billy Bob'), - ('subject', 'dc:subject', 'Eggs'), - ('title', 'dc:title', 'Dissertation'), - ('version', 'cp:version', '81.2.8'), - ]) + @pytest.fixture( + params=[ + ("author", "dc:creator", "scanny"), + ("category", "cp:category", "silly stories"), + ("comments", "dc:description", "Bar foo to you"), + ("content_status", "cp:contentStatus", "FINAL"), + ("identifier", "dc:identifier", "GT 5.2.xab"), + ("keywords", "cp:keywords", "dog cat moo"), + ("language", "dc:language", "GB-EN"), + ("last_modified_by", "cp:lastModifiedBy", "Billy Bob"), + ("subject", "dc:subject", "Eggs"), + ("title", "dc:title", "Dissertation"), + ("version", "cp:version", "81.2.8"), + ] + ) def text_prop_set_fixture(self, request): prop_name, tagname, value = request.param coreProperties = self.coreProperties(None, None) @@ -134,7 +148,7 @@ def text_prop_set_fixture(self, request): # fixture components --------------------------------------------- - def coreProperties(self, tagname, str_val, attrs=''): + def coreProperties(self, tagname, str_val, attrs=""): tmpl = ( '%s\n' ) if not tagname: - child_element = '' + child_element = "" elif not str_val: - child_element = '\n <%s%s/>\n' % (tagname, attrs) + child_element = "\n <%s%s/>\n" % (tagname, attrs) else: - child_element = ( - '\n <%s%s>%s\n' % (tagname, attrs, str_val, tagname) - ) + child_element = "\n <%s%s>%s\n" % (tagname, attrs, str_val, tagname) return tmpl % child_element @pytest.fixture def core_properties(self): element = parse_xml( - b'' + b"" b'\n\n' - b' DRAFT\n' - b' python-docx\n' + b" DRAFT\n" + b" python-docx\n" b' 2012-11-17T11:07:' - b'40-05:30\n' - b' \n' - b' GXS 10.2.1ab\n' - b' US-EN\n' - b' 2014-06-04T04:28:00Z\n' - b' foo bar baz\n' - b' Steve Canny\n' - b' 4\n' - b' Spam\n' - b' Word Document\n' - b' 1.2.88\n' - b'\n' + b"40-05:30\n" + b" \n" + b" GXS 10.2.1ab\n" + b" US-EN\n" + b" 2014-06-04T04:28:00Z\n" + b" foo bar baz\n" + b" Steve Canny\n" + b" 4\n" + b" Spam\n" + b" Word Document\n" + b" 1.2.88\n" + b"\n" ) return CoreProperties(element) diff --git a/tests/opc/test_oxml.py b/tests/opc/test_oxml.py index 79e77c880..0b3e5e36f 100644 --- a/tests/opc/test_oxml.py +++ b/tests/opc/test_oxml.py @@ -1,60 +1,61 @@ -# encoding: utf-8 - -""" -Test suite for opc.oxml module -""" +"""Test suite for opc.oxml module.""" from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM from docx.opc.oxml import ( - CT_Default, CT_Override, CT_Relationship, CT_Relationships, CT_Types + CT_Default, + CT_Override, + CT_Relationship, + CT_Relationships, + CT_Types, ) from docx.oxml.xmlchemy import serialize_for_reading from .unitdata.rels import ( - a_Default, an_Override, a_Relationship, a_Relationships, a_Types + a_Default, + a_Relationship, + a_Relationships, + a_Types, + an_Override, ) -class DescribeCT_Default(object): - +class DescribeCT_Default: def it_provides_read_access_to_xml_values(self): default = a_Default().element - assert default.extension == 'xml' - assert default.content_type == 'application/xml' + assert default.extension == "xml" + assert default.content_type == "application/xml" def it_can_construct_a_new_default_element(self): - default = CT_Default.new('xml', 'application/xml') + default = CT_Default.new("xml", "application/xml") expected_xml = a_Default().xml assert default.xml == expected_xml -class DescribeCT_Override(object): - +class DescribeCT_Override: def it_provides_read_access_to_xml_values(self): override = an_Override().element - assert override.partname == '/part/name.xml' - assert override.content_type == 'app/vnd.type' + assert override.partname == "/part/name.xml" + assert override.content_type == "app/vnd.type" def it_can_construct_a_new_override_element(self): - override = CT_Override.new('/part/name.xml', 'app/vnd.type') + override = CT_Override.new("/part/name.xml", "app/vnd.type") expected_xml = an_Override().xml assert override.xml == expected_xml -class DescribeCT_Relationship(object): - +class DescribeCT_Relationship: def it_provides_read_access_to_xml_values(self): rel = a_Relationship().element - assert rel.rId == 'rId9' - assert rel.reltype == 'ReLtYpE' - assert rel.target_ref == 'docProps/core.xml' + assert rel.rId == "rId9" + assert rel.reltype == "ReLtYpE" + assert rel.target_ref == "docProps/core.xml" assert rel.target_mode == RTM.INTERNAL def it_can_construct_from_attribute_values(self): cases = ( - ('rId9', 'ReLtYpE', 'foo/bar.xml', None), - ('rId9', 'ReLtYpE', 'bar/foo.xml', RTM.INTERNAL), - ('rId9', 'ReLtYpE', 'http://some/link', RTM.EXTERNAL), + ("rId9", "ReLtYpE", "foo/bar.xml", None), + ("rId9", "ReLtYpE", "bar/foo.xml", RTM.INTERNAL), + ("rId9", "ReLtYpE", "http://some/link", RTM.EXTERNAL), ) for rId, reltype, target, target_mode in cases: if target_mode is None: @@ -68,8 +69,7 @@ def it_can_construct_from_attribute_values(self): assert rel.xml == expected_rel_xml -class DescribeCT_Relationships(object): - +class DescribeCT_Relationships: def it_can_construct_a_new_relationships_element(self): rels = CT_Relationships.new() expected_xml = ( @@ -82,24 +82,23 @@ def it_can_build_rels_element_incrementally(self): # setup ------------------------ rels = CT_Relationships.new() # exercise --------------------- - rels.add_rel('rId1', 'http://reltype1', 'docProps/core.xml') - rels.add_rel('rId2', 'http://linktype', 'http://some/link', True) - rels.add_rel('rId3', 'http://reltype2', '../slides/slide1.xml') + rels.add_rel("rId1", "http://reltype1", "docProps/core.xml") + rels.add_rel("rId2", "http://linktype", "http://some/link", True) + rels.add_rel("rId3", "http://reltype2", "../slides/slide1.xml") # verify ----------------------- expected_rels_xml = a_Relationships().xml assert serialize_for_reading(rels) == expected_rels_xml def it_can_generate_rels_file_xml(self): expected_xml = ( - '\n' + "\n" ''.encode('utf-8') + '/2006/relationships"/>'.encode("utf-8") ) assert CT_Relationships.new().xml == expected_xml -class DescribeCT_Types(object): - +class DescribeCT_Types: def it_provides_access_to_default_child_elements(self): types = a_Types().element assert len(types.defaults) == 2 @@ -124,10 +123,10 @@ def it_can_construct_a_new_types_element(self): def it_can_build_types_element_incrementally(self): types = CT_Types.new() - types.add_default('xml', 'application/xml') - types.add_default('jpeg', 'image/jpeg') - types.add_override('/docProps/core.xml', 'app/vnd.type1') - types.add_override('/ppt/presentation.xml', 'app/vnd.type2') - types.add_override('/docProps/thumbnail.jpeg', 'image/jpeg') + types.add_default("xml", "application/xml") + types.add_default("jpeg", "image/jpeg") + types.add_override("/docProps/core.xml", "app/vnd.type1") + types.add_override("/ppt/presentation.xml", "app/vnd.type2") + types.add_override("/docProps/thumbnail.jpeg", "image/jpeg") expected_types_xml = a_Types().xml assert types.xml == expected_types_xml diff --git a/tests/opc/test_package.py b/tests/opc/test_package.py index 8b2de5728..7fdeaa422 100644 --- a/tests/opc/test_package.py +++ b/tests/opc/test_package.py @@ -1,9 +1,5 @@ -# encoding: utf-8 - """Unit test suite for docx.opc.package module""" -from __future__ import absolute_import, division, print_function, unicode_literals - import pytest from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -13,38 +9,34 @@ from docx.opc.part import Part from docx.opc.parts.coreprops import CorePropertiesPart from docx.opc.pkgreader import PackageReader -from docx.opc.rel import _Relationship, Relationships +from docx.opc.rel import Relationships, _Relationship from ..unitutil.mock import ( + Mock, + PropertyMock, call, class_mock, instance_mock, loose_mock, method_mock, - Mock, patch, - PropertyMock, property_mock, ) -class DescribeOpcPackage(object): - - def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, - Unmarshaller_): +class DescribeOpcPackage: + def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_, Unmarshaller_): # mockery ---------------------- - pkg_file = Mock(name='pkg_file') + pkg_file = Mock(name="pkg_file") pkg_reader = PackageReader_.from_file.return_value # exercise --------------------- pkg = OpcPackage.open(pkg_file) # verify ----------------------- PackageReader_.from_file.assert_called_once_with(pkg_file) - Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, - PartFactory_) + Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg, PartFactory_) assert isinstance(pkg, OpcPackage) - def it_initializes_its_rels_collection_on_first_reference( - self, Relationships_): + def it_initializes_its_rels_collection_on_first_reference(self, Relationships_): pkg = OpcPackage() rels = pkg.rels Relationships_.assert_called_once_with(PACKAGE_URI.baseURI) @@ -56,12 +48,9 @@ def it_can_add_a_relationship_to_a_part(self, pkg_with_rels_, rel_attrs_): # exercise --------------------- pkg.load_rel(reltype, target, rId) # verify ----------------------- - pkg._rels.add_relationship.assert_called_once_with( - reltype, target, rId, False - ) + pkg._rels.add_relationship.assert_called_once_with(reltype, target, rId, False) - def it_can_establish_a_relationship_to_another_part( - self, relate_to_part_fixture_): + def it_can_establish_a_relationship_to_another_part(self, relate_to_part_fixture_): pkg, part_, reltype, rId = relate_to_part_fixture_ _rId = pkg.relate_to(part_, reltype) pkg.rels.get_or_add.assert_called_once_with(reltype, part_) @@ -69,10 +58,10 @@ def it_can_establish_a_relationship_to_another_part( def it_can_provide_a_list_of_the_parts_it_contains(self): # mockery ---------------------- - parts = [Mock(name='part1'), Mock(name='part2')] + parts = [Mock(name="part1"), Mock(name="part2")] pkg = OpcPackage() # verify ----------------------- - with patch.object(OpcPackage, 'iter_parts', return_value=parts): + with patch.object(OpcPackage, "iter_parts", return_value=parts): assert pkg.parts == [parts[0], parts[1]] def it_can_iterate_over_parts_by_walking_rels_graph(self): @@ -84,22 +73,18 @@ def it_can_iterate_over_parts_by_walking_rels_graph(self): # external +--------+ # | part_2 | # +--------+ - part1, part2 = (Mock(name='part1'), Mock(name='part2')) - part1.rels = { - 1: Mock(name='rel1', is_external=False, target_part=part2) - } - part2.rels = { - 1: Mock(name='rel2', is_external=False, target_part=part1) - } + part1, part2 = (Mock(name="part1"), Mock(name="part2")) + part1.rels = {1: Mock(name="rel1", is_external=False, target_part=part2)} + part2.rels = {1: Mock(name="rel2", is_external=False, target_part=part1)} pkg = OpcPackage() pkg._rels = { - 1: Mock(name='rel3', is_external=False, target_part=part1), - 2: Mock(name='rel4', is_external=True), + 1: Mock(name="rel3", is_external=False, target_part=part1), + 2: Mock(name="rel4", is_external=True), } # verify ----------------------- assert part1 in pkg.iter_parts() assert part2 in pkg.iter_parts() - assert len([p for p in pkg.iter_parts()]) == 2 + assert len(list(pkg.iter_parts())) == 2 def it_can_find_the_next_available_vector_partname( self, next_partname_fixture, iter_parts_, PackURI_, packuri_ @@ -121,15 +106,12 @@ def it_can_find_a_part_related_by_reltype(self, related_part_fixture_): pkg.rels.part_with_reltype.assert_called_once_with(reltype) assert related_part is related_part_ - def it_can_save_to_a_pkg_file( - self, pkg_file_, PackageWriter_, parts, parts_): + def it_can_save_to_a_pkg_file(self, pkg_file_, PackageWriter_, parts, parts_): pkg = OpcPackage() pkg.save(pkg_file_) for part in parts_: part.before_marshal.assert_called_once_with() - PackageWriter_.write.assert_called_once_with( - pkg_file_, pkg._rels, parts_ - ) + PackageWriter_.write.assert_called_once_with(pkg_file_, pkg._rels, parts_) def it_provides_access_to_the_core_properties(self, core_props_fixture): opc_package, core_properties_ = core_props_fixture @@ -137,7 +119,8 @@ def it_provides_access_to_the_core_properties(self, core_props_fixture): assert core_properties is core_properties_ def it_provides_access_to_the_core_properties_part_to_help( - self, core_props_part_fixture): + self, core_props_part_fixture + ): opc_package, core_properties_part_ = core_props_part_fixture core_properties_part = opc_package._core_properties_part assert core_properties_part is core_properties_part_ @@ -161,23 +144,20 @@ def it_creates_a_default_core_props_part_if_none_present( @pytest.fixture def core_props_fixture( - self, _core_properties_part_prop_, core_properties_part_, - core_properties_): + self, _core_properties_part_prop_, core_properties_part_, core_properties_ + ): opc_package = OpcPackage() _core_properties_part_prop_.return_value = core_properties_part_ core_properties_part_.core_properties = core_properties_ return opc_package, core_properties_ @pytest.fixture - def core_props_part_fixture( - self, part_related_by_, core_properties_part_): + def core_props_part_fixture(self, part_related_by_, core_properties_part_): opc_package = OpcPackage() part_related_by_.return_value = core_properties_part_ return opc_package, core_properties_part_ - @pytest.fixture( - params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)] - ) + @pytest.fixture(params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)]) def next_partname_fixture(self, request, iter_parts_): existing_partname_ns, next_partname_n = request.param parts_ = [ @@ -191,16 +171,16 @@ def next_partname_fixture(self, request, iter_parts_): @pytest.fixture def relate_to_part_fixture_(self, request, pkg, rels_, reltype): - rId = 'rId99' - rel_ = instance_mock(request, _Relationship, name='rel_', rId=rId) + rId = "rId99" + rel_ = instance_mock(request, _Relationship, name="rel_", rId=rId) rels_.get_or_add.return_value = rel_ pkg._rels = rels_ - part_ = instance_mock(request, Part, name='part_') + part_ = instance_mock(request, Part, name="part_") return pkg, part_, reltype, rId @pytest.fixture def related_part_fixture_(self, request, rels_, reltype): - related_part_ = instance_mock(request, Part, name='related_part_') + related_part_ = instance_mock(request, Part, name="related_part_") rels_.part_with_reltype.return_value = related_part_ pkg = OpcPackage() pkg._rels = rels_ @@ -210,7 +190,7 @@ def related_part_fixture_(self, request, rels_, reltype): @pytest.fixture def CorePropertiesPart_(self, request): - return class_mock(request, 'docx.opc.package.CorePropertiesPart') + return class_mock(request, "docx.opc.package.CorePropertiesPart") @pytest.fixture def core_properties_(self, request): @@ -222,7 +202,7 @@ def core_properties_part_(self, request): @pytest.fixture def _core_properties_part_prop_(self, request): - return property_mock(request, OpcPackage, '_core_properties_part') + return property_mock(request, OpcPackage, "_core_properties_part") @pytest.fixture def iter_parts_(self, request): @@ -230,7 +210,7 @@ def iter_parts_(self, request): @pytest.fixture def PackageReader_(self, request): - return class_mock(request, 'docx.opc.package.PackageReader') + return class_mock(request, "docx.opc.package.PackageReader") @pytest.fixture def PackURI_(self, request): @@ -242,33 +222,32 @@ def packuri_(self, request): @pytest.fixture def PackageWriter_(self, request): - return class_mock(request, 'docx.opc.package.PackageWriter') + return class_mock(request, "docx.opc.package.PackageWriter") @pytest.fixture def PartFactory_(self, request): - return class_mock(request, 'docx.opc.package.PartFactory') + return class_mock(request, "docx.opc.package.PartFactory") @pytest.fixture def part_related_by_(self, request): - return method_mock(request, OpcPackage, 'part_related_by') + return method_mock(request, OpcPackage, "part_related_by") @pytest.fixture - def parts(self, request, parts_): + def parts(self, parts_): """ Return a mock patching property OpcPackage.parts, reversing the patch after each use. """ - _patch = patch.object( - OpcPackage, 'parts', new_callable=PropertyMock, - return_value=parts_ + p = patch.object( + OpcPackage, "parts", new_callable=PropertyMock, return_value=parts_ ) - request.addfinalizer(_patch.stop) - return _patch.start() + yield p.start() + p.stop() @pytest.fixture def parts_(self, request): - part_ = instance_mock(request, Part, name='part_') - part_2_ = instance_mock(request, Part, name='part_2_') + part_ = instance_mock(request, Part, name="part_") + part_2_ = instance_mock(request, Part, name="part_2_") return [part_, part_2_] @pytest.fixture @@ -287,18 +266,18 @@ def pkg_with_rels_(self, request, rels_): @pytest.fixture def Relationships_(self, request): - return class_mock(request, 'docx.opc.package.Relationships') + return class_mock(request, "docx.opc.package.Relationships") @pytest.fixture def rel_attrs_(self, request): - reltype = 'http://rel/type' - target_ = instance_mock(request, Part, name='target_') - rId = 'rId99' + reltype = "http://rel/type" + target_ = instance_mock(request, Part, name="target_") + rId = "rId99" return reltype, target_, rId @pytest.fixture def relate_to_(self, request): - return method_mock(request, OpcPackage, 'relate_to') + return method_mock(request, OpcPackage, "relate_to") @pytest.fixture def rels_(self, request): @@ -306,15 +285,14 @@ def rels_(self, request): @pytest.fixture def reltype(self, request): - return 'http://rel/type' + return "http://rel/type" @pytest.fixture def Unmarshaller_(self, request): - return class_mock(request, 'docx.opc.package.Unmarshaller') - + return class_mock(request, "docx.opc.package.Unmarshaller") -class DescribeUnmarshaller(object): +class DescribeUnmarshaller: def it_can_unmarshal_from_a_pkg_reader( self, pkg_reader_, @@ -336,56 +314,92 @@ def it_can_unmarshal_from_a_pkg_reader( pkg_.after_unmarshal.assert_called_once_with() def it_can_unmarshal_parts( - self, pkg_reader_, pkg_, part_factory_, parts_dict_, partnames_, - content_types_, reltypes_, blobs_): + self, + pkg_reader_, + pkg_, + part_factory_, + parts_dict_, + partnames_, + content_types_, + reltypes_, + blobs_, + ): # fixture ---------------------- partname_, partname_2_ = partnames_ content_type_, content_type_2_ = content_types_ reltype_, reltype_2_ = reltypes_ blob_, blob_2_ = blobs_ # exercise --------------------- - parts = Unmarshaller._unmarshal_parts( - pkg_reader_, pkg_, part_factory_ - ) + parts = Unmarshaller._unmarshal_parts(pkg_reader_, pkg_, part_factory_) # verify ----------------------- - assert ( - part_factory_.call_args_list == [ - call(partname_, content_type_, reltype_, blob_, pkg_), - call(partname_2_, content_type_2_, reltype_2_, blob_2_, pkg_) - ] - ) + assert part_factory_.call_args_list == [ + call(partname_, content_type_, reltype_, blob_, pkg_), + call(partname_2_, content_type_2_, reltype_2_, blob_2_, pkg_), + ] assert parts == parts_dict_ def it_can_unmarshal_relationships(self): # test data -------------------- - reltype = 'http://reltype' + reltype = "http://reltype" # mockery ---------------------- - pkg_reader = Mock(name='pkg_reader') + pkg_reader = Mock(name="pkg_reader") pkg_reader.iter_srels.return_value = ( - ('/', Mock(name='srel1', rId='rId1', reltype=reltype, - target_partname='partname1', is_external=False)), - ('/', Mock(name='srel2', rId='rId2', reltype=reltype, - target_ref='target_ref_1', is_external=True)), - ('partname1', Mock(name='srel3', rId='rId3', reltype=reltype, - target_partname='partname2', is_external=False)), - ('partname2', Mock(name='srel4', rId='rId4', reltype=reltype, - target_ref='target_ref_2', is_external=True)), + ( + "/", + Mock( + name="srel1", + rId="rId1", + reltype=reltype, + target_partname="partname1", + is_external=False, + ), + ), + ( + "/", + Mock( + name="srel2", + rId="rId2", + reltype=reltype, + target_ref="target_ref_1", + is_external=True, + ), + ), + ( + "partname1", + Mock( + name="srel3", + rId="rId3", + reltype=reltype, + target_partname="partname2", + is_external=False, + ), + ), + ( + "partname2", + Mock( + name="srel4", + rId="rId4", + reltype=reltype, + target_ref="target_ref_2", + is_external=True, + ), + ), ) - pkg = Mock(name='pkg') + pkg = Mock(name="pkg") parts = {} for num in range(1, 3): - name = 'part%d' % num + name = "part%d" % num part = Mock(name=name) - parts['partname%d' % num] = part + parts["partname%d" % num] = part pkg.attach_mock(part, name) # exercise --------------------- Unmarshaller._unmarshal_relationships(pkg_reader, pkg, parts) # verify ----------------------- expected_pkg_calls = [ - call.load_rel(reltype, parts['partname1'], 'rId1', False), - call.load_rel(reltype, 'target_ref_1', 'rId2', True), - call.part1.load_rel(reltype, parts['partname2'], 'rId3', False), - call.part2.load_rel(reltype, 'target_ref_2', 'rId4', True), + call.load_rel(reltype, parts["partname1"], "rId1", False), + call.load_rel(reltype, "target_ref_1", "rId2", True), + call.part1.load_rel(reltype, parts["partname2"], "rId3", False), + call.part2.load_rel(reltype, "target_ref_2", "rId4", True), ] assert pkg.mock_calls == expected_pkg_calls @@ -393,14 +407,14 @@ def it_can_unmarshal_relationships(self): @pytest.fixture def blobs_(self, request): - blob_ = loose_mock(request, spec=str, name='blob_') - blob_2_ = loose_mock(request, spec=str, name='blob_2_') + blob_ = loose_mock(request, spec=str, name="blob_") + blob_2_ = loose_mock(request, spec=str, name="blob_2_") return blob_, blob_2_ @pytest.fixture def content_types_(self, request): - content_type_ = loose_mock(request, spec=str, name='content_type_') - content_type_2_ = loose_mock(request, spec=str, name='content_type_2_') + content_type_ = loose_mock(request, spec=str, name="content_type_") + content_type_2_ = loose_mock(request, spec=str, name="content_type_2_") return content_type_, content_type_2_ @pytest.fixture @@ -411,14 +425,14 @@ def part_factory_(self, request, parts_): @pytest.fixture def partnames_(self, request): - partname_ = loose_mock(request, spec=str, name='partname_') - partname_2_ = loose_mock(request, spec=str, name='partname_2_') + partname_ = loose_mock(request, spec=str, name="partname_") + partname_2_ = loose_mock(request, spec=str, name="partname_2_") return partname_, partname_2_ @pytest.fixture def parts_(self, request): - part_ = instance_mock(request, Part, name='part_') - part_2_ = instance_mock(request, Part, name='part_2') + part_ = instance_mock(request, Part, name="part_") + part_2_ = instance_mock(request, Part, name="part_2") return part_, part_2_ @pytest.fixture @@ -432,8 +446,7 @@ def pkg_(self, request): return instance_mock(request, OpcPackage) @pytest.fixture - def pkg_reader_( - self, request, partnames_, content_types_, reltypes_, blobs_): + def pkg_reader_(self, request, partnames_, content_types_, reltypes_, blobs_): partname_, partname_2_ = partnames_ content_type_, content_type_2_ = content_types_ reltype_, reltype_2_ = reltypes_ @@ -448,16 +461,16 @@ def pkg_reader_( @pytest.fixture def reltypes_(self, request): - reltype_ = instance_mock(request, str, name='reltype_') - reltype_2_ = instance_mock(request, str, name='reltype_2') + reltype_ = instance_mock(request, str, name="reltype_") + reltype_2_ = instance_mock(request, str, name="reltype_2") return reltype_, reltype_2_ @pytest.fixture def _unmarshal_parts_(self, request): - return method_mock(request, Unmarshaller, '_unmarshal_parts', autospec=False) + return method_mock(request, Unmarshaller, "_unmarshal_parts", autospec=False) @pytest.fixture def _unmarshal_relationships_(self, request): return method_mock( - request, Unmarshaller, '_unmarshal_relationships', autospec=False + request, Unmarshaller, "_unmarshal_relationships", autospec=False ) diff --git a/tests/opc/test_packuri.py b/tests/opc/test_packuri.py index f2527eba3..987da69c4 100644 --- a/tests/opc/test_packuri.py +++ b/tests/opc/test_packuri.py @@ -1,25 +1,20 @@ -# encoding: utf-8 - -""" -Test suite for the docx.opc.packuri module -""" +"""Test suite for the docx.opc.packuri module.""" import pytest from docx.opc.packuri import PackURI -class DescribePackURI(object): - +class DescribePackURI: def cases(self, expected_values): """ Return list of tuples zipped from uri_str cases and - *expected_values*. Raise if lengths don't match. + `expected_values`. Raise if lengths don't match. """ uri_str_cases = [ - '/', - '/ppt/presentation.xml', - '/ppt/slides/slide1.xml', + "/", + "/ppt/presentation.xml", + "/ppt/slides/slide1.xml", ] if len(expected_values) != len(uri_str_cases): msg = "len(expected_values) differs from len(uri_str_cases)" @@ -28,27 +23,27 @@ def cases(self, expected_values): return zip(pack_uris, expected_values) def it_can_construct_from_relative_ref(self): - baseURI = '/ppt/slides' - relative_ref = '../slideLayouts/slideLayout1.xml' + baseURI = "/ppt/slides" + relative_ref = "../slideLayouts/slideLayout1.xml" pack_uri = PackURI.from_rel_ref(baseURI, relative_ref) - assert pack_uri == '/ppt/slideLayouts/slideLayout1.xml' + assert pack_uri == "/ppt/slideLayouts/slideLayout1.xml" def it_should_raise_on_construct_with_bad_pack_uri_str(self): - with pytest.raises(ValueError): - PackURI('foobar') + with pytest.raises(ValueError, match="PackURI must begin with slash"): + PackURI("foobar") def it_can_calculate_baseURI(self): - expected_values = ('/', '/ppt', '/ppt/slides') + expected_values = ("/", "/ppt", "/ppt/slides") for pack_uri, expected_baseURI in self.cases(expected_values): assert pack_uri.baseURI == expected_baseURI def it_can_calculate_extension(self): - expected_values = ('', 'xml', 'xml') + expected_values = ("", "xml", "xml") for pack_uri, expected_ext in self.cases(expected_values): assert pack_uri.ext == expected_ext def it_can_calculate_filename(self): - expected_values = ('', 'presentation.xml', 'slide1.xml') + expected_values = ("", "presentation.xml", "slide1.xml") for pack_uri, expected_filename in self.cases(expected_values): assert pack_uri.filename == expected_filename @@ -59,20 +54,26 @@ def it_knows_the_filename_index(self): def it_can_calculate_membername(self): expected_values = ( - '', - 'ppt/presentation.xml', - 'ppt/slides/slide1.xml', + "", + "ppt/presentation.xml", + "ppt/slides/slide1.xml", ) for pack_uri, expected_membername in self.cases(expected_values): assert pack_uri.membername == expected_membername def it_can_calculate_relative_ref_value(self): cases = ( - ('/', '/ppt/presentation.xml', 'ppt/presentation.xml'), - ('/ppt', '/ppt/slideMasters/slideMaster1.xml', - 'slideMasters/slideMaster1.xml'), - ('/ppt/slides', '/ppt/slideLayouts/slideLayout1.xml', - '../slideLayouts/slideLayout1.xml'), + ("/", "/ppt/presentation.xml", "ppt/presentation.xml"), + ( + "/ppt", + "/ppt/slideMasters/slideMaster1.xml", + "slideMasters/slideMaster1.xml", + ), + ( + "/ppt/slides", + "/ppt/slideLayouts/slideLayout1.xml", + "../slideLayouts/slideLayout1.xml", + ), ) for baseURI, uri_str, expected_relative_ref in cases: pack_uri = PackURI(uri_str) @@ -80,9 +81,9 @@ def it_can_calculate_relative_ref_value(self): def it_can_calculate_rels_uri(self): expected_values = ( - '/_rels/.rels', - '/ppt/_rels/presentation.xml.rels', - '/ppt/slides/_rels/slide1.xml.rels', + "/_rels/.rels", + "/ppt/_rels/presentation.xml.rels", + "/ppt/slides/_rels/slide1.xml.rels", ) for pack_uri, expected_rels_uri in self.cases(expected_values): assert pack_uri.rels_uri == expected_rels_uri diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index cc60beaae..163912154 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -1,32 +1,27 @@ -# encoding: utf-8 - """Unit test suite for docx.opc.part module""" -from __future__ import absolute_import, division, print_function, unicode_literals - import pytest from docx.opc.package import OpcPackage from docx.opc.packuri import PackURI from docx.opc.part import Part, PartFactory, XmlPart -from docx.opc.rel import _Relationship, Relationships +from docx.opc.rel import Relationships, _Relationship from docx.oxml.xmlchemy import BaseOxmlElement from ..unitutil.cxml import element from ..unitutil.mock import ( ANY, + Mock, class_mock, cls_attr_mock, function_mock, initializer_mock, instance_mock, loose_mock, - Mock, ) -class DescribePart(object): - +class DescribePart: def it_can_be_constructed_by_PartFactory( self, partname_, content_type_, blob_, package_, __init_ ): @@ -71,7 +66,7 @@ def blob_fixture(self, blob_): @pytest.fixture def content_type_fixture(self): - content_type = 'content/type' + content_type = "content/type" part = Part(None, content_type, None, None) return part, content_type @@ -87,14 +82,14 @@ def part(self): @pytest.fixture def partname_get_fixture(self): - partname = PackURI('/part/name') + partname = PackURI("/part/name") part = Part(partname, None, None, None) return part, partname @pytest.fixture def partname_set_fixture(self): - old_partname = PackURI('/old/part/name') - new_partname = PackURI('/new/part/name') + old_partname = PackURI("/old/part/name") + new_partname = PackURI("/new/part/name") part = Part(old_partname, None, None, None) return part, new_partname @@ -121,8 +116,7 @@ def partname_(self, request): return instance_mock(request, PackURI) -class DescribePartRelationshipManagementInterface(object): - +class DescribePartRelationshipManagementInterface: def it_provides_access_to_its_relationships(self, rels_fixture): part, Relationships_, partname_, rels_ = rels_fixture rels = part.rels @@ -132,19 +126,15 @@ def it_provides_access_to_its_relationships(self, rels_fixture): def it_can_load_a_relationship(self, load_rel_fixture): part, rels_, reltype_, target_, rId_ = load_rel_fixture part.load_rel(reltype_, target_, rId_) - rels_.add_relationship.assert_called_once_with( - reltype_, target_, rId_, False - ) + rels_.add_relationship.assert_called_once_with(reltype_, target_, rId_, False) - def it_can_establish_a_relationship_to_another_part( - self, relate_to_part_fixture): + def it_can_establish_a_relationship_to_another_part(self, relate_to_part_fixture): part, target_, reltype_, rId_ = relate_to_part_fixture rId = part.relate_to(target_, reltype_) part.rels.get_or_add.assert_called_once_with(reltype_, target_) assert rId is rId_ - def it_can_establish_an_external_relationship( - self, relate_to_url_fixture): + def it_can_establish_an_external_relationship(self, relate_to_url_fixture): part, url_, reltype_, rId_ = relate_to_url_fixture rId = part.relate_to(url_, reltype_, is_external=True) part.rels.get_or_add_ext_rel.assert_called_once_with(reltype_, url_) @@ -168,22 +158,23 @@ def it_can_find_a_related_part_by_rId(self, related_parts_fixture): part, related_parts_ = related_parts_fixture assert part.related_parts is related_parts_ - def it_can_find_the_uri_of_an_external_relationship( - self, target_ref_fixture): + def it_can_find_the_uri_of_an_external_relationship(self, target_ref_fixture): part, rId_, url_ = target_ref_fixture url = part.target_ref(rId_) assert url == url_ # fixtures --------------------------------------------- - @pytest.fixture(params=[ - ('w:p', True), - ('w:p/r:a{r:id=rId42}', True), - ('w:p/r:a{r:id=rId42}/r:b{r:id=rId42}', False), - ]) + @pytest.fixture( + params=[ + ("w:p", True), + ("w:p/r:a{r:id=rId42}", True), + ("w:p/r:a{r:id=rId42}/r:b{r:id=rId42}", False), + ] + ) def drop_rel_fixture(self, request, part): part_cxml, rel_should_be_dropped = request.param - rId = 'rId42' + rId = "rId42" part._element = element(part_cxml) part._rels = {rId: None} return part, rId, rel_should_be_dropped @@ -194,15 +185,13 @@ def load_rel_fixture(self, part, rels_, reltype_, part_, rId_): return part, rels_, reltype_, part_, rId_ @pytest.fixture - def relate_to_part_fixture( - self, request, part, reltype_, part_, rels_, rId_): + def relate_to_part_fixture(self, request, part, reltype_, part_, rels_, rId_): part._rels = rels_ target_ = part_ return part, target_, reltype_, rId_ @pytest.fixture - def relate_to_url_fixture( - self, request, part, rels_, url_, reltype_, rId_): + def relate_to_url_fixture(self, request, part, rels_, url_, reltype_, rId_): part._rels = rels_ return part, url_, reltype_, rId_ @@ -242,15 +231,11 @@ def partname_(self, request): @pytest.fixture def Relationships_(self, request, rels_): - return class_mock( - request, 'docx.opc.part.Relationships', return_value=rels_ - ) + return class_mock(request, "docx.opc.part.Relationships", return_value=rels_) @pytest.fixture def rel_(self, request, rId_, url_): - return instance_mock( - request, _Relationship, rId=rId_, target_ref=url_ - ) + return instance_mock(request, _Relationship, rId=rId_, target_ref=url_) @pytest.fixture def rels_(self, request, part_, rel_, rId_, related_parts_): @@ -278,13 +263,15 @@ def url_(self, request): return instance_mock(request, str) -class DescribePartFactory(object): - - def it_constructs_part_from_selector_if_defined( - self, cls_selector_fixture): +class DescribePartFactory: + def it_constructs_part_from_selector_if_defined(self, cls_selector_fixture): # fixture ---------------------- - (cls_selector_fn_, part_load_params, CustomPartClass_, - part_of_custom_type_) = cls_selector_fixture + ( + cls_selector_fn_, + part_load_params, + CustomPartClass_, + part_of_custom_type_, + ) = cls_selector_fixture partname, content_type, reltype, blob, package = part_load_params # exercise --------------------- PartFactory.part_class_selector = cls_selector_fn_ @@ -297,7 +284,8 @@ def it_constructs_part_from_selector_if_defined( assert part is part_of_custom_type_ def it_constructs_custom_part_type_for_registered_content_types( - self, part_args_, CustomPartClass_, part_of_custom_type_): + self, part_args_, CustomPartClass_, part_of_custom_type_ + ): # fixture ---------------------- partname, content_type, reltype, package, blob = part_args_ # exercise --------------------- @@ -310,7 +298,8 @@ def it_constructs_custom_part_type_for_registered_content_types( assert part is part_of_custom_type_ def it_constructs_part_using_default_class_when_no_custom_registered( - self, part_args_2_, DefaultPartClass_, part_of_default_type_): + self, part_args_2_, DefaultPartClass_, part_of_default_type_ + ): partname, content_type, reltype, blob, package = part_args_2_ part = PartFactory(partname, content_type, reltype, blob, package) DefaultPartClass_.load.assert_called_once_with( @@ -331,22 +320,26 @@ def blob_2_(self, request): @pytest.fixture def cls_method_fn_(self, request, cls_selector_fn_): return function_mock( - request, 'docx.opc.part.cls_method_fn', - return_value=cls_selector_fn_ + request, "docx.opc.part.cls_method_fn", return_value=cls_selector_fn_ ) @pytest.fixture def cls_selector_fixture( - self, request, cls_selector_fn_, cls_method_fn_, part_load_params, - CustomPartClass_, part_of_custom_type_): - def reset_part_class_selector(): - PartFactory.part_class_selector = original_part_class_selector + self, + cls_selector_fn_, + cls_method_fn_, + part_load_params, + CustomPartClass_, + part_of_custom_type_, + ): original_part_class_selector = PartFactory.part_class_selector - request.addfinalizer(reset_part_class_selector) - return ( - cls_selector_fn_, part_load_params, CustomPartClass_, - part_of_custom_type_ + yield ( + cls_selector_fn_, + part_load_params, + CustomPartClass_, + part_of_custom_type_, ) + PartFactory.part_class_selector = original_part_class_selector @pytest.fixture def cls_selector_fn_(self, request, CustomPartClass_): @@ -355,7 +348,7 @@ def cls_selector_fn_(self, request, CustomPartClass_): cls_selector_fn_.return_value = CustomPartClass_ # Python 2 version cls_selector_fn_.__func__ = loose_mock( - request, name='__func__', return_value=cls_selector_fn_ + request, name="__func__", return_value=cls_selector_fn_ ) return cls_selector_fn_ @@ -369,15 +362,13 @@ def content_type_2_(self, request): @pytest.fixture def CustomPartClass_(self, request, part_of_custom_type_): - CustomPartClass_ = Mock(name='CustomPartClass', spec=Part) + CustomPartClass_ = Mock(name="CustomPartClass", spec=Part) CustomPartClass_.load.return_value = part_of_custom_type_ return CustomPartClass_ @pytest.fixture def DefaultPartClass_(self, request, part_of_default_type_): - DefaultPartClass_ = cls_attr_mock( - request, PartFactory, 'default_part_type' - ) + DefaultPartClass_ = cls_attr_mock(request, PartFactory, "default_part_type") DefaultPartClass_.load.return_value = part_of_default_type_ return DefaultPartClass_ @@ -390,8 +381,7 @@ def package_2_(self, request): return instance_mock(request, OpcPackage) @pytest.fixture - def part_load_params( - self, partname_, content_type_, reltype_, blob_, package_): + def part_load_params(self, partname_, content_type_, reltype_, blob_, package_): return partname_, content_type_, reltype_, blob_, package_ @pytest.fixture @@ -411,15 +401,13 @@ def partname_2_(self, request): return instance_mock(request, PackURI) @pytest.fixture - def part_args_( - self, request, partname_, content_type_, reltype_, package_, - blob_): + def part_args_(self, request, partname_, content_type_, reltype_, package_, blob_): return partname_, content_type_, reltype_, blob_, package_ @pytest.fixture def part_args_2_( - self, request, partname_2_, content_type_2_, reltype_2_, - package_2_, blob_2_): + self, request, partname_2_, content_type_2_, reltype_2_, package_2_, blob_2_ + ): return partname_2_, content_type_2_, reltype_2_, blob_2_, package_2_ @pytest.fixture @@ -431,8 +419,7 @@ def reltype_2_(self, request): return instance_mock(request, str) -class DescribeXmlPart(object): - +class DescribeXmlPart: def it_can_be_constructed_by_PartFactory( self, partname_, content_type_, blob_, package_, element_, parse_xml_, __init_ ): @@ -489,9 +476,7 @@ def package_(self, request): @pytest.fixture def parse_xml_(self, request, element_): - return function_mock( - request, 'docx.opc.part.parse_xml', return_value=element_ - ) + return function_mock(request, "docx.opc.part.parse_xml", return_value=element_) @pytest.fixture def partname_(self, request): @@ -499,6 +484,4 @@ def partname_(self, request): @pytest.fixture def serialize_part_xml_(self, request): - return function_mock( - request, 'docx.opc.part.serialize_part_xml' - ) + return function_mock(request, "docx.opc.part.serialize_part_xml") diff --git a/tests/opc/test_phys_pkg.py b/tests/opc/test_phys_pkg.py index 7e62cfd8e..6de0d868b 100644 --- a/tests/opc/test_phys_pkg.py +++ b/tests/opc/test_phys_pkg.py @@ -1,63 +1,54 @@ -# encoding: utf-8 - -""" -Test suite for docx.opc.phys_pkg module -""" - -from __future__ import absolute_import - -try: - from io import BytesIO # Python 3 -except ImportError: - from StringIO import StringIO as BytesIO +"""Test suite for docx.opc.phys_pkg module.""" import hashlib -import pytest - +import io from zipfile import ZIP_DEFLATED, ZipFile +import pytest + from docx.opc.exceptions import PackageNotFoundError from docx.opc.packuri import PACKAGE_URI, PackURI from docx.opc.phys_pkg import ( - _DirPkgReader, PhysPkgReader, PhysPkgWriter, _ZipPkgReader, _ZipPkgWriter + PhysPkgReader, + PhysPkgWriter, + _DirPkgReader, + _ZipPkgReader, + _ZipPkgWriter, ) from ..unitutil.file import absjoin, test_file_dir -from ..unitutil.mock import class_mock, loose_mock, Mock +from ..unitutil.mock import Mock, class_mock, loose_mock - -test_docx_path = absjoin(test_file_dir, 'test.docx') -dir_pkg_path = absjoin(test_file_dir, 'expanded_docx') +test_docx_path = absjoin(test_file_dir, "test.docx") +dir_pkg_path = absjoin(test_file_dir, "expanded_docx") zip_pkg_path = test_docx_path -class DescribeDirPkgReader(object): - +class DescribeDirPkgReader: def it_is_used_by_PhysPkgReader_when_pkg_is_a_dir(self): phys_reader = PhysPkgReader(dir_pkg_path) assert isinstance(phys_reader, _DirPkgReader) - def it_doesnt_mind_being_closed_even_though_it_doesnt_need_it( - self, dir_reader): + def it_doesnt_mind_being_closed_even_though_it_doesnt_need_it(self, dir_reader): dir_reader.close() def it_can_retrieve_the_blob_for_a_pack_uri(self, dir_reader): - pack_uri = PackURI('/word/document.xml') + pack_uri = PackURI("/word/document.xml") blob = dir_reader.blob_for(pack_uri) sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == '0e62d87ea74ea2b8088fd11ee97b42da9b4c77b0' + assert sha1 == "0e62d87ea74ea2b8088fd11ee97b42da9b4c77b0" def it_can_get_the_content_types_xml(self, dir_reader): sha1 = hashlib.sha1(dir_reader.content_types_xml).hexdigest() - assert sha1 == '89aadbb12882dd3d7340cd47382dc2c73d75dd81' + assert sha1 == "89aadbb12882dd3d7340cd47382dc2c73d75dd81" def it_can_retrieve_the_rels_xml_for_a_source_uri(self, dir_reader): rels_xml = dir_reader.rels_xml_for(PACKAGE_URI) sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == 'ebacdddb3e7843fdd54c2f00bc831551b26ac823' + assert sha1 == "ebacdddb3e7843fdd54c2f00bc831551b26ac823" def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): - partname = PackURI('/ppt/viewProps.xml') + partname = PackURI("/ppt/viewProps.xml") rels_xml = dir_reader.rels_xml_for(partname) assert rels_xml is None @@ -67,32 +58,30 @@ def it_returns_none_when_part_has_no_rels_xml(self, dir_reader): def pkg_file_(self, request): return loose_mock(request) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def dir_reader(self): return _DirPkgReader(dir_pkg_path) -class DescribePhysPkgReader(object): - +class DescribePhysPkgReader: def it_raises_when_pkg_path_is_not_a_package(self): with pytest.raises(PackageNotFoundError): - PhysPkgReader('foobar') + PhysPkgReader("foobar") -class DescribeZipPkgReader(object): - +class DescribeZipPkgReader: def it_is_used_by_PhysPkgReader_when_pkg_is_a_zip(self): phys_reader = PhysPkgReader(zip_pkg_path) assert isinstance(phys_reader, _ZipPkgReader) def it_is_used_by_PhysPkgReader_when_pkg_is_a_stream(self): - with open(zip_pkg_path, 'rb') as stream: + with open(zip_pkg_path, "rb") as stream: phys_reader = PhysPkgReader(stream) assert isinstance(phys_reader, _ZipPkgReader) def it_opens_pkg_file_zip_on_construction(self, ZipFile_, pkg_file_): _ZipPkgReader(pkg_file_) - ZipFile_.assert_called_once_with(pkg_file_, 'r') + ZipFile_.assert_called_once_with(pkg_file_, "r") def it_can_be_closed(self, ZipFile_): # mockery ---------------------- @@ -104,50 +93,47 @@ def it_can_be_closed(self, ZipFile_): zipf.close.assert_called_once_with() def it_can_retrieve_the_blob_for_a_pack_uri(self, phys_reader): - pack_uri = PackURI('/word/document.xml') + pack_uri = PackURI("/word/document.xml") blob = phys_reader.blob_for(pack_uri) sha1 = hashlib.sha1(blob).hexdigest() - assert sha1 == 'b9b4a98bcac7c5a162825b60c3db7df11e02ac5f' + assert sha1 == "b9b4a98bcac7c5a162825b60c3db7df11e02ac5f" def it_has_the_content_types_xml(self, phys_reader): sha1 = hashlib.sha1(phys_reader.content_types_xml).hexdigest() - assert sha1 == 'cd687f67fd6b5f526eedac77cf1deb21968d7245' + assert sha1 == "cd687f67fd6b5f526eedac77cf1deb21968d7245" def it_can_retrieve_rels_xml_for_source_uri(self, phys_reader): rels_xml = phys_reader.rels_xml_for(PACKAGE_URI) sha1 = hashlib.sha1(rels_xml).hexdigest() - assert sha1 == '90965123ed2c79af07a6963e7cfb50a6e2638565' + assert sha1 == "90965123ed2c79af07a6963e7cfb50a6e2638565" def it_returns_none_when_part_has_no_rels_xml(self, phys_reader): - partname = PackURI('/ppt/viewProps.xml') + partname = PackURI("/ppt/viewProps.xml") rels_xml = phys_reader.rels_xml_for(partname) assert rels_xml is None # fixtures --------------------------------------------- - @pytest.fixture(scope='class') - def phys_reader(self, request): + @pytest.fixture(scope="class") + def phys_reader(self): phys_reader = _ZipPkgReader(zip_pkg_path) - request.addfinalizer(phys_reader.close) - return phys_reader + yield phys_reader + phys_reader.close() @pytest.fixture def pkg_file_(self, request): return loose_mock(request) -class DescribeZipPkgWriter(object): - +class DescribeZipPkgWriter: def it_is_used_by_PhysPkgWriter_unconditionally(self, tmp_docx_path): phys_writer = PhysPkgWriter(tmp_docx_path) assert isinstance(phys_writer, _ZipPkgWriter) def it_opens_pkg_file_zip_on_construction(self, ZipFile_): - pkg_file = Mock(name='pkg_file') + pkg_file = Mock(name="pkg_file") _ZipPkgWriter(pkg_file) - ZipFile_.assert_called_once_with( - pkg_file, 'w', compression=ZIP_DEFLATED - ) + ZipFile_.assert_called_once_with(pkg_file, "w", compression=ZIP_DEFLATED) def it_can_be_closed(self, ZipFile_): # mockery ---------------------- @@ -160,15 +146,15 @@ def it_can_be_closed(self, ZipFile_): def it_can_write_a_blob(self, pkg_file): # setup ------------------------ - pack_uri = PackURI('/part/name.xml') - blob = ''.encode('utf-8') + pack_uri = PackURI("/part/name.xml") + blob = "".encode("utf-8") # exercise --------------------- pkg_writer = PhysPkgWriter(pkg_file) pkg_writer.write(pack_uri, blob) pkg_writer.close() # verify ----------------------- written_blob_sha1 = hashlib.sha1(blob).hexdigest() - zipf = ZipFile(pkg_file, 'r') + zipf = ZipFile(pkg_file, "r") retrieved_blob = zipf.read(pack_uri.membername) zipf.close() retrieved_blob_sha1 = hashlib.sha1(retrieved_blob).hexdigest() @@ -177,19 +163,20 @@ def it_can_write_a_blob(self, pkg_file): # fixtures --------------------------------------------- @pytest.fixture - def pkg_file(self, request): - pkg_file = BytesIO() - request.addfinalizer(pkg_file.close) - return pkg_file + def pkg_file(self): + pkg_file = io.BytesIO() + yield pkg_file + pkg_file.close() # fixtures ------------------------------------------------- + @pytest.fixture def tmp_docx_path(tmpdir): - return str(tmpdir.join('test_python-docx.docx')) + return str(tmpdir.join("test_python-docx.docx")) @pytest.fixture def ZipFile_(request): - return class_mock(request, 'docx.opc.phys_pkg.ZipFile') + return class_mock(request, "docx.opc.phys_pkg.ZipFile") diff --git a/tests/opc/test_pkgreader.py b/tests/opc/test_pkgreader.py index 96885efcb..8e14f0e01 100644 --- a/tests/opc/test_pkgreader.py +++ b/tests/opc/test_pkgreader.py @@ -1,25 +1,22 @@ -# encoding: utf-8 - -"""Unit test suite for docx.opc.pkgreader module""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Unit test suite for docx.opc.pkgreader module.""" import pytest -from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TARGET_MODE as RTM +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM from docx.opc.packuri import PackURI from docx.opc.phys_pkg import _ZipPkgReader from docx.opc.pkgreader import ( - _ContentTypeMap, PackageReader, + _ContentTypeMap, _SerializedPart, _SerializedRelationship, _SerializedRelationships, ) -from .unitdata.types import a_Default, a_Types, an_Override from ..unitutil.mock import ( ANY, + Mock, call, class_mock, function_mock, @@ -27,13 +24,12 @@ instance_mock, loose_mock, method_mock, - Mock, patch, ) +from .unitdata.types import a_Default, a_Types, an_Override -class DescribePackageReader(object): - +class DescribePackageReader: def it_can_construct_from_pkg_file( self, _init_, PhysPkgReader_, from_xml, _srels_for, _load_serialized_parts ): @@ -41,13 +37,13 @@ def it_can_construct_from_pkg_file( content_types = from_xml.return_value pkg_srels = _srels_for.return_value sparts = _load_serialized_parts.return_value - pkg_file = Mock(name='pkg_file') + pkg_file = Mock(name="pkg_file") pkg_reader = PackageReader.from_file(pkg_file) PhysPkgReader_.assert_called_once_with(pkg_file) from_xml.assert_called_once_with(phys_reader.content_types_xml) - _srels_for.assert_called_once_with(phys_reader, '/') + _srels_for.assert_called_once_with(phys_reader, "/") _load_serialized_parts.assert_called_once_with( phys_reader, pkg_srels, content_types ) @@ -62,41 +58,40 @@ def it_can_iterate_over_the_serialized_parts(self, iter_sparts_fixture): def it_can_iterate_over_all_the_srels(self): # mockery ---------------------- - pkg_srels = ['srel1', 'srel2'] + pkg_srels = ["srel1", "srel2"] sparts = [ - Mock(name='spart1', partname='pn1', srels=['srel3', 'srel4']), - Mock(name='spart2', partname='pn2', srels=['srel5', 'srel6']), + Mock(name="spart1", partname="pn1", srels=["srel3", "srel4"]), + Mock(name="spart2", partname="pn2", srels=["srel5", "srel6"]), ] pkg_reader = PackageReader(None, pkg_srels, sparts) # exercise --------------------- - generated_tuples = [t for t in pkg_reader.iter_srels()] + generated_tuples = list(pkg_reader.iter_srels()) # verify ----------------------- expected_tuples = [ - ('/', 'srel1'), - ('/', 'srel2'), - ('pn1', 'srel3'), - ('pn1', 'srel4'), - ('pn2', 'srel5'), - ('pn2', 'srel6'), + ("/", "srel1"), + ("/", "srel2"), + ("pn1", "srel3"), + ("pn1", "srel4"), + ("pn2", "srel5"), + ("pn2", "srel6"), ] assert generated_tuples == expected_tuples def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): # test data -------------------- test_data = ( - ('/part/name1.xml', 'app/vnd.type_1', 'reltype1', '', - 'srels_1'), - ('/part/name2.xml', 'app/vnd.type_2', 'reltype2', '', - 'srels_2'), + ("/part/name1.xml", "app/vnd.type_1", "reltype1", "", "srels_1"), + ("/part/name2.xml", "app/vnd.type_2", "reltype2", "", "srels_2"), ) iter_vals = [(t[0], t[2], t[3], t[4]) for t in test_data] - content_types = dict((t[0], t[1]) for t in test_data) + content_types = {t[0]: t[1] for t in test_data} # mockery ---------------------- - phys_reader = Mock(name='phys_reader') - pkg_srels = Mock(name='pkg_srels') + phys_reader = Mock(name="phys_reader") + pkg_srels = Mock(name="pkg_srels") _walk_phys_parts.return_value = iter_vals _SerializedPart_.side_effect = expected_sparts = ( - Mock(name='spart_1'), Mock(name='spart_2') + Mock(name="spart_1"), + Mock(name="spart_2"), ) # exercise --------------------- retval = PackageReader._load_serialized_parts( @@ -104,10 +99,12 @@ def it_can_load_serialized_parts(self, _SerializedPart_, _walk_phys_parts): ) # verify ----------------------- expected_calls = [ - call('/part/name1.xml', 'app/vnd.type_1', '', - 'reltype1', 'srels_1'), - call('/part/name2.xml', 'app/vnd.type_2', '', - 'reltype2', 'srels_2'), + call( + "/part/name1.xml", "app/vnd.type_1", "", "reltype1", "srels_1" + ), + call( + "/part/name2.xml", "app/vnd.type_2", "", "reltype2", "srels_2" + ), ] assert _SerializedPart_.call_args_list == expected_calls assert retval == expected_sparts @@ -123,37 +120,49 @@ def it_can_walk_phys_pkg_parts(self, _srels_for): # | part_2 |---> | part_3 | # +--------+ +--------+ partname_1, partname_2, partname_3 = ( - '/part/name1.xml', '/part/name2.xml', '/part/name3.xml' + "/part/name1.xml", + "/part/name2.xml", + "/part/name3.xml", ) - part_1_blob, part_2_blob, part_3_blob = ( - '', '', '' - ) - reltype1, reltype2, reltype3 = ('reltype1', 'reltype2', 'reltype3') + part_1_blob, part_2_blob, part_3_blob = ("", "", "") + reltype1, reltype2, reltype3 = ("reltype1", "reltype2", "reltype3") srels = [ - Mock(name='rId1', is_external=True), - Mock(name='rId2', is_external=False, reltype=reltype1, - target_partname=partname_1), - Mock(name='rId3', is_external=False, reltype=reltype2, - target_partname=partname_2), - Mock(name='rId4', is_external=False, reltype=reltype1, - target_partname=partname_1), - Mock(name='rId5', is_external=False, reltype=reltype3, - target_partname=partname_3), + Mock(name="rId1", is_external=True), + Mock( + name="rId2", + is_external=False, + reltype=reltype1, + target_partname=partname_1, + ), + Mock( + name="rId3", + is_external=False, + reltype=reltype2, + target_partname=partname_2, + ), + Mock( + name="rId4", + is_external=False, + reltype=reltype1, + target_partname=partname_1, + ), + Mock( + name="rId5", + is_external=False, + reltype=reltype3, + target_partname=partname_3, + ), ] pkg_srels = srels[:2] part_1_srels = srels[2:3] part_2_srels = srels[3:5] part_3_srels = [] # mockery ---------------------- - phys_reader = Mock(name='phys_reader') + phys_reader = Mock(name="phys_reader") _srels_for.side_effect = [part_1_srels, part_2_srels, part_3_srels] - phys_reader.blob_for.side_effect = [ - part_1_blob, part_2_blob, part_3_blob - ] + phys_reader.blob_for.side_effect = [part_1_blob, part_2_blob, part_3_blob] # exercise --------------------- - generated_tuples = list( - PackageReader._walk_phys_parts(phys_reader, pkg_srels) - ) + generated_tuples = list(PackageReader._walk_phys_parts(phys_reader, pkg_srels)) # verify ----------------------- expected_tuples = [ (partname_1, part_1_blob, reltype1, part_1_srels), @@ -162,11 +171,10 @@ def it_can_walk_phys_pkg_parts(self, _srels_for): ] assert generated_tuples == expected_tuples - def it_can_retrieve_srels_for_a_source_uri( - self, _SerializedRelationships_): + def it_can_retrieve_srels_for_a_source_uri(self, _SerializedRelationships_): # mockery ---------------------- - phys_reader = Mock(name='phys_reader') - source_uri = Mock(name='source_uri') + phys_reader = Mock(name="phys_reader") + source_uri = Mock(name="source_uri") rels_xml = phys_reader.rels_xml_for.return_value load_from_xml = _SerializedRelationships_.load_from_xml srels = load_from_xml.return_value @@ -181,19 +189,19 @@ def it_can_retrieve_srels_for_a_source_uri( @pytest.fixture def blobs_(self, request): - blob_ = loose_mock(request, spec=str, name='blob_') - blob_2_ = loose_mock(request, spec=str, name='blob_2_') + blob_ = loose_mock(request, spec=str, name="blob_") + blob_2_ = loose_mock(request, spec=str, name="blob_2_") return blob_, blob_2_ @pytest.fixture def content_types_(self, request): - content_type_ = loose_mock(request, spec=str, name='content_type_') - content_type_2_ = loose_mock(request, spec=str, name='content_type_2_') + content_type_ = loose_mock(request, spec=str, name="content_type_") + content_type_2_ = loose_mock(request, spec=str, name="content_type_2_") return content_type_, content_type_2_ @pytest.fixture def from_xml(self, request): - return method_mock(request, _ContentTypeMap, 'from_xml', autospec=False) + return method_mock(request, _ContentTypeMap, "from_xml", autospec=False) @pytest.fixture def _init_(self, request): @@ -201,7 +209,8 @@ def _init_(self, request): @pytest.fixture def iter_sparts_fixture( - self, sparts_, partnames_, content_types_, reltypes_, blobs_): + self, sparts_, partnames_, content_types_, reltypes_, blobs_ + ): pkg_reader = PackageReader(None, None, sparts_) expected_iter_spart_items = [ (partnames_[0], content_types_[0], reltypes_[0], blobs_[0]), @@ -212,155 +221,157 @@ def iter_sparts_fixture( @pytest.fixture def _load_serialized_parts(self, request): return method_mock( - request, PackageReader, '_load_serialized_parts', autospec=False + request, PackageReader, "_load_serialized_parts", autospec=False ) @pytest.fixture def partnames_(self, request): - partname_ = loose_mock(request, spec=str, name='partname_') - partname_2_ = loose_mock(request, spec=str, name='partname_2_') + partname_ = loose_mock(request, spec=str, name="partname_") + partname_2_ = loose_mock(request, spec=str, name="partname_2_") return partname_, partname_2_ @pytest.fixture - def PhysPkgReader_(self, request): - _patch = patch( - 'docx.opc.pkgreader.PhysPkgReader', spec_set=_ZipPkgReader - ) - request.addfinalizer(_patch.stop) - return _patch.start() + def PhysPkgReader_(self): + p = patch("docx.opc.pkgreader.PhysPkgReader", spec_set=_ZipPkgReader) + yield p.start() + p.stop() @pytest.fixture def reltypes_(self, request): - reltype_ = instance_mock(request, str, name='reltype_') - reltype_2_ = instance_mock(request, str, name='reltype_2') + reltype_ = instance_mock(request, str, name="reltype_") + reltype_2_ = instance_mock(request, str, name="reltype_2") return reltype_, reltype_2_ @pytest.fixture def _SerializedPart_(self, request): - return class_mock(request, 'docx.opc.pkgreader._SerializedPart') + return class_mock(request, "docx.opc.pkgreader._SerializedPart") @pytest.fixture def _SerializedRelationships_(self, request): - return class_mock( - request, 'docx.opc.pkgreader._SerializedRelationships' - ) + return class_mock(request, "docx.opc.pkgreader._SerializedRelationships") @pytest.fixture - def sparts_( - self, request, partnames_, content_types_, reltypes_, blobs_): + def sparts_(self, request, partnames_, content_types_, reltypes_, blobs_): sparts_ = [] for idx in range(2): - name = 'spart_%s' % (('%d_' % (idx+1)) if idx else '') + name = "spart_%s" % (("%d_" % (idx + 1)) if idx else "") spart_ = instance_mock( - request, _SerializedPart, name=name, - partname=partnames_[idx], content_type=content_types_[idx], - reltype=reltypes_[idx], blob=blobs_[idx] + request, + _SerializedPart, + name=name, + partname=partnames_[idx], + content_type=content_types_[idx], + reltype=reltypes_[idx], + blob=blobs_[idx], ) sparts_.append(spart_) return sparts_ @pytest.fixture def _srels_for(self, request): - return method_mock(request, PackageReader, '_srels_for', autospec=False) + return method_mock(request, PackageReader, "_srels_for", autospec=False) @pytest.fixture def _walk_phys_parts(self, request): - return method_mock(request, PackageReader, '_walk_phys_parts', autospec=False) - + return method_mock(request, PackageReader, "_walk_phys_parts", autospec=False) -class Describe_ContentTypeMap(object): +class Describe_ContentTypeMap: def it_can_construct_from_ct_item_xml(self, from_xml_fixture): - content_types_xml, expected_defaults, expected_overrides = ( - from_xml_fixture - ) + content_types_xml, expected_defaults, expected_overrides = from_xml_fixture ct_map = _ContentTypeMap.from_xml(content_types_xml) assert ct_map._defaults == expected_defaults assert ct_map._overrides == expected_overrides def it_matches_an_override_on_case_insensitive_partname( - self, match_override_fixture): + self, match_override_fixture + ): ct_map, partname, content_type = match_override_fixture assert ct_map[partname] == content_type def it_falls_back_to_case_insensitive_extension_default_match( - self, match_default_fixture): + self, match_default_fixture + ): ct_map, partname, content_type = match_default_fixture assert ct_map[partname] == content_type def it_should_raise_on_partname_not_found(self): ct_map = _ContentTypeMap() with pytest.raises(KeyError): - ct_map[PackURI('/!blat/rhumba.1x&')] + ct_map[PackURI("/!blat/rhumba.1x&")] def it_should_raise_on_key_not_instance_of_PackURI(self): ct_map = _ContentTypeMap() - ct_map._overrides = {PackURI('/part/name1.xml'): 'app/vnd.type1'} + ct_map._overrides = {PackURI("/part/name1.xml"): "app/vnd.type1"} with pytest.raises(KeyError): - ct_map['/part/name1.xml'] + ct_map["/part/name1.xml"] # fixtures --------------------------------------------- @pytest.fixture def from_xml_fixture(self): entries = ( - ('Default', 'xml', CT.XML), - ('Default', 'PNG', CT.PNG), - ('Override', '/ppt/presentation.xml', CT.PML_PRESENTATION_MAIN), + ("Default", "xml", CT.XML), + ("Default", "PNG", CT.PNG), + ("Override", "/ppt/presentation.xml", CT.PML_PRESENTATION_MAIN), ) content_types_xml = self._xml_from(entries) expected_defaults = {} expected_overrides = {} for entry in entries: - if entry[0] == 'Default': + if entry[0] == "Default": ext = entry[1].lower() content_type = entry[2] expected_defaults[ext] = content_type - elif entry[0] == 'Override': + elif entry[0] == "Override": partname, content_type = entry[1:] expected_overrides[partname] = content_type return content_types_xml, expected_defaults, expected_overrides - @pytest.fixture(params=[ - ('/foo/bar.xml', 'xml', 'application/xml'), - ('/foo/bar.PNG', 'png', 'image/png'), - ('/foo/bar.jpg', 'JPG', 'image/jpeg'), - ]) + @pytest.fixture( + params=[ + ("/foo/bar.xml", "xml", "application/xml"), + ("/foo/bar.PNG", "png", "image/png"), + ("/foo/bar.jpg", "JPG", "image/jpeg"), + ] + ) def match_default_fixture(self, request): partname_str, ext, content_type = request.param partname = PackURI(partname_str) ct_map = _ContentTypeMap() - ct_map._add_override(PackURI('/bar/foo.xyz'), 'application/xyz') + ct_map._add_override(PackURI("/bar/foo.xyz"), "application/xyz") ct_map._add_default(ext, content_type) return ct_map, partname, content_type - @pytest.fixture(params=[ - ('/foo/bar.xml', '/foo/bar.xml'), - ('/foo/bar.xml', '/FOO/Bar.XML'), - ('/FoO/bAr.XmL', '/foo/bar.xml'), - ]) + @pytest.fixture( + params=[ + ("/foo/bar.xml", "/foo/bar.xml"), + ("/foo/bar.xml", "/FOO/Bar.XML"), + ("/FoO/bAr.XmL", "/foo/bar.xml"), + ] + ) def match_override_fixture(self, request): partname_str, should_match_partname_str = request.param partname = PackURI(partname_str) should_match_partname = PackURI(should_match_partname_str) - content_type = 'appl/vnd-foobar' + content_type = "appl/vnd-foobar" ct_map = _ContentTypeMap() ct_map._add_override(partname, content_type) return ct_map, should_match_partname, content_type def _xml_from(self, entries): """ - Return XML for a [Content_Types].xml based on items in *entries*. + Return XML for a [Content_Types].xml based on items in `entries`. """ types_bldr = a_Types().with_nsdecls() for entry in entries: - if entry[0] == 'Default': + if entry[0] == "Default": ext, content_type = entry[1:] default_bldr = a_Default() default_bldr.with_Extension(ext) default_bldr.with_ContentType(content_type) types_bldr.with_child(default_bldr) - elif entry[0] == 'Override': + elif entry[0] == "Override": partname, content_type = entry[1:] override_bldr = an_Override() override_bldr.with_PartName(partname) @@ -369,15 +380,14 @@ def _xml_from(self, entries): return types_bldr.xml() -class Describe_SerializedPart(object): - +class Describe_SerializedPart: def it_remembers_construction_values(self): # test data -------------------- - partname = '/part/name.xml' - content_type = 'app/vnd.type' - reltype = 'http://rel/type' - blob = '' - srels = 'srels proxy' + partname = "/part/name.xml" + content_type = "app/vnd.type" + reltype = "http://rel/type" + blob = "" + srels = "srels proxy" # exercise --------------------- spart = _SerializedPart(partname, content_type, reltype, blob, srels) # verify ----------------------- @@ -388,43 +398,58 @@ def it_remembers_construction_values(self): assert spart.srels == srels -class Describe_SerializedRelationship(object): - +class Describe_SerializedRelationship: def it_remembers_construction_values(self): # test data -------------------- rel_elm = Mock( - name='rel_elm', rId='rId9', reltype='ReLtYpE', - target_ref='docProps/core.xml', target_mode=RTM.INTERNAL + name="rel_elm", + rId="rId9", + reltype="ReLtYpE", + target_ref="docProps/core.xml", + target_mode=RTM.INTERNAL, ) # exercise --------------------- - srel = _SerializedRelationship('/', rel_elm) + srel = _SerializedRelationship("/", rel_elm) # verify ----------------------- - assert srel.rId == 'rId9' - assert srel.reltype == 'ReLtYpE' - assert srel.target_ref == 'docProps/core.xml' + assert srel.rId == "rId9" + assert srel.reltype == "ReLtYpE" + assert srel.target_ref == "docProps/core.xml" assert srel.target_mode == RTM.INTERNAL def it_knows_when_it_is_external(self): - cases = (RTM.INTERNAL, RTM.EXTERNAL, 'FOOBAR') + cases = (RTM.INTERNAL, RTM.EXTERNAL, "FOOBAR") expected_values = (False, True, False) for target_mode, expected_value in zip(cases, expected_values): - rel_elm = Mock(name='rel_elm', rId=None, reltype=None, - target_ref=None, target_mode=target_mode) + rel_elm = Mock( + name="rel_elm", + rId=None, + reltype=None, + target_ref=None, + target_mode=target_mode, + ) srel = _SerializedRelationship(None, rel_elm) assert srel.is_external is expected_value def it_can_calculate_its_target_partname(self): # test data -------------------- cases = ( - ('/', 'docProps/core.xml', '/docProps/core.xml'), - ('/ppt', 'viewProps.xml', '/ppt/viewProps.xml'), - ('/ppt/slides', '../slideLayouts/slideLayout1.xml', - '/ppt/slideLayouts/slideLayout1.xml'), + ("/", "docProps/core.xml", "/docProps/core.xml"), + ("/ppt", "viewProps.xml", "/ppt/viewProps.xml"), + ( + "/ppt/slides", + "../slideLayouts/slideLayout1.xml", + "/ppt/slideLayouts/slideLayout1.xml", + ), ) for baseURI, target_ref, expected_partname in cases: # setup -------------------- - rel_elm = Mock(name='rel_elm', rId=None, reltype=None, - target_ref=target_ref, target_mode=RTM.INTERNAL) + rel_elm = Mock( + name="rel_elm", + rId=None, + reltype=None, + target_ref=target_ref, + target_mode=RTM.INTERNAL, + ) # exercise ----------------- srel = _SerializedRelationship(baseURI, rel_elm) # verify ------------------- @@ -432,29 +457,30 @@ def it_can_calculate_its_target_partname(self): def it_raises_on_target_partname_when_external(self): rel_elm = Mock( - name='rel_elm', rId='rId9', reltype='ReLtYpE', - target_ref='docProps/core.xml', target_mode=RTM.EXTERNAL + name="rel_elm", + rId="rId9", + reltype="ReLtYpE", + target_ref="docProps/core.xml", + target_mode=RTM.EXTERNAL, ) - srel = _SerializedRelationship('/', rel_elm) - with pytest.raises(ValueError): + srel = _SerializedRelationship("/", rel_elm) + with pytest.raises(ValueError, match="target_partname attribute on Relat"): srel.target_partname -class Describe_SerializedRelationships(object): - +class Describe_SerializedRelationships: def it_can_load_from_xml(self, parse_xml_, _SerializedRelationship_): # mockery ---------------------- baseURI, rels_item_xml, rel_elm_1, rel_elm_2 = ( - Mock(name='baseURI'), Mock(name='rels_item_xml'), - Mock(name='rel_elm_1'), Mock(name='rel_elm_2'), - ) - rels_elm = Mock( - name='rels_elm', Relationship_lst=[rel_elm_1, rel_elm_2] + Mock(name="baseURI"), + Mock(name="rels_item_xml"), + Mock(name="rel_elm_1"), + Mock(name="rel_elm_2"), ) + rels_elm = Mock(name="rels_elm", Relationship_lst=[rel_elm_1, rel_elm_2]) parse_xml_.return_value = rels_elm # exercise --------------------- - srels = _SerializedRelationships.load_from_xml( - baseURI, rels_item_xml) + srels = _SerializedRelationships.load_from_xml(baseURI, rels_item_xml) # verify ----------------------- expected_calls = [ call(baseURI, rel_elm_1), @@ -477,10 +503,8 @@ def it_should_be_iterable(self): @pytest.fixture def parse_xml_(self, request): - return function_mock(request, 'docx.opc.pkgreader.parse_xml') + return function_mock(request, "docx.opc.pkgreader.parse_xml") @pytest.fixture def _SerializedRelationship_(self, request): - return class_mock( - request, 'docx.opc.pkgreader._SerializedRelationship' - ) + return class_mock(request, "docx.opc.pkgreader._SerializedRelationship") diff --git a/tests/opc/test_pkgwriter.py b/tests/opc/test_pkgwriter.py index d119748dd..747300f82 100644 --- a/tests/opc/test_pkgwriter.py +++ b/tests/opc/test_pkgwriter.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for opc.pkgwriter module -""" +"""Test suite for opc.pkgwriter module.""" import pytest @@ -10,21 +6,26 @@ from docx.opc.packuri import PackURI from docx.opc.part import Part from docx.opc.phys_pkg import _ZipPkgWriter -from docx.opc.pkgwriter import _ContentTypesItem, PackageWriter +from docx.opc.pkgwriter import PackageWriter, _ContentTypesItem -from .unitdata.types import a_Default, a_Types, an_Override from ..unitutil.mock import ( - call, class_mock, instance_mock, MagicMock, method_mock, Mock, patch + MagicMock, + Mock, + call, + class_mock, + instance_mock, + method_mock, + patch, ) +from .unitdata.types import a_Default, a_Types, an_Override -class DescribePackageWriter(object): - +class DescribePackageWriter: def it_can_write_a_package(self, PhysPkgWriter_, _write_methods): # mockery ---------------------- - pkg_file = Mock(name='pkg_file') - pkg_rels = Mock(name='pkg_rels') - parts = Mock(name='parts') + pkg_file = Mock(name="pkg_file") + pkg_rels = Mock(name="pkg_rels") + parts = Mock(name="parts") phys_writer = PhysPkgWriter_.return_value # exercise --------------------- PackageWriter.write(pkg_file, pkg_rels, parts) @@ -39,32 +40,27 @@ def it_can_write_a_package(self, PhysPkgWriter_, _write_methods): phys_writer.close.assert_called_once_with() def it_can_write_a_content_types_stream(self, write_cti_fixture): - _ContentTypesItem_, parts_, phys_pkg_writer_, blob_ = ( - write_cti_fixture - ) + _ContentTypesItem_, parts_, phys_pkg_writer_, blob_ = write_cti_fixture PackageWriter._write_content_types_stream(phys_pkg_writer_, parts_) _ContentTypesItem_.from_parts.assert_called_once_with(parts_) - phys_pkg_writer_.write.assert_called_once_with( - '/[Content_Types].xml', blob_ - ) + phys_pkg_writer_.write.assert_called_once_with("/[Content_Types].xml", blob_) def it_can_write_a_pkg_rels_item(self): # mockery ---------------------- - phys_writer = Mock(name='phys_writer') - pkg_rels = Mock(name='pkg_rels') + phys_writer = Mock(name="phys_writer") + pkg_rels = Mock(name="pkg_rels") # exercise --------------------- PackageWriter._write_pkg_rels(phys_writer, pkg_rels) # verify ----------------------- - phys_writer.write.assert_called_once_with('/_rels/.rels', - pkg_rels.xml) + phys_writer.write.assert_called_once_with("/_rels/.rels", pkg_rels.xml) def it_can_write_a_list_of_parts(self): # mockery ---------------------- - phys_writer = Mock(name='phys_writer') - rels = MagicMock(name='rels') + phys_writer = Mock(name="phys_writer") + rels = MagicMock(name="rels") rels.__len__.return_value = 1 - part1 = Mock(name='part1', _rels=rels) - part2 = Mock(name='part2', _rels=[]) + part1 = Mock(name="part1", _rels=rels) + part2 = Mock(name="part2", _rels=[]) # exercise --------------------- PackageWriter._write_parts(phys_writer, [part1, part2]) # verify ----------------------- @@ -87,9 +83,7 @@ def cti_(self, request, blob_): @pytest.fixture def _ContentTypesItem_(self, request, cti_): - _ContentTypesItem_ = class_mock( - request, 'docx.opc.pkgwriter._ContentTypesItem' - ) + _ContentTypesItem_ = class_mock(request, "docx.opc.pkgwriter._ContentTypesItem") _ContentTypesItem_.from_parts.return_value = cti_ return _ContentTypesItem_ @@ -98,46 +92,42 @@ def parts_(self, request): return instance_mock(request, list) @pytest.fixture - def PhysPkgWriter_(self, request): - _patch = patch('docx.opc.pkgwriter.PhysPkgWriter') - request.addfinalizer(_patch.stop) - return _patch.start() + def PhysPkgWriter_(self): + p = patch("docx.opc.pkgwriter.PhysPkgWriter") + yield p.start() + p.stop() @pytest.fixture def phys_pkg_writer_(self, request): return instance_mock(request, _ZipPkgWriter) @pytest.fixture - def write_cti_fixture( - self, _ContentTypesItem_, parts_, phys_pkg_writer_, blob_): + def write_cti_fixture(self, _ContentTypesItem_, parts_, phys_pkg_writer_, blob_): return _ContentTypesItem_, parts_, phys_pkg_writer_, blob_ @pytest.fixture - def _write_methods(self, request): + def _write_methods(self): """Mock that patches all the _write_* methods of PackageWriter""" - root_mock = Mock(name='PackageWriter') - patch1 = patch.object(PackageWriter, '_write_content_types_stream') - patch2 = patch.object(PackageWriter, '_write_pkg_rels') - patch3 = patch.object(PackageWriter, '_write_parts') - root_mock.attach_mock(patch1.start(), '_write_content_types_stream') - root_mock.attach_mock(patch2.start(), '_write_pkg_rels') - root_mock.attach_mock(patch3.start(), '_write_parts') - - def fin(): - patch1.stop() - patch2.stop() - patch3.stop() - - request.addfinalizer(fin) - return root_mock + root_mock = Mock(name="PackageWriter") + patch1 = patch.object(PackageWriter, "_write_content_types_stream") + patch2 = patch.object(PackageWriter, "_write_pkg_rels") + patch3 = patch.object(PackageWriter, "_write_parts") + root_mock.attach_mock(patch1.start(), "_write_content_types_stream") + root_mock.attach_mock(patch2.start(), "_write_pkg_rels") + root_mock.attach_mock(patch3.start(), "_write_parts") + + yield root_mock + + patch1.stop() + patch2.stop() + patch3.stop() @pytest.fixture def xml_for_(self, request): - return method_mock(request, _ContentTypesItem, 'xml_for') + return method_mock(request, _ContentTypesItem, "xml_for") -class Describe_ContentTypesItem(object): - +class Describe_ContentTypesItem: def it_can_compose_content_types_element(self, xml_for_fixture): cti, expected_xml = xml_for_fixture types_elm = cti._element @@ -148,41 +138,41 @@ def it_can_compose_content_types_element(self, xml_for_fixture): def _mock_part(self, request, name, partname_str, content_type): partname = PackURI(partname_str) return instance_mock( - request, Part, name=name, partname=partname, - content_type=content_type + request, Part, name=name, partname=partname, content_type=content_type ) - @pytest.fixture(params=[ - ('Default', '/ppt/MEDIA/image.PNG', CT.PNG), - ('Default', '/ppt/media/image.xml', CT.XML), - ('Default', '/ppt/media/image.rels', CT.OPC_RELATIONSHIPS), - ('Default', '/ppt/media/image.jpeg', CT.JPEG), - ('Override', '/docProps/core.xml', 'app/vnd.core'), - ('Override', '/ppt/slides/slide1.xml', 'app/vnd.ct_sld'), - ('Override', '/zebra/foo.bar', 'app/vnd.foobar'), - ]) + @pytest.fixture( + params=[ + ("Default", "/ppt/MEDIA/image.PNG", CT.PNG), + ("Default", "/ppt/media/image.xml", CT.XML), + ("Default", "/ppt/media/image.rels", CT.OPC_RELATIONSHIPS), + ("Default", "/ppt/media/image.jpeg", CT.JPEG), + ("Override", "/docProps/core.xml", "app/vnd.core"), + ("Override", "/ppt/slides/slide1.xml", "app/vnd.ct_sld"), + ("Override", "/zebra/foo.bar", "app/vnd.foobar"), + ] + ) def xml_for_fixture(self, request): elm_type, partname_str, content_type = request.param - part_ = self._mock_part(request, 'part_', partname_str, content_type) + part_ = self._mock_part(request, "part_", partname_str, content_type) cti = _ContentTypesItem.from_parts([part_]) # expected_xml ----------------- types_bldr = a_Types().with_nsdecls() - ext = partname_str.split('.')[-1].lower() - if elm_type == 'Default' and ext not in ('rels', 'xml'): + ext = partname_str.split(".")[-1].lower() + if elm_type == "Default" and ext not in ("rels", "xml"): default_bldr = a_Default() default_bldr.with_Extension(ext) default_bldr.with_ContentType(content_type) types_bldr.with_child(default_bldr) types_bldr.with_child( - a_Default().with_Extension('rels') - .with_ContentType(CT.OPC_RELATIONSHIPS) + a_Default().with_Extension("rels").with_ContentType(CT.OPC_RELATIONSHIPS) ) types_bldr.with_child( - a_Default().with_Extension('xml').with_ContentType(CT.XML) + a_Default().with_Extension("xml").with_ContentType(CT.XML) ) - if elm_type == 'Override': + if elm_type == "Override": override_bldr = an_Override() override_bldr.with_PartName(partname_str) override_bldr.with_ContentType(content_type) diff --git a/tests/opc/test_rel.py b/tests/opc/test_rel.py index db9b52b59..7b7a98dfe 100644 --- a/tests/opc/test_rel.py +++ b/tests/opc/test_rel.py @@ -1,32 +1,23 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Unit test suite for the docx.opc.rel module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Unit test suite for the docx.opc.rel module.""" import pytest from docx.opc.oxml import CT_Relationships from docx.opc.packuri import PackURI from docx.opc.part import Part -from docx.opc.rel import _Relationship, Relationships - -from ..unitutil.mock import ( - call, class_mock, instance_mock, Mock, patch, PropertyMock -) +from docx.opc.rel import Relationships, _Relationship +from ..unitutil.mock import Mock, PropertyMock, call, class_mock, instance_mock, patch -class Describe_Relationship(object): +class Describe_Relationship: def it_remembers_construction_values(self): # test data -------------------- - rId = 'rId9' - reltype = 'reltype' - target = Mock(name='target_part') + rId = "rId9" + reltype = "reltype" + target = Mock(name="target_part") external = False # exercise --------------------- rel = _Relationship(rId, reltype, target, None, external) @@ -38,12 +29,12 @@ def it_remembers_construction_values(self): def it_should_raise_on_target_part_access_on_external_rel(self): rel = _Relationship(None, None, None, None, external=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="target_part property on _Relat"): rel.target_part def it_should_have_target_ref_for_external_rel(self): - rel = _Relationship(None, None, 'target', None, external=True) - assert rel.target_ref == 'target' + rel = _Relationship(None, None, "target", None, external=True) + assert rel.target_ref == "target" def it_should_have_relative_ref_for_internal_rel(self): """ @@ -51,23 +42,24 @@ def it_should_have_relative_ref_for_internal_rel(self): have a relative ref, e.g. '../slideLayouts/slideLayout1.xml', for the target_ref attribute. """ - part = Mock(name='part', partname=PackURI('/ppt/media/image1.png')) - baseURI = '/ppt/slides' + part = Mock(name="part", partname=PackURI("/ppt/media/image1.png")) + baseURI = "/ppt/slides" rel = _Relationship(None, None, part, baseURI) # external=False - assert rel.target_ref == '../media/image1.png' - + assert rel.target_ref == "../media/image1.png" -class DescribeRelationships(object): +class DescribeRelationships: def it_can_add_a_relationship(self, _Relationship_): baseURI, rId, reltype, target, external = ( - 'baseURI', 'rId9', 'reltype', 'target', False + "baseURI", + "rId9", + "reltype", + "target", + False, ) rels = Relationships(baseURI) rel = rels.add_relationship(reltype, target, rId, external) - _Relationship_.assert_called_once_with( - rId, reltype, target, baseURI, external - ) + _Relationship_.assert_called_once_with(rId, reltype, target, baseURI, external) assert rels[rId] == rel assert rel == _Relationship_.return_value @@ -80,14 +72,14 @@ def it_can_add_an_external_relationship(self, add_ext_rel_fixture_): assert rel.reltype == reltype def it_can_find_a_relationship_by_rId(self): - rel = Mock(name='rel', rId='foobar') + rel = Mock(name="rel", rId="foobar") rels = Relationships(None) - rels['foobar'] = rel - assert rels['foobar'] == rel + rels["foobar"] = rel + assert rels["foobar"] == rel def it_can_find_or_add_a_relationship( - self, rels_with_matching_rel_, rels_with_missing_rel_): - + self, rels_with_matching_rel_, rels_with_missing_rel_ + ): rels, reltype, part, matching_rel = rels_with_matching_rel_ assert rels.get_or_add(reltype, part) == matching_rel @@ -95,7 +87,8 @@ def it_can_find_or_add_a_relationship( assert rels.get_or_add(reltype, part) == new_rel def it_can_find_or_add_an_external_relationship( - self, add_matching_ext_rel_fixture_): + self, add_matching_ext_rel_fixture_ + ): rels, reltype, url, rId = add_matching_ext_rel_fixture_ _rId = rels.get_or_add_ext_rel(reltype, url) assert _rId == rId @@ -108,10 +101,9 @@ def it_can_find_a_related_part_by_rId(self, rels_with_known_target_part): def it_raises_on_related_part_not_found(self, rels): with pytest.raises(KeyError): - rels.related_parts['rId666'] + rels.related_parts["rId666"] - def it_can_find_a_related_part_by_reltype( - self, rels_with_target_known_by_reltype): + def it_can_find_a_related_part_by_reltype(self, rels_with_target_known_by_reltype): rels, reltype, known_target_part = rels_with_target_known_by_reltype part = rels.part_with_reltype(reltype) assert part is known_target_part @@ -122,15 +114,11 @@ def it_can_compose_rels_xml(self, rels, rels_elm): # verify ----------------------- rels_elm.assert_has_calls( [ - call.add_rel( - 'rId1', 'http://rt-hyperlink', 'http://some/link', True - ), - call.add_rel( - 'rId2', 'http://rt-image', '../media/image1.png', False - ), - call.xml() + call.add_rel("rId1", "http://rt-hyperlink", "http://some/link", True), + call.add_rel("rId2", "http://rt-image", "../media/image1.png", False), + call.xml(), ], - any_order=True + any_order=True, ) def it_knows_the_next_available_rId_to_help(self, rels_with_rId_gap): @@ -147,7 +135,7 @@ def add_ext_rel_fixture_(self, reltype, url): @pytest.fixture def add_matching_ext_rel_fixture_(self, request, reltype, url): - rId = 'rId369' + rId = "rId369" rels = Relationships(None) rels.add_relationship(reltype, url, rId, is_external=True) return rels, reltype, url, rId @@ -156,15 +144,14 @@ def add_matching_ext_rel_fixture_(self, request, reltype, url): @pytest.fixture def _baseURI(self): - return '/baseURI' + return "/baseURI" @pytest.fixture def _Relationship_(self, request): - return class_mock(request, 'docx.opc.rel._Relationship') + return class_mock(request, "docx.opc.rel._Relationship") @pytest.fixture - def _rel_with_target_known_by_reltype( - self, _rId, reltype, _target_part, _baseURI): + def _rel_with_target_known_by_reltype(self, _rId, reltype, _target_part, _baseURI): rel = _Relationship(_rId, reltype, _target_part, _baseURI) return rel, reltype, _target_part @@ -174,38 +161,38 @@ def rels(self): Populated Relationships instance that will exercise the rels.xml property. """ - rels = Relationships('/baseURI') + rels = Relationships("/baseURI") rels.add_relationship( - reltype='http://rt-hyperlink', target='http://some/link', - rId='rId1', is_external=True + reltype="http://rt-hyperlink", + target="http://some/link", + rId="rId1", + is_external=True, ) - part = Mock(name='part') - part.partname.relative_ref.return_value = '../media/image1.png' - rels.add_relationship(reltype='http://rt-image', target=part, - rId='rId2') + part = Mock(name="part") + part.partname.relative_ref.return_value = "../media/image1.png" + rels.add_relationship(reltype="http://rt-image", target=part, rId="rId2") return rels @pytest.fixture - def rels_elm(self, request): + def rels_elm(self): """ Return a rels_elm mock that will be returned from CT_Relationships.new() """ # create rels_elm mock with a .xml property - rels_elm = Mock(name='rels_elm') - xml = PropertyMock(name='xml') + rels_elm = Mock(name="rels_elm") + xml = PropertyMock(name="xml") type(rels_elm).xml = xml - rels_elm.attach_mock(xml, 'xml') + rels_elm.attach_mock(xml, "xml") rels_elm.reset_mock() # to clear attach_mock call # patch CT_Relationships to return that rels_elm - patch_ = patch.object(CT_Relationships, 'new', return_value=rels_elm) + patch_ = patch.object(CT_Relationships, "new", return_value=rels_elm) patch_.start() - request.addfinalizer(patch_.stop) - return rels_elm + yield rels_elm + patch_.stop() @pytest.fixture - def _rel_with_known_target_part( - self, _rId, reltype, _target_part, _baseURI): + def _rel_with_known_target_part(self, _rId, reltype, _target_part, _baseURI): rel = _Relationship(_rId, reltype, _target_part, _baseURI) return rel, _rId, _target_part @@ -217,32 +204,30 @@ def rels_with_known_target_part(self, rels, _rel_with_known_target_part): @pytest.fixture def rels_with_matching_rel_(self, request, rels): - matching_reltype_ = instance_mock( - request, str, name='matching_reltype_' - ) - matching_part_ = instance_mock( - request, Part, name='matching_part_' - ) + matching_reltype_ = instance_mock(request, str, name="matching_reltype_") + matching_part_ = instance_mock(request, Part, name="matching_part_") matching_rel_ = instance_mock( - request, _Relationship, name='matching_rel_', - reltype=matching_reltype_, target_part=matching_part_, - is_external=False + request, + _Relationship, + name="matching_rel_", + reltype=matching_reltype_, + target_part=matching_part_, + is_external=False, ) rels[1] = matching_rel_ return rels, matching_reltype_, matching_part_, matching_rel_ @pytest.fixture def rels_with_missing_rel_(self, request, rels, _Relationship_): - missing_reltype_ = instance_mock( - request, str, name='missing_reltype_' - ) - missing_part_ = instance_mock( - request, Part, name='missing_part_' - ) + missing_reltype_ = instance_mock(request, str, name="missing_reltype_") + missing_part_ = instance_mock(request, Part, name="missing_part_") new_rel_ = instance_mock( - request, _Relationship, name='new_rel_', - reltype=missing_reltype_, target_part=missing_part_, - is_external=False + request, + _Relationship, + name="new_rel_", + reltype=missing_reltype_, + target_part=missing_part_, + is_external=False, ) _Relationship_.return_value = new_rel_ return rels, missing_reltype_, missing_part_, new_rel_ @@ -251,29 +236,30 @@ def rels_with_missing_rel_(self, request, rels, _Relationship_): def rels_with_rId_gap(self, request): rels = Relationships(None) rel_with_rId1 = instance_mock( - request, _Relationship, name='rel_with_rId1', rId='rId1' + request, _Relationship, name="rel_with_rId1", rId="rId1" ) rel_with_rId3 = instance_mock( - request, _Relationship, name='rel_with_rId3', rId='rId3' + request, _Relationship, name="rel_with_rId3", rId="rId3" ) - rels['rId1'] = rel_with_rId1 - rels['rId3'] = rel_with_rId3 - return rels, 'rId2' + rels["rId1"] = rel_with_rId1 + rels["rId3"] = rel_with_rId3 + return rels, "rId2" @pytest.fixture def rels_with_target_known_by_reltype( - self, rels, _rel_with_target_known_by_reltype): + self, rels, _rel_with_target_known_by_reltype + ): rel, reltype, target_part = _rel_with_target_known_by_reltype rels[1] = rel return rels, reltype, target_part @pytest.fixture def reltype(self): - return 'http://rel/type' + return "http://rel/type" @pytest.fixture def _rId(self): - return 'rId6' + return "rId6" @pytest.fixture def _target_part(self, request): @@ -281,4 +267,4 @@ def _target_part(self, request): @pytest.fixture def url(self): - return 'https://github.com/scanny/python-docx' + return "https://github.com/scanny/python-docx" diff --git a/tests/opc/unitdata/rels.py b/tests/opc/unitdata/rels.py index 94e45167e..268216afe 100644 --- a/tests/opc/unitdata/rels.py +++ b/tests/opc/unitdata/rels.py @@ -1,38 +1,33 @@ -# encoding: utf-8 - -""" -Test data for relationship-related unit tests. -""" - -from __future__ import absolute_import - -from docx.opc.constants import RELATIONSHIP_TYPE as RT -from docx.opc.rel import Relationships +"""Test data for relationship-related unit tests.""" from docx.opc.constants import NAMESPACE as NS +from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.oxml import parse_xml +from docx.opc.rel import Relationships -class BaseBuilder(object): +class BaseBuilder: """ Provides common behavior for all data builders. """ + @property def element(self): """Return element based on XML generated by builder""" return parse_xml(self.xml) def with_indent(self, indent): - """Add integer *indent* spaces at beginning of element XML""" + """Add integer `indent` spaces at beginning of element XML""" self._indent = indent return self -class RelationshipsBuilder(object): +class RelationshipsBuilder: """Builder class for test Relationships""" + partname_tmpls = { - RT.SLIDE_MASTER: '/ppt/slideMasters/slideMaster%d.xml', - RT.SLIDE: '/ppt/slides/slide%d.xml', + RT.SLIDE_MASTER: "/ppt/slideMasters/slideMaster%d.xml", + RT.SLIDE: "/ppt/slides/slide%d.xml", } def __init__(self): @@ -49,7 +44,7 @@ def _next_partnum(self, reltype): @property def next_rId(self): - rId = 'rId%d' % self.next_rel_num + rId = "rId%d" % self.next_rel_num self.next_rel_num += 1 return rId @@ -70,35 +65,35 @@ class CT_DefaultBuilder(BaseBuilder): Test data builder for CT_Default (Default) XML element that appears in `[Content_Types].xml`. """ + def __init__(self): """Establish instance variables with default values""" - self._content_type = 'application/xml' - self._extension = 'xml' + self._content_type = "application/xml" + self._extension = "xml" self._indent = 0 self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES def with_content_type(self, content_type): - """Set ContentType attribute to *content_type*""" + """Set ContentType attribute to `content_type`""" self._content_type = content_type return self def with_extension(self, extension): - """Set Extension attribute to *extension*""" + """Set Extension attribute to `extension`""" self._extension = extension return self def without_namespace(self): """Don't include an 'xmlns=' attribute""" - self._namespace = '' + self._namespace = "" return self @property def xml(self): """Return Default element""" tmpl = '%s\n' - indent = ' ' * self._indent - return tmpl % (indent, self._namespace, self._extension, - self._content_type) + indent = " " * self._indent + return tmpl % (indent, self._namespace, self._extension, self._content_type) class CT_OverrideBuilder(BaseBuilder): @@ -106,35 +101,35 @@ class CT_OverrideBuilder(BaseBuilder): Test data builder for CT_Override (Override) XML element that appears in `[Content_Types].xml`. """ + def __init__(self): """Establish instance variables with default values""" - self._content_type = 'app/vnd.type' + self._content_type = "app/vnd.type" self._indent = 0 self._namespace = ' xmlns="%s"' % NS.OPC_CONTENT_TYPES - self._partname = '/part/name.xml' + self._partname = "/part/name.xml" def with_content_type(self, content_type): - """Set ContentType attribute to *content_type*""" + """Set ContentType attribute to `content_type`""" self._content_type = content_type return self def with_partname(self, partname): - """Set PartName attribute to *partname*""" + """Set PartName attribute to `partname`""" self._partname = partname return self def without_namespace(self): """Don't include an 'xmlns=' attribute""" - self._namespace = '' + self._namespace = "" return self @property def xml(self): """Return Override element""" tmpl = '%s\n' - indent = ' ' * self._indent - return tmpl % (indent, self._namespace, self._partname, - self._content_type) + indent = " " * self._indent + return tmpl % (indent, self._namespace, self._partname, self._content_type) class CT_RelationshipBuilder(BaseBuilder): @@ -142,53 +137,60 @@ class CT_RelationshipBuilder(BaseBuilder): Test data builder for CT_Relationship (Relationship) XML element that appears in .rels files """ + def __init__(self): """Establish instance variables with default values""" - self._rId = 'rId9' - self._reltype = 'ReLtYpE' - self._target = 'docProps/core.xml' + self._rId = "rId9" + self._reltype = "ReLtYpE" + self._target = "docProps/core.xml" self._target_mode = None self._indent = 0 self._namespace = ' xmlns="%s"' % NS.OPC_RELATIONSHIPS def with_rId(self, rId): - """Set Id attribute to *rId*""" + """Set Id attribute to `rId`""" self._rId = rId return self def with_reltype(self, reltype): - """Set Type attribute to *reltype*""" + """Set Type attribute to `reltype`""" self._reltype = reltype return self def with_target(self, target): - """Set XXX attribute to *target*""" + """Set XXX attribute to `target`""" self._target = target return self def with_target_mode(self, target_mode): - """Set TargetMode attribute to *target_mode*""" - self._target_mode = None if target_mode == 'Internal' else target_mode + """Set TargetMode attribute to `target_mode`""" + self._target_mode = None if target_mode == "Internal" else target_mode return self def without_namespace(self): """Don't include an 'xmlns=' attribute""" - self._namespace = '' + self._namespace = "" return self @property def target_mode(self): if self._target_mode is None: - return '' + return "" return ' TargetMode="%s"' % self._target_mode @property def xml(self): """Return Relationship element""" tmpl = '%s\n' - indent = ' ' * self._indent - return tmpl % (indent, self._namespace, self._rId, self._reltype, - self._target, self.target_mode) + indent = " " * self._indent + return tmpl % ( + indent, + self._namespace, + self._rId, + self._reltype, + self._target, + self.target_mode, + ) class CT_RelationshipsBuilder(BaseBuilder): @@ -196,12 +198,13 @@ class CT_RelationshipsBuilder(BaseBuilder): Test data builder for CT_Relationships (Relationships) XML element, the root element in .rels files. """ + def __init__(self): """Establish instance variables with default values""" self._rels = ( - ('rId1', 'http://reltype1', 'docProps/core.xml', 'Internal'), - ('rId2', 'http://linktype', 'http://some/link', 'External'), - ('rId3', 'http://reltype2', '../slides/slide1.xml', 'Internal'), + ("rId1", "http://reltype1", "docProps/core.xml", "Internal"), + ("rId2", "http://linktype", "http://some/link", "External"), + ("rId3", "http://reltype2", "../slides/slide1.xml", "Internal"), ) @property @@ -211,14 +214,17 @@ def xml(self): """ xml = '\n' % NS.OPC_RELATIONSHIPS for rId, reltype, target, target_mode in self._rels: - xml += (a_Relationship().with_rId(rId) - .with_reltype(reltype) - .with_target(target) - .with_target_mode(target_mode) - .with_indent(2) - .without_namespace() - .xml) - xml += '\n' + xml += ( + a_Relationship() + .with_rId(rId) + .with_reltype(reltype) + .with_target(target) + .with_target_mode(target_mode) + .with_indent(2) + .without_namespace() + .xml + ) + xml += "\n" return xml @@ -227,17 +233,18 @@ class CT_TypesBuilder(BaseBuilder): Test data builder for CT_Types () XML element, the root element in [Content_Types].xml files """ + def __init__(self): """Establish instance variables with default values""" self._defaults = ( - ('xml', 'application/xml'), - ('jpeg', 'image/jpeg'), + ("xml", "application/xml"), + ("jpeg", "image/jpeg"), ) self._empty = False self._overrides = ( - ('/docProps/core.xml', 'app/vnd.type1'), - ('/ppt/presentation.xml', 'app/vnd.type2'), - ('/docProps/thumbnail.jpeg', 'image/jpeg'), + ("/docProps/core.xml", "app/vnd.type1"), + ("/ppt/presentation.xml", "app/vnd.type2"), + ("/docProps/thumbnail.jpeg", "image/jpeg"), ) def empty(self): @@ -254,18 +261,24 @@ def xml(self): xml = '\n' % NS.OPC_CONTENT_TYPES for extension, content_type in self._defaults: - xml += (a_Default().with_extension(extension) - .with_content_type(content_type) - .with_indent(2) - .without_namespace() - .xml) + xml += ( + a_Default() + .with_extension(extension) + .with_content_type(content_type) + .with_indent(2) + .without_namespace() + .xml + ) for partname, content_type in self._overrides: - xml += (an_Override().with_partname(partname) - .with_content_type(content_type) - .with_indent(2) - .without_namespace() - .xml) - xml += '\n' + xml += ( + an_Override() + .with_partname(partname) + .with_content_type(content_type) + .with_indent(2) + .without_namespace() + .xml + ) + xml += "\n" return xml diff --git a/tests/opc/unitdata/types.py b/tests/opc/unitdata/types.py index 82864a965..206ba74a3 100644 --- a/tests/opc/unitdata/types.py +++ b/tests/opc/unitdata/types.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -XML test data builders for [Content_Types].xml elements -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""XML test data builders for [Content_Types].xml elements.""" from docx.opc.oxml import nsmap @@ -12,24 +6,24 @@ class CT_DefaultBuilder(BaseBuilder): - __tag__ = 'Default' - __nspfxs__ = ('ct',) - __attrs__ = ('Extension', 'ContentType') + __tag__ = "Default" + __nspfxs__ = ("ct",) + __attrs__ = ("Extension", "ContentType") class CT_OverrideBuilder(BaseBuilder): - __tag__ = 'Override' - __nspfxs__ = ('ct',) - __attrs__ = ('PartName', 'ContentType') + __tag__ = "Override" + __nspfxs__ = ("ct",) + __attrs__ = ("PartName", "ContentType") class CT_TypesBuilder(BaseBuilder): - __tag__ = 'Types' - __nspfxs__ = ('ct',) + __tag__ = "Types" + __nspfxs__ = ("ct",) __attrs__ = () def with_nsdecls(self, *nspfxs): - self._nsdecls = ' xmlns="%s"' % nsmap['ct'] + self._nsdecls = ' xmlns="%s"' % nsmap["ct"] return self diff --git a/tests/oxml/parts/test_document.py b/tests/oxml/parts/test_document.py index aa1bc5b05..90b587674 100644 --- a/tests/oxml/parts/test_document.py +++ b/tests/oxml/parts/test_document.py @@ -1,18 +1,11 @@ -# encoding: utf-8 - -""" -Test suite for the docx.oxml.parts module. -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for the docx.oxml.parts module.""" import pytest from ...unitutil.cxml import element, xml -class DescribeCT_Body(object): - +class DescribeCT_Body: def it_can_clear_all_its_content(self, clear_fixture): body, expected_xml = clear_fixture body.clear_content() @@ -26,13 +19,15 @@ def it_can_add_a_section_break(self, section_break_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:body', 'w:body'), - ('w:body/w:p', 'w:body'), - ('w:body/w:tbl', 'w:body'), - ('w:body/w:sectPr', 'w:body/w:sectPr'), - ('w:body/(w:p, w:sectPr)', 'w:body/w:sectPr'), - ]) + @pytest.fixture( + params=[ + ("w:body", "w:body"), + ("w:body/w:p", "w:body"), + ("w:body/w:tbl", "w:body"), + ("w:body/w:sectPr", "w:body/w:sectPr"), + ("w:body/(w:p, w:sectPr)", "w:body/w:sectPr"), + ] + ) def clear_fixture(self, request): before_cxml, after_cxml = request.param body = element(before_cxml) @@ -41,11 +36,11 @@ def clear_fixture(self, request): @pytest.fixture def section_break_fixture(self): - body = element('w:body/w:sectPr/w:type{w:val=foobar}') + body = element("w:body/w:sectPr/w:type{w:val=foobar}") expected_xml = xml( - 'w:body/(' - ' w:p/w:pPr/w:sectPr/w:type{w:val=foobar},' - ' w:sectPr/w:type{w:val=foobar}' - ')' + "w:body/(" + " w:p/w:pPr/w:sectPr/w:type{w:val=foobar}," + " w:sectPr/w:type{w:val=foobar}" + ")" ) return body, expected_xml diff --git a/tests/oxml/parts/unitdata/document.py b/tests/oxml/parts/unitdata/document.py index 27c6ed402..36d7738b8 100644 --- a/tests/oxml/parts/unitdata/document.py +++ b/tests/oxml/parts/unitdata/document.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - -""" -Test data builders for parts XML. -""" +"""Test data builders for parts XML.""" from ....unitdata import BaseBuilder class CT_BodyBuilder(BaseBuilder): - __tag__ = 'w:body' - __nspfxs__ = ('w',) + __tag__ = "w:body" + __nspfxs__ = ("w",) __attrs__ = () class CT_DocumentBuilder(BaseBuilder): - __tag__ = 'w:document' - __nspfxs__ = ('w',) + __tag__ = "w:document" + __nspfxs__ = ("w",) __attrs__ = () diff --git a/tests/oxml/test__init__.py b/tests/oxml/test__init__.py index 4a4aab6b6..5f392df38 100644 --- a/tests/oxml/test__init__.py +++ b/tests/oxml/test__init__.py @@ -1,89 +1,73 @@ -# encoding: utf-8 - -""" -Test suite for pptx.oxml.__init__.py module, primarily XML parser-related. -""" - -from __future__ import print_function, unicode_literals +"""Test suite for pptx.oxml.__init__.py module, primarily XML parser-related.""" import pytest - from lxml import etree -from docx.oxml import ( - OxmlElement, oxml_parser, parse_xml, register_element_cls -) from docx.oxml.ns import qn +from docx.oxml.parser import OxmlElement, oxml_parser, parse_xml, register_element_cls from docx.oxml.shared import BaseOxmlElement -class DescribeOxmlElement(object): - +class DescribeOxmlElement: def it_returns_an_lxml_element_with_matching_tag_name(self): - element = OxmlElement('a:foo') + element = OxmlElement("a:foo") assert isinstance(element, etree._Element) assert element.tag == ( - '{http://schemas.openxmlformats.org/drawingml/2006/main}foo' + "{http://schemas.openxmlformats.org/drawingml/2006/main}foo" ) def it_adds_supplied_attributes(self): - element = OxmlElement('a:foo', {'a': 'b', 'c': 'd'}) + element = OxmlElement("a:foo", {"a": "b", "c": "d"}) assert etree.tostring(element) == ( '' - ).encode('utf-8') + ).encode("utf-8") def it_adds_additional_namespace_declarations_when_supplied(self): - ns1 = 'http://schemas.openxmlformats.org/drawingml/2006/main' - ns2 = 'other' - element = OxmlElement('a:foo', nsdecls={'a': ns1, 'x': ns2}) + ns1 = "http://schemas.openxmlformats.org/drawingml/2006/main" + ns2 = "other" + element = OxmlElement("a:foo", nsdecls={"a": ns1, "x": ns2}) assert len(element.nsmap.items()) == 2 - assert element.nsmap['a'] == ns1 - assert element.nsmap['x'] == ns2 - + assert element.nsmap["a"] == ns1 + assert element.nsmap["x"] == ns2 -class DescribeOxmlParser(object): +class DescribeOxmlParser: def it_strips_whitespace_between_elements(self, whitespace_fixture): pretty_xml_text, stripped_xml_text = whitespace_fixture element = etree.fromstring(pretty_xml_text, oxml_parser) - xml_text = etree.tostring(element, encoding='unicode') + xml_text = etree.tostring(element, encoding="unicode") assert xml_text == stripped_xml_text # fixtures ------------------------------------------------------- @pytest.fixture def whitespace_fixture(self): - pretty_xml_text = ( - '\n' - ' text\n' - '\n' - ) - stripped_xml_text = 'text' + pretty_xml_text = "\n" " text\n" "\n" + stripped_xml_text = "text" return pretty_xml_text, stripped_xml_text -class DescribeParseXml(object): - +class DescribeParseXml: def it_accepts_bytes_and_assumes_utf8_encoding(self, xml_bytes): parse_xml(xml_bytes) def it_accepts_unicode_providing_there_is_no_encoding_declaration(self): non_enc_decl = '' enc_decl = '' - xml_body = 'føøbår' + xml_body = "føøbår" # unicode body by itself doesn't raise parse_xml(xml_body) # adding XML decl without encoding attr doesn't raise either - xml_text = '%s\n%s' % (non_enc_decl, xml_body) + xml_text = "%s\n%s" % (non_enc_decl, xml_body) parse_xml(xml_text) # but adding encoding in the declaration raises ValueError - xml_text = '%s\n%s' % (enc_decl, xml_body) - with pytest.raises(ValueError): + xml_text = "%s\n%s" % (enc_decl, xml_body) + with pytest.raises(ValueError, match="Unicode strings with encoding declara"): parse_xml(xml_text) def it_uses_registered_element_classes(self, xml_bytes): - register_element_cls('a:foo', CustElmCls) + register_element_cls("a:foo", CustElmCls) element = parse_xml(xml_bytes) assert isinstance(element, CustElmCls) @@ -94,19 +78,17 @@ def xml_bytes(self): return ( '\n' - ' foøbår\n' - '\n' - ).encode('utf-8') + " foøbår\n" + "\n" + ).encode("utf-8") -class DescribeRegisterElementCls(object): - - def it_determines_class_used_for_elements_with_matching_tagname( - self, xml_text): - register_element_cls('a:foo', CustElmCls) +class DescribeRegisterElementCls: + def it_determines_class_used_for_elements_with_matching_tagname(self, xml_text): + register_element_cls("a:foo", CustElmCls) foo = parse_xml(xml_text) assert type(foo) is CustElmCls - assert type(foo.find(qn('a:bar'))) is etree._Element + assert type(foo.find(qn("a:bar"))) is etree._Element # fixture components --------------------------------------------- @@ -115,8 +97,8 @@ def xml_text(self): return ( '\n' - ' foøbår\n' - '\n' + " foøbår\n" + "\n" ) @@ -124,5 +106,6 @@ def xml_text(self): # static fixture # =========================================================================== + class CustElmCls(BaseOxmlElement): pass diff --git a/tests/oxml/test_ns.py b/tests/oxml/test_ns.py index d17d98340..cd493e6ca 100644 --- a/tests/oxml/test_ns.py +++ b/tests/oxml/test_ns.py @@ -1,21 +1,14 @@ -# encoding: utf-8 - -""" -Test suite for docx.oxml.ns -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for docx.oxml.ns.""" import pytest from docx.oxml.ns import NamespacePrefixedTag -class DescribeNamespacePrefixedTag(object): - +class DescribeNamespacePrefixedTag: def it_behaves_like_a_string_when_you_want_it_to(self, nsptag): - s = '- %s -' % nsptag - assert s == '- a:foobar -' + s = "- %s -" % nsptag + assert s == "- a:foobar -" def it_knows_its_clark_name(self, nsptag, clark_name): assert nsptag.clark_name == clark_name @@ -27,13 +20,12 @@ def it_can_construct_from_a_clark_name(self, clark_name, nsptag): def it_knows_its_local_part(self, nsptag, local_part): assert nsptag.local_part == local_part - def it_can_compose_a_single_entry_nsmap_for_itself( - self, nsptag, namespace_uri_a): - expected_nsmap = {'a': namespace_uri_a} + def it_can_compose_a_single_entry_nsmap_for_itself(self, nsptag, namespace_uri_a): + expected_nsmap = {"a": namespace_uri_a} assert nsptag.nsmap == expected_nsmap def it_knows_its_namespace_prefix(self, nsptag): - assert nsptag.nspfx == 'a' + assert nsptag.nspfx == "a" def it_knows_its_namespace_uri(self, nsptag, namespace_uri_a): assert nsptag.nsuri == namespace_uri_a @@ -42,15 +34,15 @@ def it_knows_its_namespace_uri(self, nsptag, namespace_uri_a): @pytest.fixture def clark_name(self, namespace_uri_a, local_part): - return '{%s}%s' % (namespace_uri_a, local_part) + return "{%s}%s" % (namespace_uri_a, local_part) @pytest.fixture def local_part(self): - return 'foobar' + return "foobar" @pytest.fixture def namespace_uri_a(self): - return 'http://schemas.openxmlformats.org/drawingml/2006/main' + return "http://schemas.openxmlformats.org/drawingml/2006/main" @pytest.fixture def nsptag(self, nsptag_str): @@ -58,4 +50,4 @@ def nsptag(self, nsptag_str): @pytest.fixture def nsptag_str(self, local_part): - return 'a:%s' % local_part + return "a:%s" % local_part diff --git a/tests/oxml/test_styles.py b/tests/oxml/test_styles.py index a342323c7..7677a8a9e 100644 --- a/tests/oxml/test_styles.py +++ b/tests/oxml/test_styles.py @@ -1,12 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.oxml.styles module. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Test suite for the docx.oxml.styles module.""" import pytest @@ -15,8 +7,7 @@ from ..unitutil.cxml import element, xml -class DescribeCT_Styles(object): - +class DescribeCT_Styles: def it_can_add_a_style_of_type(self, add_fixture): styles, name, style_type, builtin, expected_xml = add_fixture style = styles.add_style_of_type(name, style_type, builtin) @@ -25,14 +16,26 @@ def it_can_add_a_style_of_type(self, add_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:styles', 'Foo Bar', WD_STYLE_TYPE.LIST, False, - 'w:styles/w:style{w:type=numbering,w:customStyle=1,w:styleId=FooBar' - '}/w:name{w:val=Foo Bar}'), - ('w:styles', 'heading 1', WD_STYLE_TYPE.PARAGRAPH, True, - 'w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val' - '=heading 1}'), - ]) + @pytest.fixture( + params=[ + ( + "w:styles", + "Foo Bar", + WD_STYLE_TYPE.LIST, + False, + "w:styles/w:style{w:type=numbering,w:customStyle=1,w:styleId=FooBar" + "}/w:name{w:val=Foo Bar}", + ), + ( + "w:styles", + "heading 1", + WD_STYLE_TYPE.PARAGRAPH, + True, + "w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val" + "=heading 1}", + ), + ] + ) def add_fixture(self, request): styles_cxml, name, style_type, builtin, expected_cxml = request.param styles = element(styles_cxml) diff --git a/tests/oxml/test_table.py b/tests/oxml/test_table.py index 61d8ba1d8..ecd0cf9d7 100644 --- a/tests/oxml/test_table.py +++ b/tests/oxml/test_table.py @@ -1,13 +1,9 @@ -# encoding: utf-8 - -"""Test suite for the docx.oxml.text module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Test suite for the docx.oxml.text module.""" import pytest from docx.exceptions import InvalidSpanError -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml from docx.oxml.table import CT_Row, CT_Tc from ..unitutil.cxml import element, xml @@ -15,8 +11,7 @@ from ..unitutil.mock import call, instance_mock, method_mock, property_mock -class DescribeCT_Row(object): - +class DescribeCT_Row: def it_can_add_a_trPr(self, add_trPr_fixture): tr, expected_xml = add_trPr_fixture tr._add_trPr() @@ -24,17 +19,19 @@ def it_can_add_a_trPr(self, add_trPr_fixture): def it_raises_on_tc_at_grid_col(self, tc_raise_fixture): tr, idx = tc_raise_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 tr.tc_at_grid_col(idx) # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:tr', 'w:tr/w:trPr'), - ('w:tr/w:tblPrEx', 'w:tr/(w:tblPrEx,w:trPr)'), - ('w:tr/w:tc', 'w:tr/(w:trPr,w:tc)'), - ('w:tr/(w:sdt,w:del,w:tc)', 'w:tr/(w:trPr,w:sdt,w:del,w:tc)'), - ]) + @pytest.fixture( + params=[ + ("w:tr", "w:tr/w:trPr"), + ("w:tr/w:tblPrEx", "w:tr/(w:tblPrEx,w:trPr)"), + ("w:tr/w:tc", "w:tr/(w:trPr,w:tc)"), + ("w:tr/(w:sdt,w:del,w:tc)", "w:tr/(w:trPr,w:sdt,w:del,w:tc)"), + ] + ) def add_trPr_fixture(self, request): tr_cxml, expected_cxml = request.param tr = element(tr_cxml) @@ -44,18 +41,17 @@ def add_trPr_fixture(self, request): @pytest.fixture(params=[(0, 0, 3), (1, 0, 1)]) def tc_raise_fixture(self, request): snippet_idx, row_idx, col_idx = request.param - tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx]) + tbl = parse_xml(snippet_seq("tbl-cells")[snippet_idx]) tr = tbl.tr_lst[row_idx] return tr, col_idx -class DescribeCT_Tc(object): - +class DescribeCT_Tc: def it_can_merge_to_another_tc( self, tr_, _span_dimensions_, _tbl_, _grow_to_, top_tc_ ): top_tr_ = tr_ - tc, other_tc = element('w:tc'), element('w:tc') + tc, other_tc = element("w:tc"), element("w:tc") top, left, height, width = 0, 1, 2, 3 _span_dimensions_.return_value = top, left, height, width _tbl_.return_value.tr_lst = [tr_] @@ -92,14 +88,15 @@ def it_can_extend_its_horz_span_to_help_merge( self, top_tc_, grid_span_, _move_content_to_, _swallow_next_tc_ ): grid_span_.side_effect = [1, 3, 4] - grid_width, vMerge = 4, 'continue' - tc = element('w:tc') + grid_width, vMerge = 4, "continue" + tc = element("w:tc") tc._span_to_width(grid_width, top_tc_, vMerge) _move_content_to_.assert_called_once_with(tc, top_tc_) assert _swallow_next_tc_.call_args_list == [ - call(tc, grid_width, top_tc_), call(tc, grid_width, top_tc_) + call(tc, grid_width, top_tc_), + call(tc, grid_width, top_tc_), ] assert tc.vMerge == vMerge @@ -126,30 +123,46 @@ def it_can_move_its_content_to_help_merge(self, move_fixture): def it_raises_on_tr_above(self, tr_above_raise_fixture): tc = tr_above_raise_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="no tr above topmost tr"): tc._tr_above # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - # both cells have a width - ('w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p),' - 'w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))', 0, 2, - 'w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=2880,w:type=dxa},' - 'w:gridSpan{w:val=2}),w:p))'), - # neither have a width - ('w:tr/(w:tc/w:p,w:tc/w:p)', 0, 2, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'), - # only second one has a width - ('w:tr/(w:tc/w:p,' - 'w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))', 0, 2, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'), - # only first one has a width - ('w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p),' - 'w:tc/w:p)', 0, 2, - 'w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa},' - 'w:gridSpan{w:val=2}),w:p))'), - ]) + @pytest.fixture( + params=[ + # both cells have a width + ( + "w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p)," + "w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))", + 0, + 2, + "w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=2880,w:type=dxa}," + "w:gridSpan{w:val=2}),w:p))", + ), + # neither have a width + ( + "w:tr/(w:tc/w:p,w:tc/w:p)", + 0, + 2, + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", + ), + # only second one has a width + ( + "w:tr/(w:tc/w:p," "w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))", + 0, + 2, + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", + ), + # only first one has a width + ( + "w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p)," "w:tc/w:p)", + 0, + 2, + "w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa}," + "w:gridSpan{w:val=2}),w:p))", + ), + ] + ) def add_width_fixture(self, request): tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param tr = element(tr_cxml) @@ -157,30 +170,42 @@ def add_width_fixture(self, request): expected_tr_xml = xml(expected_tr_cxml) return tc, grid_width, top_tc, tr, expected_tr_xml - @pytest.fixture(params=[ - (0, 0, 0, 'top', 0), (2, 0, 1, 'top', 0), - (2, 1, 1, 'top', 0), (4, 2, 1, 'top', 1), - (0, 0, 0, 'left', 0), (1, 0, 1, 'left', 2), - (3, 1, 0, 'left', 0), (3, 1, 1, 'left', 2), - (0, 0, 0, 'bottom', 1), (1, 0, 0, 'bottom', 1), - (2, 0, 1, 'bottom', 2), (4, 1, 1, 'bottom', 3), - (0, 0, 0, 'right', 1), (1, 0, 0, 'right', 2), - (0, 0, 0, 'right', 1), (4, 2, 1, 'right', 3), - ]) + @pytest.fixture( + params=[ + (0, 0, 0, "top", 0), + (2, 0, 1, "top", 0), + (2, 1, 1, "top", 0), + (4, 2, 1, "top", 1), + (0, 0, 0, "left", 0), + (1, 0, 1, "left", 2), + (3, 1, 0, "left", 0), + (3, 1, 1, "left", 2), + (0, 0, 0, "bottom", 1), + (1, 0, 0, "bottom", 1), + (2, 0, 1, "bottom", 2), + (4, 1, 1, "bottom", 3), + (0, 0, 0, "right", 1), + (1, 0, 0, "right", 2), + (0, 0, 0, "right", 1), + (4, 2, 1, "right", 3), + ] + ) def extents_fixture(self, request): snippet_idx, row, col, attr_name, expected_value = request.param tbl = self._snippet_tbl(snippet_idx) tc = tbl.tr_lst[row].tc_lst[col] return tc, attr_name, expected_value - @pytest.fixture(params=[ - (0, 0, 0, 2, 1), - (0, 0, 1, 1, 2), - (0, 1, 1, 2, 2), - (1, 0, 0, 2, 2), - (2, 0, 0, 2, 2), - (2, 1, 2, 1, 2), - ]) + @pytest.fixture( + params=[ + (0, 0, 0, 2, 1), + (0, 0, 1, 1, 2), + (0, 1, 1, 2, 2), + (1, 0, 0, 2, 2), + (2, 0, 0, 2, 2), + (2, 1, 2, 1, 2), + ] + ) def grow_to_fixture(self, request, _span_to_width_): snippet_idx, row, col, width, height = request.param tbl = self._snippet_tbl(snippet_idx) @@ -189,45 +214,47 @@ def grow_to_fixture(self, request, _span_to_width_): end = start + height expected_calls = [ call(width, tc, None), - call(width, tc, 'restart'), - call(width, tc, 'continue'), - call(width, tc, 'continue'), + call(width, tc, "restart"), + call(width, tc, "continue"), + call(width, tc, "continue"), ][start:end] return tc, width, height, None, expected_calls - @pytest.fixture(params=[ - ('w:tc/w:p', 'w:tc/w:p', - 'w:tc/w:p', 'w:tc/w:p'), - ('w:tc/w:p', 'w:tc/w:p/w:r', - 'w:tc/w:p', 'w:tc/w:p/w:r'), - ('w:tc/w:p/w:r', 'w:tc/w:p', - 'w:tc/w:p', 'w:tc/w:p/w:r'), - ('w:tc/(w:p/w:r,w:sdt)', 'w:tc/w:p', - 'w:tc/w:p', 'w:tc/(w:p/w:r,w:sdt)'), - ('w:tc/(w:p/w:r,w:sdt)', 'w:tc/(w:tbl,w:p)', - 'w:tc/w:p', 'w:tc/(w:tbl,w:p/w:r,w:sdt)'), - ]) + @pytest.fixture( + params=[ + ("w:tc/w:p", "w:tc/w:p", "w:tc/w:p", "w:tc/w:p"), + ("w:tc/w:p", "w:tc/w:p/w:r", "w:tc/w:p", "w:tc/w:p/w:r"), + ("w:tc/w:p/w:r", "w:tc/w:p", "w:tc/w:p", "w:tc/w:p/w:r"), + ("w:tc/(w:p/w:r,w:sdt)", "w:tc/w:p", "w:tc/w:p", "w:tc/(w:p/w:r,w:sdt)"), + ( + "w:tc/(w:p/w:r,w:sdt)", + "w:tc/(w:tbl,w:p)", + "w:tc/w:p", + "w:tc/(w:tbl,w:p/w:r,w:sdt)", + ), + ] + ) def move_fixture(self, request): - tc_cxml, tc_2_cxml, expected_tc_cxml, expected_tc_2_cxml = ( - request.param - ) + tc_cxml, tc_2_cxml, expected_tc_cxml, expected_tc_2_cxml = request.param tc, tc_2 = element(tc_cxml), element(tc_2_cxml) expected_tc_xml = xml(expected_tc_cxml) expected_tc_2_xml = xml(expected_tc_2_cxml) return tc, tc_2, expected_tc_xml, expected_tc_2_xml - @pytest.fixture(params=[ - (0, 0, 0, 0, 1, (0, 0, 1, 2)), - (0, 0, 1, 2, 1, (0, 1, 3, 1)), - (0, 2, 2, 1, 1, (1, 1, 2, 2)), - (0, 1, 2, 1, 0, (1, 0, 1, 3)), - (1, 0, 0, 1, 1, (0, 0, 2, 2)), - (1, 0, 1, 0, 0, (0, 0, 1, 3)), - (2, 0, 1, 2, 1, (0, 1, 3, 1)), - (2, 0, 1, 1, 0, (0, 0, 2, 2)), - (2, 1, 2, 0, 1, (0, 1, 2, 2)), - (4, 0, 1, 0, 0, (0, 0, 1, 3)), - ]) + @pytest.fixture( + params=[ + (0, 0, 0, 0, 1, (0, 0, 1, 2)), + (0, 0, 1, 2, 1, (0, 1, 3, 1)), + (0, 2, 2, 1, 1, (1, 1, 2, 2)), + (0, 1, 2, 1, 0, (1, 0, 1, 3)), + (1, 0, 0, 1, 1, (0, 0, 2, 2)), + (1, 0, 1, 0, 0, (0, 0, 1, 3)), + (2, 0, 1, 2, 1, (0, 1, 3, 1)), + (2, 0, 1, 1, 0, (0, 0, 2, 2)), + (2, 1, 2, 0, 1, (0, 1, 2, 2)), + (4, 0, 1, 0, 0, (0, 0, 1, 3)), + ] + ) def span_fixture(self, request): snippet_idx, row, col, row_2, col_2, expected_value = request.param tbl = self._snippet_tbl(snippet_idx) @@ -235,15 +262,17 @@ def span_fixture(self, request): tc_2 = tbl.tr_lst[row_2].tc_lst[col_2] return tc, tc_2, expected_value - @pytest.fixture(params=[ - (1, 0, 0, 1, 0), # inverted-L horz - (1, 1, 0, 0, 0), # same in opposite order - (2, 0, 2, 0, 1), # inverted-L vert - (5, 0, 1, 1, 0), # tee-shape horz bar - (5, 1, 0, 2, 1), # same, opposite side - (6, 1, 0, 0, 1), # tee-shape vert bar - (6, 0, 1, 1, 2), # same, opposite side - ]) + @pytest.fixture( + params=[ + (1, 0, 0, 1, 0), # inverted-L horz + (1, 1, 0, 0, 0), # same in opposite order + (2, 0, 2, 0, 1), # inverted-L vert + (5, 0, 1, 1, 0), # tee-shape horz bar + (5, 1, 0, 2, 1), # same, opposite side + (6, 1, 0, 0, 1), # tee-shape vert bar + (6, 0, 1, 1, 2), # same, opposite side + ] + ) def span_raise_fixture(self, request): snippet_idx, row, col, row_2, col_2 = request.param tbl = self._snippet_tbl(snippet_idx) @@ -251,19 +280,41 @@ def span_raise_fixture(self, request): tc_2 = tbl.tr_lst[row_2].tc_lst[col_2] return tc, tc_2 - @pytest.fixture(params=[ - ('w:tr/(w:tc/w:p,w:tc/w:p)', 0, 2, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'), - ('w:tr/(w:tc/w:p,w:tc/w:p,w:tc/w:p)', 1, 2, - 'w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'), - ('w:tr/(w:tc/w:p/w:r/w:t"a",w:tc/w:p/w:r/w:t"b")', 0, 2, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p/w:r/w:t"a",' - 'w:p/w:r/w:t"b"))'), - ('w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p),w:tc/w:p)', 0, 3, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))'), - ('w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))', 0, 3, - 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))'), - ]) + @pytest.fixture( + params=[ + ( + "w:tr/(w:tc/w:p,w:tc/w:p)", + 0, + 2, + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", + ), + ( + "w:tr/(w:tc/w:p,w:tc/w:p,w:tc/w:p)", + 1, + 2, + "w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", + ), + ( + 'w:tr/(w:tc/w:p/w:r/w:t"a",w:tc/w:p/w:r/w:t"b")', + 0, + 2, + 'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p/w:r/w:t"a",' + 'w:p/w:r/w:t"b"))', + ), + ( + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p),w:tc/w:p)", + 0, + 3, + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))", + ), + ( + "w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", + 0, + 3, + "w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))", + ), + ] + ) def swallow_fixture(self, request): tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param tr = element(tr_cxml) @@ -271,10 +322,12 @@ def swallow_fixture(self, request): expected_tr_xml = xml(expected_tr_cxml) return tc, grid_width, top_tc, tr, expected_tr_xml - @pytest.fixture(params=[ - ('w:tr/w:tc/w:p', 0, 2), - ('w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))', 0, 2), - ]) + @pytest.fixture( + params=[ + ("w:tr/w:tc/w:p", 0, 2), + ("w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))", 0, 2), + ] + ) def swallow_raise_fixture(self, request): tr_cxml, tc_idx, grid_width = request.param tr = element(tr_cxml) @@ -284,7 +337,7 @@ def swallow_raise_fixture(self, request): @pytest.fixture(params=[(0, 0, 0), (4, 0, 0)]) def tr_above_raise_fixture(self, request): snippet_idx, row_idx, col_idx = request.param - tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx]) + tbl = parse_xml(snippet_seq("tbl-cells")[snippet_idx]) tc = tbl.tr_lst[row_idx].tc_lst[col_idx] return tc @@ -292,38 +345,38 @@ def tr_above_raise_fixture(self, request): @pytest.fixture def grid_span_(self, request): - return property_mock(request, CT_Tc, 'grid_span') + return property_mock(request, CT_Tc, "grid_span") @pytest.fixture def _grow_to_(self, request): - return method_mock(request, CT_Tc, '_grow_to') + return method_mock(request, CT_Tc, "_grow_to") @pytest.fixture def _move_content_to_(self, request): - return method_mock(request, CT_Tc, '_move_content_to') + return method_mock(request, CT_Tc, "_move_content_to") @pytest.fixture def _span_dimensions_(self, request): - return method_mock(request, CT_Tc, '_span_dimensions') + return method_mock(request, CT_Tc, "_span_dimensions") @pytest.fixture def _span_to_width_(self, request): - return method_mock(request, CT_Tc, '_span_to_width', autospec=False) + return method_mock(request, CT_Tc, "_span_to_width", autospec=False) def _snippet_tbl(self, idx): """ - Return a element for snippet at *idx* in 'tbl-cells' snippet + Return a element for snippet at `idx` in 'tbl-cells' snippet file. """ - return parse_xml(snippet_seq('tbl-cells')[idx]) + return parse_xml(snippet_seq("tbl-cells")[idx]) @pytest.fixture def _swallow_next_tc_(self, request): - return method_mock(request, CT_Tc, '_swallow_next_tc') + return method_mock(request, CT_Tc, "_swallow_next_tc") @pytest.fixture def _tbl_(self, request): - return property_mock(request, CT_Tc, '_tbl') + return property_mock(request, CT_Tc, "_tbl") @pytest.fixture def top_tc_(self, request): diff --git a/tests/oxml/test_xmlchemy.py b/tests/oxml/test_xmlchemy.py index 407eaefc9..fca309851 100644 --- a/tests/oxml/test_xmlchemy.py +++ b/tests/oxml/test_xmlchemy.py @@ -1,87 +1,95 @@ -# encoding: utf-8 - -""" -Test suite for docx.oxml.xmlchemy -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for docx.oxml.xmlchemy.""" import pytest -from docx.compat import Unicode -from docx.oxml import parse_xml, register_element_cls from docx.oxml.exceptions import InvalidXmlError from docx.oxml.ns import qn +from docx.oxml.parser import parse_xml, register_element_cls from docx.oxml.simpletypes import BaseIntType from docx.oxml.xmlchemy import ( - BaseOxmlElement, Choice, serialize_for_reading, OneOrMore, OneAndOnlyOne, - OptionalAttribute, RequiredAttribute, ZeroOrMore, ZeroOrOne, - ZeroOrOneChoice, XmlString + BaseOxmlElement, + Choice, + OneAndOnlyOne, + OneOrMore, + OptionalAttribute, + RequiredAttribute, + XmlString, + ZeroOrMore, + ZeroOrOne, + ZeroOrOneChoice, + serialize_for_reading, ) from ..unitdata import BaseBuilder from .unitdata.text import a_b, a_u, an_i, an_rPr -class DescribeBaseOxmlElement(object): - - def it_can_find_the_first_of_its_children_named_in_a_sequence( - self, first_fixture): +class DescribeBaseOxmlElement: + def it_can_find_the_first_of_its_children_named_in_a_sequence(self, first_fixture): element, tagnames, matching_child = first_fixture assert element.first_child_found_in(*tagnames) is matching_child - def it_can_insert_an_element_before_named_successors( - self, insert_fixture): + def it_can_insert_an_element_before_named_successors(self, insert_fixture): element, child, tagnames, expected_xml = insert_fixture element.insert_element_before(child, *tagnames) assert element.xml == expected_xml - def it_can_remove_all_children_with_name_in_sequence( - self, remove_fixture): + def it_can_remove_all_children_with_name_in_sequence(self, remove_fixture): element, tagnames, expected_xml = remove_fixture element.remove_all(*tagnames) assert element.xml == expected_xml # fixtures --------------------------------------------- - @pytest.fixture(params=[ - ('biu', 'iu', 'i'), - ('bu', 'iu', 'u'), - ('bi', 'u', None), - ('b', 'iu', None), - ('iu', 'biu', 'i'), - ('', 'biu', None), - ]) + @pytest.fixture( + params=[ + ("biu", "iu", "i"), + ("bu", "iu", "u"), + ("bi", "u", None), + ("b", "iu", None), + ("iu", "biu", "i"), + ("", "biu", None), + ] + ) def first_fixture(self, request): present, matching, match = request.param element = self.rPr_bldr(present).element tagnames = self.nsptags(matching) - matching_child = element.find(qn('w:%s' % match)) if match else None + matching_child = element.find(qn("w:%s" % match)) if match else None return element, tagnames, matching_child - @pytest.fixture(params=[ - ('iu', 'b', 'iu', 'biu'), - ('u', 'b', 'iu', 'bu'), - ('', 'b', 'iu', 'b'), - ('bu', 'i', 'u', 'biu'), - ('bi', 'u', '', 'biu'), - ]) + @pytest.fixture( + params=[ + ("iu", "b", "iu", "biu"), + ("u", "b", "iu", "bu"), + ("", "b", "iu", "b"), + ("bu", "i", "u", "biu"), + ("bi", "u", "", "biu"), + ] + ) def insert_fixture(self, request): present, new, successors, after = request.param element = self.rPr_bldr(present).element - child = { - 'b': a_b(), 'i': an_i(), 'u': a_u() - }[new].with_nsdecls().element - tagnames = [('w:%s' % char) for char in successors] + child = {"b": a_b(), "i": an_i(), "u": a_u()}[new].with_nsdecls().element + tagnames = [("w:%s" % char) for char in successors] expected_xml = self.rPr_bldr(after).xml() return element, child, tagnames, expected_xml - @pytest.fixture(params=[ - ('biu', 'b', 'iu'), ('biu', 'bi', 'u'), ('bbiiuu', 'i', 'bbuu'), - ('biu', 'i', 'bu'), ('biu', 'bu', 'i'), ('bbiiuu', '', 'bbiiuu'), - ('biu', 'u', 'bi'), ('biu', 'ui', 'b'), ('bbiiuu', 'bi', 'uu'), - ('bu', 'i', 'bu'), ('', 'ui', ''), - ]) + @pytest.fixture( + params=[ + ("biu", "b", "iu"), + ("biu", "bi", "u"), + ("bbiiuu", "i", "bbuu"), + ("biu", "i", "bu"), + ("biu", "bu", "i"), + ("bbiiuu", "", "bbiiuu"), + ("biu", "u", "bi"), + ("biu", "ui", "b"), + ("bbiiuu", "bi", "uu"), + ("bu", "i", "bu"), + ("", "ui", ""), + ] + ) def remove_fixture(self, request): present, remove, after = request.param element = self.rPr_bldr(present).element @@ -92,24 +100,23 @@ def remove_fixture(self, request): # fixture components --------------------------------------------- def nsptags(self, letters): - return [('w:%s' % letter) for letter in letters] + return [("w:%s" % letter) for letter in letters] def rPr_bldr(self, children): rPr_bldr = an_rPr().with_nsdecls() for char in children: - if char == 'b': + if char == "b": rPr_bldr.with_child(a_b()) - elif char == 'i': + elif char == "i": rPr_bldr.with_child(an_i()) - elif char == 'u': + elif char == "u": rPr_bldr.with_child(a_u()) else: raise NotImplementedError("got '%s'" % char) return rPr_bldr -class DescribeSerializeForReading(object): - +class DescribeSerializeForReading: def it_pretty_prints_an_lxml_element(self, pretty_fixture): element, expected_xml_text = pretty_fixture xml_text = serialize_for_reading(element) @@ -118,17 +125,13 @@ def it_pretty_prints_an_lxml_element(self, pretty_fixture): def it_returns_unicode_text(self, type_fixture): element = type_fixture xml_text = serialize_for_reading(element) - assert isinstance(xml_text, Unicode) + assert isinstance(xml_text, str) # fixtures --------------------------------------------- @pytest.fixture def pretty_fixture(self, element): - expected_xml_text = ( - '\n' - ' text\n' - '\n' - ) + expected_xml_text = "\n" " text\n" "\n" return element, expected_xml_text @pytest.fixture @@ -139,11 +142,10 @@ def type_fixture(self, element): @pytest.fixture def element(self): - return parse_xml('text') - + return parse_xml("text") -class DescribeXmlString(object): +class DescribeXmlString: def it_parses_a_line_to_help_compare(self, parse_fixture): """ This internal function is important to test separately because if it @@ -167,64 +169,75 @@ def it_knows_if_two_xml_lines_are_equivalent(self, xml_line_case): # fixtures --------------------------------------------- - @pytest.fixture(params=[ - ('text', '', 'text'), - ('', '', None), - ('', '', None), - ('t', '', 't'), - ('2013-12-23T23:15:00Z', '', '2013-12-23T23:15:00Z'), - ]) + @pytest.fixture( + params=[ + ("text", "", "text"), + ("", "", None), + ('', "", None), + ("t", "", "t"), + ( + '2013-12-23T23:15:00Z", + "", + "2013-12-23T23:15:00Z", + ), + ] + ) def parse_fixture(self, request): line, front, attrs, close, text = request.param return line, front, attrs, close, text - @pytest.fixture(params=[ - 'simple_elm', 'nsp_tagname', 'indent', 'attrs', 'nsdecl_order', - 'closing_elm', - ]) + @pytest.fixture( + params=[ + "simple_elm", + "nsp_tagname", + "indent", + "attrs", + "nsdecl_order", + "closing_elm", + ] + ) def xml_line_case(self, request): cases = { - 'simple_elm': ( - '', - '', - '', + "simple_elm": ( + "", + "", + "", ), - 'nsp_tagname': ( - '', - '', - '', + "nsp_tagname": ( + "", + "", + "", ), - 'indent': ( - ' ', - ' ', - '', + "indent": ( + " ", + " ", + "", ), - 'attrs': ( + "attrs": ( ' ', ' ', ' ', ), - 'nsdecl_order': ( + "nsdecl_order": ( ' ', ' ', ' ', ), - 'closing_elm': ( - '', - '', - '', + "closing_elm": ( + "", + "", + "", ), } line, other, differs = cases[request.param] return line, other, differs -class DescribeChoice(object): - - def it_adds_a_getter_property_for_the_choice_element( - self, getter_fixture): +class DescribeChoice: + def it_adds_a_getter_property_for_the_choice_element(self, getter_fixture): parent, expected_choice = getter_fixture assert parent.choice is expected_choice @@ -238,7 +251,7 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent._insert_choice(choice) assert parent.xml == expected_xml assert parent._insert_choice.__doc__.startswith( - 'Return the passed ```` ' + "Return the passed ```` " ) def it_adds_an_add_method_for_the_child_element(self, add_fixture): @@ -247,11 +260,12 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(choice, CT_Choice) assert parent._add_choice.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) def it_adds_a_get_or_change_to_method_for_the_child_element( - self, get_or_change_to_fixture): + self, get_or_change_to_fixture + ): parent, expected_xml = get_or_change_to_fixture choice = parent.get_or_change_to_choice() assert isinstance(choice, CT_Choice) @@ -262,40 +276,44 @@ def it_adds_a_get_or_change_to_method_for_the_child_element( @pytest.fixture def add_fixture(self): parent = self.parent_bldr().element - expected_xml = self.parent_bldr('choice').xml() + expected_xml = self.parent_bldr("choice").xml() return parent, expected_xml - @pytest.fixture(params=[ - ('choice2', 'choice'), - (None, 'choice'), - ('choice', 'choice'), - ]) + @pytest.fixture( + params=[ + ("choice2", "choice"), + (None, "choice"), + ("choice", "choice"), + ] + ) def get_or_change_to_fixture(self, request): before_member_tag, after_member_tag = request.param parent = self.parent_bldr(before_member_tag).element expected_xml = self.parent_bldr(after_member_tag).xml() return parent, expected_xml - @pytest.fixture(params=['choice', None]) + @pytest.fixture(params=["choice", None]) def getter_fixture(self, request): choice_tag = request.param parent = self.parent_bldr(choice_tag).element - expected_choice = parent.find(qn('w:choice')) # None if not found + expected_choice = parent.find(qn("w:choice")) # None if not found return parent, expected_choice @pytest.fixture def insert_fixture(self): parent = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) ).element choice = a_choice().with_nsdecls().element expected_xml = ( - a_parent().with_nsdecls().with_child( - a_choice()).with_child( - an_oomChild()).with_child( - an_oooChild()) + a_parent() + .with_nsdecls() + .with_child(a_choice()) + .with_child(an_oomChild()) + .with_child(an_oooChild()) ).xml() return parent, choice, expected_xml @@ -309,15 +327,14 @@ def new_fixture(self): def parent_bldr(self, choice_tag=None): parent_bldr = a_parent().with_nsdecls() - if choice_tag == 'choice': + if choice_tag == "choice": parent_bldr.with_child(a_choice()) - if choice_tag == 'choice2': + if choice_tag == "choice2": parent_bldr.with_child(a_choice2()) return parent_bldr -class DescribeOneAndOnlyOne(object): - +class DescribeOneAndOnlyOne: def it_adds_a_getter_property_for_the_child_element(self, getter_fixture): parent, oooChild = getter_fixture assert parent.oooChild is oooChild @@ -327,14 +344,12 @@ def it_adds_a_getter_property_for_the_child_element(self, getter_fixture): @pytest.fixture def getter_fixture(self): parent = a_parent().with_nsdecls().with_child(an_oooChild()).element - oooChild = parent.find(qn('w:oooChild')) + oooChild = parent.find(qn("w:oooChild")) return parent, oooChild -class DescribeOneOrMore(object): - - def it_adds_a_getter_property_for_the_child_element_list( - self, getter_fixture): +class DescribeOneOrMore: + def it_adds_a_getter_property_for_the_child_element_list(self, getter_fixture): parent, oomChild = getter_fixture assert parent.oomChild_lst[0] is oomChild @@ -348,7 +363,7 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent._insert_oomChild(oomChild) assert parent.xml == expected_xml assert parent._insert_oomChild.__doc__.startswith( - 'Return the passed ```` ' + "Return the passed ```` " ) def it_adds_a_private_add_method_for_the_child_element(self, add_fixture): @@ -357,7 +372,7 @@ def it_adds_a_private_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) assert parent._add_oomChild.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): @@ -366,7 +381,7 @@ def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(oomChild, CT_OomChild) assert parent._add_oomChild.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) # fixtures ------------------------------------------------------- @@ -380,24 +395,26 @@ def add_fixture(self): @pytest.fixture def getter_fixture(self): parent = self.parent_bldr(True).element - oomChild = parent.find(qn('w:oomChild')) + oomChild = parent.find(qn("w:oomChild")) return parent, oomChild @pytest.fixture def insert_fixture(self): parent = ( - a_parent().with_nsdecls().with_child( - an_oooChild()).with_child( - a_zomChild()).with_child( - a_zooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oooChild()) + .with_child(a_zomChild()) + .with_child(a_zooChild()) ).element oomChild = an_oomChild().with_nsdecls().element expected_xml = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()).with_child( - a_zomChild()).with_child( - a_zooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) + .with_child(a_zomChild()) + .with_child(a_zooChild()) ).xml() return parent, oomChild, expected_xml @@ -416,8 +433,7 @@ def parent_bldr(self, oomChild_is_present): return parent_bldr -class DescribeOptionalAttribute(object): - +class DescribeOptionalAttribute: def it_adds_a_getter_property_for_the_attr_value(self, getter_fixture): parent, optAttr_python_value = getter_fixture assert parent.optAttr == optAttr_python_value @@ -436,13 +452,13 @@ def it_adds_a_docstring_for_the_property(self): @pytest.fixture def getter_fixture(self): - parent = a_parent().with_nsdecls().with_optAttr('24').element + parent = a_parent().with_nsdecls().with_optAttr("24").element return parent, 24 @pytest.fixture(params=[36, None]) def setter_fixture(self, request): value = request.param - parent = a_parent().with_nsdecls().with_optAttr('42').element + parent = a_parent().with_nsdecls().with_optAttr("42").element if value is None: expected_xml = a_parent().with_nsdecls().xml() else: @@ -450,8 +466,7 @@ def setter_fixture(self, request): return parent, value, expected_xml -class DescribeRequiredAttribute(object): - +class DescribeRequiredAttribute: def it_adds_a_getter_property_for_the_attr_value(self, getter_fixture): parent, reqAttr_python_value = getter_fixture assert parent.reqAttr == reqAttr_python_value @@ -480,14 +495,16 @@ def it_raises_on_assign_invalid_value(self, invalid_assign_fixture): @pytest.fixture def getter_fixture(self): - parent = a_parent().with_nsdecls().with_reqAttr('42').element + parent = a_parent().with_nsdecls().with_reqAttr("42").element return parent, 42 - @pytest.fixture(params=[ - (None, TypeError), - (-4, ValueError), - ('2', TypeError), - ]) + @pytest.fixture( + params=[ + (None, TypeError), + (-4, ValueError), + ("2", TypeError), + ] + ) def invalid_assign_fixture(self, request): invalid_value, expected_exception = request.param parent = a_parent().with_nsdecls().with_reqAttr(1).element @@ -495,16 +512,14 @@ def invalid_assign_fixture(self, request): @pytest.fixture def setter_fixture(self): - parent = a_parent().with_nsdecls().with_reqAttr('42').element + parent = a_parent().with_nsdecls().with_reqAttr("42").element value = 24 expected_xml = a_parent().with_nsdecls().with_reqAttr(value).xml() return parent, value, expected_xml -class DescribeZeroOrMore(object): - - def it_adds_a_getter_property_for_the_child_element_list( - self, getter_fixture): +class DescribeZeroOrMore: + def it_adds_a_getter_property_for_the_child_element_list(self, getter_fixture): parent, zomChild = getter_fixture assert parent.zomChild_lst[0] is zomChild @@ -518,7 +533,7 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent._insert_zomChild(zomChild) assert parent.xml == expected_xml assert parent._insert_zomChild.__doc__.startswith( - 'Return the passed ```` ' + "Return the passed ```` " ) def it_adds_an_add_method_for_the_child_element(self, add_fixture): @@ -527,7 +542,7 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(zomChild, CT_ZomChild) assert parent._add_zomChild.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): @@ -536,11 +551,11 @@ def it_adds_a_public_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(zomChild, CT_ZomChild) assert parent._add_zomChild.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) def it_removes_the_property_root_name_used_for_declaration(self): - assert not hasattr(CT_Parent, 'zomChild') + assert not hasattr(CT_Parent, "zomChild") # fixtures ------------------------------------------------------- @@ -553,24 +568,26 @@ def add_fixture(self): @pytest.fixture def getter_fixture(self): parent = self.parent_bldr(True).element - zomChild = parent.find(qn('w:zomChild')) + zomChild = parent.find(qn("w:zomChild")) return parent, zomChild @pytest.fixture def insert_fixture(self): parent = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()).with_child( - a_zooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) + .with_child(a_zooChild()) ).element zomChild = a_zomChild().with_nsdecls().element expected_xml = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()).with_child( - a_zomChild()).with_child( - a_zooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) + .with_child(a_zomChild()) + .with_child(a_zooChild()) ).xml() return parent, zomChild, expected_xml @@ -587,8 +604,7 @@ def parent_bldr(self, zomChild_is_present): return parent_bldr -class DescribeZeroOrOne(object): - +class DescribeZeroOrOne: def it_adds_a_getter_property_for_the_child_element(self, getter_fixture): parent, zooChild = getter_fixture assert parent.zooChild is zooChild @@ -599,7 +615,7 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture): assert parent.xml == expected_xml assert isinstance(zooChild, CT_ZooChild) assert parent._add_zooChild.__doc__.startswith( - 'Add a new ```` child element ' + "Add a new ```` child element " ) def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): @@ -607,11 +623,10 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture): parent._insert_zooChild(zooChild) assert parent.xml == expected_xml assert parent._insert_zooChild.__doc__.startswith( - 'Return the passed ```` ' + "Return the passed ```` " ) - def it_adds_a_get_or_add_method_for_the_child_element( - self, get_or_add_fixture): + def it_adds_a_get_or_add_method_for_the_child_element(self, get_or_add_fixture): parent, expected_xml = get_or_add_fixture zooChild = parent.get_or_add_zooChild() assert isinstance(zooChild, CT_ZooChild) @@ -634,7 +649,7 @@ def add_fixture(self): def getter_fixture(self, request): zooChild_is_present = request.param parent = self.parent_bldr(zooChild_is_present).element - zooChild = parent.find(qn('w:zooChild')) # None if not found + zooChild = parent.find(qn("w:zooChild")) # None if not found return parent, zooChild @pytest.fixture(params=[True, False]) @@ -647,18 +662,20 @@ def get_or_add_fixture(self, request): @pytest.fixture def insert_fixture(self): parent = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()).with_child( - a_zomChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) + .with_child(a_zomChild()) ).element zooChild = a_zooChild().with_nsdecls().element expected_xml = ( - a_parent().with_nsdecls().with_child( - an_oomChild()).with_child( - an_oooChild()).with_child( - a_zomChild()).with_child( - a_zooChild()) + a_parent() + .with_nsdecls() + .with_child(an_oomChild()) + .with_child(an_oooChild()) + .with_child(a_zomChild()) + .with_child(a_zooChild()) ).xml() return parent, zooChild, expected_xml @@ -678,19 +695,18 @@ def parent_bldr(self, zooChild_is_present): return parent_bldr -class DescribeZeroOrOneChoice(object): - +class DescribeZeroOrOneChoice: def it_adds_a_getter_for_the_current_choice(self, getter_fixture): parent, expected_choice = getter_fixture assert parent.eg_zooChoice is expected_choice # fixtures ------------------------------------------------------- - @pytest.fixture(params=[None, 'choice', 'choice2']) + @pytest.fixture(params=[None, "choice", "choice2"]) def getter_fixture(self, request): choice_tag = request.param parent = self.parent_bldr(choice_tag).element - tagname = 'w:%s' % choice_tag + tagname = "w:%s" % choice_tag expected_choice = parent.find(qn(tagname)) # None if not found return parent, expected_choice @@ -698,9 +714,9 @@ def getter_fixture(self, request): def parent_bldr(self, choice_tag=None): parent_bldr = a_parent().with_nsdecls() - if choice_tag == 'choice': + if choice_tag == "choice": parent_bldr.with_child(a_choice()) - if choice_tag == 'choice2': + if choice_tag == "choice2": parent_bldr.with_child(a_choice2()) return parent_bldr @@ -709,33 +725,32 @@ def parent_bldr(self, choice_tag=None): # static shared fixture # -------------------------------------------------------------------- -class ST_IntegerType(BaseIntType): +class ST_IntegerType(BaseIntType): @classmethod def validate(cls, value): cls.validate_int(value) if value < 1 or value > 42: - raise ValueError( - "value must be in range 1 to 42 inclusive" - ) + raise ValueError("value must be in range 1 to 42 inclusive") class CT_Parent(BaseOxmlElement): """ ```` element, an invented element for use in testing. """ + eg_zooChoice = ZeroOrOneChoice( - (Choice('w:choice'), Choice('w:choice2')), - successors=('w:oomChild', 'w:oooChild') + (Choice("w:choice"), Choice("w:choice2")), + successors=("w:oomChild", "w:oooChild"), + ) + oomChild = OneOrMore( + "w:oomChild", successors=("w:oooChild", "w:zomChild", "w:zooChild") ) - oomChild = OneOrMore('w:oomChild', successors=( - 'w:oooChild', 'w:zomChild', 'w:zooChild' - )) - oooChild = OneAndOnlyOne('w:oooChild') - zomChild = ZeroOrMore('w:zomChild', successors=('w:zooChild',)) - zooChild = ZeroOrOne('w:zooChild', successors=()) - optAttr = OptionalAttribute('w:optAttr', ST_IntegerType) - reqAttr = RequiredAttribute('reqAttr', ST_IntegerType) + oooChild = OneAndOnlyOne("w:oooChild") + zomChild = ZeroOrMore("w:zomChild", successors=("w:zooChild",)) + zooChild = ZeroOrOne("w:zooChild", successors=()) + optAttr = OptionalAttribute("w:optAttr", ST_IntegerType) + reqAttr = RequiredAttribute("reqAttr", ST_IntegerType) class CT_Choice(BaseOxmlElement): @@ -766,52 +781,52 @@ class CT_ZooChild(BaseOxmlElement): """ -register_element_cls('w:parent', CT_Parent) -register_element_cls('w:choice', CT_Choice) -register_element_cls('w:oomChild', CT_OomChild) -register_element_cls('w:zomChild', CT_ZomChild) -register_element_cls('w:zooChild', CT_ZooChild) +register_element_cls("w:parent", CT_Parent) +register_element_cls("w:choice", CT_Choice) +register_element_cls("w:oomChild", CT_OomChild) +register_element_cls("w:zomChild", CT_ZomChild) +register_element_cls("w:zooChild", CT_ZooChild) class CT_ChoiceBuilder(BaseBuilder): - __tag__ = 'w:choice' - __nspfxs__ = ('w',) + __tag__ = "w:choice" + __nspfxs__ = ("w",) __attrs__ = () class CT_Choice2Builder(BaseBuilder): - __tag__ = 'w:choice2' - __nspfxs__ = ('w',) + __tag__ = "w:choice2" + __nspfxs__ = ("w",) __attrs__ = () class CT_ParentBuilder(BaseBuilder): - __tag__ = 'w:parent' - __nspfxs__ = ('w',) - __attrs__ = ('w:optAttr', 'reqAttr') + __tag__ = "w:parent" + __nspfxs__ = ("w",) + __attrs__ = ("w:optAttr", "reqAttr") class CT_OomChildBuilder(BaseBuilder): - __tag__ = 'w:oomChild' - __nspfxs__ = ('w',) + __tag__ = "w:oomChild" + __nspfxs__ = ("w",) __attrs__ = () class CT_OooChildBuilder(BaseBuilder): - __tag__ = 'w:oooChild' - __nspfxs__ = ('w',) + __tag__ = "w:oooChild" + __nspfxs__ = ("w",) __attrs__ = () class CT_ZomChildBuilder(BaseBuilder): - __tag__ = 'w:zomChild' - __nspfxs__ = ('w',) + __tag__ = "w:zomChild" + __nspfxs__ = ("w",) __attrs__ = () class CT_ZooChildBuilder(BaseBuilder): - __tag__ = 'w:zooChild' - __nspfxs__ = ('w',) + __tag__ = "w:zooChild" + __nspfxs__ = ("w",) __attrs__ = () diff --git a/tests/oxml/text/test_hyperlink.py b/tests/oxml/text/test_hyperlink.py new file mode 100644 index 000000000..f55ab9c22 --- /dev/null +++ b/tests/oxml/text/test_hyperlink.py @@ -0,0 +1,47 @@ +"""Test suite for the docx.oxml.text.hyperlink module.""" + +from typing import cast + +import pytest + +from docx.oxml.text.hyperlink import CT_Hyperlink +from docx.oxml.text.run import CT_R + +from ...unitutil.cxml import element + + +class DescribeCT_Hyperlink: + """Unit-test suite for the CT_Hyperlink () element.""" + + def it_has_a_relationship_that_contains_the_hyperlink_address(self): + cxml = 'w:hyperlink{r:id=rId6}/w:r/w:t"post"' + hyperlink = cast(CT_Hyperlink, element(cxml)) + + rId = hyperlink.rId + + assert rId == "rId6" + + @pytest.mark.parametrize( + ("cxml", "expected_value"), + [ + # -- default (when omitted) is True, somewhat surprisingly -- + ("w:hyperlink{r:id=rId6}", True), + ("w:hyperlink{r:id=rId6,w:history=0}", False), + ("w:hyperlink{r:id=rId6,w:history=1}", True), + ], + ) + def it_knows_whether_it_has_been_clicked_on_aka_visited( + self, cxml: str, expected_value: bool + ): + hyperlink = cast(CT_Hyperlink, element(cxml)) + assert hyperlink.history is expected_value + + def it_has_zero_or_more_runs_containing_the_hyperlink_text(self): + cxml = 'w:hyperlink{r:id=rId6,w:history=1}/(w:r/w:t"blog",w:r/w:t" post")' + hyperlink = cast(CT_Hyperlink, element(cxml)) + + rs = hyperlink.r_lst + + assert [type(r) for r in rs] == [CT_R, CT_R] + assert rs[0].text == "blog" + assert rs[1].text == " post" diff --git a/tests/oxml/text/test_run.py b/tests/oxml/text/test_run.py index 57b8580fe..6aad7cd02 100644 --- a/tests/oxml/text/test_run.py +++ b/tests/oxml/text/test_run.py @@ -1,35 +1,41 @@ -# encoding: utf-8 +"""Test suite for the docx.oxml.text.run module.""" -""" -Test suite for the docx.oxml.text.run module. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from typing import cast import pytest +from docx.oxml.text.run import CT_R + from ...unitutil.cxml import element, xml -class DescribeCT_R(object): +class DescribeCT_R: + """Unit-test suite for the CT_R (run, ) element.""" + + @pytest.mark.parametrize( + ("initial_cxml", "text", "expected_cxml"), + [ + ("w:r", "foobar", 'w:r/w:t"foobar"'), + ("w:r", "foobar ", 'w:r/w:t{xml:space=preserve}"foobar "'), + ( + "w:r/(w:rPr/w:rStyle{w:val=emphasis}, w:cr)", + "foobar", + 'w:r/(w:rPr/w:rStyle{w:val=emphasis}, w:cr, w:t"foobar")', + ), + ], + ) + def it_can_add_a_t_preserving_edge_whitespace( + self, initial_cxml: str, text: str, expected_cxml: str + ): + r = cast(CT_R, element(initial_cxml)) + expected_xml = xml(expected_cxml) - def it_can_add_a_t_preserving_edge_whitespace(self, add_t_fixture): - r, text, expected_xml = add_t_fixture r.add_t(text) + assert r.xml == expected_xml - # fixtures ------------------------------------------------------- - - @pytest.fixture(params=[ - ('w:r', 'foobar', 'w:r/w:t"foobar"'), - ('w:r', 'foobar ', 'w:r/w:t{xml:space=preserve}"foobar "'), - ('w:r/(w:rPr/w:rStyle{w:val=emphasis}, w:cr)', 'foobar', - 'w:r/(w:rPr/w:rStyle{w:val=emphasis}, w:cr, w:t"foobar")'), - ]) - def add_t_fixture(self, request): - initial_cxml, text, expected_cxml = request.param - r = element(initial_cxml) - expected_xml = xml(expected_cxml) - return r, text, expected_xml + def it_can_assemble_the_text_in_the_run(self): + cxml = 'w:r/(w:br,w:cr,w:noBreakHyphen,w:ptab,w:t"foobar",w:tab)' + r = cast(CT_R, element(cxml)) + + assert r.text == "\n\n-\tfoobar\t" diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 84518f8b7..325a3f690 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -1,132 +1,44 @@ -# encoding: utf-8 - -""" -Test data builders for DrawingML XML elements -""" +"""Test data builders for DrawingML XML elements.""" from ...unitdata import BaseBuilder class CT_BlipBuilder(BaseBuilder): - __tag__ = 'a:blip' - __nspfxs__ = ('a',) - __attrs__ = ('r:embed', 'r:link', 'cstate') + __tag__ = "a:blip" + __nspfxs__ = ("a",) + __attrs__ = ("r:embed", "r:link", "cstate") class CT_BlipFillPropertiesBuilder(BaseBuilder): - __tag__ = 'pic:blipFill' - __nspfxs__ = ('pic',) - __attrs__ = () - - -class CT_DrawingBuilder(BaseBuilder): - __tag__ = 'w:drawing' - __nspfxs__ = ('w',) + __tag__ = "pic:blipFill" + __nspfxs__ = ("pic",) __attrs__ = () class CT_GraphicalObjectBuilder(BaseBuilder): - __tag__ = 'a:graphic' - __nspfxs__ = ('a',) + __tag__ = "a:graphic" + __nspfxs__ = ("a",) __attrs__ = () class CT_GraphicalObjectDataBuilder(BaseBuilder): - __tag__ = 'a:graphicData' - __nspfxs__ = ('a',) - __attrs__ = ('uri',) - - -class CT_GraphicalObjectFrameLockingBuilder(BaseBuilder): - __tag__ = 'a:graphicFrameLocks' - __nspfxs__ = ('a',) - __attrs__ = ('noChangeAspect',) + __tag__ = "a:graphicData" + __nspfxs__ = ("a",) + __attrs__ = ("uri",) class CT_InlineBuilder(BaseBuilder): - __tag__ = 'wp:inline' - __nspfxs__ = ('wp',) - __attrs__ = ('distT', 'distB', 'distL', 'distR') - - -class CT_NonVisualDrawingPropsBuilder(BaseBuilder): - __nspfxs__ = ('wp',) - __attrs__ = ('id', 'name', 'descr', 'hidden', 'title') - - def __init__(self, tag): - self.__tag__ = tag - super(CT_NonVisualDrawingPropsBuilder, self).__init__() - - -class CT_NonVisualGraphicFramePropertiesBuilder(BaseBuilder): - __tag__ = 'wp:cNvGraphicFramePr' - __nspfxs__ = ('wp',) - __attrs__ = () - - -class CT_NonVisualPicturePropertiesBuilder(BaseBuilder): - __tag__ = 'pic:cNvPicPr' - __nspfxs__ = ('pic',) - __attrs__ = ('preferRelativeResize') + __tag__ = "wp:inline" + __nspfxs__ = ("wp",) + __attrs__ = ("distT", "distB", "distL", "distR") class CT_PictureBuilder(BaseBuilder): - __tag__ = 'pic:pic' - __nspfxs__ = ('pic',) - __attrs__ = () - - -class CT_PictureNonVisualBuilder(BaseBuilder): - __tag__ = 'pic:nvPicPr' - __nspfxs__ = ('pic',) - __attrs__ = () - - -class CT_Point2DBuilder(BaseBuilder): - __tag__ = 'a:off' - __nspfxs__ = ('a',) - __attrs__ = ('x', 'y') - - -class CT_PositiveSize2DBuilder(BaseBuilder): - __nspfxs__ = () - __attrs__ = ('cx', 'cy') - - def __init__(self, tag): - self.__tag__ = tag - super(CT_PositiveSize2DBuilder, self).__init__() - - -class CT_PresetGeometry2DBuilder(BaseBuilder): - __tag__ = 'a:prstGeom' - __nspfxs__ = ('a',) - __attrs__ = ('prst',) - - -class CT_RelativeRectBuilder(BaseBuilder): - __tag__ = 'a:fillRect' - __nspfxs__ = ('a',) - __attrs__ = ('l', 't', 'r', 'b') - - -class CT_ShapePropertiesBuilder(BaseBuilder): - __tag__ = 'pic:spPr' - __nspfxs__ = ('pic', 'a') - __attrs__ = ('bwMode',) - - -class CT_StretchInfoPropertiesBuilder(BaseBuilder): - __tag__ = 'a:stretch' - __nspfxs__ = ('a',) + __tag__ = "pic:pic" + __nspfxs__ = ("pic",) __attrs__ = () -class CT_Transform2DBuilder(BaseBuilder): - __tag__ = 'a:xfrm' - __nspfxs__ = ('a',) - __attrs__ = ('rot', 'flipH', 'flipV') - - def a_blip(): return CT_BlipBuilder() @@ -135,30 +47,6 @@ def a_blipFill(): return CT_BlipFillPropertiesBuilder() -def a_cNvGraphicFramePr(): - return CT_NonVisualGraphicFramePropertiesBuilder() - - -def a_cNvPicPr(): - return CT_NonVisualPicturePropertiesBuilder() - - -def a_cNvPr(): - return CT_NonVisualDrawingPropsBuilder('pic:cNvPr') - - -def a_docPr(): - return CT_NonVisualDrawingPropsBuilder('wp:docPr') - - -def a_drawing(): - return CT_DrawingBuilder() - - -def a_fillRect(): - return CT_RelativeRectBuilder() - - def a_graphic(): return CT_GraphicalObjectBuilder() @@ -167,45 +55,9 @@ def a_graphicData(): return CT_GraphicalObjectDataBuilder() -def a_graphicFrameLocks(): - return CT_GraphicalObjectFrameLockingBuilder() - - def a_pic(): return CT_PictureBuilder() -def a_prstGeom(): - return CT_PresetGeometry2DBuilder() - - -def a_stretch(): - return CT_StretchInfoPropertiesBuilder() - - -def an_ext(): - return CT_PositiveSize2DBuilder('a:ext') - - -def an_extent(): - return CT_PositiveSize2DBuilder('wp:extent') - - def an_inline(): return CT_InlineBuilder() - - -def an_nvPicPr(): - return CT_PictureNonVisualBuilder() - - -def an_off(): - return CT_Point2DBuilder() - - -def an_spPr(): - return CT_ShapePropertiesBuilder() - - -def an_xfrm(): - return CT_Transform2DBuilder() diff --git a/tests/oxml/unitdata/numbering.py b/tests/oxml/unitdata/numbering.py index 984667b32..386ec6117 100644 --- a/tests/oxml/unitdata/numbering.py +++ b/tests/oxml/unitdata/numbering.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - -""" -Test data builders for numbering part XML elements -""" +"""Test data builders for numbering part XML elements.""" from ...unitdata import BaseBuilder class CT_NumBuilder(BaseBuilder): - __tag__ = 'w:num' - __nspfxs__ = ('w',) - __attrs__ = ('w:numId') + __tag__ = "w:num" + __nspfxs__ = ("w",) + __attrs__ = "w:numId" class CT_NumberingBuilder(BaseBuilder): - __tag__ = 'w:numbering' - __nspfxs__ = ('w',) + __tag__ = "w:numbering" + __nspfxs__ = ("w",) __attrs__ = () diff --git a/tests/oxml/unitdata/section.py b/tests/oxml/unitdata/section.py index 2f5f41151..397ade7db 100644 --- a/tests/oxml/unitdata/section.py +++ b/tests/oxml/unitdata/section.py @@ -1,37 +1,38 @@ -# encoding: utf-8 - -""" -Test data builders for section-related XML elements -""" +"""Test data builders for section-related XML elements.""" from ...unitdata import BaseBuilder class CT_PageMarBuilder(BaseBuilder): - __tag__ = 'w:pgMar' - __nspfxs__ = ('w',) + __tag__ = "w:pgMar" + __nspfxs__ = ("w",) __attrs__ = ( - 'w:top', 'w:right', 'w:bottom', 'w:left', 'w:header', 'w:footer', - 'w:gutter' + "w:top", + "w:right", + "w:bottom", + "w:left", + "w:header", + "w:footer", + "w:gutter", ) class CT_PageSzBuilder(BaseBuilder): - __tag__ = 'w:pgSz' - __nspfxs__ = ('w',) - __attrs__ = ('w:w', 'w:h', 'w:orient', 'w:code') + __tag__ = "w:pgSz" + __nspfxs__ = ("w",) + __attrs__ = ("w:w", "w:h", "w:orient", "w:code") class CT_SectPrBuilder(BaseBuilder): - __tag__ = 'w:sectPr' - __nspfxs__ = ('w',) + __tag__ = "w:sectPr" + __nspfxs__ = ("w",) __attrs__ = () class CT_SectTypeBuilder(BaseBuilder): - __tag__ = 'w:type' - __nspfxs__ = ('w',) - __attrs__ = ('w:val',) + __tag__ = "w:type" + __nspfxs__ = ("w",) + __attrs__ = ("w:val",) def a_pgMar(): diff --git a/tests/oxml/unitdata/shared.py b/tests/oxml/unitdata/shared.py index 4bee3ae0f..bf67463e0 100644 --- a/tests/oxml/unitdata/shared.py +++ b/tests/oxml/unitdata/shared.py @@ -1,27 +1,23 @@ -# encoding: utf-8 - -""" -Test data builders shared by more than one other module -""" +"""Test data builders shared by more than one other module.""" from ...unitdata import BaseBuilder class CT_OnOffBuilder(BaseBuilder): - __nspfxs__ = ('w',) - __attrs__ = ('w:val') + __nspfxs__ = ("w",) + __attrs__ = "w:val" def __init__(self, tag): self.__tag__ = tag super(CT_OnOffBuilder, self).__init__() def with_val(self, value): - self._set_xmlattr('w:val', str(value)) + self._set_xmlattr("w:val", str(value)) return self class CT_StringBuilder(BaseBuilder): - __nspfxs__ = ('w',) + __nspfxs__ = ("w",) __attrs__ = () def __init__(self, tag): @@ -29,5 +25,5 @@ def __init__(self, tag): super(CT_StringBuilder, self).__init__() def with_val(self, value): - self._set_xmlattr('w:val', str(value)) + self._set_xmlattr("w:val", str(value)) return self diff --git a/tests/oxml/unitdata/styles.py b/tests/oxml/unitdata/styles.py index cf7dd4fa6..24acd5ed2 100644 --- a/tests/oxml/unitdata/styles.py +++ b/tests/oxml/unitdata/styles.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - -""" -Test data builders for styles part XML elements -""" +"""Test data builders for styles part XML elements.""" from ...unitdata import BaseBuilder class CT_StyleBuilder(BaseBuilder): - __tag__ = 'w:style' - __nspfxs__ = ('w',) - __attrs__ = ('w:type', 'w:styleId', 'w:default', 'w:customStyle') + __tag__ = "w:style" + __nspfxs__ = ("w",) + __attrs__ = ("w:type", "w:styleId", "w:default", "w:customStyle") class CT_StylesBuilder(BaseBuilder): - __tag__ = 'w:styles' - __nspfxs__ = ('w',) + __tag__ = "w:styles" + __nspfxs__ = ("w",) __attrs__ = () diff --git a/tests/oxml/unitdata/table.py b/tests/oxml/unitdata/table.py index 5f0cb2722..4f760c1a8 100644 --- a/tests/oxml/unitdata/table.py +++ b/tests/oxml/unitdata/table.py @@ -1,58 +1,54 @@ -# encoding: utf-8 - -""" -Test data builders for text XML elements -""" +"""Test data builders for text XML elements.""" from ...unitdata import BaseBuilder from .shared import CT_StringBuilder class CT_RowBuilder(BaseBuilder): - __tag__ = 'w:tr' - __nspfxs__ = ('w',) - __attrs__ = ('w:w',) + __tag__ = "w:tr" + __nspfxs__ = ("w",) + __attrs__ = ("w:w",) class CT_TblBuilder(BaseBuilder): - __tag__ = 'w:tbl' - __nspfxs__ = ('w',) + __tag__ = "w:tbl" + __nspfxs__ = ("w",) __attrs__ = () class CT_TblGridBuilder(BaseBuilder): - __tag__ = 'w:tblGrid' - __nspfxs__ = ('w',) - __attrs__ = ('w:w',) + __tag__ = "w:tblGrid" + __nspfxs__ = ("w",) + __attrs__ = ("w:w",) class CT_TblGridColBuilder(BaseBuilder): - __tag__ = 'w:gridCol' - __nspfxs__ = ('w',) - __attrs__ = ('w:w',) + __tag__ = "w:gridCol" + __nspfxs__ = ("w",) + __attrs__ = ("w:w",) class CT_TblPrBuilder(BaseBuilder): - __tag__ = 'w:tblPr' - __nspfxs__ = ('w',) + __tag__ = "w:tblPr" + __nspfxs__ = ("w",) __attrs__ = () class CT_TblWidthBuilder(BaseBuilder): - __tag__ = 'w:tblW' - __nspfxs__ = ('w',) - __attrs__ = ('w:w', 'w:type') + __tag__ = "w:tblW" + __nspfxs__ = ("w",) + __attrs__ = ("w:w", "w:type") class CT_TcBuilder(BaseBuilder): - __tag__ = 'w:tc' - __nspfxs__ = ('w',) - __attrs__ = ('w:id',) + __tag__ = "w:tc" + __nspfxs__ = ("w",) + __attrs__ = ("w:id",) class CT_TcPrBuilder(BaseBuilder): - __tag__ = 'w:tcPr' - __nspfxs__ = ('w',) + __tag__ = "w:tcPr" + __nspfxs__ = ("w",) __attrs__ = () @@ -73,7 +69,7 @@ def a_tblPr(): def a_tblStyle(): - return CT_StringBuilder('w:tblStyle') + return CT_StringBuilder("w:tblStyle") def a_tblW(): diff --git a/tests/oxml/unitdata/text.py b/tests/oxml/unitdata/text.py index 361296147..8bf60bbe3 100644 --- a/tests/oxml/unitdata/text.py +++ b/tests/oxml/unitdata/text.py @@ -1,21 +1,17 @@ -# encoding: utf-8 - -""" -Test data builders for text XML elements -""" +"""Test data builders for text XML elements.""" from ...unitdata import BaseBuilder from .shared import CT_OnOffBuilder, CT_StringBuilder class CT_BrBuilder(BaseBuilder): - __tag__ = 'w:br' - __nspfxs__ = ('w',) - __attrs__ = ('w:type', 'w:clear') + __tag__ = "w:br" + __nspfxs__ = ("w",) + __attrs__ = ("w:type", "w:clear") class CT_EmptyBuilder(BaseBuilder): - __nspfxs__ = ('w',) + __nspfxs__ = ("w",) __attrs__ = () def __init__(self, tag): @@ -24,65 +20,63 @@ def __init__(self, tag): class CT_JcBuilder(BaseBuilder): - __tag__ = 'w:jc' - __nspfxs__ = ('w',) - __attrs__ = ('w:val',) + __tag__ = "w:jc" + __nspfxs__ = ("w",) + __attrs__ = ("w:val",) class CT_PBuilder(BaseBuilder): - __tag__ = 'w:p' - __nspfxs__ = ('w',) + __tag__ = "w:p" + __nspfxs__ = ("w",) __attrs__ = () class CT_PPrBuilder(BaseBuilder): - __tag__ = 'w:pPr' - __nspfxs__ = ('w',) + __tag__ = "w:pPr" + __nspfxs__ = ("w",) __attrs__ = () class CT_RBuilder(BaseBuilder): - __tag__ = 'w:r' - __nspfxs__ = ('w',) + __tag__ = "w:r" + __nspfxs__ = ("w",) __attrs__ = () class CT_RPrBuilder(BaseBuilder): - __tag__ = 'w:rPr' - __nspfxs__ = ('w',) + __tag__ = "w:rPr" + __nspfxs__ = ("w",) __attrs__ = () class CT_SectPrBuilder(BaseBuilder): - __tag__ = 'w:sectPr' - __nspfxs__ = ('w',) + __tag__ = "w:sectPr" + __nspfxs__ = ("w",) __attrs__ = () class CT_TextBuilder(BaseBuilder): - __tag__ = 'w:t' - __nspfxs__ = ('w',) + __tag__ = "w:t" + __nspfxs__ = ("w",) __attrs__ = () def with_space(self, value): - self._set_xmlattr('xml:space', str(value)) + self._set_xmlattr("xml:space", str(value)) return self class CT_UnderlineBuilder(BaseBuilder): - __tag__ = 'w:u' - __nspfxs__ = ('w',) - __attrs__ = ( - 'w:val', 'w:color', 'w:themeColor', 'w:themeTint', 'w:themeShade' - ) + __tag__ = "w:u" + __nspfxs__ = ("w",) + __attrs__ = ("w:val", "w:color", "w:themeColor", "w:themeTint", "w:themeShade") def a_b(): - return CT_OnOffBuilder('w:b') + return CT_OnOffBuilder("w:b") def a_bCs(): - return CT_OnOffBuilder('w:bCs') + return CT_OnOffBuilder("w:bCs") def a_br(): @@ -90,19 +84,19 @@ def a_br(): def a_caps(): - return CT_OnOffBuilder('w:caps') + return CT_OnOffBuilder("w:caps") def a_cr(): - return CT_EmptyBuilder('w:cr') + return CT_EmptyBuilder("w:cr") def a_cs(): - return CT_OnOffBuilder('w:cs') + return CT_OnOffBuilder("w:cs") def a_dstrike(): - return CT_OnOffBuilder('w:dstrike') + return CT_OnOffBuilder("w:dstrike") def a_jc(): @@ -110,39 +104,39 @@ def a_jc(): def a_noProof(): - return CT_OnOffBuilder('w:noProof') + return CT_OnOffBuilder("w:noProof") def a_shadow(): - return CT_OnOffBuilder('w:shadow') + return CT_OnOffBuilder("w:shadow") def a_smallCaps(): - return CT_OnOffBuilder('w:smallCaps') + return CT_OnOffBuilder("w:smallCaps") def a_snapToGrid(): - return CT_OnOffBuilder('w:snapToGrid') + return CT_OnOffBuilder("w:snapToGrid") def a_specVanish(): - return CT_OnOffBuilder('w:specVanish') + return CT_OnOffBuilder("w:specVanish") def a_strike(): - return CT_OnOffBuilder('w:strike') + return CT_OnOffBuilder("w:strike") def a_tab(): - return CT_EmptyBuilder('w:tab') + return CT_EmptyBuilder("w:tab") def a_vanish(): - return CT_OnOffBuilder('w:vanish') + return CT_OnOffBuilder("w:vanish") def a_webHidden(): - return CT_OnOffBuilder('w:webHidden') + return CT_OnOffBuilder("w:webHidden") def a_p(): @@ -154,7 +148,7 @@ def a_pPr(): def a_pStyle(): - return CT_StringBuilder('w:pStyle') + return CT_StringBuilder("w:pStyle") def a_sectPr(): @@ -170,27 +164,27 @@ def a_u(): def an_emboss(): - return CT_OnOffBuilder('w:emboss') + return CT_OnOffBuilder("w:emboss") def an_i(): - return CT_OnOffBuilder('w:i') + return CT_OnOffBuilder("w:i") def an_iCs(): - return CT_OnOffBuilder('w:iCs') + return CT_OnOffBuilder("w:iCs") def an_imprint(): - return CT_OnOffBuilder('w:imprint') + return CT_OnOffBuilder("w:imprint") def an_oMath(): - return CT_OnOffBuilder('w:oMath') + return CT_OnOffBuilder("w:oMath") def an_outline(): - return CT_OnOffBuilder('w:outline') + return CT_OnOffBuilder("w:outline") def an_r(): @@ -202,8 +196,8 @@ def an_rPr(): def an_rStyle(): - return CT_StringBuilder('w:rStyle') + return CT_StringBuilder("w:rStyle") def an_rtl(): - return CT_OnOffBuilder('w:rtl') + return CT_OnOffBuilder("w:rtl") diff --git a/tests/parts/test_document.py b/tests/parts/test_document.py index d6f0e7731..3a86b5168 100644 --- a/tests/parts/test_document.py +++ b/tests/parts/test_document.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.parts.document module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.parts.document module.""" import pytest @@ -23,8 +19,7 @@ from ..unitutil.mock import class_mock, instance_mock, method_mock, property_mock -class DescribeDocumentPart(object): - +class DescribeDocumentPart: def it_can_add_a_footer_part(self, package_, FooterPart_, footer_part_, relate_to_): FooterPart_.new.return_value = footer_part_ relate_to_.return_value = "rId12" @@ -101,7 +96,8 @@ def it_provides_access_to_its_core_properties(self, core_props_fixture): assert core_properties is core_properties_ def it_provides_access_to_the_inline_shapes_in_the_document( - self, inline_shapes_fixture): + self, inline_shapes_fixture + ): document, InlineShapes_, body_elm = inline_shapes_fixture inline_shapes = document.inline_shapes InlineShapes_.assert_called_once_with(body_elm, document) @@ -209,10 +205,7 @@ def core_props_fixture(self, package_, core_properties_): @pytest.fixture def inline_shapes_fixture(self, request, InlineShapes_): - document_elm = ( - a_document().with_nsdecls().with_child( - a_body()) - ).element + document_elm = (a_document().with_nsdecls().with_child(a_body())).element body_elm = document_elm[0] document = DocumentPart(None, None, document_elm, None) return document, InlineShapes_, body_elm @@ -220,12 +213,11 @@ def inline_shapes_fixture(self, request, InlineShapes_): @pytest.fixture def save_fixture(self, package_): document_part = DocumentPart(None, None, None, package_) - file_ = 'foobar.docx' + file_ = "foobar.docx" return document_part, file_ @pytest.fixture - def settings_fixture(self, _settings_part_prop_, settings_part_, - settings_): + def settings_fixture(self, _settings_part_prop_, settings_part_, settings_): document_part = DocumentPart(None, None, None, None) _settings_part_prop_.return_value = settings_part_ settings_part_.settings = settings_ @@ -250,7 +242,7 @@ def drop_rel_(self, request): @pytest.fixture def FooterPart_(self, request): - return class_mock(request, 'docx.parts.document.FooterPart') + return class_mock(request, "docx.parts.document.FooterPart") @pytest.fixture def footer_part_(self, request): @@ -258,7 +250,7 @@ def footer_part_(self, request): @pytest.fixture def HeaderPart_(self, request): - return class_mock(request, 'docx.parts.document.HeaderPart') + return class_mock(request, "docx.parts.document.HeaderPart") @pytest.fixture def header_part_(self, request): @@ -266,11 +258,11 @@ def header_part_(self, request): @pytest.fixture def InlineShapes_(self, request): - return class_mock(request, 'docx.parts.document.InlineShapes') + return class_mock(request, "docx.parts.document.InlineShapes") @pytest.fixture def NumberingPart_(self, request): - return class_mock(request, 'docx.parts.document.NumberingPart') + return class_mock(request, "docx.parts.document.NumberingPart") @pytest.fixture def numbering_part_(self, request): @@ -282,11 +274,11 @@ def package_(self, request): @pytest.fixture def part_related_by_(self, request): - return method_mock(request, DocumentPart, 'part_related_by') + return method_mock(request, DocumentPart, "part_related_by") @pytest.fixture def relate_to_(self, request): - return method_mock(request, DocumentPart, 'relate_to') + return method_mock(request, DocumentPart, "relate_to") @pytest.fixture def related_parts_(self, request): @@ -294,11 +286,11 @@ def related_parts_(self, request): @pytest.fixture def related_parts_prop_(self, request): - return property_mock(request, DocumentPart, 'related_parts') + return property_mock(request, DocumentPart, "related_parts") @pytest.fixture def SettingsPart_(self, request): - return class_mock(request, 'docx.parts.document.SettingsPart') + return class_mock(request, "docx.parts.document.SettingsPart") @pytest.fixture def settings_(self, request): @@ -310,7 +302,7 @@ def settings_part_(self, request): @pytest.fixture def _settings_part_prop_(self, request): - return property_mock(request, DocumentPart, '_settings_part') + return property_mock(request, DocumentPart, "_settings_part") @pytest.fixture def style_(self, request): @@ -322,7 +314,7 @@ def styles_(self, request): @pytest.fixture def StylesPart_(self, request): - return class_mock(request, 'docx.parts.document.StylesPart') + return class_mock(request, "docx.parts.document.StylesPart") @pytest.fixture def styles_part_(self, request): @@ -330,8 +322,8 @@ def styles_part_(self, request): @pytest.fixture def styles_prop_(self, request): - return property_mock(request, DocumentPart, 'styles') + return property_mock(request, DocumentPart, "styles") @pytest.fixture def _styles_part_prop_(self, request): - return property_mock(request, DocumentPart, '_styles_part') + return property_mock(request, DocumentPart, "_styles_part") diff --git a/tests/parts/test_hdrftr.py b/tests/parts/test_hdrftr.py index 205738036..ee0cc7134 100644 --- a/tests/parts/test_hdrftr.py +++ b/tests/parts/test_hdrftr.py @@ -1,12 +1,9 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.parts.hdrftr module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.parts.hdrftr module.""" import pytest -from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.part import PartFactory from docx.package import Package from docx.parts.hdrftr import FooterPart, HeaderPart @@ -15,8 +12,7 @@ from ..unitutil.mock import function_mock, initializer_mock, instance_mock, method_mock -class DescribeFooterPart(object): - +class DescribeFooterPart: def it_is_used_by_loader_to_construct_footer_part( self, package_, FooterPart_load_, footer_part_ ): @@ -84,8 +80,7 @@ def parse_xml_(self, request): return function_mock(request, "docx.parts.hdrftr.parse_xml") -class DescribeHeaderPart(object): - +class DescribeHeaderPart: def it_is_used_by_loader_to_construct_header_part( self, package_, HeaderPart_load_, header_part_ ): diff --git a/tests/parts/test_image.py b/tests/parts/test_image.py index 1ab2490be..acf0b0727 100644 --- a/tests/parts/test_image.py +++ b/tests/parts/test_image.py @@ -1,13 +1,10 @@ -# encoding: utf-8 - -"""Unit test suite for docx.parts.image module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for docx.parts.image module.""" import pytest from docx.image.image import Image -from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT +from docx.opc.constants import CONTENT_TYPE as CT +from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.packuri import PackURI from docx.opc.part import PartFactory from docx.package import Package @@ -17,8 +14,7 @@ from ..unitutil.mock import ANY, initializer_mock, instance_mock, method_mock -class DescribeImagePart(object): - +class DescribeImagePart: def it_is_used_by_PartFactory_to_construct_image_part( self, image_part_load_, partname_, blob_, package_, image_part_ ): @@ -51,9 +47,9 @@ def it_knows_its_filename(self, filename_fixture): assert image_part.filename == expected_filename def it_knows_the_sha1_of_its_image(self): - blob = b'fO0Bar' + blob = b"fO0Bar" image_part = ImagePart(None, None, blob) - assert image_part.sha1 == '4921e7002ddfba690a937d54bda226a7b8bdeb68' + assert image_part.sha1 == "4921e7002ddfba690a937d54bda226a7b8bdeb68" # fixtures ------------------------------------------------------- @@ -61,33 +57,31 @@ def it_knows_the_sha1_of_its_image(self): def blob_(self, request): return instance_mock(request, str) - @pytest.fixture(params=['loaded', 'new']) + @pytest.fixture(params=["loaded", "new"]) def dimensions_fixture(self, request): - image_file_path = test_file('monty-truth.png') + image_file_path = test_file("monty-truth.png") image = Image.from_file(image_file_path) expected_cx, expected_cy = 1905000, 2717800 # case 1: image part is loaded by PartFactory w/no Image inst - if request.param == 'loaded': - partname = PackURI('/word/media/image1.png') + if request.param == "loaded": + partname = PackURI("/word/media/image1.png") content_type = CT.PNG - image_part = ImagePart.load( - partname, content_type, image.blob, None - ) + image_part = ImagePart.load(partname, content_type, image.blob, None) # case 2: image part is newly created from image file - elif request.param == 'new': + elif request.param == "new": image_part = ImagePart.from_image(image, None) return image_part, expected_cx, expected_cy - @pytest.fixture(params=['loaded', 'new']) + @pytest.fixture(params=["loaded", "new"]) def filename_fixture(self, request, image_): - partname = PackURI('/word/media/image666.png') - if request.param == 'loaded': + partname = PackURI("/word/media/image666.png") + if request.param == "loaded": image_part = ImagePart(partname, None, None, None) - expected_filename = 'image.png' - elif request.param == 'new': - image_.filename = 'foobar.PXG' + expected_filename = "image.png" + elif request.param == "new": + image_.filename = "foobar.PXG" image_part = ImagePart(partname, None, None, image_) expected_filename = image_.filename return image_part, expected_filename @@ -106,7 +100,7 @@ def image_part_(self, request): @pytest.fixture def image_part_load_(self, request): - return method_mock(request, ImagePart, 'load', autospec=False) + return method_mock(request, ImagePart, "load", autospec=False) @pytest.fixture def package_(self, request): diff --git a/tests/parts/test_numbering.py b/tests/parts/test_numbering.py index e5292a67c..7655206ec 100644 --- a/tests/parts/test_numbering.py +++ b/tests/parts/test_numbering.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.parts.numbering module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for the docx.parts.numbering module.""" import pytest @@ -15,12 +9,14 @@ from ..unitutil.mock import class_mock, instance_mock -class DescribeNumberingPart(object): - - def it_provides_access_to_the_numbering_definitions( - self, num_defs_fixture): - (numbering_part, _NumberingDefinitions_, numbering_elm_, - numbering_definitions_) = num_defs_fixture +class DescribeNumberingPart: + def it_provides_access_to_the_numbering_definitions(self, num_defs_fixture): + ( + numbering_part, + _NumberingDefinitions_, + numbering_elm_, + numbering_definitions_, + ) = num_defs_fixture numbering_definitions = numbering_part.numbering_definitions _NumberingDefinitions_.assert_called_once_with(numbering_elm_) assert numbering_definitions is numbering_definitions_ @@ -29,12 +25,14 @@ def it_provides_access_to_the_numbering_definitions( @pytest.fixture def num_defs_fixture( - self, _NumberingDefinitions_, numbering_elm_, - numbering_definitions_): + self, _NumberingDefinitions_, numbering_elm_, numbering_definitions_ + ): numbering_part = NumberingPart(None, None, numbering_elm_, None) return ( - numbering_part, _NumberingDefinitions_, numbering_elm_, - numbering_definitions_ + numbering_part, + _NumberingDefinitions_, + numbering_elm_, + numbering_definitions_, ) # fixture components --------------------------------------------- @@ -42,8 +40,9 @@ def num_defs_fixture( @pytest.fixture def _NumberingDefinitions_(self, request, numbering_definitions_): return class_mock( - request, 'docx.parts.numbering._NumberingDefinitions', - return_value=numbering_definitions_ + request, + "docx.parts.numbering._NumberingDefinitions", + return_value=numbering_definitions_, ) @pytest.fixture @@ -55,10 +54,8 @@ def numbering_elm_(self, request): return instance_mock(request, CT_Numbering) -class Describe_NumberingDefinitions(object): - - def it_knows_how_many_numbering_definitions_it_contains( - self, len_fixture): +class Describe_NumberingDefinitions: + def it_knows_how_many_numbering_definitions_it_contains(self, len_fixture): numbering_definitions, numbering_definition_count = len_fixture assert len(numbering_definitions) == numbering_definition_count diff --git a/tests/parts/test_settings.py b/tests/parts/test_settings.py index 12c6b1d27..581cc6173 100644 --- a/tests/parts/test_settings.py +++ b/tests/parts/test_settings.py @@ -1,9 +1,5 @@ -# encoding: utf-8 - """Unit test suite for the docx.parts.settings module""" -from __future__ import absolute_import, division, print_function, unicode_literals - import pytest from docx.opc.constants import CONTENT_TYPE as CT @@ -17,12 +13,11 @@ from ..unitutil.mock import class_mock, instance_mock, method_mock -class DescribeSettingsPart(object): - +class DescribeSettingsPart: def it_is_used_by_loader_to_construct_settings_part( self, load_, package_, settings_part_ ): - partname, blob = 'partname', 'blob' + partname, blob = "partname", "blob" content_type = CT.WML_SETTINGS load_.return_value = settings_part_ @@ -41,7 +36,7 @@ def it_constructs_a_default_settings_part_to_help(self): package = OpcPackage() settings_part = SettingsPart.default(package) assert isinstance(settings_part, SettingsPart) - assert settings_part.partname == '/word/settings.xml' + assert settings_part.partname == "/word/settings.xml" assert settings_part.content_type == CT.WML_SETTINGS assert settings_part.package is package assert len(settings_part.element) == 6 @@ -50,7 +45,7 @@ def it_constructs_a_default_settings_part_to_help(self): @pytest.fixture def settings_fixture(self, Settings_, settings_): - settings_elm = element('w:settings') + settings_elm = element("w:settings") settings_part = SettingsPart(None, None, settings_elm, None) return settings_part, Settings_, settings_ @@ -58,7 +53,7 @@ def settings_fixture(self, Settings_, settings_): @pytest.fixture def load_(self, request): - return method_mock(request, SettingsPart, 'load', autospec=False) + return method_mock(request, SettingsPart, "load", autospec=False) @pytest.fixture def package_(self, request): @@ -67,7 +62,7 @@ def package_(self, request): @pytest.fixture def Settings_(self, request, settings_): return class_mock( - request, 'docx.parts.settings.Settings', return_value=settings_ + request, "docx.parts.settings.Settings", return_value=settings_ ) @pytest.fixture diff --git a/tests/parts/test_story.py b/tests/parts/test_story.py index e42fc49ae..b65abe8b7 100644 --- a/tests/parts/test_story.py +++ b/tests/parts/test_story.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.parts.story module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.parts.story module.""" import pytest @@ -12,7 +8,7 @@ from docx.package import Package from docx.parts.document import DocumentPart from docx.parts.image import ImagePart -from docx.parts.story import BaseStoryPart +from docx.parts.story import StoryPart from docx.styles.style import BaseStyle from ..unitutil.cxml import element @@ -20,13 +16,12 @@ from ..unitutil.mock import instance_mock, method_mock, property_mock -class DescribeBaseStoryPart(object): - +class DescribeStoryPart: def it_can_get_or_add_an_image(self, package_, image_part_, image_, relate_to_): package_.get_or_add_image_part.return_value = image_part_ relate_to_.return_value = "rId42" image_part_.image = image_ - story_part = BaseStoryPart(None, None, None, package_) + story_part = StoryPart(None, None, None, package_) rId, image = story_part.get_or_add_image("image.png") @@ -42,7 +37,7 @@ def it_can_get_a_style_by_id_and_type( style_type = WD_STYLE_TYPE.PARAGRAPH _document_part_prop_.return_value = document_part_ document_part_.get_style.return_value = style_ - story_part = BaseStoryPart(None, None, None, None) + story_part = StoryPart(None, None, None, None) style = story_part.get_style(style_id, style_type) @@ -55,7 +50,7 @@ def it_can_get_a_style_id_by_style_or_name_and_type( style_type = WD_STYLE_TYPE.PARAGRAPH _document_part_prop_.return_value = document_part_ document_part_.get_style_id.return_value = "BodyText" - story_part = BaseStoryPart(None, None, None, None) + story_part = StoryPart(None, None, None, None) style_id = story_part.get_style_id(style_, style_type) @@ -68,7 +63,7 @@ def it_can_create_a_new_pic_inline(self, get_or_add_image_, image_, next_id_prop image_.filename = "bar.png" next_id_prop_.return_value = 24 expected_xml = snippet_text("inline") - story_part = BaseStoryPart(None, None, None, None) + story_part = StoryPart(None, None, None, None) inline = story_part.new_pic_inline("foo/bar.png", width=100, height=200) @@ -78,7 +73,7 @@ def it_can_create_a_new_pic_inline(self, get_or_add_image_, image_, next_id_prop def it_knows_the_next_available_xml_id(self, next_id_fixture): story_element, expected_value = next_id_fixture - story_part = BaseStoryPart(None, None, story_element, None) + story_part = StoryPart(None, None, story_element, None) next_id = story_part.next_id @@ -86,7 +81,7 @@ def it_knows_the_next_available_xml_id(self, next_id_fixture): def it_knows_the_main_document_part_to_help(self, package_, document_part_): package_.main_document_part = document_part_ - story_part = BaseStoryPart(None, None, None, package_) + story_part = StoryPart(None, None, None, package_) document_part = story_part._document_part @@ -120,11 +115,11 @@ def document_part_(self, request): @pytest.fixture def _document_part_prop_(self, request): - return property_mock(request, BaseStoryPart, "_document_part") + return property_mock(request, StoryPart, "_document_part") @pytest.fixture def get_or_add_image_(self, request): - return method_mock(request, BaseStoryPart, "get_or_add_image") + return method_mock(request, StoryPart, "get_or_add_image") @pytest.fixture def image_(self, request): @@ -136,7 +131,7 @@ def image_part_(self, request): @pytest.fixture def next_id_prop_(self, request): - return property_mock(request, BaseStoryPart, "next_id") + return property_mock(request, StoryPart, "next_id") @pytest.fixture def package_(self, request): @@ -144,7 +139,7 @@ def package_(self, request): @pytest.fixture def relate_to_(self, request): - return method_mock(request, BaseStoryPart, "relate_to") + return method_mock(request, StoryPart, "relate_to") @pytest.fixture def style_(self, request): diff --git a/tests/parts/test_styles.py b/tests/parts/test_styles.py index 8ff600359..d0f63cfbb 100644 --- a/tests/parts/test_styles.py +++ b/tests/parts/test_styles.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.parts.styles module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for the docx.parts.styles module.""" import pytest @@ -17,8 +11,7 @@ from ..unitutil.mock import class_mock, instance_mock -class DescribeStylesPart(object): - +class DescribeStylesPart: def it_provides_access_to_its_styles(self, styles_fixture): styles_part, Styles_, styles_ = styles_fixture styles = styles_part.styles @@ -29,7 +22,7 @@ def it_can_construct_a_default_styles_part_to_help(self): package = OpcPackage() styles_part = StylesPart.default(package) assert isinstance(styles_part, StylesPart) - assert styles_part.partname == '/word/styles.xml' + assert styles_part.partname == "/word/styles.xml" assert styles_part.content_type == CT.WML_STYLES assert styles_part.package is package assert len(styles_part.element) == 6 @@ -45,9 +38,7 @@ def styles_fixture(self, Styles_, styles_elm_, styles_): @pytest.fixture def Styles_(self, request, styles_): - return class_mock( - request, 'docx.parts.styles.Styles', return_value=styles_ - ) + return class_mock(request, "docx.parts.styles.Styles", return_value=styles_) @pytest.fixture def styles_(self, request): diff --git a/tests/styles/test_latent.py b/tests/styles/test_latent.py index f42214cf6..9479d6b44 100644 --- a/tests/styles/test_latent.py +++ b/tests/styles/test_latent.py @@ -1,22 +1,13 @@ -# encoding: utf-8 - -""" -Unit test suite for the docx.styles.latent module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Unit test suite for the docx.styles.latent module.""" import pytest -from docx.styles.latent import _LatentStyle, LatentStyles +from docx.styles.latent import LatentStyles, _LatentStyle from ..unitutil.cxml import element, xml -class DescribeLatentStyle(object): - +class DescribeLatentStyle: def it_can_delete_itself(self, delete_fixture): latent_style, latent_styles, expected_xml = delete_fixture latent_style.delete() @@ -50,77 +41,87 @@ def it_can_change_its_on_off_properties(self, on_off_set_fixture): @pytest.fixture def delete_fixture(self): - latent_styles = element('w:latentStyles/w:lsdException{w:name=Foo}') + latent_styles = element("w:latentStyles/w:lsdException{w:name=Foo}") latent_style = _LatentStyle(latent_styles[0]) - expected_xml = xml('w:latentStyles') + expected_xml = xml("w:latentStyles") return latent_style, latent_styles, expected_xml - @pytest.fixture(params=[ - ('w:lsdException{w:name=heading 1}', 'Heading 1'), - ]) + @pytest.fixture( + params=[ + ("w:lsdException{w:name=heading 1}", "Heading 1"), + ] + ) def name_get_fixture(self, request): lsdException_cxml, expected_value = request.param latent_style = _LatentStyle(element(lsdException_cxml)) return latent_style, expected_value - @pytest.fixture(params=[ - ('w:lsdException', 'hidden', None), - ('w:lsdException', 'locked', None), - ('w:lsdException', 'quick_style', None), - ('w:lsdException', 'unhide_when_used', None), - ('w:lsdException{w:semiHidden=1}', 'hidden', True), - ('w:lsdException{w:locked=1}', 'locked', True), - ('w:lsdException{w:qFormat=1}', 'quick_style', True), - ('w:lsdException{w:unhideWhenUsed=1}', 'unhide_when_used', True), - ('w:lsdException{w:semiHidden=0}', 'hidden', False), - ('w:lsdException{w:locked=0}', 'locked', False), - ('w:lsdException{w:qFormat=0}', 'quick_style', False), - ('w:lsdException{w:unhideWhenUsed=0}', 'unhide_when_used', False), - ]) + @pytest.fixture( + params=[ + ("w:lsdException", "hidden", None), + ("w:lsdException", "locked", None), + ("w:lsdException", "quick_style", None), + ("w:lsdException", "unhide_when_used", None), + ("w:lsdException{w:semiHidden=1}", "hidden", True), + ("w:lsdException{w:locked=1}", "locked", True), + ("w:lsdException{w:qFormat=1}", "quick_style", True), + ("w:lsdException{w:unhideWhenUsed=1}", "unhide_when_used", True), + ("w:lsdException{w:semiHidden=0}", "hidden", False), + ("w:lsdException{w:locked=0}", "locked", False), + ("w:lsdException{w:qFormat=0}", "quick_style", False), + ("w:lsdException{w:unhideWhenUsed=0}", "unhide_when_used", False), + ] + ) def on_off_get_fixture(self, request): lsdException_cxml, prop_name, expected_value = request.param latent_style = _LatentStyle(element(lsdException_cxml)) return latent_style, prop_name, expected_value - @pytest.fixture(params=[ - ('w:lsdException', 'hidden', True, - 'w:lsdException{w:semiHidden=1}'), - ('w:lsdException{w:semiHidden=1}', 'hidden', False, - 'w:lsdException{w:semiHidden=0}'), - ('w:lsdException{w:semiHidden=0}', 'hidden', None, - 'w:lsdException'), - ('w:lsdException', 'locked', True, - 'w:lsdException{w:locked=1}'), - ('w:lsdException', 'quick_style', False, - 'w:lsdException{w:qFormat=0}'), - ('w:lsdException', 'unhide_when_used', True, - 'w:lsdException{w:unhideWhenUsed=1}'), - ('w:lsdException{w:locked=1}', 'locked', None, - 'w:lsdException'), - ]) + @pytest.fixture( + params=[ + ("w:lsdException", "hidden", True, "w:lsdException{w:semiHidden=1}"), + ( + "w:lsdException{w:semiHidden=1}", + "hidden", + False, + "w:lsdException{w:semiHidden=0}", + ), + ("w:lsdException{w:semiHidden=0}", "hidden", None, "w:lsdException"), + ("w:lsdException", "locked", True, "w:lsdException{w:locked=1}"), + ("w:lsdException", "quick_style", False, "w:lsdException{w:qFormat=0}"), + ( + "w:lsdException", + "unhide_when_used", + True, + "w:lsdException{w:unhideWhenUsed=1}", + ), + ("w:lsdException{w:locked=1}", "locked", None, "w:lsdException"), + ] + ) def on_off_set_fixture(self, request): lsdException_cxml, prop_name, value, expected_cxml = request.param latent_styles = _LatentStyle(element(lsdException_cxml)) expected_xml = xml(expected_cxml) return latent_styles, prop_name, value, expected_xml - @pytest.fixture(params=[ - ('w:lsdException', None), - ('w:lsdException{w:uiPriority=42}', 42), - ]) + @pytest.fixture( + params=[ + ("w:lsdException", None), + ("w:lsdException{w:uiPriority=42}", 42), + ] + ) def priority_get_fixture(self, request): lsdException_cxml, expected_value = request.param latent_style = _LatentStyle(element(lsdException_cxml)) return latent_style, expected_value - @pytest.fixture(params=[ - ('w:lsdException', 42, - 'w:lsdException{w:uiPriority=42}'), - ('w:lsdException{w:uiPriority=42}', 24, - 'w:lsdException{w:uiPriority=24}'), - ('w:lsdException{w:uiPriority=24}', None, - 'w:lsdException'), - ]) + @pytest.fixture( + params=[ + ("w:lsdException", 42, "w:lsdException{w:uiPriority=42}"), + ("w:lsdException{w:uiPriority=42}", 24, "w:lsdException{w:uiPriority=24}"), + ("w:lsdException{w:uiPriority=24}", None, "w:lsdException"), + ] + ) def priority_set_fixture(self, request): lsdException_cxml, new_value, expected_cxml = request.param latent_style = _LatentStyle(element(lsdException_cxml)) @@ -128,8 +129,7 @@ def priority_set_fixture(self, request): return latent_style, new_value, expected_xml -class DescribeLatentStyles(object): - +class DescribeLatentStyles: def it_can_add_a_latent_style(self, add_fixture): latent_styles, name, expected_xml = add_fixture @@ -145,7 +145,7 @@ def it_knows_how_many_latent_styles_it_contains(self, len_fixture): def it_can_iterate_over_its_latent_styles(self, iter_fixture): latent_styles, expected_count = iter_fixture - lst = [ls for ls in latent_styles] + lst = list(latent_styles) assert len(lst) == expected_count for latent_style in lst: assert isinstance(latent_style, _LatentStyle) @@ -193,79 +193,113 @@ def it_can_change_its_boolean_properties(self, bool_prop_set_fixture): @pytest.fixture def add_fixture(self): - latent_styles = LatentStyles(element('w:latentStyles')) - name = 'Heading 1' - expected_xml = xml('w:latentStyles/w:lsdException{w:name=heading 1}') + latent_styles = LatentStyles(element("w:latentStyles")) + name = "Heading 1" + expected_xml = xml("w:latentStyles/w:lsdException{w:name=heading 1}") return latent_styles, name, expected_xml - @pytest.fixture(params=[ - ('w:latentStyles', 'default_to_hidden', False), - ('w:latentStyles', 'default_to_locked', False), - ('w:latentStyles', 'default_to_quick_style', False), - ('w:latentStyles', 'default_to_unhide_when_used', False), - ('w:latentStyles{w:defSemiHidden=1}', - 'default_to_hidden', True), - ('w:latentStyles{w:defLockedState=0}', - 'default_to_locked', False), - ('w:latentStyles{w:defQFormat=on}', - 'default_to_quick_style', True), - ('w:latentStyles{w:defUnhideWhenUsed=false}', - 'default_to_unhide_when_used', False), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", "default_to_hidden", False), + ("w:latentStyles", "default_to_locked", False), + ("w:latentStyles", "default_to_quick_style", False), + ("w:latentStyles", "default_to_unhide_when_used", False), + ("w:latentStyles{w:defSemiHidden=1}", "default_to_hidden", True), + ("w:latentStyles{w:defLockedState=0}", "default_to_locked", False), + ("w:latentStyles{w:defQFormat=on}", "default_to_quick_style", True), + ( + "w:latentStyles{w:defUnhideWhenUsed=false}", + "default_to_unhide_when_used", + False, + ), + ] + ) def bool_prop_get_fixture(self, request): latentStyles_cxml, prop_name, expected_value = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) return latent_styles, prop_name, expected_value - @pytest.fixture(params=[ - ('w:latentStyles', 'default_to_hidden', True, - 'w:latentStyles{w:defSemiHidden=1}'), - ('w:latentStyles', 'default_to_locked', False, - 'w:latentStyles{w:defLockedState=0}'), - ('w:latentStyles', 'default_to_quick_style', True, - 'w:latentStyles{w:defQFormat=1}'), - ('w:latentStyles', 'default_to_unhide_when_used', False, - 'w:latentStyles{w:defUnhideWhenUsed=0}'), - ('w:latentStyles{w:defSemiHidden=0}', 'default_to_hidden', 'Foo', - 'w:latentStyles{w:defSemiHidden=1}'), - ('w:latentStyles{w:defLockedState=1}', 'default_to_locked', None, - 'w:latentStyles{w:defLockedState=0}'), - ]) + @pytest.fixture( + params=[ + ( + "w:latentStyles", + "default_to_hidden", + True, + "w:latentStyles{w:defSemiHidden=1}", + ), + ( + "w:latentStyles", + "default_to_locked", + False, + "w:latentStyles{w:defLockedState=0}", + ), + ( + "w:latentStyles", + "default_to_quick_style", + True, + "w:latentStyles{w:defQFormat=1}", + ), + ( + "w:latentStyles", + "default_to_unhide_when_used", + False, + "w:latentStyles{w:defUnhideWhenUsed=0}", + ), + ( + "w:latentStyles{w:defSemiHidden=0}", + "default_to_hidden", + "Foo", + "w:latentStyles{w:defSemiHidden=1}", + ), + ( + "w:latentStyles{w:defLockedState=1}", + "default_to_locked", + None, + "w:latentStyles{w:defLockedState=0}", + ), + ] + ) def bool_prop_set_fixture(self, request): latentStyles_cxml, prop_name, value, expected_cxml = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) expected_xml = xml(expected_cxml) return latent_styles, prop_name, value, expected_xml - @pytest.fixture(params=[ - ('w:latentStyles', None), - ('w:latentStyles{w:count=42}', 42), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", None), + ("w:latentStyles{w:count=42}", 42), + ] + ) def count_get_fixture(self, request): latentStyles_cxml, expected_value = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) return latent_styles, expected_value - @pytest.fixture(params=[ - ('w:latentStyles', 42, 'w:latentStyles{w:count=42}'), - ('w:latentStyles{w:count=24}', 42, 'w:latentStyles{w:count=42}'), - ('w:latentStyles{w:count=24}', None, 'w:latentStyles'), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", 42, "w:latentStyles{w:count=42}"), + ("w:latentStyles{w:count=24}", 42, "w:latentStyles{w:count=42}"), + ("w:latentStyles{w:count=24}", None, "w:latentStyles"), + ] + ) def count_set_fixture(self, request): latentStyles_cxml, value, expected_cxml = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) expected_xml = xml(expected_cxml) return latent_styles, value, expected_xml - @pytest.fixture(params=[ - ('w:lsdException{w:name=Ab},w:lsdException,w:lsdException', 'Ab', 0), - ('w:lsdException,w:lsdException{w:name=Cd},w:lsdException', 'Cd', 1), - ('w:lsdException,w:lsdException,w:lsdException{w:name=Ef}', 'Ef', 2), - ('w:lsdException{w:name=heading 1}', 'Heading 1', 0), - ]) + @pytest.fixture( + params=[ + ("w:lsdException{w:name=Ab},w:lsdException,w:lsdException", "Ab", 0), + ("w:lsdException,w:lsdException{w:name=Cd},w:lsdException", "Cd", 1), + ("w:lsdException,w:lsdException,w:lsdException{w:name=Ef}", "Ef", 2), + ("w:lsdException{w:name=heading 1}", "Heading 1", 0), + ] + ) def getitem_fixture(self, request): cxml, name, idx = request.param - latentStyles_cxml = 'w:latentStyles/(%s)' % cxml + latentStyles_cxml = "w:latentStyles/(%s)" % cxml latentStyles = element(latentStyles_cxml) lsdException = latentStyles[idx] latent_styles = LatentStyles(latentStyles) @@ -273,46 +307,55 @@ def getitem_fixture(self, request): @pytest.fixture def getitem_raises_fixture(self): - latent_styles = LatentStyles(element('w:latentStyles')) - return latent_styles, 'Foobar' - - @pytest.fixture(params=[ - ('w:latentStyles', 0), - ('w:latentStyles/w:lsdException', 1), - ('w:latentStyles/(w:lsdException,w:lsdException)', 2), - ]) + latent_styles = LatentStyles(element("w:latentStyles")) + return latent_styles, "Foobar" + + @pytest.fixture( + params=[ + ("w:latentStyles", 0), + ("w:latentStyles/w:lsdException", 1), + ("w:latentStyles/(w:lsdException,w:lsdException)", 2), + ] + ) def iter_fixture(self, request): latentStyles_cxml, count = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) return latent_styles, count - @pytest.fixture(params=[ - ('w:latentStyles', 0), - ('w:latentStyles/w:lsdException', 1), - ('w:latentStyles/(w:lsdException,w:lsdException)', 2), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", 0), + ("w:latentStyles/w:lsdException", 1), + ("w:latentStyles/(w:lsdException,w:lsdException)", 2), + ] + ) def len_fixture(self, request): latentStyles_cxml, count = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) return latent_styles, count - @pytest.fixture(params=[ - ('w:latentStyles', None), - ('w:latentStyles{w:defUIPriority=42}', 42), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", None), + ("w:latentStyles{w:defUIPriority=42}", 42), + ] + ) def priority_get_fixture(self, request): latentStyles_cxml, expected_value = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) return latent_styles, expected_value - @pytest.fixture(params=[ - ('w:latentStyles', 42, - 'w:latentStyles{w:defUIPriority=42}'), - ('w:latentStyles{w:defUIPriority=24}', 42, - 'w:latentStyles{w:defUIPriority=42}'), - ('w:latentStyles{w:defUIPriority=24}', None, - 'w:latentStyles'), - ]) + @pytest.fixture( + params=[ + ("w:latentStyles", 42, "w:latentStyles{w:defUIPriority=42}"), + ( + "w:latentStyles{w:defUIPriority=24}", + 42, + "w:latentStyles{w:defUIPriority=42}", + ), + ("w:latentStyles{w:defUIPriority=24}", None, "w:latentStyles"), + ] + ) def priority_set_fixture(self, request): latentStyles_cxml, value, expected_cxml = request.param latent_styles = LatentStyles(element(latentStyles_cxml)) diff --git a/tests/styles/test_style.py b/tests/styles/test_style.py index 437e390e1..b24e02733 100644 --- a/tests/styles/test_style.py +++ b/tests/styles/test_style.py @@ -1,19 +1,15 @@ -# encoding: utf-8 - -""" -Test suite for the docx.styles.style module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Test suite for the docx.styles.style module.""" import pytest from docx.enum.style import WD_STYLE_TYPE from docx.styles.style import ( - BaseStyle, _CharacterStyle, _ParagraphStyle, _NumberingStyle, - StyleFactory, _TableStyle + BaseStyle, + CharacterStyle, + ParagraphStyle, + StyleFactory, + _NumberingStyle, + _TableStyle, ) from docx.text.font import Font from docx.text.parfmt import ParagraphFormat @@ -22,8 +18,7 @@ from ..unitutil.mock import call, class_mock, function_mock, instance_mock -class DescribeStyleFactory(object): - +class DescribeStyleFactory: def it_constructs_the_right_type_of_style(self, factory_fixture): style_elm, StyleCls_, style_ = factory_fixture style = StyleFactory(style_elm) @@ -32,51 +27,56 @@ def it_constructs_the_right_type_of_style(self, factory_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=['paragraph', 'character', 'table', 'numbering']) + @pytest.fixture(params=["paragraph", "character", "table", "numbering"]) def factory_fixture( - self, request, paragraph_style_, _ParagraphStyle_, - character_style_, _CharacterStyle_, table_style_, _TableStyle_, - numbering_style_, _NumberingStyle_): + self, + request, + paragraph_style_, + ParagraphStyle_, + character_style_, + CharacterStyle_, + table_style_, + _TableStyle_, + numbering_style_, + _NumberingStyle_, + ): type_attr_val = request.param StyleCls_, style_mock = { - 'paragraph': (_ParagraphStyle_, paragraph_style_), - 'character': (_CharacterStyle_, character_style_), - 'table': (_TableStyle_, table_style_), - 'numbering': (_NumberingStyle_, numbering_style_), + "paragraph": (ParagraphStyle_, paragraph_style_), + "character": (CharacterStyle_, character_style_), + "table": (_TableStyle_, table_style_), + "numbering": (_NumberingStyle_, numbering_style_), }[request.param] - style_cxml = 'w:style{w:type=%s}' % type_attr_val + style_cxml = "w:style{w:type=%s}" % type_attr_val style_elm = element(style_cxml) return style_elm, StyleCls_, style_mock # fixture components ----------------------------------- @pytest.fixture - def _ParagraphStyle_(self, request, paragraph_style_): + def ParagraphStyle_(self, request, paragraph_style_): return class_mock( - request, 'docx.styles.style._ParagraphStyle', - return_value=paragraph_style_ + request, "docx.styles.style.ParagraphStyle", return_value=paragraph_style_ ) @pytest.fixture def paragraph_style_(self, request): - return instance_mock(request, _ParagraphStyle) + return instance_mock(request, ParagraphStyle) @pytest.fixture - def _CharacterStyle_(self, request, character_style_): + def CharacterStyle_(self, request, character_style_): return class_mock( - request, 'docx.styles.style._CharacterStyle', - return_value=character_style_ + request, "docx.styles.style.CharacterStyle", return_value=character_style_ ) @pytest.fixture def character_style_(self, request): - return instance_mock(request, _CharacterStyle) + return instance_mock(request, CharacterStyle) @pytest.fixture def _TableStyle_(self, request, table_style_): return class_mock( - request, 'docx.styles.style._TableStyle', - return_value=table_style_ + request, "docx.styles.style._TableStyle", return_value=table_style_ ) @pytest.fixture @@ -86,8 +86,7 @@ def table_style_(self, request): @pytest.fixture def _NumberingStyle_(self, request, numbering_style_): return class_mock( - request, 'docx.styles.style._NumberingStyle', - return_value=numbering_style_ + request, "docx.styles.style._NumberingStyle", return_value=numbering_style_ ) @pytest.fixture @@ -95,8 +94,7 @@ def numbering_style_(self, request): return instance_mock(request, _NumberingStyle) -class DescribeBaseStyle(object): - +class DescribeBaseStyle: def it_knows_its_style_id(self, id_get_fixture): style, expected_value = id_get_fixture assert style.style_id == expected_value @@ -176,11 +174,13 @@ def it_can_delete_itself_from_the_document(self, delete_fixture): # fixture -------------------------------------------------------- - @pytest.fixture(params=[ - ('w:style', True), - ('w:style{w:customStyle=0}', True), - ('w:style{w:customStyle=1}', False), - ]) + @pytest.fixture( + params=[ + ("w:style", True), + ("w:style{w:customStyle=0}", True), + ("w:style{w:customStyle=1}", False), + ] + ) def builtin_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) @@ -188,186 +188,207 @@ def builtin_get_fixture(self, request): @pytest.fixture def delete_fixture(self): - styles = element('w:styles/w:style') + styles = element("w:styles/w:style") style = BaseStyle(styles[0]) - expected_xml = xml('w:styles') + expected_xml = xml("w:styles") return style, styles, expected_xml - @pytest.fixture(params=[ - ('w:style', False), - ('w:style/w:semiHidden', True), - ('w:style/w:semiHidden{w:val=0}', False), - ('w:style/w:semiHidden{w:val=1}', True), - ]) + @pytest.fixture( + params=[ + ("w:style", False), + ("w:style/w:semiHidden", True), + ("w:style/w:semiHidden{w:val=0}", False), + ("w:style/w:semiHidden{w:val=1}", True), + ] + ) def hidden_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', True, 'w:style/w:semiHidden'), - ('w:style/w:semiHidden{w:val=0}', True, 'w:style/w:semiHidden'), - ('w:style/w:semiHidden{w:val=1}', True, 'w:style/w:semiHidden'), - ('w:style', False, 'w:style'), - ('w:style/w:semiHidden', False, 'w:style'), - ('w:style/w:semiHidden{w:val=1}', False, 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", True, "w:style/w:semiHidden"), + ("w:style/w:semiHidden{w:val=0}", True, "w:style/w:semiHidden"), + ("w:style/w:semiHidden{w:val=1}", True, "w:style/w:semiHidden"), + ("w:style", False, "w:style"), + ("w:style/w:semiHidden", False, "w:style"), + ("w:style/w:semiHidden{w:val=1}", False, "w:style"), + ] + ) def hidden_set_fixture(self, request): style_cxml, value, expected_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_cxml) return style, value, expected_xml - @pytest.fixture(params=[ - ('w:style', None), - ('w:style{w:styleId=Foobar}', 'Foobar'), - ]) + @pytest.fixture( + params=[ + ("w:style", None), + ("w:style{w:styleId=Foobar}", "Foobar"), + ] + ) def id_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', 'Foo', 'w:style{w:styleId=Foo}'), - ('w:style{w:styleId=Foo}', 'Bar', 'w:style{w:styleId=Bar}'), - ('w:style{w:styleId=Bar}', None, 'w:style'), - ('w:style', None, 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", "Foo", "w:style{w:styleId=Foo}"), + ("w:style{w:styleId=Foo}", "Bar", "w:style{w:styleId=Bar}"), + ("w:style{w:styleId=Bar}", None, "w:style"), + ("w:style", None, "w:style"), + ] + ) def id_set_fixture(self, request): style_cxml, new_value, expected_style_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_style_cxml) return style, new_value, expected_xml - @pytest.fixture(params=[ - ('w:style', False), - ('w:style/w:locked', True), - ('w:style/w:locked{w:val=0}', False), - ('w:style/w:locked{w:val=1}', True), - ]) + @pytest.fixture( + params=[ + ("w:style", False), + ("w:style/w:locked", True), + ("w:style/w:locked{w:val=0}", False), + ("w:style/w:locked{w:val=1}", True), + ] + ) def locked_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', True, 'w:style/w:locked'), - ('w:style/w:locked{w:val=0}', True, 'w:style/w:locked'), - ('w:style/w:locked{w:val=1}', True, 'w:style/w:locked'), - ('w:style', False, 'w:style'), - ('w:style/w:locked', False, 'w:style'), - ('w:style/w:locked{w:val=1}', False, 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", True, "w:style/w:locked"), + ("w:style/w:locked{w:val=0}", True, "w:style/w:locked"), + ("w:style/w:locked{w:val=1}", True, "w:style/w:locked"), + ("w:style", False, "w:style"), + ("w:style/w:locked", False, "w:style"), + ("w:style/w:locked{w:val=1}", False, "w:style"), + ] + ) def locked_set_fixture(self, request): style_cxml, value, expected_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_cxml) return style, value, expected_xml - @pytest.fixture(params=[ - ('w:style{w:type=table}', None), - ('w:style{w:type=table}/w:name{w:val=Boofar}', 'Boofar'), - ('w:style{w:type=table}/w:name{w:val=heading 1}', 'Heading 1'), - ]) + @pytest.fixture( + params=[ + ("w:style{w:type=table}", None), + ("w:style{w:type=table}/w:name{w:val=Boofar}", "Boofar"), + ("w:style{w:type=table}/w:name{w:val=heading 1}", "Heading 1"), + ] + ) def name_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', 'Foo', 'w:style/w:name{w:val=Foo}'), - ('w:style/w:name{w:val=Foo}', 'Bar', 'w:style/w:name{w:val=Bar}'), - ('w:style/w:name{w:val=Bar}', None, 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", "Foo", "w:style/w:name{w:val=Foo}"), + ("w:style/w:name{w:val=Foo}", "Bar", "w:style/w:name{w:val=Bar}"), + ("w:style/w:name{w:val=Bar}", None, "w:style"), + ] + ) def name_set_fixture(self, request): style_cxml, new_value, expected_style_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_style_cxml) return style, new_value, expected_xml - @pytest.fixture(params=[ - ('w:style', None), - ('w:style/w:uiPriority{w:val=42}', 42), - ]) + @pytest.fixture( + params=[ + ("w:style", None), + ("w:style/w:uiPriority{w:val=42}", 42), + ] + ) def priority_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', 42, - 'w:style/w:uiPriority{w:val=42}'), - ('w:style/w:uiPriority{w:val=42}', 24, - 'w:style/w:uiPriority{w:val=24}'), - ('w:style/w:uiPriority{w:val=24}', None, - 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", 42, "w:style/w:uiPriority{w:val=42}"), + ("w:style/w:uiPriority{w:val=42}", 24, "w:style/w:uiPriority{w:val=24}"), + ("w:style/w:uiPriority{w:val=24}", None, "w:style"), + ] + ) def priority_set_fixture(self, request): style_cxml, value, expected_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_cxml) return style, value, expected_xml - @pytest.fixture(params=[ - ('w:style', False), - ('w:style/w:qFormat', True), - ('w:style/w:qFormat{w:val=0}', False), - ('w:style/w:qFormat{w:val=on}', True), - ]) + @pytest.fixture( + params=[ + ("w:style", False), + ("w:style/w:qFormat", True), + ("w:style/w:qFormat{w:val=0}", False), + ("w:style/w:qFormat{w:val=on}", True), + ] + ) def quick_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', True, 'w:style/w:qFormat'), - ('w:style/w:qFormat', False, 'w:style'), - ('w:style/w:qFormat', True, 'w:style/w:qFormat'), - ('w:style/w:qFormat{w:val=0}', False, 'w:style'), - ('w:style/w:qFormat{w:val=on}', True, 'w:style/w:qFormat'), - ]) + @pytest.fixture( + params=[ + ("w:style", True, "w:style/w:qFormat"), + ("w:style/w:qFormat", False, "w:style"), + ("w:style/w:qFormat", True, "w:style/w:qFormat"), + ("w:style/w:qFormat{w:val=0}", False, "w:style"), + ("w:style/w:qFormat{w:val=on}", True, "w:style/w:qFormat"), + ] + ) def quick_set_fixture(self, request): style_cxml, new_value, expected_style_cxml = request.param style = BaseStyle(element(style_cxml)) expected_xml = xml(expected_style_cxml) return style, new_value, expected_xml - @pytest.fixture(params=[ - ('w:style', WD_STYLE_TYPE.PARAGRAPH), - ('w:style{w:type=paragraph}', WD_STYLE_TYPE.PARAGRAPH), - ('w:style{w:type=character}', WD_STYLE_TYPE.CHARACTER), - ('w:style{w:type=numbering}', WD_STYLE_TYPE.LIST), - ]) + @pytest.fixture( + params=[ + ("w:style", WD_STYLE_TYPE.PARAGRAPH), + ("w:style{w:type=paragraph}", WD_STYLE_TYPE.PARAGRAPH), + ("w:style{w:type=character}", WD_STYLE_TYPE.CHARACTER), + ("w:style{w:type=numbering}", WD_STYLE_TYPE.LIST), + ] + ) def type_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', False), - ('w:style/w:unhideWhenUsed', True), - ('w:style/w:unhideWhenUsed{w:val=0}', False), - ('w:style/w:unhideWhenUsed{w:val=1}', True), - ]) + @pytest.fixture( + params=[ + ("w:style", False), + ("w:style/w:unhideWhenUsed", True), + ("w:style/w:unhideWhenUsed{w:val=0}", False), + ("w:style/w:unhideWhenUsed{w:val=1}", True), + ] + ) def unhide_get_fixture(self, request): style_cxml, expected_value = request.param style = BaseStyle(element(style_cxml)) return style, expected_value - @pytest.fixture(params=[ - ('w:style', True, - 'w:style/w:unhideWhenUsed'), - ('w:style/w:unhideWhenUsed', False, - 'w:style'), - ('w:style/w:unhideWhenUsed{w:val=0}', True, - 'w:style/w:unhideWhenUsed'), - ('w:style/w:unhideWhenUsed{w:val=1}', True, - 'w:style/w:unhideWhenUsed'), - ('w:style/w:unhideWhenUsed{w:val=1}', False, - 'w:style'), - ('w:style', False, - 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", True, "w:style/w:unhideWhenUsed"), + ("w:style/w:unhideWhenUsed", False, "w:style"), + ("w:style/w:unhideWhenUsed{w:val=0}", True, "w:style/w:unhideWhenUsed"), + ("w:style/w:unhideWhenUsed{w:val=1}", True, "w:style/w:unhideWhenUsed"), + ("w:style/w:unhideWhenUsed{w:val=1}", False, "w:style"), + ("w:style", False, "w:style"), + ] + ) def unhide_set_fixture(self, request): style_cxml, value, expected_cxml = request.param style = BaseStyle(element(style_cxml)) @@ -375,12 +396,9 @@ def unhide_set_fixture(self, request): return style, value, expected_xml -class Describe_CharacterStyle(object): - +class DescribeCharacterStyle: def it_knows_which_style_it_is_based_on(self, base_get_fixture): - style, StyleFactory_, StyleFactory_calls, base_style_ = ( - base_get_fixture - ) + style, StyleFactory_, StyleFactory_calls, base_style_ = base_get_fixture base_style = style.base_style assert StyleFactory_.call_args_list == StyleFactory_calls @@ -399,18 +417,17 @@ def it_provides_access_to_its_font(self, font_fixture): # fixture -------------------------------------------------------- - @pytest.fixture(params=[ - ('w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Foo})', - 1, 0), - ('w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Bar})', - 1, -1), - ('w:styles/w:style', - 0, -1), - ]) + @pytest.fixture( + params=[ + ("w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Foo})", 1, 0), + ("w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Bar})", 1, -1), + ("w:styles/w:style", 0, -1), + ] + ) def base_get_fixture(self, request, StyleFactory_): styles_cxml, style_idx, base_style_idx = request.param styles = element(styles_cxml) - style = _CharacterStyle(styles[style_idx]) + style = CharacterStyle(styles[style_idx]) if base_style_idx >= 0: base_style = styles[base_style_idx] StyleFactory_calls = [call(base_style)] @@ -420,17 +437,16 @@ def base_get_fixture(self, request, StyleFactory_): expected_value = None return style, StyleFactory_, StyleFactory_calls, expected_value - @pytest.fixture(params=[ - ('w:style', 'Foo', - 'w:style/w:basedOn{w:val=Foo}'), - ('w:style/w:basedOn{w:val=Foo}', 'Bar', - 'w:style/w:basedOn{w:val=Bar}'), - ('w:style/w:basedOn{w:val=Bar}', None, - 'w:style'), - ]) + @pytest.fixture( + params=[ + ("w:style", "Foo", "w:style/w:basedOn{w:val=Foo}"), + ("w:style/w:basedOn{w:val=Foo}", "Bar", "w:style/w:basedOn{w:val=Bar}"), + ("w:style/w:basedOn{w:val=Bar}", None, "w:style"), + ] + ) def base_set_fixture(self, request, style_): style_cxml, base_style_id, expected_style_cxml = request.param - style = _CharacterStyle(element(style_cxml)) + style = CharacterStyle(element(style_cxml)) style_.style_id = base_style_id base_style = style_ if base_style_id is not None else None expected_xml = xml(expected_style_cxml) @@ -438,16 +454,14 @@ def base_set_fixture(self, request, style_): @pytest.fixture def font_fixture(self, Font_, font_): - style = _CharacterStyle(element('w:style')) + style = CharacterStyle(element("w:style")) return style, Font_, font_ # fixture components --------------------------------------------- @pytest.fixture def Font_(self, request, font_): - return class_mock( - request, 'docx.styles.style.Font', return_value=font_ - ) + return class_mock(request, "docx.styles.style.Font", return_value=font_) @pytest.fixture def font_(self, request): @@ -459,11 +473,10 @@ def style_(self, request): @pytest.fixture def StyleFactory_(self, request): - return function_mock(request, 'docx.styles.style.StyleFactory') + return function_mock(request, "docx.styles.style.StyleFactory") -class Describe_ParagraphStyle(object): - +class DescribeParagraphStyle: def it_knows_its_next_paragraph_style(self, next_get_fixture): style, expected_value = next_get_fixture assert style.next_paragraph_style == expected_value @@ -481,56 +494,58 @@ def it_provides_access_to_its_paragraph_format(self, parfmt_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('H1', 'Body'), - ('H2', 'H2'), - ('Body', 'Body'), - ('Foo', 'Foo'), - ]) + @pytest.fixture( + params=[ + ("H1", "Body"), + ("H2", "H2"), + ("Body", "Body"), + ("Foo", "Foo"), + ] + ) def next_get_fixture(self, request): style_name, next_style_name = request.param styles = element( - 'w:styles/(' - 'w:style{w:type=paragraph,w:styleId=H1}/w:next{w:val=Body},' - 'w:style{w:type=paragraph,w:styleId=H2}/w:next{w:val=Char},' - 'w:style{w:type=paragraph,w:styleId=Body},' - 'w:style{w:type=paragraph,w:styleId=Foo}/w:next{w:val=Bar},' - 'w:style{w:type=character,w:styleId=Char})' + "w:styles/(" + "w:style{w:type=paragraph,w:styleId=H1}/w:next{w:val=Body}," + "w:style{w:type=paragraph,w:styleId=H2}/w:next{w:val=Char}," + "w:style{w:type=paragraph,w:styleId=Body}," + "w:style{w:type=paragraph,w:styleId=Foo}/w:next{w:val=Bar}," + "w:style{w:type=character,w:styleId=Char})" ) - style_names = ['H1', 'H2', 'Body', 'Foo', 'Char'] + style_names = ["H1", "H2", "Body", "Foo", "Char"] style_elm = styles[style_names.index(style_name)] next_style_elm = styles[style_names.index(next_style_name)] - style = _ParagraphStyle(style_elm) - if style_name == 'H1': - next_style = _ParagraphStyle(next_style_elm) - else: - next_style = style + style = ParagraphStyle(style_elm) + next_style = ParagraphStyle(next_style_elm) if style_name == "H1" else style return style, next_style - @pytest.fixture(params=[ - ('H', 'B', 'w:style{w:type=paragraph,w:styleId=H}/w:next{w:val=B}'), - ('H', None, 'w:style{w:type=paragraph,w:styleId=H}'), - ('H', 'H', 'w:style{w:type=paragraph,w:styleId=H}'), - ]) + @pytest.fixture( + params=[ + ("H", "B", "w:style{w:type=paragraph,w:styleId=H}/w:next{w:val=B}"), + ("H", None, "w:style{w:type=paragraph,w:styleId=H}"), + ("H", "H", "w:style{w:type=paragraph,w:styleId=H}"), + ] + ) def next_set_fixture(self, request): style_name, next_style_name, style_cxml = request.param styles = element( - 'w:styles/(' - 'w:style{w:type=paragraph,w:styleId=H},' - 'w:style{w:type=paragraph,w:styleId=B})' + "w:styles/(" + "w:style{w:type=paragraph,w:styleId=H}," + "w:style{w:type=paragraph,w:styleId=B})" ) - style_elms = {'H': styles[0], 'B': styles[1]} - style = _ParagraphStyle(style_elms[style_name]) + style_elms = {"H": styles[0], "B": styles[1]} + style = ParagraphStyle(style_elms[style_name]) next_style = ( - None if next_style_name is None else - _ParagraphStyle(style_elms[next_style_name]) + None + if next_style_name is None + else ParagraphStyle(style_elms[next_style_name]) ) expected_xml = xml(style_cxml) return style, next_style, expected_xml @pytest.fixture def parfmt_fixture(self, ParagraphFormat_, paragraph_format_): - style = _ParagraphStyle(element('w:style')) + style = ParagraphStyle(element("w:style")) return style, ParagraphFormat_, paragraph_format_ # fixture components --------------------------------------------- @@ -538,8 +553,7 @@ def parfmt_fixture(self, ParagraphFormat_, paragraph_format_): @pytest.fixture def ParagraphFormat_(self, request, paragraph_format_): return class_mock( - request, 'docx.styles.style.ParagraphFormat', - return_value=paragraph_format_ + request, "docx.styles.style.ParagraphFormat", return_value=paragraph_format_ ) @pytest.fixture diff --git a/tests/styles/test_styles.py b/tests/styles/test_styles.py index 650acf342..ea9346bdc 100644 --- a/tests/styles/test_styles.py +++ b/tests/styles/test_styles.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.styles.styles module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.styles.styles module.""" import pytest @@ -13,13 +9,10 @@ from docx.styles.styles import Styles from ..unitutil.cxml import element -from ..unitutil.mock import ( - call, class_mock, function_mock, instance_mock, method_mock -) - +from ..unitutil.mock import call, class_mock, function_mock, instance_mock, method_mock -class DescribeStyles(object): +class DescribeStyles: def it_supports_the_in_operator_on_style_name(self, in_fixture): styles, name, expected_value = in_fixture assert (name in styles) is expected_value @@ -29,9 +22,7 @@ def it_knows_its_length(self, len_fixture): assert len(styles) == expected_value def it_can_iterate_over_its_styles(self, iter_fixture): - styles, expected_count, style_, StyleFactory_, expected_calls = ( - iter_fixture - ) + styles, expected_count, style_, StyleFactory_, expected_calls = iter_fixture count = 0 for style in styles: assert style is style_ @@ -39,7 +30,7 @@ def it_can_iterate_over_its_styles(self, iter_fixture): assert count == expected_count assert StyleFactory_.call_args_list == expected_calls - @pytest.mark.filterwarnings('ignore::UserWarning') + @pytest.mark.filterwarnings("ignore::UserWarning") def it_can_get_a_style_by_id(self, getitem_id_fixture): styles, key, expected_element = getitem_id_fixture style = styles[key] @@ -69,7 +60,7 @@ def it_can_add_a_new_style(self, add_fixture): def it_raises_when_style_name_already_used(self, add_raises_fixture): styles, name = add_raises_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="document already contains style 'Hea"): styles.add_style(name, None) def it_can_get_the_default_style_for_a_type(self, default_fixture): @@ -144,7 +135,7 @@ def it_gets_a_style_by_id_to_help(self, _get_by_id_fixture): def it_gets_a_style_id_from_a_name_to_help( self, _getitem_, _get_style_id_from_style_, style_ ): - style_name, style_type, style_id_ = 'Foo Bar', 1, 'FooBar' + style_name, style_type, style_id_ = "Foo Bar", 1, "FooBar" _getitem_.return_value = style_ _get_style_id_from_style_.return_value = style_id_ styles = Styles(None) @@ -165,7 +156,7 @@ def it_gets_a_style_id_from_a_style_to_help(self, id_style_fixture): def it_raises_on_style_type_mismatch(self, id_style_raises_fixture): styles, style_, style_type = id_style_raises_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="assigned style is type 1, need type 2"): styles._get_style_id_from_style(style_, style_type) def it_provides_access_to_the_latent_styles(self, latent_styles_fixture): @@ -176,37 +167,53 @@ def it_provides_access_to_the_latent_styles(self, latent_styles_fixture): # fixture -------------------------------------------------------- - @pytest.fixture(params=[ - ('Foo Bar', 'Foo Bar', WD_STYLE_TYPE.CHARACTER, False), - ('Heading 1', 'heading 1', WD_STYLE_TYPE.PARAGRAPH, True), - ]) - def add_fixture(self, request, styles_elm_, _getitem_, style_elm_, - StyleFactory_, style_): + @pytest.fixture( + params=[ + ("Foo Bar", "Foo Bar", WD_STYLE_TYPE.CHARACTER, False), + ("Heading 1", "heading 1", WD_STYLE_TYPE.PARAGRAPH, True), + ] + ) + def add_fixture( + self, request, styles_elm_, _getitem_, style_elm_, StyleFactory_, style_ + ): name, name_, style_type, builtin = request.param styles = Styles(styles_elm_) _getitem_.return_value = None styles_elm_.add_style_of_type.return_value = style_elm_ StyleFactory_.return_value = style_ return ( - styles, name, style_type, builtin, name_, StyleFactory_, - style_elm_, style_ + styles, + name, + style_type, + builtin, + name_, + StyleFactory_, + style_elm_, + style_, ) @pytest.fixture def add_raises_fixture(self, _getitem_): - styles = Styles(element('w:styles/w:style/w:name{w:val=heading 1}')) - name = 'Heading 1' + styles = Styles(element("w:styles/w:style/w:name{w:val=heading 1}")) + name = "Heading 1" return styles, name - @pytest.fixture(params=[ - ('w:styles', - False, WD_STYLE_TYPE.CHARACTER), - ('w:styles/w:style{w:type=paragraph,w:default=1}', - True, WD_STYLE_TYPE.PARAGRAPH), - ('w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w' - ':default=1})', - True, WD_STYLE_TYPE.TABLE), - ]) + @pytest.fixture( + params=[ + ("w:styles", False, WD_STYLE_TYPE.CHARACTER), + ( + "w:styles/w:style{w:type=paragraph,w:default=1}", + True, + WD_STYLE_TYPE.PARAGRAPH, + ), + ( + "w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w" + ":default=1})", + True, + WD_STYLE_TYPE.TABLE, + ), + ] + ) def default_fixture(self, request, StyleFactory_, style_): styles_cxml, is_defined, style_type = request.param styles_elm = element(styles_cxml) @@ -214,70 +221,89 @@ def default_fixture(self, request, StyleFactory_, style_): StyleFactory_calls = [call(styles_elm[-1])] if is_defined else [] StyleFactory_.return_value = style_ expected_value = style_ if is_defined else None - return ( - styles, style_type, StyleFactory_, StyleFactory_calls, - expected_value - ) - - @pytest.fixture(params=[ - ('w:styles/w:style{w:type=paragraph,w:styleId=Foo}', 'Foo', - WD_STYLE_TYPE.PARAGRAPH), - ('w:styles/w:style{w:type=paragraph,w:styleId=Foo}', 'Bar', - WD_STYLE_TYPE.PARAGRAPH), - ('w:styles/w:style{w:type=table,w:styleId=Bar}', 'Bar', - WD_STYLE_TYPE.PARAGRAPH), - ]) + return (styles, style_type, StyleFactory_, StyleFactory_calls, expected_value) + + @pytest.fixture( + params=[ + ( + "w:styles/w:style{w:type=paragraph,w:styleId=Foo}", + "Foo", + WD_STYLE_TYPE.PARAGRAPH, + ), + ( + "w:styles/w:style{w:type=paragraph,w:styleId=Foo}", + "Bar", + WD_STYLE_TYPE.PARAGRAPH, + ), + ( + "w:styles/w:style{w:type=table,w:styleId=Bar}", + "Bar", + WD_STYLE_TYPE.PARAGRAPH, + ), + ] + ) def _get_by_id_fixture(self, request, default_, StyleFactory_, style_): styles_cxml, style_id, style_type = request.param styles_elm = element(styles_cxml) style_elm = styles_elm[0] styles = Styles(styles_elm) - default_calls = [] if style_id == 'Foo' else [call(styles, style_type)] - StyleFactory_calls = [call(style_elm)] if style_id == 'Foo' else [] + default_calls = [] if style_id == "Foo" else [call(styles, style_type)] + StyleFactory_calls = [call(style_elm)] if style_id == "Foo" else [] default_.return_value = StyleFactory_.return_value = style_ return ( - styles, style_id, style_type, default_calls, StyleFactory_, - StyleFactory_calls, style_ + styles, + style_id, + style_type, + default_calls, + StyleFactory_, + StyleFactory_calls, + style_, ) - @pytest.fixture(params=[ - ('w:styles/(w:style{%s,w:styleId=Foobar},w:style,w:style)', 0), - ('w:styles/(w:style,w:style{%s,w:styleId=Foobar},w:style)', 1), - ('w:styles/(w:style,w:style,w:style{%s,w:styleId=Foobar})', 2), - ]) + @pytest.fixture( + params=[ + ("w:styles/(w:style{%s,w:styleId=Foobar},w:style,w:style)", 0), + ("w:styles/(w:style,w:style{%s,w:styleId=Foobar},w:style)", 1), + ("w:styles/(w:style,w:style,w:style{%s,w:styleId=Foobar})", 2), + ] + ) def getitem_id_fixture(self, request): styles_cxml_tmpl, style_idx = request.param - styles_cxml = styles_cxml_tmpl % 'w:type=paragraph' + styles_cxml = styles_cxml_tmpl % "w:type=paragraph" styles = Styles(element(styles_cxml)) expected_element = styles._element[style_idx] - return styles, 'Foobar', expected_element - - @pytest.fixture(params=[ - ('w:styles/(w:style%s/w:name{w:val=foo},w:style)', 'foo', 0), - ('w:styles/(w:style,w:style%s/w:name{w:val=foo})', 'foo', 1), - ('w:styles/w:style%s/w:name{w:val=heading 1}', 'Heading 1', 0), - ]) + return styles, "Foobar", expected_element + + @pytest.fixture( + params=[ + ("w:styles/(w:style%s/w:name{w:val=foo},w:style)", "foo", 0), + ("w:styles/(w:style,w:style%s/w:name{w:val=foo})", "foo", 1), + ("w:styles/w:style%s/w:name{w:val=heading 1}", "Heading 1", 0), + ] + ) def getitem_name_fixture(self, request): styles_cxml_tmpl, key, style_idx = request.param - styles_cxml = styles_cxml_tmpl % '{w:type=character}' + styles_cxml = styles_cxml_tmpl % "{w:type=character}" styles = Styles(element(styles_cxml)) expected_element = styles._element[style_idx] return styles, key, expected_element - @pytest.fixture(params=[ - ('w:styles/(w:style,w:style/w:name{w:val=foo},w:style)'), - ('w:styles/(w:style{w:styleId=foo},w:style,w:style)'), - ]) + @pytest.fixture( + params=[ + ("w:styles/(w:style,w:style/w:name{w:val=foo},w:style)"), + ("w:styles/(w:style{w:styleId=foo},w:style,w:style)"), + ] + ) def get_raises_fixture(self, request): styles_cxml = request.param styles = Styles(element(styles_cxml)) - return styles, 'bar' + return styles, "bar" @pytest.fixture(params=[True, False]) def id_style_fixture(self, request, default_, style_): style_is_default = request.param styles = Styles(None) - style_id, style_type = 'FooBar', 1 + style_id, style_type = "FooBar", 1 default_.return_value = style_ if style_is_default else None style_.style_id, style_.type = style_id, style_type expected_value = None if style_is_default else style_id @@ -290,23 +316,27 @@ def id_style_raises_fixture(self, style_): style_type = 2 return styles, style_, style_type - @pytest.fixture(params=[ - ('w:styles/w:style/w:name{w:val=heading 1}', 'Heading 1', True), - ('w:styles/w:style/w:name{w:val=Foo Bar}', 'Foo Bar', True), - ('w:styles/w:style/w:name{w:val=heading 1}', 'Foobar', False), - ('w:styles', 'Foobar', False), - ]) + @pytest.fixture( + params=[ + ("w:styles/w:style/w:name{w:val=heading 1}", "Heading 1", True), + ("w:styles/w:style/w:name{w:val=Foo Bar}", "Foo Bar", True), + ("w:styles/w:style/w:name{w:val=heading 1}", "Foobar", False), + ("w:styles", "Foobar", False), + ] + ) def in_fixture(self, request): styles_cxml, name, expected_value = request.param styles = Styles(element(styles_cxml)) return styles, name, expected_value - @pytest.fixture(params=[ - ('w:styles', 0), - ('w:styles/w:style', 1), - ('w:styles/(w:style,w:style)', 2), - ('w:styles/(w:style,w:style,w:style)', 3), - ]) + @pytest.fixture( + params=[ + ("w:styles", 0), + ("w:styles/w:style", 1), + ("w:styles/(w:style,w:style)", 2), + ("w:styles/(w:style,w:style,w:style)", 3), + ] + ) def iter_fixture(self, request, StyleFactory_, style_): styles_cxml, expected_count = request.param styles_elm = element(styles_cxml) @@ -317,15 +347,17 @@ def iter_fixture(self, request, StyleFactory_, style_): @pytest.fixture def latent_styles_fixture(self, LatentStyles_, latent_styles_): - styles = Styles(element('w:styles/w:latentStyles')) + styles = Styles(element("w:styles/w:latentStyles")) return styles, LatentStyles_, latent_styles_ - @pytest.fixture(params=[ - ('w:styles', 0), - ('w:styles/w:style', 1), - ('w:styles/(w:style,w:style)', 2), - ('w:styles/(w:style,w:style,w:style)', 3), - ]) + @pytest.fixture( + params=[ + ("w:styles", 0), + ("w:styles/w:style", 1), + ("w:styles/(w:style,w:style)", 2), + ("w:styles/(w:style,w:style,w:style)", 3), + ] + ) def len_fixture(self, request): styles_cxml, expected_value = request.param styles = Styles(element(styles_cxml)) @@ -335,29 +367,28 @@ def len_fixture(self, request): @pytest.fixture def default_(self, request): - return method_mock(request, Styles, 'default') + return method_mock(request, Styles, "default") @pytest.fixture def _get_by_id_(self, request): - return method_mock(request, Styles, '_get_by_id') + return method_mock(request, Styles, "_get_by_id") @pytest.fixture def _getitem_(self, request): - return method_mock(request, Styles, '__getitem__') + return method_mock(request, Styles, "__getitem__") @pytest.fixture def _get_style_id_from_name_(self, request): - return method_mock(request, Styles, '_get_style_id_from_name') + return method_mock(request, Styles, "_get_style_id_from_name") @pytest.fixture def _get_style_id_from_style_(self, request): - return method_mock(request, Styles, '_get_style_id_from_style') + return method_mock(request, Styles, "_get_style_id_from_style") @pytest.fixture def LatentStyles_(self, request, latent_styles_): return class_mock( - request, 'docx.styles.styles.LatentStyles', - return_value=latent_styles_ + request, "docx.styles.styles.LatentStyles", return_value=latent_styles_ ) @pytest.fixture @@ -370,7 +401,7 @@ def style_(self, request): @pytest.fixture def StyleFactory_(self, request): - return function_mock(request, 'docx.styles.styles.StyleFactory') + return function_mock(request, "docx.styles.styles.StyleFactory") @pytest.fixture def style_elm_(self, request): diff --git a/tests/test_api.py b/tests/test_api.py index cc3553ccf..b6e6818b5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,25 +1,15 @@ -# encoding: utf-8 - -""" -Test suite for the docx.api module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Test suite for the docx.api module.""" import pytest import docx - from docx.api import Document from docx.opc.constants import CONTENT_TYPE as CT -from .unitutil.mock import function_mock, instance_mock, class_mock - +from .unitutil.mock import class_mock, function_mock, instance_mock -class DescribeDocument(object): +class DescribeDocument: def it_opens_a_docx_file(self, open_fixture): docx, Package_, document_ = open_fixture document = Document(docx) @@ -34,14 +24,14 @@ def it_opens_the_default_docx_if_none_specified(self, default_fixture): def it_raises_on_not_a_Word_file(self, raise_fixture): not_a_docx = raise_fixture - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="file 'foobar.xlsx' is not a Word file,"): Document(not_a_docx) # fixtures ------------------------------------------------------- @pytest.fixture def default_fixture(self, _default_docx_path_, Package_, document_): - docx = 'barfoo.docx' + docx = "barfoo.docx" _default_docx_path_.return_value = docx document_part = Package_.open.return_value.main_document_part document_part.document = document_ @@ -50,7 +40,7 @@ def default_fixture(self, _default_docx_path_, Package_, document_): @pytest.fixture def open_fixture(self, Package_, document_): - docx = 'foobar.docx' + docx = "foobar.docx" document_part = Package_.open.return_value.main_document_part document_part.document = document_ document_part.content_type = CT.WML_DOCUMENT_MAIN @@ -58,15 +48,15 @@ def open_fixture(self, Package_, document_): @pytest.fixture def raise_fixture(self, Package_): - not_a_docx = 'foobar.xlsx' - Package_.open.return_value.main_document_part.content_type = 'BOGUS' + not_a_docx = "foobar.xlsx" + Package_.open.return_value.main_document_part.content_type = "BOGUS" return not_a_docx # fixture components --------------------------------------------- @pytest.fixture def _default_docx_path_(self, request): - return function_mock(request, 'docx.api._default_docx_path') + return function_mock(request, "docx.api._default_docx_path") @pytest.fixture def document_(self, request): @@ -74,4 +64,4 @@ def document_(self, request): @pytest.fixture def Package_(self, request): - return class_mock(request, 'docx.api.Package') + return class_mock(request, "docx.api.Package") diff --git a/tests/test_blkcntnr.py b/tests/test_blkcntnr.py index 5bc9bab3f..6f1d28a46 100644 --- a/tests/test_blkcntnr.py +++ b/tests/test_blkcntnr.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Test suite for the docx.blkcntnr (block item container) module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Test suite for the docx.blkcntnr (block item container) module.""" import pytest @@ -16,8 +12,7 @@ from .unitutil.mock import call, instance_mock, method_mock -class DescribeBlockItemContainer(object): - +class DescribeBlockItemContainer: def it_can_add_a_paragraph(self, add_paragraph_fixture, _add_paragraph_): text, style, paragraph_, add_run_calls = add_paragraph_fixture _add_paragraph_.return_value = paragraph_ @@ -37,8 +32,7 @@ def it_can_add_a_table(self, add_table_fixture): assert table._element.xml == expected_xml assert table._parent is blkcntnr - def it_provides_access_to_the_paragraphs_it_contains( - self, paragraphs_fixture): + def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture): # test len(), iterable, and indexed access blkcntnr, expected_count = paragraphs_fixture paragraphs = blkcntnr.paragraphs @@ -71,12 +65,14 @@ def it_adds_a_paragraph_to_help(self, _add_paragraph_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('', None), - ('Foo', None), - ('', 'Bar'), - ('Foo', 'Bar'), - ]) + @pytest.fixture( + params=[ + ("", None), + ("Foo", None), + ("", "Bar"), + ("Foo", "Bar"), + ] + ) def add_paragraph_fixture(self, request, paragraph_): text, style = request.param paragraph_.style = None @@ -85,37 +81,41 @@ def add_paragraph_fixture(self, request, paragraph_): @pytest.fixture def _add_paragraph_fixture(self, request): - blkcntnr_cxml, after_cxml = 'w:body', 'w:body/w:p' + blkcntnr_cxml, after_cxml = "w:body", "w:body/w:p" blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None) expected_xml = xml(after_cxml) return blkcntnr, expected_xml @pytest.fixture def add_table_fixture(self): - blkcntnr = BlockItemContainer(element('w:body'), None) + blkcntnr = BlockItemContainer(element("w:body"), None) rows, cols, width = 2, 2, Inches(2) - expected_xml = snippet_seq('new-tbl')[0] + expected_xml = snippet_seq("new-tbl")[0] return blkcntnr, rows, cols, width, expected_xml - @pytest.fixture(params=[ - ('w:body', 0), - ('w:body/w:p', 1), - ('w:body/(w:p,w:p)', 2), - ('w:body/(w:p,w:tbl)', 1), - ('w:body/(w:p,w:tbl,w:p)', 2), - ]) + @pytest.fixture( + params=[ + ("w:body", 0), + ("w:body/w:p", 1), + ("w:body/(w:p,w:p)", 2), + ("w:body/(w:p,w:tbl)", 1), + ("w:body/(w:p,w:tbl,w:p)", 2), + ] + ) def paragraphs_fixture(self, request): blkcntnr_cxml, expected_count = request.param blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None) return blkcntnr, expected_count - @pytest.fixture(params=[ - ('w:body', 0), - ('w:body/w:tbl', 1), - ('w:body/(w:tbl,w:tbl)', 2), - ('w:body/(w:p,w:tbl)', 1), - ('w:body/(w:tbl,w:tbl,w:p)', 2), - ]) + @pytest.fixture( + params=[ + ("w:body", 0), + ("w:body/w:tbl", 1), + ("w:body/(w:tbl,w:tbl)", 2), + ("w:body/(w:p,w:tbl)", 1), + ("w:body/(w:tbl,w:tbl,w:p)", 2), + ] + ) def tables_fixture(self, request): blkcntnr_cxml, expected_count = request.param blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None) @@ -125,7 +125,7 @@ def tables_fixture(self, request): @pytest.fixture def _add_paragraph_(self, request): - return method_mock(request, BlockItemContainer, '_add_paragraph') + return method_mock(request, BlockItemContainer, "_add_paragraph") @pytest.fixture def paragraph_(self, request): diff --git a/tests/test_document.py b/tests/test_document.py index 0de469c38..12e3361db 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1,12 +1,8 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.document module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.document module.""" import pytest -from docx.document import _Body, Document +from docx.document import Document, _Body from docx.enum.section import WD_SECTION from docx.enum.text import WD_BREAK from docx.opc.coreprops import CoreProperties @@ -24,8 +20,7 @@ from .unitutil.mock import class_mock, instance_mock, method_mock, property_mock -class DescribeDocument(object): - +class DescribeDocument: def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_): level, style = add_heading_fixture add_paragraph_.return_value = paragraph_ @@ -38,9 +33,9 @@ def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_): def it_raises_on_heading_level_out_of_range(self): document = Document(None, None) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="level must be in range 0-9, got -1"): document.add_heading(level=-1) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="level must be in range 0-9, got 10"): document.add_heading(level=10) def it_can_add_a_page_break(self, add_paragraph_, paragraph_, run_): @@ -77,7 +72,7 @@ def it_can_add_a_section( section = document.add_section(start_type) assert document.element.xml == expected_xml - sectPr = document.element.xpath('w:body/w:sectPr')[0] + sectPr = document.element.xpath("w:body/w:sectPr")[0] Section_.assert_called_once_with(sectPr, document_part_) assert section is section_ @@ -108,7 +103,7 @@ def it_provides_access_to_its_paragraphs(self, paragraphs_fixture): assert paragraphs is paragraphs_ def it_provides_access_to_its_sections(self, document_part_, Sections_, sections_): - document_elm = element('w:document') + document_elm = element("w:document") Sections_.return_value = sections_ document = Document(document_elm, document_part_) @@ -148,21 +143,25 @@ def it_determines_block_width_to_help(self, block_width_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (0, 'Title'), - (1, 'Heading 1'), - (2, 'Heading 2'), - (9, 'Heading 9'), - ]) + @pytest.fixture( + params=[ + (0, "Title"), + (1, "Heading 1"), + (2, "Heading 2"), + (9, "Heading 9"), + ] + ) def add_heading_fixture(self, request): level, style = request.param return level, style - @pytest.fixture(params=[ - ('', None), - ('', 'Heading 1'), - ('foo\rbar', 'Body Text'), - ]) + @pytest.fixture( + params=[ + ("", None), + ("", "Heading 1"), + ("foo\rbar", "Body Text"), + ] + ) def add_paragraph_fixture(self, request, body_prop_, paragraph_): text, style = request.param document = Document(None, None) @@ -172,32 +171,34 @@ def add_paragraph_fixture(self, request, body_prop_, paragraph_): @pytest.fixture def add_picture_fixture(self, request, add_paragraph_, run_, picture_): document = Document(None, None) - path, width, height = 'foobar.png', 100, 200 + path, width, height = "foobar.png", 100, 200 add_paragraph_.return_value.add_run.return_value = run_ run_.add_picture.return_value = picture_ return document, path, width, height, run_, picture_ - @pytest.fixture(params=[ - ('w:sectPr', WD_SECTION.EVEN_PAGE, - 'w:sectPr/w:type{w:val=evenPage}'), - ('w:sectPr/w:type{w:val=evenPage}', WD_SECTION.ODD_PAGE, - 'w:sectPr/w:type{w:val=oddPage}'), - ('w:sectPr/w:type{w:val=oddPage}', WD_SECTION.NEW_PAGE, - 'w:sectPr'), - ]) + @pytest.fixture( + params=[ + ("w:sectPr", WD_SECTION.EVEN_PAGE, "w:sectPr/w:type{w:val=evenPage}"), + ( + "w:sectPr/w:type{w:val=evenPage}", + WD_SECTION.ODD_PAGE, + "w:sectPr/w:type{w:val=oddPage}", + ), + ("w:sectPr/w:type{w:val=oddPage}", WD_SECTION.NEW_PAGE, "w:sectPr"), + ] + ) def add_section_fixture(self, request): sentinel, start_type, new_sentinel = request.param - document_elm = element('w:document/w:body/(w:p,%s)' % sentinel) + document_elm = element("w:document/w:body/(w:p,%s)" % sentinel) expected_xml = xml( - 'w:document/w:body/(w:p,w:p/w:pPr/%s,%s)' % - (sentinel, new_sentinel) + "w:document/w:body/(w:p,w:p/w:pPr/%s,%s)" % (sentinel, new_sentinel) ) return document_elm, start_type, expected_xml @pytest.fixture def add_table_fixture(self, _block_width_prop_, body_prop_, table_): document = Document(None, None) - rows, cols, style = 4, 2, 'Light Shading Accent 1' + rows, cols, style = 4, 2, "Light Shading Accent 1" body_prop_.return_value.add_table.return_value = table_ _block_width_prop_.return_value = width = 42 return document, rows, cols, style, width, table_ @@ -214,7 +215,7 @@ def block_width_fixture(self, sections_prop_, section_): @pytest.fixture def body_fixture(self, _Body_, body_): - document_elm = element('w:document/w:body') + document_elm = element("w:document/w:body") body_elm = document_elm[0] document = Document(document_elm, None) return document, body_elm, _Body_, body_ @@ -245,7 +246,7 @@ def part_fixture(self, document_part_): @pytest.fixture def save_fixture(self, document_part_): document = Document(None, document_part_) - file_ = 'foobar.docx' + file_ = "foobar.docx" return document, file_ @pytest.fixture @@ -270,11 +271,11 @@ def tables_fixture(self, body_prop_, tables_): @pytest.fixture def add_paragraph_(self, request): - return method_mock(request, Document, 'add_paragraph') + return method_mock(request, Document, "add_paragraph") @pytest.fixture def _Body_(self, request, body_): - return class_mock(request, 'docx.document._Body', return_value=body_) + return class_mock(request, "docx.document._Body", return_value=body_) @pytest.fixture def body_(self, request): @@ -282,11 +283,11 @@ def body_(self, request): @pytest.fixture def _block_width_prop_(self, request): - return property_mock(request, Document, '_block_width') + return property_mock(request, Document, "_block_width") @pytest.fixture def body_prop_(self, request, body_): - return property_mock(request, Document, '_body', return_value=body_) + return property_mock(request, Document, "_body", return_value=body_) @pytest.fixture def core_properties_(self, request): @@ -318,7 +319,7 @@ def run_(self, request): @pytest.fixture def Section_(self, request): - return class_mock(request, 'docx.document.Section') + return class_mock(request, "docx.document.Section") @pytest.fixture def section_(self, request): @@ -326,7 +327,7 @@ def section_(self, request): @pytest.fixture def Sections_(self, request): - return class_mock(request, 'docx.document.Sections') + return class_mock(request, "docx.document.Sections") @pytest.fixture def sections_(self, request): @@ -334,7 +335,7 @@ def sections_(self, request): @pytest.fixture def sections_prop_(self, request): - return property_mock(request, Document, 'sections') + return property_mock(request, Document, "sections") @pytest.fixture def settings_(self, request): @@ -346,15 +347,14 @@ def styles_(self, request): @pytest.fixture def table_(self, request): - return instance_mock(request, Table, style='UNASSIGNED') + return instance_mock(request, Table, style="UNASSIGNED") @pytest.fixture def tables_(self, request): return instance_mock(request, list) -class Describe_Body(object): - +class Describe_Body: def it_can_clear_itself_of_all_content_it_holds(self, clear_fixture): body, expected_xml = clear_fixture _body = body.clear_content() @@ -363,12 +363,14 @@ def it_can_clear_itself_of_all_content_it_holds(self, clear_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:body', 'w:body'), - ('w:body/w:p', 'w:body'), - ('w:body/w:sectPr', 'w:body/w:sectPr'), - ('w:body/(w:p, w:sectPr)', 'w:body/w:sectPr'), - ]) + @pytest.fixture( + params=[ + ("w:body", "w:body"), + ("w:body/w:p", "w:body"), + ("w:body/w:sectPr", "w:body/w:sectPr"), + ("w:body/(w:p, w:sectPr)", "w:body/w:sectPr"), + ] + ) def clear_fixture(self, request): before_cxml, after_cxml = request.param body = _Body(element(before_cxml), None) diff --git a/tests/test_enum.py b/tests/test_enum.py index edfe595dc..1b8a14f5b 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -1,101 +1,94 @@ -# encoding: utf-8 +"""Test suite for docx.enum module, focused on base classes. -""" -Test suite for docx.enum module, focused on base classes. Configured a little -differently because of the meta-programming, the two enumeration classes at -the top constitute the entire fixture and the tests themselves just make +Configured a little differently because of the meta-programming, the two enumeration +classes at the top constitute the entire fixture and the tests themselves just make assertions on those. """ -from __future__ import absolute_import, print_function +import enum import pytest -from docx.enum.base import ( - alias, Enumeration, EnumMember, ReturnValueOnlyEnumMember, - XmlEnumeration, XmlMappedEnumMember -) +from docx.enum.base import BaseXmlEnum -@alias('BARFOO') -class FOOBAR(Enumeration): - """ - Enumeration docstring - """ +class SomeXmlAttr(BaseXmlEnum): + """SomeXmlAttr docstring.""" - __ms_name__ = 'MsoFoobar' + FOO = (1, "foo", "Do foo instead of bar.") + """Do foo instead of bar.""" - __url__ = 'http://msdn.microsoft.com/foobar.aspx' + BAR = (2, "bar", "Do bar instead of foo.") + """Do bar instead of foo.""" - __members__ = ( - EnumMember(None, None, 'No setting/remove setting'), - EnumMember('READ_WRITE', 1, 'Readable and settable'), - ReturnValueOnlyEnumMember('READ_ONLY', -2, 'Return value only'), - ) + BAZ = (3, None, "Maps to the value assumed when the attribute is omitted.") + """Maps to the value assumed when the attribute is omitted.""" -@alias('XML-FU') -class XMLFOO(XmlEnumeration): - """ - XmlEnumeration docstring - """ +class DescribeBaseXmlEnum: + """Unit-test suite for `docx.enum.base.BaseXmlEnum`.""" - __ms_name__ = 'MsoXmlFoobar' + def it_is_an_instance_of_EnumMeta_just_like_a_regular_Enum(self): + assert type(SomeXmlAttr) is enum.EnumMeta - __url__ = 'http://msdn.microsoft.com/msoxmlfoobar.aspx' + def it_has_the_same_repr_as_a_regular_Enum(self): + assert repr(SomeXmlAttr) == "" - __members__ = ( - XmlMappedEnumMember(None, None, None, 'No setting'), - XmlMappedEnumMember('XML_RW', 42, 'attrVal', 'Read/write setting'), - ReturnValueOnlyEnumMember('RO', -2, 'Return value only;'), - ) + def it_has_an_MRO_that_goes_through_the_base_class_int_and_Enum(self): + assert SomeXmlAttr.__mro__ == ( + SomeXmlAttr, + BaseXmlEnum, + int, + enum.Enum, + object, + ), f"got: {SomeXmlAttr.__mro__}" + def it_knows_the_XML_value_for_each_member_by_the_member_instance(self): + assert SomeXmlAttr.to_xml(SomeXmlAttr.FOO) == "foo" -class DescribeEnumeration(object): + def it_knows_the_XML_value_for_each_member_by_the_member_value(self): + assert SomeXmlAttr.to_xml(2) == "bar" - def it_has_the_right_metaclass(self): - assert type(FOOBAR).__name__ == 'MetaEnumeration' + def but_it_raises_when_there_is_no_such_member(self): + with pytest.raises(ValueError, match="42 is not a valid SomeXmlAttr"): + SomeXmlAttr.to_xml(42) - def it_provides_an_EnumValue_instance_for_each_named_member(self): - with pytest.raises(AttributeError): - getattr(FOOBAR, 'None') - for obj in (FOOBAR.READ_WRITE, FOOBAR.READ_ONLY): - assert type(obj).__name__ == 'EnumValue' + def it_can_find_the_member_from_the_XML_attr_value(self): + assert SomeXmlAttr.from_xml("bar") == SomeXmlAttr.BAR - def it_provides_the_enumeration_value_for_each_named_member(self): - assert FOOBAR.READ_WRITE == 1 - assert FOOBAR.READ_ONLY == -2 + def and_it_can_find_the_member_from_None_when_a_member_maps_that(self): + assert SomeXmlAttr.from_xml(None) == SomeXmlAttr.BAZ - def it_knows_if_a_setting_is_valid(self): - FOOBAR.validate(None) - FOOBAR.validate(FOOBAR.READ_WRITE) - with pytest.raises(ValueError): - FOOBAR.validate('foobar') - with pytest.raises(ValueError): - FOOBAR.validate(FOOBAR.READ_ONLY) + def but_it_raises_when_there_is_no_such_mapped_XML_value(self): + with pytest.raises( + ValueError, match="SomeXmlAttr has no XML mapping for 'baz'" + ): + SomeXmlAttr.from_xml("baz") - def it_can_be_referred_to_by_a_convenience_alias_if_defined(self): - assert BARFOO is FOOBAR # noqa +class DescribeBaseXmlEnumMembers: + """Unit-test suite for `docx.enum.base.BaseXmlEnum`.""" -class DescribeEnumValue(object): + def it_is_an_instance_of_its_XmlEnum_subtype_class(self): + assert type(SomeXmlAttr.FOO) is SomeXmlAttr - def it_provides_its_symbolic_name_as_its_string_value(self): - assert ('%s' % FOOBAR.READ_WRITE) == 'READ_WRITE (1)' + def it_has_the_default_Enum_repr(self): + assert repr(SomeXmlAttr.BAR) == "" - def it_provides_its_description_as_its_docstring(self): - assert FOOBAR.READ_ONLY.__doc__ == 'Return value only' + def but_its_str_value_is_customized(self): + assert str(SomeXmlAttr.FOO) == "FOO (1)" + def its_value_is_the_same_int_as_its_corresponding_MS_API_enum_member(self): + assert SomeXmlAttr.FOO.value == 1 -class DescribeXmlEnumeration(object): + def its_name_is_its_member_name_the_same_as_a_regular_Enum(self): + assert SomeXmlAttr.FOO.name == "FOO" - def it_knows_the_XML_value_for_each_of_its_xml_members(self): - assert XMLFOO.to_xml(XMLFOO.XML_RW) == 'attrVal' - assert XMLFOO.to_xml(42) == 'attrVal' - with pytest.raises(ValueError): - XMLFOO.to_xml(XMLFOO.RO) + def it_has_an_individual_member_specific_docstring(self): + assert SomeXmlAttr.FOO.__doc__ == "Do foo instead of bar." - def it_can_map_each_of_its_xml_members_from_the_XML_value(self): - assert XMLFOO.from_xml(None) is None - assert XMLFOO.from_xml('attrVal') == XMLFOO.XML_RW - assert str(XMLFOO.from_xml('attrVal')) == 'XML_RW (42)' + def it_is_equivalent_to_its_int_value(self): + assert SomeXmlAttr.FOO == 1 + assert SomeXmlAttr.FOO != 2 + assert SomeXmlAttr.BAR == 2 + assert SomeXmlAttr.BAR != 1 diff --git a/tests/test_files/sct-inner-content.docx b/tests/test_files/sct-inner-content.docx new file mode 100644 index 000000000..a94165f8d Binary files /dev/null and b/tests/test_files/sct-inner-content.docx differ diff --git a/tests/test_package.py b/tests/test_package.py index c4df206ad..eda5f0132 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for docx.package module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for docx.package module.""" import pytest @@ -15,8 +11,7 @@ from .unitutil.mock import class_mock, instance_mock, method_mock, property_mock -class DescribePackage(object): - +class DescribePackage: def it_can_get_or_add_an_image_part_containing_a_specified_image( self, image_parts_prop_, image_parts_, image_part_ ): @@ -30,7 +25,7 @@ def it_can_get_or_add_an_image_part_containing_a_specified_image( assert image_part is image_part_ def it_gathers_package_image_parts_after_unmarshalling(self): - package = Package.open(docx_path('having-images')) + package = Package.open(docx_path("having-images")) image_parts = package.image_parts assert len(image_parts) == 3 for image_part in image_parts: @@ -51,8 +46,7 @@ def image_parts_prop_(self, request): return property_mock(request, Package, "image_parts") -class DescribeImageParts(object): - +class DescribeImageParts: def it_can_get_a_matching_image_part( self, Image_, image_, _get_by_sha1_, image_part_ ): @@ -104,20 +98,19 @@ def it_can_really_add_a_new_image_part( @pytest.fixture(params=[((2, 3), 1), ((1, 3), 2), ((1, 2), 3)]) def next_partname_fixture(self, request): - def image_part_with_partname_(n): partname = image_partname(n) return instance_mock(request, ImagePart, partname=partname) def image_partname(n): - return PackURI('/word/media/image%d.png' % n) + return PackURI("/word/media/image%d.png" % n) existing_partname_numbers, expected_partname_number = request.param image_parts = ImageParts() for n in existing_partname_numbers: image_part_ = image_part_with_partname_(n) image_parts.append(image_part_) - ext = 'png' + ext = "png" expected_image_partname = image_partname(expected_partname_number) return image_parts, ext, expected_image_partname @@ -125,15 +118,15 @@ def image_partname(n): @pytest.fixture def _add_image_part_(self, request): - return method_mock(request, ImageParts, '_add_image_part') + return method_mock(request, ImageParts, "_add_image_part") @pytest.fixture def _get_by_sha1_(self, request): - return method_mock(request, ImageParts, '_get_by_sha1') + return method_mock(request, ImageParts, "_get_by_sha1") @pytest.fixture def Image_(self, request): - return class_mock(request, 'docx.package.Image') + return class_mock(request, "docx.package.Image") @pytest.fixture def image_(self, request): @@ -141,7 +134,7 @@ def image_(self, request): @pytest.fixture def ImagePart_(self, request): - return class_mock(request, 'docx.package.ImagePart') + return class_mock(request, "docx.package.ImagePart") @pytest.fixture def image_part_(self, request): @@ -149,7 +142,7 @@ def image_part_(self, request): @pytest.fixture def _next_image_partname_(self, request): - return method_mock(request, ImageParts, '_next_image_partname') + return method_mock(request, ImageParts, "_next_image_partname") @pytest.fixture def partname_(self, request): diff --git a/tests/test_section.py b/tests/test_section.py index 7ac29a0f5..333e755b7 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -1,49 +1,73 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Unit test suite for the docx.section module""" +"""Unit test suite for the docx.section module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations + +from typing import cast import pytest -from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENT, WD_SECTION +from docx import Document +from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION +from docx.oxml.document import CT_Document +from docx.oxml.section import CT_SectPr from docx.parts.document import DocumentPart from docx.parts.hdrftr import FooterPart, HeaderPart -from docx.section import _BaseHeaderFooter, _Footer, _Header, Section, Sections -from docx.shared import Inches +from docx.section import Section, Sections, _BaseHeaderFooter, _Footer, _Header +from docx.shared import Inches, Length +from docx.table import Table +from docx.text.paragraph import Paragraph from .unitutil.cxml import element, xml -from .unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock - - -class DescribeSections(object): - - def it_knows_how_many_sections_it_contains(self): - sections = Sections( - element("w:document/w:body/(w:p/w:pPr/w:sectPr, w:sectPr)"), None +from .unitutil.file import test_file +from .unitutil.mock import ( + FixtureRequest, + Mock, + call, + class_mock, + instance_mock, + method_mock, + property_mock, +) + + +class DescribeSections: + """Unit-test suite for `docx.section.Sections`.""" + + def it_knows_how_many_sections_it_contains(self, document_part_: Mock): + document_elm = cast( + CT_Document, element("w:document/w:body/(w:p/w:pPr/w:sectPr, w:sectPr)") ) + sections = Sections(document_elm, document_part_) assert len(sections) == 2 def it_can_iterate_over_its_Section_instances( - self, Section_, section_, document_part_ + self, Section_: Mock, section_: Mock, document_part_: Mock ): - document_elm = element("w:document/w:body/(w:p/w:pPr/w:sectPr, w:sectPr)") + document_elm = cast( + CT_Document, element("w:document/w:body/(w:p/w:pPr/w:sectPr, w:sectPr)") + ) sectPrs = document_elm.xpath("//w:sectPr") Section_.return_value = section_ sections = Sections(document_elm, document_part_) - section_lst = [s for s in sections] + section_lst = list(sections) assert Section_.call_args_list == [ - call(sectPrs[0], document_part_), call(sectPrs[1], document_part_) + call(sectPrs[0], document_part_), + call(sectPrs[1], document_part_), ] assert section_lst == [section_, section_] def it_can_access_its_Section_instances_by_index( - self, Section_, section_, document_part_ + self, Section_: Mock, section_: Mock, document_part_: Mock ): - document_elm = element( - "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" + document_elm = cast( + CT_Document, + element( + "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" + ), ) sectPrs = document_elm.xpath("//w:sectPr") Section_.return_value = section_ @@ -59,10 +83,13 @@ def it_can_access_its_Section_instances_by_index( assert section_lst == [section_, section_, section_] def it_can_access_its_Section_instances_by_slice( - self, Section_, section_, document_part_ + self, Section_: Mock, section_: Mock, document_part_: Mock ): - document_elm = element( - "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" + document_elm = cast( + CT_Document, + element( + "w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)" + ), ) sectPrs = document_elm.xpath("//w:sectPr") Section_.return_value = section_ @@ -71,51 +98,73 @@ def it_can_access_its_Section_instances_by_slice( section_lst = sections[1:9] assert Section_.call_args_list == [ - call(sectPrs[1], document_part_), call(sectPrs[2], document_part_) + call(sectPrs[1], document_part_), + call(sectPrs[2], document_part_), ] assert section_lst == [section_, section_] # fixture components --------------------------------------------- @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def Section_(self, request): + def Section_(self, request: FixtureRequest): return class_mock(request, "docx.section.Section") @pytest.fixture - def section_(self, request): + def section_(self, request: FixtureRequest): return instance_mock(request, Section) -class DescribeSection(object): +class DescribeSection: + """Unit-test suite for `docx.section.Section`.""" + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [ + ("w:sectPr", False), + ("w:sectPr/w:titlePg", True), + ("w:sectPr/w:titlePg{w:val=0}", False), + ("w:sectPr/w:titlePg{w:val=1}", True), + ("w:sectPr/w:titlePg{w:val=true}", True), + ], + ) def it_knows_when_it_displays_a_distinct_first_page_header( - self, diff_first_header_get_fixture + self, sectPr_cxml: str, expected_value: bool, document_part_: Mock ): - sectPr, expected_value = diff_first_header_get_fixture - section = Section(sectPr, None) + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) different_first_page_header_footer = section.different_first_page_header_footer assert different_first_page_header_footer is expected_value + @pytest.mark.parametrize( + ("sectPr_cxml", "value", "expected_cxml"), + [ + ("w:sectPr", True, "w:sectPr/w:titlePg"), + ("w:sectPr/w:titlePg", False, "w:sectPr"), + ("w:sectPr/w:titlePg{w:val=1}", True, "w:sectPr/w:titlePg"), + ("w:sectPr/w:titlePg{w:val=off}", False, "w:sectPr"), + ], + ) def it_can_change_whether_the_document_has_distinct_odd_and_even_headers( - self, diff_first_header_set_fixture + self, sectPr_cxml: str, value: bool, expected_cxml: str, document_part_: Mock ): - sectPr, value, expected_xml = diff_first_header_set_fixture - section = Section(sectPr, None) + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) section.different_first_page_header_footer = value assert sectPr.xml == expected_xml def it_provides_access_to_its_even_page_footer( - self, document_part_, _Footer_, footer_ + self, document_part_: Mock, _Footer_: Mock, footer_: Mock ): - sectPr = element('w:sectPr') + sectPr = cast(CT_SectPr, element("w:sectPr")) _Footer_.return_value = footer_ section = Section(sectPr, document_part_) @@ -127,9 +176,9 @@ def it_provides_access_to_its_even_page_footer( assert footer is footer_ def it_provides_access_to_its_even_page_header( - self, document_part_, _Header_, header_ + self, document_part_: Mock, _Header_: Mock, header_: Mock ): - sectPr = element("w:sectPr") + sectPr = cast(CT_SectPr, element("w:sectPr")) _Header_.return_value = header_ section = Section(sectPr, document_part_) @@ -141,9 +190,9 @@ def it_provides_access_to_its_even_page_header( assert header is header_ def it_provides_access_to_its_first_page_footer( - self, document_part_, _Footer_, footer_ + self, document_part_: Mock, _Footer_: Mock, footer_: Mock ): - sectPr = element("w:sectPr") + sectPr = cast(CT_SectPr, element("w:sectPr")) _Footer_.return_value = footer_ section = Section(sectPr, document_part_) @@ -155,9 +204,9 @@ def it_provides_access_to_its_first_page_footer( assert footer is footer_ def it_provides_access_to_its_first_page_header( - self, document_part_, _Header_, header_ + self, document_part_: Mock, _Header_: Mock, header_: Mock ): - sectPr = element("w:sectPr") + sectPr = cast(CT_SectPr, element("w:sectPr")) _Header_.return_value = header_ section = Section(sectPr, document_part_) @@ -169,9 +218,9 @@ def it_provides_access_to_its_first_page_header( assert header is header_ def it_provides_access_to_its_default_footer( - self, document_part_, _Footer_, footer_ + self, document_part_: Mock, _Footer_: Mock, footer_: Mock ): - sectPr = element('w:sectPr') + sectPr = cast(CT_SectPr, element("w:sectPr")) _Footer_.return_value = footer_ section = Section(sectPr, document_part_) @@ -183,9 +232,9 @@ def it_provides_access_to_its_default_footer( assert footer is footer_ def it_provides_access_to_its_default_header( - self, document_part_, _Header_, header_ + self, document_part_: Mock, _Header_: Mock, header_: Mock ): - sectPr = element('w:sectPr') + sectPr = cast(CT_SectPr, element("w:sectPr")) _Header_.return_value = header_ section = Section(sectPr, document_part_) @@ -196,311 +245,375 @@ def it_provides_access_to_its_default_header( ) assert header is header_ - def it_knows_its_start_type(self, start_type_get_fixture): - sectPr, expected_start_type = start_type_get_fixture - section = Section(sectPr, None) + def it_can_iterate_its_inner_content(self): + document = Document(test_file("sct-inner-content.docx")) + + assert len(document.sections) == 3 + + inner_content = list(document.sections[0].iter_inner_content()) + + assert len(inner_content) == 3 + p = inner_content[0] + assert isinstance(p, Paragraph) + assert p.text == "P1" + t = inner_content[1] + assert isinstance(t, Table) + assert t.rows[0].cells[0].text == "T2" + p = inner_content[2] + assert isinstance(p, Paragraph) + assert p.text == "P3" + + inner_content = list(document.sections[1].iter_inner_content()) + + assert len(inner_content) == 3 + t = inner_content[0] + assert isinstance(t, Table) + assert t.rows[0].cells[0].text == "T4" + p = inner_content[1] + assert isinstance(p, Paragraph) + assert p.text == "P5" + p = inner_content[2] + assert isinstance(p, Paragraph) + assert p.text == "P6" + + inner_content = list(document.sections[2].iter_inner_content()) + + assert len(inner_content) == 3 + p = inner_content[0] + assert isinstance(p, Paragraph) + assert p.text == "P7" + p = inner_content[1] + assert isinstance(p, Paragraph) + assert p.text == "P8" + p = inner_content[2] + assert isinstance(p, Paragraph) + assert p.text == "P9" + + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [ + ("w:sectPr", WD_SECTION.NEW_PAGE), + ("w:sectPr/w:type", WD_SECTION.NEW_PAGE), + ("w:sectPr/w:type{w:val=continuous}", WD_SECTION.CONTINUOUS), + ("w:sectPr/w:type{w:val=nextPage}", WD_SECTION.NEW_PAGE), + ("w:sectPr/w:type{w:val=oddPage}", WD_SECTION.ODD_PAGE), + ("w:sectPr/w:type{w:val=evenPage}", WD_SECTION.EVEN_PAGE), + ("w:sectPr/w:type{w:val=nextColumn}", WD_SECTION.NEW_COLUMN), + ], + ) + def it_knows_its_start_type( + self, sectPr_cxml: str, expected_value: WD_SECTION, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) start_type = section.start_type - assert start_type is expected_start_type - - def it_can_change_its_start_type(self, start_type_set_fixture): - sectPr, new_start_type, expected_xml = start_type_set_fixture - section = Section(sectPr, None) + assert start_type is expected_value + + @pytest.mark.parametrize( + ("sectPr_cxml", "value", "expected_cxml"), + [ + ( + "w:sectPr/w:type{w:val=oddPage}", + WD_SECTION.EVEN_PAGE, + "w:sectPr/w:type{w:val=evenPage}", + ), + ("w:sectPr/w:type{w:val=nextPage}", None, "w:sectPr"), + ("w:sectPr", None, "w:sectPr"), + ("w:sectPr/w:type{w:val=continuous}", WD_SECTION.NEW_PAGE, "w:sectPr"), + ("w:sectPr/w:type", WD_SECTION.NEW_PAGE, "w:sectPr"), + ( + "w:sectPr/w:type", + WD_SECTION.NEW_COLUMN, + "w:sectPr/w:type{w:val=nextColumn}", + ), + ], + ) + def it_can_change_its_start_type( + self, + sectPr_cxml: str, + value: WD_SECTION | None, + expected_cxml: str, + document_part_: Mock, + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) - section.start_type = new_start_type + section.start_type = value assert section._sectPr.xml == expected_xml - def it_knows_its_page_width(self, page_width_get_fixture): - sectPr, expected_page_width = page_width_get_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [ + ("w:sectPr/w:pgSz{w:w=1440}", Inches(1)), + ("w:sectPr/w:pgSz", None), + ("w:sectPr", None), + ], + ) + def it_knows_its_page_width( + self, sectPr_cxml: str, expected_value: Length | None, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) page_width = section.page_width - assert page_width == expected_page_width + assert page_width == expected_value - def it_can_change_its_page_width(self, page_width_set_fixture): - sectPr, new_page_width, expected_xml = page_width_set_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("value", "expected_cxml"), + [ + (None, "w:sectPr/w:pgSz"), + (Inches(4), "w:sectPr/w:pgSz{w:w=5760}"), + ], + ) + def it_can_change_its_page_width( + self, + value: Length | None, + expected_cxml: str, + document_part_: Mock, + ): + sectPr = cast(CT_SectPr, element("w:sectPr")) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) - section.page_width = new_page_width + section.page_width = value assert section._sectPr.xml == expected_xml - def it_knows_its_page_height(self, page_height_get_fixture): - sectPr, expected_page_height = page_height_get_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [ + ("w:sectPr/w:pgSz{w:h=2880}", Inches(2)), + ("w:sectPr/w:pgSz", None), + ("w:sectPr", None), + ], + ) + def it_knows_its_page_height( + self, sectPr_cxml: str, expected_value: Length | None, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) page_height = section.page_height - assert page_height == expected_page_height + assert page_height == expected_value - def it_can_change_its_page_height(self, page_height_set_fixture): - sectPr, new_page_height, expected_xml = page_height_set_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("value", "expected_cxml"), + [ + (None, "w:sectPr/w:pgSz"), + (Inches(2), "w:sectPr/w:pgSz{w:h=2880}"), + ], + ) + def it_can_change_its_page_height( + self, value: Length | None, expected_cxml: str, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element("w:sectPr")) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) - section.page_height = new_page_height + section.page_height = value assert section._sectPr.xml == expected_xml - def it_knows_its_page_orientation(self, orientation_get_fixture): - sectPr, expected_orientation = orientation_get_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [ + ("w:sectPr/w:pgSz{w:orient=landscape}", WD_ORIENTATION.LANDSCAPE), + ("w:sectPr/w:pgSz{w:orient=portrait}", WD_ORIENTATION.PORTRAIT), + ("w:sectPr/w:pgSz", WD_ORIENTATION.PORTRAIT), + ("w:sectPr", WD_ORIENTATION.PORTRAIT), + ], + ) + def it_knows_its_page_orientation( + self, sectPr_cxml: str, expected_value: WD_ORIENTATION, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) orientation = section.orientation - assert orientation is expected_orientation + assert orientation is expected_value - def it_can_change_its_orientation(self, orientation_set_fixture): - sectPr, new_orientation, expected_xml = orientation_set_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("value", "expected_cxml"), + [ + (WD_ORIENTATION.LANDSCAPE, "w:sectPr/w:pgSz{w:orient=landscape}"), + (WD_ORIENTATION.PORTRAIT, "w:sectPr/w:pgSz"), + (None, "w:sectPr/w:pgSz"), + ], + ) + def it_can_change_its_orientation( + self, value: WD_ORIENTATION | None, expected_cxml: str, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element("w:sectPr")) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) - section.orientation = new_orientation + section.orientation = value assert section._sectPr.xml == expected_xml - def it_knows_its_page_margins(self, margins_get_fixture): - sectPr, margin_prop_name, expected_value = margins_get_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("sectPr_cxml", "margin_prop_name", "expected_value"), + [ + ("w:sectPr/w:pgMar{w:left=120}", "left_margin", 76200), + ("w:sectPr/w:pgMar{w:right=240}", "right_margin", 152400), + ("w:sectPr/w:pgMar{w:top=-360}", "top_margin", -228600), + ("w:sectPr/w:pgMar{w:bottom=480}", "bottom_margin", 304800), + ("w:sectPr/w:pgMar{w:gutter=600}", "gutter", 381000), + ("w:sectPr/w:pgMar{w:header=720}", "header_distance", 457200), + ("w:sectPr/w:pgMar{w:footer=840}", "footer_distance", 533400), + ("w:sectPr/w:pgMar", "left_margin", None), + ("w:sectPr", "top_margin", None), + ], + ) + def it_knows_its_page_margins( + self, + sectPr_cxml: str, + margin_prop_name: str, + expected_value: int | None, + document_part_: Mock, + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + section = Section(sectPr, document_part_) value = getattr(section, margin_prop_name) assert value == expected_value - def it_can_change_its_page_margins(self, margins_set_fixture): - sectPr, margin_prop_name, new_value, expected_xml = margins_set_fixture - section = Section(sectPr, None) + @pytest.mark.parametrize( + ("sectPr_cxml", "margin_prop_name", "value", "expected_cxml"), + [ + ("w:sectPr", "left_margin", Inches(1), "w:sectPr/w:pgMar{w:left=1440}"), + ("w:sectPr", "right_margin", Inches(0.5), "w:sectPr/w:pgMar{w:right=720}"), + ("w:sectPr", "top_margin", Inches(-0.25), "w:sectPr/w:pgMar{w:top=-360}"), + ( + "w:sectPr", + "bottom_margin", + Inches(0.75), + "w:sectPr/w:pgMar{w:bottom=1080}", + ), + ("w:sectPr", "gutter", Inches(0.25), "w:sectPr/w:pgMar{w:gutter=360}"), + ( + "w:sectPr", + "header_distance", + Inches(1.25), + "w:sectPr/w:pgMar{w:header=1800}", + ), + ( + "w:sectPr", + "footer_distance", + Inches(1.35), + "w:sectPr/w:pgMar{w:footer=1944}", + ), + ("w:sectPr", "left_margin", None, "w:sectPr/w:pgMar"), + ( + "w:sectPr/w:pgMar{w:top=-360}", + "top_margin", + Inches(0.6), + "w:sectPr/w:pgMar{w:top=864}", + ), + ], + ) + def it_can_change_its_page_margins( + self, + sectPr_cxml: str, + margin_prop_name: str, + value: Length | None, + expected_cxml: str, + document_part_: Mock, + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + expected_xml = xml(expected_cxml) + section = Section(sectPr, document_part_) - setattr(section, margin_prop_name, new_value) + setattr(section, margin_prop_name, value) assert section._sectPr.xml == expected_xml - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ - ("w:sectPr", False), - ("w:sectPr/w:titlePg", True), - ("w:sectPr/w:titlePg{w:val=0}", False), - ("w:sectPr/w:titlePg{w:val=1}", True), - ("w:sectPr/w:titlePg{w:val=true}", True), - ] - ) - def diff_first_header_get_fixture(self, request): - sectPr_cxml, expected_value = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_value - - @pytest.fixture( - params=[ - ("w:sectPr", True, "w:sectPr/w:titlePg"), - ("w:sectPr/w:titlePg", False, "w:sectPr"), - ("w:sectPr/w:titlePg{w:val=1}", True, "w:sectPr/w:titlePg"), - ("w:sectPr/w:titlePg{w:val=off}", False, "w:sectPr"), - ] - ) - def diff_first_header_set_fixture(self, request): - sectPr_cxml, value, expected_cxml = request.param - sectPr = element(sectPr_cxml) - expected_xml = xml(expected_cxml) - return sectPr, value, expected_xml - - @pytest.fixture(params=[ - ('w:sectPr/w:pgMar{w:left=120}', 'left_margin', 76200), - ('w:sectPr/w:pgMar{w:right=240}', 'right_margin', 152400), - ('w:sectPr/w:pgMar{w:top=-360}', 'top_margin', -228600), - ('w:sectPr/w:pgMar{w:bottom=480}', 'bottom_margin', 304800), - ('w:sectPr/w:pgMar{w:gutter=600}', 'gutter', 381000), - ('w:sectPr/w:pgMar{w:header=720}', 'header_distance', 457200), - ('w:sectPr/w:pgMar{w:footer=840}', 'footer_distance', 533400), - ('w:sectPr/w:pgMar', 'left_margin', None), - ('w:sectPr', 'top_margin', None), - ]) - def margins_get_fixture(self, request): - sectPr_cxml, margin_prop_name, expected_value = request.param - sectPr = element(sectPr_cxml) - return sectPr, margin_prop_name, expected_value - - @pytest.fixture(params=[ - ('w:sectPr', 'left_margin', Inches(1), - 'w:sectPr/w:pgMar{w:left=1440}'), - ('w:sectPr', 'right_margin', Inches(0.5), - 'w:sectPr/w:pgMar{w:right=720}'), - ('w:sectPr', 'top_margin', Inches(-0.25), - 'w:sectPr/w:pgMar{w:top=-360}'), - ('w:sectPr', 'bottom_margin', Inches(0.75), - 'w:sectPr/w:pgMar{w:bottom=1080}'), - ('w:sectPr', 'gutter', Inches(0.25), - 'w:sectPr/w:pgMar{w:gutter=360}'), - ('w:sectPr', 'header_distance', Inches(1.25), - 'w:sectPr/w:pgMar{w:header=1800}'), - ('w:sectPr', 'footer_distance', Inches(1.35), - 'w:sectPr/w:pgMar{w:footer=1944}'), - ('w:sectPr', 'left_margin', None, 'w:sectPr/w:pgMar'), - ('w:sectPr/w:pgMar{w:top=-360}', 'top_margin', Inches(0.6), - 'w:sectPr/w:pgMar{w:top=864}'), - ]) - def margins_set_fixture(self, request): - sectPr_cxml, property_name, new_value, expected_cxml = request.param - sectPr = element(sectPr_cxml) - expected_xml = xml(expected_cxml) - return sectPr, property_name, new_value, expected_xml - - @pytest.fixture(params=[ - ('w:sectPr/w:pgSz{w:orient=landscape}', WD_ORIENT.LANDSCAPE), - ('w:sectPr/w:pgSz{w:orient=portrait}', WD_ORIENT.PORTRAIT), - ('w:sectPr/w:pgSz', WD_ORIENT.PORTRAIT), - ('w:sectPr', WD_ORIENT.PORTRAIT), - ]) - def orientation_get_fixture(self, request): - sectPr_cxml, expected_orientation = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_orientation - - @pytest.fixture(params=[ - (WD_ORIENT.LANDSCAPE, 'w:sectPr/w:pgSz{w:orient=landscape}'), - (WD_ORIENT.PORTRAIT, 'w:sectPr/w:pgSz'), - (None, 'w:sectPr/w:pgSz'), - ]) - def orientation_set_fixture(self, request): - new_orientation, expected_cxml = request.param - sectPr = element('w:sectPr') - expected_xml = xml(expected_cxml) - return sectPr, new_orientation, expected_xml - - @pytest.fixture(params=[ - ('w:sectPr/w:pgSz{w:h=2880}', Inches(2)), - ('w:sectPr/w:pgSz', None), - ('w:sectPr', None), - ]) - def page_height_get_fixture(self, request): - sectPr_cxml, expected_page_height = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_page_height - - @pytest.fixture(params=[ - (None, 'w:sectPr/w:pgSz'), - (Inches(2), 'w:sectPr/w:pgSz{w:h=2880}'), - ]) - def page_height_set_fixture(self, request): - new_page_height, expected_cxml = request.param - sectPr = element('w:sectPr') - expected_xml = xml(expected_cxml) - return sectPr, new_page_height, expected_xml - - @pytest.fixture(params=[ - ('w:sectPr/w:pgSz{w:w=1440}', Inches(1)), - ('w:sectPr/w:pgSz', None), - ('w:sectPr', None), - ]) - def page_width_get_fixture(self, request): - sectPr_cxml, expected_page_width = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_page_width - - @pytest.fixture(params=[ - (None, 'w:sectPr/w:pgSz'), - (Inches(4), 'w:sectPr/w:pgSz{w:w=5760}'), - ]) - def page_width_set_fixture(self, request): - new_page_width, expected_cxml = request.param - sectPr = element('w:sectPr') - expected_xml = xml(expected_cxml) - return sectPr, new_page_width, expected_xml - - @pytest.fixture(params=[ - ('w:sectPr', WD_SECTION.NEW_PAGE), - ('w:sectPr/w:type', WD_SECTION.NEW_PAGE), - ('w:sectPr/w:type{w:val=continuous}', WD_SECTION.CONTINUOUS), - ('w:sectPr/w:type{w:val=nextPage}', WD_SECTION.NEW_PAGE), - ('w:sectPr/w:type{w:val=oddPage}', WD_SECTION.ODD_PAGE), - ('w:sectPr/w:type{w:val=evenPage}', WD_SECTION.EVEN_PAGE), - ('w:sectPr/w:type{w:val=nextColumn}', WD_SECTION.NEW_COLUMN), - ]) - def start_type_get_fixture(self, request): - sectPr_cxml, expected_start_type = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_start_type - - @pytest.fixture(params=[ - ('w:sectPr/w:type{w:val=oddPage}', WD_SECTION.EVEN_PAGE, - 'w:sectPr/w:type{w:val=evenPage}'), - ('w:sectPr/w:type{w:val=nextPage}', None, - 'w:sectPr'), - ('w:sectPr', None, - 'w:sectPr'), - ('w:sectPr/w:type{w:val=continuous}', WD_SECTION.NEW_PAGE, - 'w:sectPr'), - ('w:sectPr/w:type', WD_SECTION.NEW_PAGE, - 'w:sectPr'), - ('w:sectPr/w:type', WD_SECTION.NEW_COLUMN, - 'w:sectPr/w:type{w:val=nextColumn}'), - ]) - def start_type_set_fixture(self, request): - initial_cxml, new_start_type, expected_cxml = request.param - sectPr = element(initial_cxml) - expected_xml = xml(expected_cxml) - return sectPr, new_start_type, expected_xml - - # fixture components --------------------------------------------- + # -- fixtures----------------------------------------------------- @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def _Footer_(self, request): + def _Footer_(self, request: FixtureRequest): return class_mock(request, "docx.section._Footer") @pytest.fixture - def footer_(self, request): + def footer_(self, request: FixtureRequest): return instance_mock(request, _Footer) @pytest.fixture - def _Header_(self, request): + def _Header_(self, request: FixtureRequest): return class_mock(request, "docx.section._Header") @pytest.fixture - def header_(self, request): + def header_(self, request: FixtureRequest): return instance_mock(request, _Header) -class Describe_BaseHeaderFooter(object): +class Describe_BaseHeaderFooter: + """Unit-test suite for `docx.section._BaseHeaderFooter`.""" + @pytest.mark.parametrize( + ("has_definition", "expected_value"), [(False, True), (True, False)] + ) def it_knows_when_its_linked_to_the_previous_header_or_footer( - self, is_linked_get_fixture, _has_definition_prop_ + self, has_definition: bool, expected_value: bool, _has_definition_prop_: Mock ): - has_definition, expected_value = is_linked_get_fixture _has_definition_prop_.return_value = has_definition - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) is_linked = header.is_linked_to_previous assert is_linked is expected_value + @pytest.mark.parametrize( + ("has_definition", "value", "drop_calls", "add_calls"), + [ + (False, True, 0, 0), + (True, False, 0, 0), + (True, True, 1, 0), + (False, False, 0, 1), + ], + ) def it_can_change_whether_it_is_linked_to_previous_header_or_footer( self, - is_linked_set_fixture, - _has_definition_prop_, - _drop_definition_, - _add_definition_, + has_definition: bool, + value: bool, + drop_calls: int, + add_calls: int, + _has_definition_prop_: Mock, + _drop_definition_: Mock, + _add_definition_: Mock, ): - has_definition, new_value, drop_calls, add_calls = is_linked_set_fixture _has_definition_prop_.return_value = has_definition - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) - header.is_linked_to_previous = new_value + header.is_linked_to_previous = value assert _drop_definition_.call_args_list == [call(header)] * drop_calls assert _add_definition_.call_args_list == [call(header)] * add_calls def it_provides_access_to_the_header_or_footer_part_for_BlockItemContainer( - self, _get_or_add_definition_, header_part_ + self, _get_or_add_definition_: Mock, header_part_: Mock ): # ---this override fulfills part of the BlockItemContainer subclass interface--- _get_or_add_definition_.return_value = header_part_ - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) header_part = header.part @@ -508,12 +621,14 @@ def it_provides_access_to_the_header_or_footer_part_for_BlockItemContainer( assert header_part is header_part_ def it_provides_access_to_the_hdr_or_ftr_element_to_help( - self, _get_or_add_definition_, header_part_ + self, _get_or_add_definition_: Mock, header_part_: Mock ): hdr = element("w:hdr") _get_or_add_definition_.return_value = header_part_ header_part_.element = hdr - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) hdr_elm = header._element @@ -521,11 +636,13 @@ def it_provides_access_to_the_hdr_or_ftr_element_to_help( assert hdr_elm is hdr def it_gets_the_definition_when_it_has_one( - self, _has_definition_prop_, _definition_prop_, header_part_ + self, _has_definition_prop_: Mock, _definition_prop_: Mock, header_part_: Mock ): _has_definition_prop_.return_value = True _definition_prop_.return_value = header_part_ - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) header_part = header._get_or_add_definition() @@ -533,15 +650,17 @@ def it_gets_the_definition_when_it_has_one( def but_it_gets_the_prior_definition_when_it_is_linked( self, - _has_definition_prop_, - _prior_headerfooter_prop_, - prior_headerfooter_, - header_part_, + _has_definition_prop_: Mock, + _prior_headerfooter_prop_: Mock, + prior_headerfooter_: Mock, + header_part_: Mock, ): _has_definition_prop_.return_value = False _prior_headerfooter_prop_.return_value = prior_headerfooter_ prior_headerfooter_._get_or_add_definition.return_value = header_part_ - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) header_part = header._get_or_add_definition() @@ -550,78 +669,64 @@ def but_it_gets_the_prior_definition_when_it_is_linked( def and_it_adds_a_definition_when_it_is_linked_and_the_first_section( self, - _has_definition_prop_, - _prior_headerfooter_prop_, - _add_definition_, - header_part_ + _has_definition_prop_: Mock, + _prior_headerfooter_prop_: Mock, + _add_definition_: Mock, + header_part_: Mock, ): _has_definition_prop_.return_value = False _prior_headerfooter_prop_.return_value = None _add_definition_.return_value = header_part_ - header = _BaseHeaderFooter(None, None, None) + header = _BaseHeaderFooter( + None, None, None # pyright: ignore[reportGeneralTypeIssues] + ) header_part = header._get_or_add_definition() _add_definition_.assert_called_once_with(header) assert header_part is header_part_ - # fixtures ------------------------------------------------------- - - @pytest.fixture(params=[(False, True), (True, False)]) - def is_linked_get_fixture(self, request): - has_definition, expected_value = request.param - return has_definition, expected_value - - @pytest.fixture( - params=[ - (False, True, 0, 0), - (True, False, 0, 0), - (True, True, 1, 0), - (False, False, 0, 1), - ] - ) - def is_linked_set_fixture(self, request): - has_definition, new_value, drop_calls, add_calls = request.param - return has_definition, new_value, drop_calls, add_calls - - # fixture components --------------------------------------------- + # -- fixture ----------------------------------------------------- @pytest.fixture - def _add_definition_(self, request): + def _add_definition_(self, request: FixtureRequest): return method_mock(request, _BaseHeaderFooter, "_add_definition") @pytest.fixture - def _definition_prop_(self, request): + def _definition_prop_(self, request: FixtureRequest): return property_mock(request, _BaseHeaderFooter, "_definition") @pytest.fixture - def _drop_definition_(self, request): + def _drop_definition_(self, request: FixtureRequest): return method_mock(request, _BaseHeaderFooter, "_drop_definition") @pytest.fixture - def _get_or_add_definition_(self, request): + def _get_or_add_definition_(self, request: FixtureRequest): return method_mock(request, _BaseHeaderFooter, "_get_or_add_definition") @pytest.fixture - def _has_definition_prop_(self, request): + def _has_definition_prop_(self, request: FixtureRequest): return property_mock(request, _BaseHeaderFooter, "_has_definition") @pytest.fixture - def header_part_(self, request): + def header_part_(self, request: FixtureRequest): return instance_mock(request, HeaderPart) @pytest.fixture - def prior_headerfooter_(self, request): + def prior_headerfooter_(self, request: FixtureRequest): return instance_mock(request, _BaseHeaderFooter) @pytest.fixture - def _prior_headerfooter_prop_(self, request): + def _prior_headerfooter_prop_(self, request: FixtureRequest): return property_mock(request, _BaseHeaderFooter, "_prior_headerfooter") -class Describe_Footer(object): +class Describe_Footer: + """Unit-test suite for `docx.section._Footer`.""" - def it_can_add_a_footer_part_to_help(self, document_part_, footer_part_): + def it_can_add_a_footer_part_to_help( + self, document_part_: Mock, footer_part_: Mock + ): sectPr = element("w:sectPr{r:a=b}") document_part_.add_footer_part.return_value = footer_part_, "rId3" footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) @@ -635,7 +740,7 @@ def it_can_add_a_footer_part_to_help(self, document_part_, footer_part_): assert footer_part is footer_part_ def it_provides_access_to_its_footer_part_to_help( - self, document_part_, footer_part_ + self, document_part_: Mock, footer_part_: Mock ): sectPr = element("w:sectPr/w:footerReference{w:type=even,r:id=rId3}") document_part_.footer_part.return_value = footer_part_ @@ -646,7 +751,7 @@ def it_provides_access_to_its_footer_part_to_help( document_part_.footer_part.assert_called_once_with("rId3") assert footer_part is footer_part_ - def it_can_drop_the_related_footer_part_to_help(self, document_part_): + def it_can_drop_the_related_footer_part_to_help(self, document_part_: Mock): sectPr = element("w:sectPr{r:a=b}/w:footerReference{w:type=first,r:id=rId42}") footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE) @@ -655,16 +760,22 @@ def it_can_drop_the_related_footer_part_to_help(self, document_part_): assert sectPr.xml == xml("w:sectPr{r:a=b}") document_part_.drop_rel.assert_called_once_with("rId42") - def it_knows_when_it_has_a_definition_to_help(self, has_definition_fixture): - sectPr, expected_value = has_definition_fixture - footer = _Footer(sectPr, None, WD_HEADER_FOOTER.PRIMARY) + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [("w:sectPr", False), ("w:sectPr/w:footerReference{w:type=default}", True)], + ) + def it_knows_when_it_has_a_definition_to_help( + self, sectPr_cxml: str, expected_value: bool, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY) has_definition = footer._has_definition assert has_definition is expected_value def it_provides_access_to_the_prior_Footer_to_help( - self, request, document_part_, footer_ + self, request: FixtureRequest, document_part_: Mock, footer_: Mock ): doc_elm = element("w:document/(w:sectPr,w:sectPr)") prior_sectPr, sectPr = doc_elm[0], doc_elm[1] @@ -680,7 +791,7 @@ def it_provides_access_to_the_prior_Footer_to_help( assert prior_footer is footer_ def but_it_returns_None_when_its_the_first_footer(self): - doc_elm = element("w:document/w:sectPr") + doc_elm = cast(CT_Document, element("w:document/w:sectPr")) sectPr = doc_elm[0] footer = _Footer(sectPr, None, None) @@ -688,36 +799,25 @@ def but_it_returns_None_when_its_the_first_footer(self): assert prior_footer is None - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ - ("w:sectPr", False), ("w:sectPr/w:footerReference{w:type=default}", True) - ] - ) - def has_definition_fixture(self, request): - sectPr_cxml, expected_value = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_value - - # fixture components --------------------------------------------- + # -- fixtures ---------------------------------------------------- @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def footer_(self, request): + def footer_(self, request: FixtureRequest): return instance_mock(request, _Footer) @pytest.fixture - def footer_part_(self, request): + def footer_part_(self, request: FixtureRequest): return instance_mock(request, FooterPart) -class Describe_Header(object): - - def it_can_add_a_header_part_to_help(self, document_part_, header_part_): +class Describe_Header: + def it_can_add_a_header_part_to_help( + self, document_part_: Mock, header_part_: Mock + ): sectPr = element("w:sectPr{r:a=b}") document_part_.add_header_part.return_value = header_part_, "rId3" header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE) @@ -731,7 +831,7 @@ def it_can_add_a_header_part_to_help(self, document_part_, header_part_): assert header_part is header_part_ def it_provides_access_to_its_header_part_to_help( - self, document_part_, header_part_ + self, document_part_: Mock, header_part_: Mock ): sectPr = element("w:sectPr/w:headerReference{w:type=default,r:id=rId8}") document_part_.header_part.return_value = header_part_ @@ -742,7 +842,7 @@ def it_provides_access_to_its_header_part_to_help( document_part_.header_part.assert_called_once_with("rId8") assert header_part is header_part_ - def it_can_drop_the_related_header_part_to_help(self, document_part_): + def it_can_drop_the_related_header_part_to_help(self, document_part_: Mock): sectPr = element("w:sectPr{r:a=b}/w:headerReference{w:type=even,r:id=rId42}") header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE) @@ -751,16 +851,22 @@ def it_can_drop_the_related_header_part_to_help(self, document_part_): assert sectPr.xml == xml("w:sectPr{r:a=b}") document_part_.drop_header_part.assert_called_once_with("rId42") - def it_knows_when_it_has_a_header_part_to_help(self, has_definition_fixture): - sectPr, expected_value = has_definition_fixture - header = _Header(sectPr, None, WD_HEADER_FOOTER.FIRST_PAGE) + @pytest.mark.parametrize( + ("sectPr_cxml", "expected_value"), + [("w:sectPr", False), ("w:sectPr/w:headerReference{w:type=first}", True)], + ) + def it_knows_when_it_has_a_header_part_to_help( + self, sectPr_cxml: str, expected_value: bool, document_part_: Mock + ): + sectPr = cast(CT_SectPr, element(sectPr_cxml)) + header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE) has_definition = header._has_definition assert has_definition is expected_value def it_provides_access_to_the_prior_Header_to_help( - self, request, document_part_, header_ + self, request, document_part_: Mock, header_: Mock ): doc_elm = element("w:document/(w:sectPr,w:sectPr)") prior_sectPr, sectPr = doc_elm[0], doc_elm[1] @@ -784,28 +890,16 @@ def but_it_returns_None_when_its_the_first_header(self): assert prior_header is None - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ - ("w:sectPr", False), ("w:sectPr/w:headerReference{w:type=first}", True) - ] - ) - def has_definition_fixture(self, request): - sectPr_cxml, expected_value = request.param - sectPr = element(sectPr_cxml) - return sectPr, expected_value - - # fixture components --------------------------------------------- + # -- fixtures----------------------------------------------------- @pytest.fixture - def document_part_(self, request): + def document_part_(self, request: FixtureRequest): return instance_mock(request, DocumentPart) @pytest.fixture - def header_(self, request): + def header_(self, request: FixtureRequest): return instance_mock(request, _Header) @pytest.fixture - def header_part_(self, request): + def header_part_(self, request: FixtureRequest): return instance_mock(request, HeaderPart) diff --git a/tests/test_settings.py b/tests/test_settings.py index 5873ffa1d..9f430822d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,8 +1,4 @@ -# encoding: utf-8 - -"""Unit test suite for the docx.settings module""" - -from __future__ import absolute_import, division, print_function, unicode_literals +"""Unit test suite for the docx.settings module.""" import pytest @@ -11,8 +7,7 @@ from .unitutil.cxml import element, xml -class DescribeSettings(object): - +class DescribeSettings: def it_knows_when_the_document_has_distinct_odd_and_even_headers( self, odd_and_even_get_fixture ): @@ -56,7 +51,7 @@ def odd_and_even_get_fixture(self, request): ( "w:settings/w:evenAndOddHeaders{w:val=1}", True, - "w:settings/w:evenAndOddHeaders" + "w:settings/w:evenAndOddHeaders", ), ("w:settings/w:evenAndOddHeaders{w:val=off}", False, "w:settings"), ] diff --git a/tests/test_shape.py b/tests/test_shape.py index 105d2fa40..da307e48f 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -1,10 +1,4 @@ -# encoding: utf-8 - -""" -Test suite for the docx.shape module -""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for the docx.shape module.""" import pytest @@ -14,21 +8,23 @@ from docx.shared import Length from .oxml.unitdata.dml import ( - a_blip, a_blipFill, a_graphic, a_graphicData, a_pic, an_inline, + a_blip, + a_blipFill, + a_graphic, + a_graphicData, + a_pic, + an_inline, ) from .unitutil.cxml import element, xml from .unitutil.mock import loose_mock -class DescribeInlineShapes(object): - - def it_knows_how_many_inline_shapes_it_contains( - self, inline_shapes_fixture): +class DescribeInlineShapes: + def it_knows_how_many_inline_shapes_it_contains(self, inline_shapes_fixture): inline_shapes, expected_count = inline_shapes_fixture assert len(inline_shapes) == expected_count - def it_can_iterate_over_its_InlineShape_instances( - self, inline_shapes_fixture): + def it_can_iterate_over_its_InlineShape_instances(self, inline_shapes_fixture): inline_shapes, inline_shape_count = inline_shapes_fixture actual_count = 0 for inline_shape in inline_shapes: @@ -36,21 +32,19 @@ def it_can_iterate_over_its_InlineShape_instances( actual_count += 1 assert actual_count == inline_shape_count - def it_provides_indexed_access_to_inline_shapes( - self, inline_shapes_fixture): + def it_provides_indexed_access_to_inline_shapes(self, inline_shapes_fixture): inline_shapes, inline_shape_count = inline_shapes_fixture for idx in range(-inline_shape_count, inline_shape_count): inline_shape = inline_shapes[idx] assert isinstance(inline_shape, InlineShape) - def it_raises_on_indexed_access_out_of_range( - self, inline_shapes_fixture): + def it_raises_on_indexed_access_out_of_range(self, inline_shapes_fixture): inline_shapes, inline_shape_count = inline_shapes_fixture - with pytest.raises(IndexError): - too_low = -1 - inline_shape_count + too_low = -1 - inline_shape_count + with pytest.raises(IndexError, match=r"inline shape index \[-3\] out of rang"): inline_shapes[too_low] - with pytest.raises(IndexError): - too_high = inline_shape_count + too_high = inline_shape_count + with pytest.raises(IndexError, match=r"inline shape index \[2\] out of range"): inline_shapes[too_high] def it_knows_the_part_it_belongs_to(self, inline_shapes_with_parent_): @@ -62,9 +56,7 @@ def it_knows_the_part_it_belongs_to(self, inline_shapes_with_parent_): @pytest.fixture def inline_shapes_fixture(self): - body = element( - 'w:body/w:p/(w:r/w:drawing/wp:inline, w:r/w:drawing/wp:inline)' - ) + body = element("w:body/w:p/(w:r/w:drawing/wp:inline, w:r/w:drawing/wp:inline)") inline_shapes = InlineShapes(body, None) expected_count = 2 return inline_shapes, expected_count @@ -73,13 +65,12 @@ def inline_shapes_fixture(self): @pytest.fixture def inline_shapes_with_parent_(self, request): - parent_ = loose_mock(request, name='parent_') + parent_ = loose_mock(request, name="parent_") inline_shapes = InlineShapes(None, parent_) return inline_shapes, parent_ -class DescribeInlineShape(object): - +class DescribeInlineShape: def it_knows_what_type_of_shape_it_is(self, shape_type_fixture): inline_shape, inline_shape_type = shape_type_fixture assert inline_shape.type == inline_shape_type @@ -104,7 +95,9 @@ def it_can_change_its_display_dimensions(self, dimensions_set_fixture): @pytest.fixture def dimensions_get_fixture(self): inline_cxml, expected_cx, expected_cy = ( - 'wp:inline/wp:extent{cx=333, cy=666}', 333, 666 + "wp:inline/wp:extent{cx=333, cy=666}", + 333, + 666, ) inline_shape = InlineShape(element(inline_cxml)) return inline_shape, expected_cx, expected_cy @@ -112,43 +105,50 @@ def dimensions_get_fixture(self): @pytest.fixture def dimensions_set_fixture(self): inline_cxml, new_cx, new_cy, expected_cxml = ( - 'wp:inline/(wp:extent{cx=333,cy=666},a:graphic/a:graphicData/' - 'pic:pic/pic:spPr/a:xfrm/a:ext{cx=333,cy=666})', - 444, 888, - 'wp:inline/(wp:extent{cx=444,cy=888},a:graphic/a:graphicData/' - 'pic:pic/pic:spPr/a:xfrm/a:ext{cx=444,cy=888})' + "wp:inline/(wp:extent{cx=333,cy=666},a:graphic/a:graphicData/" + "pic:pic/pic:spPr/a:xfrm/a:ext{cx=333,cy=666})", + 444, + 888, + "wp:inline/(wp:extent{cx=444,cy=888},a:graphic/a:graphicData/" + "pic:pic/pic:spPr/a:xfrm/a:ext{cx=444,cy=888})", ) inline_shape = InlineShape(element(inline_cxml)) expected_xml = xml(expected_cxml) return inline_shape, new_cx, new_cy, expected_xml - @pytest.fixture(params=[ - 'embed pic', 'link pic', 'link+embed pic', 'chart', 'smart art', - 'not implemented' - ]) + @pytest.fixture( + params=[ + "embed pic", + "link pic", + "link+embed pic", + "chart", + "smart art", + "not implemented", + ] + ) def shape_type_fixture(self, request): - if request.param == 'embed pic': + if request.param == "embed pic": inline = self._inline_with_picture(embed=True) shape_type = WD_INLINE_SHAPE.PICTURE - elif request.param == 'link pic': + elif request.param == "link pic": inline = self._inline_with_picture(link=True) shape_type = WD_INLINE_SHAPE.LINKED_PICTURE - elif request.param == 'link+embed pic': + elif request.param == "link+embed pic": inline = self._inline_with_picture(embed=True, link=True) shape_type = WD_INLINE_SHAPE.LINKED_PICTURE - elif request.param == 'chart': - inline = self._inline_with_uri(nsmap['c']) + elif request.param == "chart": + inline = self._inline_with_uri(nsmap["c"]) shape_type = WD_INLINE_SHAPE.CHART - elif request.param == 'smart art': - inline = self._inline_with_uri(nsmap['dgm']) + elif request.param == "smart art": + inline = self._inline_with_uri(nsmap["dgm"]) shape_type = WD_INLINE_SHAPE.SMART_ART - elif request.param == 'not implemented': - inline = self._inline_with_uri('foobar') + elif request.param == "not implemented": + inline = self._inline_with_uri("foobar") shape_type = WD_INLINE_SHAPE.NOT_IMPLEMENTED return InlineShape(inline), shape_type @@ -156,28 +156,39 @@ def shape_type_fixture(self, request): # fixture components --------------------------------------------- def _inline_with_picture(self, embed=False, link=False): - picture_ns = nsmap['pic'] + picture_ns = nsmap["pic"] blip_bldr = a_blip() if embed: - blip_bldr.with_embed('rId1') + blip_bldr.with_embed("rId1") if link: - blip_bldr.with_link('rId2') + blip_bldr.with_link("rId2") inline = ( - an_inline().with_nsdecls('wp', 'r').with_child( - a_graphic().with_nsdecls().with_child( - a_graphicData().with_uri(picture_ns).with_child( - a_pic().with_nsdecls().with_child( - a_blipFill().with_child( - blip_bldr))))) + an_inline() + .with_nsdecls("wp", "r") + .with_child( + a_graphic() + .with_nsdecls() + .with_child( + a_graphicData() + .with_uri(picture_ns) + .with_child( + a_pic() + .with_nsdecls() + .with_child(a_blipFill().with_child(blip_bldr)) + ) + ) + ) ).element return inline def _inline_with_uri(self, uri): inline = ( - an_inline().with_nsdecls('wp').with_child( - a_graphic().with_nsdecls().with_child( - a_graphicData().with_uri(uri))) + an_inline() + .with_nsdecls("wp") + .with_child( + a_graphic().with_nsdecls().with_child(a_graphicData().with_uri(uri)) + ) ).element return inline diff --git a/tests/test_shared.py b/tests/test_shared.py index 102a661ea..3fbe54b07 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -1,26 +1,15 @@ -# encoding: utf-8 - -""" -Test suite for the docx.shared module -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +"""Test suite for the docx.shared module.""" import pytest from docx.opc.part import XmlPart -from docx.shared import ( - ElementProxy, Length, Cm, Emu, Inches, Mm, Pt, RGBColor, Twips -) +from docx.shared import Cm, ElementProxy, Emu, Inches, Length, Mm, Pt, RGBColor, Twips from .unitutil.cxml import element from .unitutil.mock import instance_mock -class DescribeElementProxy(object): - +class DescribeElementProxy: def it_knows_when_its_equal_to_another_proxy_object(self, eq_fixture): proxy, proxy_2, proxy_3, not_a_proxy = eq_fixture @@ -44,17 +33,17 @@ def it_knows_its_part(self, part_fixture): @pytest.fixture def element_fixture(self): - p = element('w:p') + p = element("w:p") proxy = ElementProxy(p) return proxy, p @pytest.fixture def eq_fixture(self): - p, q = element('w:p'), element('w:p') + p, q = element("w:p"), element("w:p") proxy = ElementProxy(p) proxy_2 = ElementProxy(p) proxy_3 = ElementProxy(q) - not_a_proxy = 'Foobar' + not_a_proxy = "Foobar" return proxy, proxy_2, proxy_3, not_a_proxy @pytest.fixture @@ -74,8 +63,7 @@ def part_(self, request): return instance_mock(request, XmlPart) -class DescribeLength(object): - +class DescribeLength: def it_can_construct_from_convenient_units(self, construct_fixture): UnitCls, units_val, emu = construct_fixture length = UnitCls(units_val) @@ -91,50 +79,53 @@ def it_can_self_convert_to_convenient_units(self, units_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (Length, 914400, 914400), - (Inches, 1.1, 1005840), - (Cm, 2.53, 910799), - (Emu, 9144.9, 9144), - (Mm, 13.8, 496800), - (Pt, 24.5, 311150), - (Twips, 360, 228600), - ]) + @pytest.fixture( + params=[ + (Length, 914400, 914400), + (Inches, 1.1, 1005840), + (Cm, 2.53, 910799), + (Emu, 9144.9, 9144), + (Mm, 13.8, 496800), + (Pt, 24.5, 311150), + (Twips, 360, 228600), + ] + ) def construct_fixture(self, request): UnitCls, units_val, emu = request.param return UnitCls, units_val, emu - @pytest.fixture(params=[ - (914400, 'inches', 1.0, float), - (914400, 'cm', 2.54, float), - (914400, 'emu', 914400, int), - (914400, 'mm', 25.4, float), - (914400, 'pt', 72.0, float), - (914400, 'twips', 1440, int), - ]) + @pytest.fixture( + params=[ + (914400, "inches", 1.0, float), + (914400, "cm", 2.54, float), + (914400, "emu", 914400, int), + (914400, "mm", 25.4, float), + (914400, "pt", 72.0, float), + (914400, "twips", 1440, int), + ] + ) def units_fixture(self, request): emu, units_prop_name, expected_length_in_units, type_ = request.param return emu, units_prop_name, expected_length_in_units, type_ -class DescribeRGBColor(object): - +class DescribeRGBColor: def it_is_natively_constructed_using_three_ints_0_to_255(self): RGBColor(0x12, 0x34, 0x56) - with pytest.raises(ValueError): - RGBColor('12', '34', '56') - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"): + RGBColor("12", "34", "56") + with pytest.raises(ValueError, match=r"\(\) takes three integer values 0-255"): RGBColor(-1, 34, 56) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"): RGBColor(12, 256, 56) def it_can_construct_from_a_hex_string_rgb_value(self): - rgb = RGBColor.from_string('123456') + rgb = RGBColor.from_string("123456") assert rgb == RGBColor(0x12, 0x34, 0x56) def it_can_provide_a_hex_string_rgb_value(self): - assert str(RGBColor(0x12, 0x34, 0x56)) == '123456' + assert str(RGBColor(0x12, 0x34, 0x56)) == "123456" def it_has_a_custom_repr(self): rgb_color = RGBColor(0x42, 0xF0, 0xBA) - assert repr(rgb_color) == 'RGBColor(0x42, 0xf0, 0xba)' + assert repr(rgb_color) == "RGBColor(0x42, 0xf0, 0xba)" diff --git a/tests/test_table.py b/tests/test_table.py index 1d2183fd6..0ef273e3f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,20 +1,19 @@ -# encoding: utf-8 - -"""Test suite for the docx.table module""" - -from __future__ import absolute_import, print_function, unicode_literals +"""Test suite for the docx.table module.""" import pytest from docx.enum.style import WD_STYLE_TYPE from docx.enum.table import ( - WD_ALIGN_VERTICAL, WD_ROW_HEIGHT, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION + WD_ALIGN_VERTICAL, + WD_ROW_HEIGHT, + WD_TABLE_ALIGNMENT, + WD_TABLE_DIRECTION, ) -from docx.oxml import parse_xml +from docx.oxml.parser import parse_xml from docx.oxml.table import CT_Tc from docx.parts.document import DocumentPart from docx.shared import Inches -from docx.table import _Cell, _Column, _Columns, _Row, _Rows, Table +from docx.table import Table, _Cell, _Column, _Columns, _Row, _Rows from docx.text.paragraph import Paragraph from .oxml.unitdata.table import a_gridCol, a_tbl, a_tblGrid, a_tc, a_tr @@ -24,8 +23,7 @@ from .unitutil.mock import instance_mock, property_mock -class DescribeTable(object): - +class DescribeTable: def it_can_add_a_row(self, add_row_fixture): table, expected_xml = add_row_fixture row = table.add_row() @@ -103,17 +101,13 @@ def it_can_change_its_direction(self, direction_set_fixture): def it_knows_its_table_style(self, style_get_fixture): table, style_id_, style_ = style_get_fixture style = table.style - table.part.get_style.assert_called_once_with( - style_id_, WD_STYLE_TYPE.TABLE - ) + table.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.TABLE) assert style is style_ def it_can_change_its_table_style(self, style_set_fixture): table, value, expected_xml = style_set_fixture table.style = value - table.part.get_style_id.assert_called_once_with( - value, WD_STYLE_TYPE.TABLE - ) + table.part.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.TABLE) assert table._tbl.xml == expected_xml def it_provides_access_to_its_cells_to_help(self, cells_fixture): @@ -135,7 +129,7 @@ def it_knows_its_column_count_to_help(self, column_count_fixture): @pytest.fixture def add_column_fixture(self): - snippets = snippet_seq('add-row-col') + snippets = snippet_seq("add-row-col") tbl = parse_xml(snippets[0]) table = Table(tbl, None) width = Inches(1.5) @@ -144,76 +138,94 @@ def add_column_fixture(self): @pytest.fixture def add_row_fixture(self): - snippets = snippet_seq('add-row-col') + snippets = snippet_seq("add-row-col") tbl = parse_xml(snippets[0]) table = Table(tbl, None) expected_xml = snippets[1] return table, expected_xml - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', None), - ('w:tbl/w:tblPr/w:jc{w:val=center}', WD_TABLE_ALIGNMENT.CENTER), - ('w:tbl/w:tblPr/w:jc{w:val=right}', WD_TABLE_ALIGNMENT.RIGHT), - ('w:tbl/w:tblPr/w:jc{w:val=left}', WD_TABLE_ALIGNMENT.LEFT), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", None), + ("w:tbl/w:tblPr/w:jc{w:val=center}", WD_TABLE_ALIGNMENT.CENTER), + ("w:tbl/w:tblPr/w:jc{w:val=right}", WD_TABLE_ALIGNMENT.RIGHT), + ("w:tbl/w:tblPr/w:jc{w:val=left}", WD_TABLE_ALIGNMENT.LEFT), + ] + ) def alignment_get_fixture(self, request): tbl_cxml, expected_value = request.param table = Table(element(tbl_cxml), None) return table, expected_value - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', WD_TABLE_ALIGNMENT.LEFT, - 'w:tbl/w:tblPr/w:jc{w:val=left}'), - ('w:tbl/w:tblPr/w:jc{w:val=left}', WD_TABLE_ALIGNMENT.RIGHT, - 'w:tbl/w:tblPr/w:jc{w:val=right}'), - ('w:tbl/w:tblPr/w:jc{w:val=right}', None, - 'w:tbl/w:tblPr'), - ]) + @pytest.fixture( + params=[ + ( + "w:tbl/w:tblPr", + WD_TABLE_ALIGNMENT.LEFT, + "w:tbl/w:tblPr/w:jc{w:val=left}", + ), + ( + "w:tbl/w:tblPr/w:jc{w:val=left}", + WD_TABLE_ALIGNMENT.RIGHT, + "w:tbl/w:tblPr/w:jc{w:val=right}", + ), + ("w:tbl/w:tblPr/w:jc{w:val=right}", None, "w:tbl/w:tblPr"), + ] + ) def alignment_set_fixture(self, request): tbl_cxml, new_value, expected_tbl_cxml = request.param table = Table(element(tbl_cxml), None) expected_xml = xml(expected_tbl_cxml) return table, new_value, expected_xml - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', True), - ('w:tbl/w:tblPr/w:tblLayout', True), - ('w:tbl/w:tblPr/w:tblLayout{w:type=autofit}', True), - ('w:tbl/w:tblPr/w:tblLayout{w:type=fixed}', False), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", True), + ("w:tbl/w:tblPr/w:tblLayout", True), + ("w:tbl/w:tblPr/w:tblLayout{w:type=autofit}", True), + ("w:tbl/w:tblPr/w:tblLayout{w:type=fixed}", False), + ] + ) def autofit_get_fixture(self, request): tbl_cxml, expected_autofit = request.param table = Table(element(tbl_cxml), None) return table, expected_autofit - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', True, - 'w:tbl/w:tblPr/w:tblLayout{w:type=autofit}'), - ('w:tbl/w:tblPr', False, - 'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'), - ('w:tbl/w:tblPr', None, - 'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'), - ('w:tbl/w:tblPr/w:tblLayout{w:type=fixed}', True, - 'w:tbl/w:tblPr/w:tblLayout{w:type=autofit}'), - ('w:tbl/w:tblPr/w:tblLayout{w:type=autofit}', False, - 'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", True, "w:tbl/w:tblPr/w:tblLayout{w:type=autofit}"), + ("w:tbl/w:tblPr", False, "w:tbl/w:tblPr/w:tblLayout{w:type=fixed}"), + ("w:tbl/w:tblPr", None, "w:tbl/w:tblPr/w:tblLayout{w:type=fixed}"), + ( + "w:tbl/w:tblPr/w:tblLayout{w:type=fixed}", + True, + "w:tbl/w:tblPr/w:tblLayout{w:type=autofit}", + ), + ( + "w:tbl/w:tblPr/w:tblLayout{w:type=autofit}", + False, + "w:tbl/w:tblPr/w:tblLayout{w:type=fixed}", + ), + ] + ) def autofit_set_fixture(self, request): tbl_cxml, new_value, expected_tbl_cxml = request.param table = Table(element(tbl_cxml), None) expected_xml = xml(expected_tbl_cxml) return table, new_value, expected_xml - @pytest.fixture(params=[ - (0, 9, 9, ()), - (1, 9, 8, ((0, 1),)), - (2, 9, 8, ((1, 4),)), - (3, 9, 6, ((0, 1, 3, 4),)), - (4, 9, 4, ((0, 1), (3, 6), (4, 5, 7, 8))), - ]) + @pytest.fixture( + params=[ + (0, 9, 9, ()), + (1, 9, 8, ((0, 1),)), + (2, 9, 8, ((1, 4),)), + (3, 9, 6, ((0, 1, 3, 4),)), + (4, 9, 4, ((0, 1), (3, 6), (4, 5, 7, 8))), + ] + ) def cells_fixture(self, request): snippet_idx, cell_count, unique_count, matches = request.param - tbl_xml = snippet_seq('tbl-cells')[snippet_idx] + tbl_xml = snippet_seq("tbl-cells")[snippet_idx] table = Table(parse_xml(tbl_xml), None) return table, cell_count, unique_count, matches @@ -228,32 +240,40 @@ def col_cells_fixture(self, _cells_, _column_count_): @pytest.fixture def column_count_fixture(self): - tbl_cxml = 'w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)' + tbl_cxml = "w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)" expected_value = 3 table = Table(element(tbl_cxml), None) return table, expected_value - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', None), - ('w:tbl/w:tblPr/w:bidiVisual', WD_TABLE_DIRECTION.RTL), - ('w:tbl/w:tblPr/w:bidiVisual{w:val=0}', WD_TABLE_DIRECTION.LTR), - ('w:tbl/w:tblPr/w:bidiVisual{w:val=on}', WD_TABLE_DIRECTION.RTL), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", None), + ("w:tbl/w:tblPr/w:bidiVisual", WD_TABLE_DIRECTION.RTL), + ("w:tbl/w:tblPr/w:bidiVisual{w:val=0}", WD_TABLE_DIRECTION.LTR), + ("w:tbl/w:tblPr/w:bidiVisual{w:val=on}", WD_TABLE_DIRECTION.RTL), + ] + ) def direction_get_fixture(self, request): tbl_cxml, expected_value = request.param table = Table(element(tbl_cxml), None) return table, expected_value - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', WD_TABLE_DIRECTION.RTL, - 'w:tbl/w:tblPr/w:bidiVisual'), - ('w:tbl/w:tblPr/w:bidiVisual', WD_TABLE_DIRECTION.LTR, - 'w:tbl/w:tblPr/w:bidiVisual{w:val=0}'), - ('w:tbl/w:tblPr/w:bidiVisual{w:val=0}', WD_TABLE_DIRECTION.RTL, - 'w:tbl/w:tblPr/w:bidiVisual'), - ('w:tbl/w:tblPr/w:bidiVisual{w:val=1}', None, - 'w:tbl/w:tblPr'), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", WD_TABLE_DIRECTION.RTL, "w:tbl/w:tblPr/w:bidiVisual"), + ( + "w:tbl/w:tblPr/w:bidiVisual", + WD_TABLE_DIRECTION.LTR, + "w:tbl/w:tblPr/w:bidiVisual{w:val=0}", + ), + ( + "w:tbl/w:tblPr/w:bidiVisual{w:val=0}", + WD_TABLE_DIRECTION.RTL, + "w:tbl/w:tblPr/w:bidiVisual", + ), + ("w:tbl/w:tblPr/w:bidiVisual{w:val=1}", None, "w:tbl/w:tblPr"), + ] + ) def direction_set_fixture(self, request): tbl_cxml, new_value, expected_cxml = request.param table = Table(element(tbl_cxml), None) @@ -271,20 +291,24 @@ def row_cells_fixture(self, _cells_, _column_count_): @pytest.fixture def style_get_fixture(self, part_prop_): - style_id = 'Barbaz' - tbl_cxml = 'w:tbl/w:tblPr/w:tblStyle{w:val=%s}' % style_id + style_id = "Barbaz" + tbl_cxml = "w:tbl/w:tblPr/w:tblStyle{w:val=%s}" % style_id table = Table(element(tbl_cxml), None) style_ = part_prop_.return_value.get_style.return_value return table, style_id, style_ - @pytest.fixture(params=[ - ('w:tbl/w:tblPr', 'Tbl A', 'TblA', - 'w:tbl/w:tblPr/w:tblStyle{w:val=TblA}'), - ('w:tbl/w:tblPr/w:tblStyle{w:val=TblA}', 'Tbl B', 'TblB', - 'w:tbl/w:tblPr/w:tblStyle{w:val=TblB}'), - ('w:tbl/w:tblPr/w:tblStyle{w:val=TblB}', None, None, - 'w:tbl/w:tblPr'), - ]) + @pytest.fixture( + params=[ + ("w:tbl/w:tblPr", "Tbl A", "TblA", "w:tbl/w:tblPr/w:tblStyle{w:val=TblA}"), + ( + "w:tbl/w:tblPr/w:tblStyle{w:val=TblA}", + "Tbl B", + "TblB", + "w:tbl/w:tblPr/w:tblStyle{w:val=TblB}", + ), + ("w:tbl/w:tblPr/w:tblStyle{w:val=TblB}", None, None, "w:tbl/w:tblPr"), + ] + ) def style_set_fixture(self, request, part_prop_): tbl_cxml, value, style_id, expected_cxml = request.param table = Table(element(tbl_cxml), None) @@ -301,11 +325,11 @@ def table_fixture(self): @pytest.fixture def _cells_(self, request): - return property_mock(request, Table, '_cells') + return property_mock(request, Table, "_cells") @pytest.fixture def _column_count_(self, request): - return property_mock(request, Table, '_column_count') + return property_mock(request, Table, "_column_count") @pytest.fixture def document_part_(self, request): @@ -313,9 +337,7 @@ def document_part_(self, request): @pytest.fixture def part_prop_(self, request, document_part_): - return property_mock( - request, Table, 'part', return_value=document_part_ - ) + return property_mock(request, Table, "part", return_value=document_part_) @pytest.fixture def table(self): @@ -324,15 +346,13 @@ def table(self): return table -class Describe_Cell(object): - +class Describe_Cell: def it_knows_what_text_it_contains(self, text_get_fixture): cell, expected_text = text_get_fixture text = cell.text assert text == expected_text - def it_can_replace_its_content_with_a_string_of_text( - self, text_set_fixture): + def it_can_replace_its_content_with_a_string_of_text(self, text_set_fixture): cell, text, expected_xml = text_set_fixture cell.text = text assert cell._tc.xml == expected_xml @@ -357,8 +377,7 @@ def it_can_change_its_width(self, width_set_fixture): assert cell.width == value assert cell._tc.xml == expected_xml - def it_provides_access_to_the_paragraphs_it_contains( - self, paragraphs_fixture): + def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture): cell = paragraphs_fixture paragraphs = cell.paragraphs assert len(paragraphs) == 2 @@ -403,11 +422,13 @@ def it_can_merge_itself_with_other_cells(self, merge_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:tc', 'w:tc/w:p'), - ('w:tc/w:p', 'w:tc/(w:p, w:p)'), - ('w:tc/w:tbl', 'w:tc/(w:tbl, w:p)'), - ]) + @pytest.fixture( + params=[ + ("w:tc", "w:tc/w:p"), + ("w:tc/w:p", "w:tc/(w:p, w:p)"), + ("w:tc/w:tbl", "w:tc/(w:tbl, w:p)"), + ] + ) def add_paragraph_fixture(self, request): tc_cxml, after_tc_cxml = request.param cell = _Cell(element(tc_cxml), None) @@ -416,35 +437,41 @@ def add_paragraph_fixture(self, request): @pytest.fixture def add_table_fixture(self, request): - cell = _Cell(element('w:tc/w:p'), None) - expected_xml = snippet_seq('new-tbl')[1] + cell = _Cell(element("w:tc/w:p"), None) + expected_xml = snippet_seq("new-tbl")[1] return cell, expected_xml - @pytest.fixture(params=[ - ('w:tc', None), - ('w:tc/w:tcPr', None), - ('w:tc/w:tcPr/w:vAlign{w:val=bottom}', WD_ALIGN_VERTICAL.BOTTOM), - ('w:tc/w:tcPr/w:vAlign{w:val=top}', WD_ALIGN_VERTICAL.TOP), - ]) + @pytest.fixture( + params=[ + ("w:tc", None), + ("w:tc/w:tcPr", None), + ("w:tc/w:tcPr/w:vAlign{w:val=bottom}", WD_ALIGN_VERTICAL.BOTTOM), + ("w:tc/w:tcPr/w:vAlign{w:val=top}", WD_ALIGN_VERTICAL.TOP), + ] + ) def alignment_get_fixture(self, request): tc_cxml, expected_value = request.param cell = _Cell(element(tc_cxml), None) return cell, expected_value - @pytest.fixture(params=[ - ('w:tc', WD_ALIGN_VERTICAL.TOP, - 'w:tc/w:tcPr/w:vAlign{w:val=top}'), - ('w:tc/w:tcPr', WD_ALIGN_VERTICAL.CENTER, - 'w:tc/w:tcPr/w:vAlign{w:val=center}'), - ('w:tc/w:tcPr/w:vAlign{w:val=center}', WD_ALIGN_VERTICAL.BOTTOM, - 'w:tc/w:tcPr/w:vAlign{w:val=bottom}'), - ('w:tc/w:tcPr/w:vAlign{w:val=center}', None, - 'w:tc/w:tcPr'), - ('w:tc', None, - 'w:tc/w:tcPr'), - ('w:tc/w:tcPr', None, - 'w:tc/w:tcPr'), - ]) + @pytest.fixture( + params=[ + ("w:tc", WD_ALIGN_VERTICAL.TOP, "w:tc/w:tcPr/w:vAlign{w:val=top}"), + ( + "w:tc/w:tcPr", + WD_ALIGN_VERTICAL.CENTER, + "w:tc/w:tcPr/w:vAlign{w:val=center}", + ), + ( + "w:tc/w:tcPr/w:vAlign{w:val=center}", + WD_ALIGN_VERTICAL.BOTTOM, + "w:tc/w:tcPr/w:vAlign{w:val=bottom}", + ), + ("w:tc/w:tcPr/w:vAlign{w:val=center}", None, "w:tc/w:tcPr"), + ("w:tc", None, "w:tc/w:tcPr"), + ("w:tc/w:tcPr", None, "w:tc/w:tcPr"), + ] + ) def alignment_set_fixture(self, request): cxml, new_value, expected_cxml = request.param cell = _Cell(element(cxml), None) @@ -459,64 +486,80 @@ def merge_fixture(self, tc_, tc_2_, parent_, merged_tc_): @pytest.fixture def paragraphs_fixture(self): - return _Cell(element('w:tc/(w:p, w:p)'), None) - - @pytest.fixture(params=[ - ('w:tc', 0), - ('w:tc/w:tbl', 1), - ('w:tc/(w:tbl,w:tbl)', 2), - ('w:tc/(w:p,w:tbl)', 1), - ('w:tc/(w:tbl,w:tbl,w:p)', 2), - ]) + return _Cell(element("w:tc/(w:p, w:p)"), None) + + @pytest.fixture( + params=[ + ("w:tc", 0), + ("w:tc/w:tbl", 1), + ("w:tc/(w:tbl,w:tbl)", 2), + ("w:tc/(w:p,w:tbl)", 1), + ("w:tc/(w:tbl,w:tbl,w:p)", 2), + ] + ) def tables_fixture(self, request): cell_cxml, expected_count = request.param cell = _Cell(element(cell_cxml), None) return cell, expected_count - @pytest.fixture(params=[ - ('w:tc', ''), - ('w:tc/w:p/w:r/w:t"foobar"', 'foobar'), - ('w:tc/(w:p/w:r/w:t"foo",w:p/w:r/w:t"bar")', 'foo\nbar'), - ('w:tc/(w:tcPr,w:p/w:r/w:t"foobar")', 'foobar'), - ('w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)', - 'fo\tob\nar\n'), - ]) + @pytest.fixture( + params=[ + ("w:tc", ""), + ('w:tc/w:p/w:r/w:t"foobar"', "foobar"), + ('w:tc/(w:p/w:r/w:t"foo",w:p/w:r/w:t"bar")', "foo\nbar"), + ('w:tc/(w:tcPr,w:p/w:r/w:t"foobar")', "foobar"), + ('w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)', "fo\tob\nar\n"), + ] + ) def text_get_fixture(self, request): tc_cxml, expected_text = request.param cell = _Cell(element(tc_cxml), None) return cell, expected_text - @pytest.fixture(params=[ - ('w:tc/w:p', 'foobar', - 'w:tc/w:p/w:r/w:t"foobar"'), - ('w:tc/w:p', 'fo\tob\rar\n', - 'w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)'), - ('w:tc/(w:tcPr, w:p, w:tbl, w:p)', 'foobar', - 'w:tc/(w:tcPr, w:p/w:r/w:t"foobar")'), - ]) + @pytest.fixture( + params=[ + ("w:tc/w:p", "foobar", 'w:tc/w:p/w:r/w:t"foobar"'), + ( + "w:tc/w:p", + "fo\tob\rar\n", + 'w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)', + ), + ( + "w:tc/(w:tcPr, w:p, w:tbl, w:p)", + "foobar", + 'w:tc/(w:tcPr, w:p/w:r/w:t"foobar")', + ), + ] + ) def text_set_fixture(self, request): tc_cxml, new_text, expected_cxml = request.param cell = _Cell(element(tc_cxml), None) expected_xml = xml(expected_cxml) return cell, new_text, expected_xml - @pytest.fixture(params=[ - ('w:tc', None), - ('w:tc/w:tcPr', None), - ('w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}', None), - ('w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}', 914400), - ]) + @pytest.fixture( + params=[ + ("w:tc", None), + ("w:tc/w:tcPr", None), + ("w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}", None), + ("w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}", 914400), + ] + ) def width_get_fixture(self, request): tc_cxml, expected_width = request.param cell = _Cell(element(tc_cxml), None) return cell, expected_width - @pytest.fixture(params=[ - ('w:tc', Inches(1), - 'w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}'), - ('w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}', Inches(2), - 'w:tc/w:tcPr/w:tcW{w:w=2880,w:type=dxa}'), - ]) + @pytest.fixture( + params=[ + ("w:tc", Inches(1), "w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}"), + ( + "w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}", + Inches(2), + "w:tc/w:tcPr/w:tcW{w:w=2880,w:type=dxa}", + ), + ] + ) def width_set_fixture(self, request): tc_cxml, new_value, expected_cxml = request.param cell = _Cell(element(tc_cxml), None) @@ -542,8 +585,7 @@ def tc_2_(self, request): return instance_mock(request, CT_Tc) -class Describe_Column(object): - +class Describe_Column: def it_provides_access_to_its_cells(self, cells_fixture): column, column_idx, expected_cells = cells_fixture cells = column.cells @@ -580,7 +622,7 @@ def cells_fixture(self, _index_, table_prop_, table_): @pytest.fixture def index_fixture(self): - tbl = element('w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)') + tbl = element("w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)") gridCol, expected_idx = tbl.tblGrid[1], 1 column = _Column(gridCol, None) return column, expected_idx @@ -591,25 +633,29 @@ def table_fixture(self, parent_, table_): parent_.table = table_ return column, table_ - @pytest.fixture(params=[ - ('w:gridCol{w:w=4242}', 2693670), - ('w:gridCol{w:w=1440}', 914400), - ('w:gridCol{w:w=2.54cm}', 914400), - ('w:gridCol{w:w=54mm}', 1944000), - ('w:gridCol{w:w=12.5pt}', 158750), - ('w:gridCol', None), - ]) + @pytest.fixture( + params=[ + ("w:gridCol{w:w=4242}", 2693670), + ("w:gridCol{w:w=1440}", 914400), + ("w:gridCol{w:w=2.54cm}", 914400), + ("w:gridCol{w:w=54mm}", 1944000), + ("w:gridCol{w:w=12.5pt}", 158750), + ("w:gridCol", None), + ] + ) def width_get_fixture(self, request): gridCol_cxml, expected_width = request.param column = _Column(element(gridCol_cxml), None) return column, expected_width - @pytest.fixture(params=[ - ('w:gridCol', 914400, 'w:gridCol{w:w=1440}'), - ('w:gridCol{w:w=4242}', 457200, 'w:gridCol{w:w=720}'), - ('w:gridCol{w:w=4242}', None, 'w:gridCol'), - ('w:gridCol', None, 'w:gridCol'), - ]) + @pytest.fixture( + params=[ + ("w:gridCol", 914400, "w:gridCol{w:w=1440}"), + ("w:gridCol{w:w=4242}", 457200, "w:gridCol{w:w=720}"), + ("w:gridCol{w:w=4242}", None, "w:gridCol"), + ("w:gridCol", None, "w:gridCol"), + ] + ) def width_set_fixture(self, request): gridCol_cxml, new_value, expected_cxml = request.param column = _Column(element(gridCol_cxml), None) @@ -620,7 +666,7 @@ def width_set_fixture(self, request): @pytest.fixture def _index_(self, request): - return property_mock(request, _Column, '_index') + return property_mock(request, _Column, "_index") @pytest.fixture def parent_(self, request): @@ -632,11 +678,10 @@ def table_(self, request): @pytest.fixture def table_prop_(self, request, table_): - return property_mock(request, _Column, 'table', return_value=table_) - + return property_mock(request, _Column, "table", return_value=table_) -class Describe_Columns(object): +class Describe_Columns: def it_knows_how_many_columns_it_contains(self, columns_fixture): columns, column_count = columns_fixture assert len(columns) == column_count @@ -690,8 +735,7 @@ def table_(self, request): return instance_mock(request, Table) -class Describe_Row(object): - +class Describe_Row: def it_knows_its_height(self, height_get_fixture): row, expected_height = height_get_fixture assert row.height == expected_height @@ -734,74 +778,90 @@ def cells_fixture(self, _index_, table_prop_, table_): table_.row_cells.return_value = list(expected_cells) return row, row_idx, expected_cells - @pytest.fixture(params=[ - ('w:tr', None), - ('w:tr/w:trPr', None), - ('w:tr/w:trPr/w:trHeight', None), - ('w:tr/w:trPr/w:trHeight{w:val=0}', 0), - ('w:tr/w:trPr/w:trHeight{w:val=1440}', 914400), - ]) + @pytest.fixture( + params=[ + ("w:tr", None), + ("w:tr/w:trPr", None), + ("w:tr/w:trPr/w:trHeight", None), + ("w:tr/w:trPr/w:trHeight{w:val=0}", 0), + ("w:tr/w:trPr/w:trHeight{w:val=1440}", 914400), + ] + ) def height_get_fixture(self, request): tr_cxml, expected_height = request.param row = _Row(element(tr_cxml), None) return row, expected_height - @pytest.fixture(params=[ - ('w:tr', Inches(1), - 'w:tr/w:trPr/w:trHeight{w:val=1440}'), - ('w:tr/w:trPr', Inches(1), - 'w:tr/w:trPr/w:trHeight{w:val=1440}'), - ('w:tr/w:trPr/w:trHeight', Inches(1), - 'w:tr/w:trPr/w:trHeight{w:val=1440}'), - ('w:tr/w:trPr/w:trHeight{w:val=1440}', Inches(2), - 'w:tr/w:trPr/w:trHeight{w:val=2880}'), - ('w:tr/w:trPr/w:trHeight{w:val=2880}', None, - 'w:tr/w:trPr/w:trHeight'), - ('w:tr', None, 'w:tr/w:trPr'), - ('w:tr/w:trPr', None, 'w:tr/w:trPr'), - ('w:tr/w:trPr/w:trHeight', None, 'w:tr/w:trPr/w:trHeight'), - ]) + @pytest.fixture( + params=[ + ("w:tr", Inches(1), "w:tr/w:trPr/w:trHeight{w:val=1440}"), + ("w:tr/w:trPr", Inches(1), "w:tr/w:trPr/w:trHeight{w:val=1440}"), + ("w:tr/w:trPr/w:trHeight", Inches(1), "w:tr/w:trPr/w:trHeight{w:val=1440}"), + ( + "w:tr/w:trPr/w:trHeight{w:val=1440}", + Inches(2), + "w:tr/w:trPr/w:trHeight{w:val=2880}", + ), + ("w:tr/w:trPr/w:trHeight{w:val=2880}", None, "w:tr/w:trPr/w:trHeight"), + ("w:tr", None, "w:tr/w:trPr"), + ("w:tr/w:trPr", None, "w:tr/w:trPr"), + ("w:tr/w:trPr/w:trHeight", None, "w:tr/w:trPr/w:trHeight"), + ] + ) def height_set_fixture(self, request): tr_cxml, new_value, expected_cxml = request.param row = _Row(element(tr_cxml), None) expected_xml = xml(expected_cxml) return row, new_value, expected_xml - @pytest.fixture(params=[ - ('w:tr', None), - ('w:tr/w:trPr', None), - ('w:tr/w:trPr/w:trHeight{w:val=0, w:hRule=auto}', - WD_ROW_HEIGHT.AUTO), - ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=atLeast}', - WD_ROW_HEIGHT.AT_LEAST), - ('w:tr/w:trPr/w:trHeight{w:val=2880, w:hRule=exact}', - WD_ROW_HEIGHT.EXACTLY), - ]) + @pytest.fixture( + params=[ + ("w:tr", None), + ("w:tr/w:trPr", None), + ("w:tr/w:trPr/w:trHeight{w:val=0, w:hRule=auto}", WD_ROW_HEIGHT.AUTO), + ( + "w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=atLeast}", + WD_ROW_HEIGHT.AT_LEAST, + ), + ( + "w:tr/w:trPr/w:trHeight{w:val=2880, w:hRule=exact}", + WD_ROW_HEIGHT.EXACTLY, + ), + ] + ) def height_rule_get_fixture(self, request): tr_cxml, expected_rule = request.param row = _Row(element(tr_cxml), None) return row, expected_rule - @pytest.fixture(params=[ - ('w:tr', - WD_ROW_HEIGHT.AUTO, - 'w:tr/w:trPr/w:trHeight{w:hRule=auto}'), - ('w:tr/w:trPr', - WD_ROW_HEIGHT.AT_LEAST, - 'w:tr/w:trPr/w:trHeight{w:hRule=atLeast}'), - ('w:tr/w:trPr/w:trHeight', - WD_ROW_HEIGHT.EXACTLY, - 'w:tr/w:trPr/w:trHeight{w:hRule=exact}'), - ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=exact}', - WD_ROW_HEIGHT.AUTO, - 'w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}'), - ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}', - None, - 'w:tr/w:trPr/w:trHeight{w:val=1440}'), - ('w:tr', None, 'w:tr/w:trPr'), - ('w:tr/w:trPr', None, 'w:tr/w:trPr'), - ('w:tr/w:trPr/w:trHeight', None, 'w:tr/w:trPr/w:trHeight'), - ]) + @pytest.fixture( + params=[ + ("w:tr", WD_ROW_HEIGHT.AUTO, "w:tr/w:trPr/w:trHeight{w:hRule=auto}"), + ( + "w:tr/w:trPr", + WD_ROW_HEIGHT.AT_LEAST, + "w:tr/w:trPr/w:trHeight{w:hRule=atLeast}", + ), + ( + "w:tr/w:trPr/w:trHeight", + WD_ROW_HEIGHT.EXACTLY, + "w:tr/w:trPr/w:trHeight{w:hRule=exact}", + ), + ( + "w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=exact}", + WD_ROW_HEIGHT.AUTO, + "w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}", + ), + ( + "w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}", + None, + "w:tr/w:trPr/w:trHeight{w:val=1440}", + ), + ("w:tr", None, "w:tr/w:trPr"), + ("w:tr/w:trPr", None, "w:tr/w:trPr"), + ("w:tr/w:trPr/w:trHeight", None, "w:tr/w:trPr/w:trHeight"), + ] + ) def height_rule_set_fixture(self, request): tr_cxml, new_rule, expected_cxml = request.param row = _Row(element(tr_cxml), None) @@ -810,7 +870,7 @@ def height_rule_set_fixture(self, request): @pytest.fixture def idx_fixture(self): - tbl = element('w:tbl/(w:tr,w:tr,w:tr)') + tbl = element("w:tbl/(w:tr,w:tr,w:tr)") tr, expected_idx = tbl[1], 1 row = _Row(tr, None) return row, expected_idx @@ -825,7 +885,7 @@ def table_fixture(self, parent_, table_): @pytest.fixture def _index_(self, request): - return property_mock(request, _Row, '_index') + return property_mock(request, _Row, "_index") @pytest.fixture def parent_(self, request): @@ -837,11 +897,10 @@ def table_(self, request): @pytest.fixture def table_prop_(self, request, table_): - return property_mock(request, _Row, 'table', return_value=table_) - + return property_mock(request, _Row, "table", return_value=table_) -class Describe_Rows(object): +class Describe_Rows: def it_knows_how_many_rows_it_contains(self, rows_fixture): rows, row_count = rows_fixture assert len(rows) == row_count @@ -871,11 +930,12 @@ def it_provides_sliced_access_to_rows(self, slice_fixture): def it_raises_on_indexed_access_out_of_range(self, rows_fixture): rows, row_count = rows_fixture - with pytest.raises(IndexError): - too_low = -1 - row_count + too_low = -1 - row_count + too_high = row_count + + with pytest.raises(IndexError, match="list index out of range"): rows[too_low] - with pytest.raises(IndexError): - too_high = row_count + with pytest.raises(IndexError, match="list index out of range"): rows[too_high] def it_provides_access_to_the_table_it_belongs_to(self, table_fixture): @@ -891,10 +951,12 @@ def rows_fixture(self): rows = _Rows(tbl, None) return rows, row_count - @pytest.fixture(params=[ - (3, 1, 3, 2), - (3, 0, -1, 2), - ]) + @pytest.fixture( + params=[ + (3, 1, 3, 2), + (3, 0, -1, 2), + ] + ) def slice_fixture(self, request): row_count, start, end, expected_count = request.param tbl = _tbl_bldr(rows=row_count, cols=2).element @@ -916,6 +978,7 @@ def table_(self, request): # fixtures ----------------------------------------------------------- + def _tbl_bldr(rows, cols): tblGrid_bldr = a_tblGrid() for i in range(cols): diff --git a/tests/text/test_font.py b/tests/text/test_font.py index 52564b12b..6a9da0223 100644 --- a/tests/text/test_font.py +++ b/tests/text/test_font.py @@ -1,392 +1,437 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -""" -Test suite for the docx.text.run module -""" +"""Test suite for the docx.text.run module.""" -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import annotations + +from typing import cast + +import pytest +from _pytest.fixtures import FixtureRequest from docx.dml.color import ColorFormat from docx.enum.text import WD_COLOR, WD_UNDERLINE -from docx.shared import Pt +from docx.oxml.text.run import CT_R +from docx.shared import Length, Pt from docx.text.font import Font -import pytest - from ..unitutil.cxml import element, xml -from ..unitutil.mock import class_mock, instance_mock +from ..unitutil.mock import Mock, class_mock, instance_mock -class DescribeFont(object): +class DescribeFont: + """Unit-test suite for `docx.text.font.Font`.""" + + def it_provides_access_to_its_color_object(self, ColorFormat_: Mock, color_: Mock): + r = cast(CT_R, element("w:r")) + font = Font(r) - def it_provides_access_to_its_color_object(self, color_fixture): - font, color_, ColorFormat_ = color_fixture color = font.color + ColorFormat_.assert_called_once_with(font.element) assert color is color_ - def it_knows_its_typeface_name(self, name_get_fixture): - font, expected_value = name_get_fixture + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:rFonts", None), + ("w:r/w:rPr/w:rFonts{w:ascii=Arial}", "Arial"), + ], + ) + def it_knows_its_typeface_name(self, r_cxml: str, expected_value: str | None): + r = cast(CT_R, element(r_cxml)) + font = Font(r) assert font.name == expected_value - def it_can_change_its_typeface_name(self, name_set_fixture): - font, value, expected_xml = name_set_fixture + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", "Foo", "w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}"), + ("w:r/w:rPr", "Foo", "w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}"), + ( + "w:r/w:rPr/w:rFonts{w:hAnsi=Foo}", + "Bar", + "w:r/w:rPr/w:rFonts{w:ascii=Bar,w:hAnsi=Bar}", + ), + ( + "w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}", + "Bar", + "w:r/w:rPr/w:rFonts{w:ascii=Bar,w:hAnsi=Bar}", + ), + ], + ) + def it_can_change_its_typeface_name( + self, r_cxml: str, value: str, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_r_cxml) + font.name = value + assert font._element.xml == expected_xml - def it_knows_its_size(self, size_get_fixture): - font, expected_value = size_get_fixture + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:sz{w:val=28}", Pt(14)), + ], + ) + def it_knows_its_size(self, r_cxml: str, expected_value: Length | None): + r = cast(CT_R, element(r_cxml)) + font = Font(r) assert font.size == expected_value - def it_can_change_its_size(self, size_set_fixture): - font, value, expected_xml = size_set_fixture + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", Pt(12), "w:r/w:rPr/w:sz{w:val=24}"), + ("w:r/w:rPr", Pt(12), "w:r/w:rPr/w:sz{w:val=24}"), + ("w:r/w:rPr/w:sz{w:val=24}", Pt(18), "w:r/w:rPr/w:sz{w:val=36}"), + ("w:r/w:rPr/w:sz{w:val=36}", None, "w:r/w:rPr"), + ], + ) + def it_can_change_its_size( + self, r_cxml: str, value: Length | None, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_r_cxml) + font.size = value + assert font._element.xml == expected_xml - def it_knows_its_bool_prop_states(self, bool_prop_get_fixture): - font, prop_name, expected_state = bool_prop_get_fixture - assert getattr(font, prop_name) == expected_state + @pytest.mark.parametrize( + ("r_cxml", "bool_prop_name", "expected_value"), + [ + ("w:r/w:rPr", "all_caps", None), + ("w:r/w:rPr/w:caps", "all_caps", True), + ("w:r/w:rPr/w:caps{w:val=on}", "all_caps", True), + ("w:r/w:rPr/w:caps{w:val=off}", "all_caps", False), + ("w:r/w:rPr/w:b{w:val=1}", "bold", True), + ("w:r/w:rPr/w:i{w:val=0}", "italic", False), + ("w:r/w:rPr/w:cs{w:val=true}", "complex_script", True), + ("w:r/w:rPr/w:bCs{w:val=false}", "cs_bold", False), + ("w:r/w:rPr/w:iCs{w:val=on}", "cs_italic", True), + ("w:r/w:rPr/w:dstrike{w:val=off}", "double_strike", False), + ("w:r/w:rPr/w:emboss{w:val=1}", "emboss", True), + ("w:r/w:rPr/w:vanish{w:val=0}", "hidden", False), + ("w:r/w:rPr/w:i{w:val=true}", "italic", True), + ("w:r/w:rPr/w:imprint{w:val=false}", "imprint", False), + ("w:r/w:rPr/w:oMath{w:val=on}", "math", True), + ("w:r/w:rPr/w:noProof{w:val=off}", "no_proof", False), + ("w:r/w:rPr/w:outline{w:val=1}", "outline", True), + ("w:r/w:rPr/w:rtl{w:val=0}", "rtl", False), + ("w:r/w:rPr/w:shadow{w:val=true}", "shadow", True), + ("w:r/w:rPr/w:smallCaps{w:val=false}", "small_caps", False), + ("w:r/w:rPr/w:snapToGrid{w:val=on}", "snap_to_grid", True), + ("w:r/w:rPr/w:specVanish{w:val=off}", "spec_vanish", False), + ("w:r/w:rPr/w:strike{w:val=1}", "strike", True), + ("w:r/w:rPr/w:webHidden{w:val=0}", "web_hidden", False), + ], + ) + def it_knows_its_bool_prop_states( + self, r_cxml: str, bool_prop_name: str, expected_value: bool | None + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + assert getattr(font, bool_prop_name) == expected_value + + @pytest.mark.parametrize( + ("r_cxml", "prop_name", "value", "expected_cxml"), + [ + # nothing to True, False, and None --------------------------- + ("w:r", "all_caps", True, "w:r/w:rPr/w:caps"), + ("w:r", "bold", False, "w:r/w:rPr/w:b{w:val=0}"), + ("w:r", "italic", None, "w:r/w:rPr"), + # default to True, False, and None --------------------------- + ("w:r/w:rPr/w:cs", "complex_script", True, "w:r/w:rPr/w:cs"), + ("w:r/w:rPr/w:bCs", "cs_bold", False, "w:r/w:rPr/w:bCs{w:val=0}"), + ("w:r/w:rPr/w:iCs", "cs_italic", None, "w:r/w:rPr"), + # True to True, False, and None ------------------------------ + ( + "w:r/w:rPr/w:dstrike{w:val=1}", + "double_strike", + True, + "w:r/w:rPr/w:dstrike", + ), + ( + "w:r/w:rPr/w:emboss{w:val=on}", + "emboss", + False, + "w:r/w:rPr/w:emboss{w:val=0}", + ), + ("w:r/w:rPr/w:vanish{w:val=1}", "hidden", None, "w:r/w:rPr"), + # False to True, False, and None ----------------------------- + ("w:r/w:rPr/w:i{w:val=false}", "italic", True, "w:r/w:rPr/w:i"), + ( + "w:r/w:rPr/w:imprint{w:val=0}", + "imprint", + False, + "w:r/w:rPr/w:imprint{w:val=0}", + ), + ("w:r/w:rPr/w:oMath{w:val=off}", "math", None, "w:r/w:rPr"), + # random mix ------------------------------------------------- + ( + "w:r/w:rPr/w:noProof{w:val=1}", + "no_proof", + False, + "w:r/w:rPr/w:noProof{w:val=0}", + ), + ("w:r/w:rPr", "outline", True, "w:r/w:rPr/w:outline"), + ("w:r/w:rPr/w:rtl{w:val=true}", "rtl", False, "w:r/w:rPr/w:rtl{w:val=0}"), + ("w:r/w:rPr/w:shadow{w:val=on}", "shadow", True, "w:r/w:rPr/w:shadow"), + ( + "w:r/w:rPr/w:smallCaps", + "small_caps", + False, + "w:r/w:rPr/w:smallCaps{w:val=0}", + ), + ("w:r/w:rPr/w:snapToGrid", "snap_to_grid", True, "w:r/w:rPr/w:snapToGrid"), + ("w:r/w:rPr/w:specVanish", "spec_vanish", None, "w:r/w:rPr"), + ("w:r/w:rPr/w:strike{w:val=foo}", "strike", True, "w:r/w:rPr/w:strike"), + ( + "w:r/w:rPr/w:webHidden", + "web_hidden", + False, + "w:r/w:rPr/w:webHidden{w:val=0}", + ), + ], + ) + def it_can_change_its_bool_prop_settings( + self, r_cxml: str, prop_name: str, value: bool | None, expected_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_cxml) - def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture): - font, prop_name, value, expected_xml = bool_prop_set_fixture setattr(font, prop_name, value) + assert font._element.xml == expected_xml - def it_knows_whether_it_is_subscript(self, subscript_get_fixture): - font, expected_value = subscript_get_fixture + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:vertAlign{w:val=baseline}", False), + ("w:r/w:rPr/w:vertAlign{w:val=subscript}", True), + ("w:r/w:rPr/w:vertAlign{w:val=superscript}", False), + ], + ) + def it_knows_whether_it_is_subscript( + self, r_cxml: str, expected_value: bool | None + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) assert font.subscript == expected_value - def it_can_change_whether_it_is_subscript(self, subscript_set_fixture): - font, value, expected_xml = subscript_set_fixture + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", True, "w:r/w:rPr/w:vertAlign{w:val=subscript}"), + ("w:r", False, "w:r/w:rPr"), + ("w:r", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + True, + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + ), + ("w:r/w:rPr/w:vertAlign{w:val=subscript}", False, "w:r/w:rPr"), + ("w:r/w:rPr/w:vertAlign{w:val=subscript}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + True, + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + ), + ( + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + False, + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + ), + ("w:r/w:rPr/w:vertAlign{w:val=superscript}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=baseline}", + True, + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + ), + ], + ) + def it_can_change_whether_it_is_subscript( + self, r_cxml: str, value: bool | None, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_r_cxml) + font.subscript = value + assert font._element.xml == expected_xml - def it_knows_whether_it_is_superscript(self, superscript_get_fixture): - font, expected_value = superscript_get_fixture + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:vertAlign{w:val=baseline}", False), + ("w:r/w:rPr/w:vertAlign{w:val=subscript}", False), + ("w:r/w:rPr/w:vertAlign{w:val=superscript}", True), + ], + ) + def it_knows_whether_it_is_superscript( + self, r_cxml: str, expected_value: bool | None + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) assert font.superscript == expected_value - def it_can_change_whether_it_is_superscript(self, superscript_set_fixture): - font, value, expected_xml = superscript_set_fixture + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", True, "w:r/w:rPr/w:vertAlign{w:val=superscript}"), + ("w:r", False, "w:r/w:rPr"), + ("w:r", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + True, + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + ), + ("w:r/w:rPr/w:vertAlign{w:val=superscript}", False, "w:r/w:rPr"), + ("w:r/w:rPr/w:vertAlign{w:val=superscript}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + True, + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + ), + ( + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + False, + "w:r/w:rPr/w:vertAlign{w:val=subscript}", + ), + ("w:r/w:rPr/w:vertAlign{w:val=subscript}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:vertAlign{w:val=baseline}", + True, + "w:r/w:rPr/w:vertAlign{w:val=superscript}", + ), + ], + ) + def it_can_change_whether_it_is_superscript( + self, r_cxml: str, value: bool | None, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_r_cxml) + font.superscript = value + assert font._element.xml == expected_xml - def it_knows_its_underline_type(self, underline_get_fixture): - font, expected_value = underline_get_fixture + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr/w:u", None), + ("w:r/w:rPr/w:u{w:val=single}", True), + ("w:r/w:rPr/w:u{w:val=none}", False), + ("w:r/w:rPr/w:u{w:val=double}", WD_UNDERLINE.DOUBLE), + ("w:r/w:rPr/w:u{w:val=wave}", WD_UNDERLINE.WAVY), + ], + ) + def it_knows_its_underline_type( + self, r_cxml: str, expected_value: WD_UNDERLINE | bool | None + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) assert font.underline is expected_value - def it_can_change_its_underline_type(self, underline_set_fixture): - font, underline, expected_xml = underline_set_fixture - font.underline = underline - assert font._element.xml == expected_xml + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", True, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r", False, "w:r/w:rPr/w:u{w:val=none}"), + ("w:r", None, "w:r/w:rPr"), + ("w:r", WD_UNDERLINE.SINGLE, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r", WD_UNDERLINE.THICK, "w:r/w:rPr/w:u{w:val=thick}"), + ("w:r/w:rPr/w:u{w:val=single}", True, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r/w:rPr/w:u{w:val=single}", False, "w:r/w:rPr/w:u{w:val=none}"), + ("w:r/w:rPr/w:u{w:val=single}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:u{w:val=single}", + WD_UNDERLINE.SINGLE, + "w:r/w:rPr/w:u{w:val=single}", + ), + ( + "w:r/w:rPr/w:u{w:val=single}", + WD_UNDERLINE.DOTTED, + "w:r/w:rPr/w:u{w:val=dotted}", + ), + ], + ) + def it_can_change_its_underline_type( + self, r_cxml: str, value: bool | None, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + expected_xml = xml(expected_r_cxml) - def it_knows_its_highlight_color(self, highlight_get_fixture): - font, expected_value = highlight_get_fixture - assert font.highlight_color is expected_value + font.underline = value - def it_can_change_its_highlight_color(self, highlight_set_fixture): - font, highlight_color, expected_xml = highlight_set_fixture - font.highlight_color = highlight_color assert font._element.xml == expected_xml - # fixtures ------------------------------------------------------- - - @pytest.fixture(params=[ - ('w:r/w:rPr', 'all_caps', None), - ('w:r/w:rPr/w:caps', 'all_caps', True), - ('w:r/w:rPr/w:caps{w:val=on}', 'all_caps', True), - ('w:r/w:rPr/w:caps{w:val=off}', 'all_caps', False), - ('w:r/w:rPr/w:b{w:val=1}', 'bold', True), - ('w:r/w:rPr/w:i{w:val=0}', 'italic', False), - ('w:r/w:rPr/w:cs{w:val=true}', 'complex_script', True), - ('w:r/w:rPr/w:bCs{w:val=false}', 'cs_bold', False), - ('w:r/w:rPr/w:iCs{w:val=on}', 'cs_italic', True), - ('w:r/w:rPr/w:dstrike{w:val=off}', 'double_strike', False), - ('w:r/w:rPr/w:emboss{w:val=1}', 'emboss', True), - ('w:r/w:rPr/w:vanish{w:val=0}', 'hidden', False), - ('w:r/w:rPr/w:i{w:val=true}', 'italic', True), - ('w:r/w:rPr/w:imprint{w:val=false}', 'imprint', False), - ('w:r/w:rPr/w:oMath{w:val=on}', 'math', True), - ('w:r/w:rPr/w:noProof{w:val=off}', 'no_proof', False), - ('w:r/w:rPr/w:outline{w:val=1}', 'outline', True), - ('w:r/w:rPr/w:rtl{w:val=0}', 'rtl', False), - ('w:r/w:rPr/w:shadow{w:val=true}', 'shadow', True), - ('w:r/w:rPr/w:smallCaps{w:val=false}', 'small_caps', False), - ('w:r/w:rPr/w:snapToGrid{w:val=on}', 'snap_to_grid', True), - ('w:r/w:rPr/w:specVanish{w:val=off}', 'spec_vanish', False), - ('w:r/w:rPr/w:strike{w:val=1}', 'strike', True), - ('w:r/w:rPr/w:webHidden{w:val=0}', 'web_hidden', False), - ]) - def bool_prop_get_fixture(self, request): - r_cxml, bool_prop_name, expected_value = request.param - font = Font(element(r_cxml)) - return font, bool_prop_name, expected_value - - @pytest.fixture(params=[ - # nothing to True, False, and None --------------------------- - ('w:r', 'all_caps', True, - 'w:r/w:rPr/w:caps'), - ('w:r', 'bold', False, - 'w:r/w:rPr/w:b{w:val=0}'), - ('w:r', 'italic', None, - 'w:r/w:rPr'), - # default to True, False, and None --------------------------- - ('w:r/w:rPr/w:cs', 'complex_script', True, - 'w:r/w:rPr/w:cs'), - ('w:r/w:rPr/w:bCs', 'cs_bold', False, - 'w:r/w:rPr/w:bCs{w:val=0}'), - ('w:r/w:rPr/w:iCs', 'cs_italic', None, - 'w:r/w:rPr'), - # True to True, False, and None ------------------------------ - ('w:r/w:rPr/w:dstrike{w:val=1}', 'double_strike', True, - 'w:r/w:rPr/w:dstrike'), - ('w:r/w:rPr/w:emboss{w:val=on}', 'emboss', False, - 'w:r/w:rPr/w:emboss{w:val=0}'), - ('w:r/w:rPr/w:vanish{w:val=1}', 'hidden', None, - 'w:r/w:rPr'), - # False to True, False, and None ----------------------------- - ('w:r/w:rPr/w:i{w:val=false}', 'italic', True, - 'w:r/w:rPr/w:i'), - ('w:r/w:rPr/w:imprint{w:val=0}', 'imprint', False, - 'w:r/w:rPr/w:imprint{w:val=0}'), - ('w:r/w:rPr/w:oMath{w:val=off}', 'math', None, - 'w:r/w:rPr'), - # random mix ------------------------------------------------- - ('w:r/w:rPr/w:noProof{w:val=1}', 'no_proof', False, - 'w:r/w:rPr/w:noProof{w:val=0}'), - ('w:r/w:rPr', 'outline', True, - 'w:r/w:rPr/w:outline'), - ('w:r/w:rPr/w:rtl{w:val=true}', 'rtl', False, - 'w:r/w:rPr/w:rtl{w:val=0}'), - ('w:r/w:rPr/w:shadow{w:val=on}', 'shadow', True, - 'w:r/w:rPr/w:shadow'), - ('w:r/w:rPr/w:smallCaps', 'small_caps', False, - 'w:r/w:rPr/w:smallCaps{w:val=0}'), - ('w:r/w:rPr/w:snapToGrid', 'snap_to_grid', True, - 'w:r/w:rPr/w:snapToGrid'), - ('w:r/w:rPr/w:specVanish', 'spec_vanish', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:strike{w:val=foo}', 'strike', True, - 'w:r/w:rPr/w:strike'), - ('w:r/w:rPr/w:webHidden', 'web_hidden', False, - 'w:r/w:rPr/w:webHidden{w:val=0}'), - ]) - def bool_prop_set_fixture(self, request): - r_cxml, prop_name, value, expected_cxml = request.param - font = Font(element(r_cxml)) - expected_xml = xml(expected_cxml) - return font, prop_name, value, expected_xml + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", None), + ("w:r/w:rPr", None), + ("w:r/w:rPr/w:highlight{w:val=default}", WD_COLOR.AUTO), + ("w:r/w:rPr/w:highlight{w:val=blue}", WD_COLOR.BLUE), + ], + ) + def it_knows_its_highlight_color( + self, r_cxml: str, expected_value: WD_COLOR | None + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) + assert font.highlight_color is expected_value - @pytest.fixture - def color_fixture(self, ColorFormat_, color_): - font = Font(element('w:r')) - return font, color_, ColorFormat_ - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:highlight{w:val=default}', WD_COLOR.AUTO), - ('w:r/w:rPr/w:highlight{w:val=blue}', WD_COLOR.BLUE), - ]) - def highlight_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml), None) - return font, expected_value - - @pytest.fixture(params=[ - ('w:r', WD_COLOR.AUTO, - 'w:r/w:rPr/w:highlight{w:val=default}'), - ('w:r/w:rPr', WD_COLOR.BRIGHT_GREEN, - 'w:r/w:rPr/w:highlight{w:val=green}'), - ('w:r/w:rPr/w:highlight{w:val=green}', WD_COLOR.YELLOW, - 'w:r/w:rPr/w:highlight{w:val=yellow}'), - ('w:r/w:rPr/w:highlight{w:val=yellow}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr', None, - 'w:r/w:rPr'), - ('w:r', None, - 'w:r/w:rPr'), - ]) - def highlight_set_fixture(self, request): - r_cxml, value, expected_cxml = request.param - font = Font(element(r_cxml), None) - expected_xml = xml(expected_cxml) - return font, value, expected_xml - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:rFonts', None), - ('w:r/w:rPr/w:rFonts{w:ascii=Arial}', 'Arial'), - ]) - def name_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value - - @pytest.fixture(params=[ - ('w:r', 'Foo', - 'w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}'), - ('w:r/w:rPr', 'Foo', - 'w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}'), - ('w:r/w:rPr/w:rFonts{w:hAnsi=Foo}', 'Bar', - 'w:r/w:rPr/w:rFonts{w:ascii=Bar,w:hAnsi=Bar}'), - ('w:r/w:rPr/w:rFonts{w:ascii=Foo,w:hAnsi=Foo}', 'Bar', - 'w:r/w:rPr/w:rFonts{w:ascii=Bar,w:hAnsi=Bar}'), - ]) - def name_set_fixture(self, request): - r_cxml, value, expected_r_cxml = request.param - font = Font(element(r_cxml)) - expected_xml = xml(expected_r_cxml) - return font, value, expected_xml - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:sz{w:val=28}', Pt(14)), - ]) - def size_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value - - @pytest.fixture(params=[ - ('w:r', Pt(12), 'w:r/w:rPr/w:sz{w:val=24}'), - ('w:r/w:rPr', Pt(12), 'w:r/w:rPr/w:sz{w:val=24}'), - ('w:r/w:rPr/w:sz{w:val=24}', Pt(18), 'w:r/w:rPr/w:sz{w:val=36}'), - ('w:r/w:rPr/w:sz{w:val=36}', None, 'w:r/w:rPr'), - ]) - def size_set_fixture(self, request): - r_cxml, value, expected_r_cxml = request.param - font = Font(element(r_cxml)) - expected_xml = xml(expected_r_cxml) - return font, value, expected_xml - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:vertAlign{w:val=baseline}', False), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', True), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', False), - ]) - def subscript_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value - - @pytest.fixture(params=[ - ('w:r', True, - 'w:r/w:rPr/w:vertAlign{w:val=subscript}'), - ('w:r', False, - 'w:r/w:rPr'), - ('w:r', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', True, - 'w:r/w:rPr/w:vertAlign{w:val=subscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', False, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', True, - 'w:r/w:rPr/w:vertAlign{w:val=subscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', False, - 'w:r/w:rPr/w:vertAlign{w:val=superscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=baseline}', True, - 'w:r/w:rPr/w:vertAlign{w:val=subscript}'), - ]) - def subscript_set_fixture(self, request): - r_cxml, value, expected_r_cxml = request.param - font = Font(element(r_cxml)) - expected_xml = xml(expected_r_cxml) - return font, value, expected_xml - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr', None), - ('w:r/w:rPr/w:vertAlign{w:val=baseline}', False), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', False), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', True), - ]) - def superscript_get_fixture(self, request): - r_cxml, expected_value = request.param - font = Font(element(r_cxml)) - return font, expected_value - - @pytest.fixture(params=[ - ('w:r', True, - 'w:r/w:rPr/w:vertAlign{w:val=superscript}'), - ('w:r', False, - 'w:r/w:rPr'), - ('w:r', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', True, - 'w:r/w:rPr/w:vertAlign{w:val=superscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', False, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=superscript}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', True, - 'w:r/w:rPr/w:vertAlign{w:val=superscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', False, - 'w:r/w:rPr/w:vertAlign{w:val=subscript}'), - ('w:r/w:rPr/w:vertAlign{w:val=subscript}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:vertAlign{w:val=baseline}', True, - 'w:r/w:rPr/w:vertAlign{w:val=superscript}'), - ]) - def superscript_set_fixture(self, request): - r_cxml, value, expected_r_cxml = request.param - font = Font(element(r_cxml)) + @pytest.mark.parametrize( + ("r_cxml", "value", "expected_r_cxml"), + [ + ("w:r", WD_COLOR.AUTO, "w:r/w:rPr/w:highlight{w:val=default}"), + ("w:r/w:rPr", WD_COLOR.BRIGHT_GREEN, "w:r/w:rPr/w:highlight{w:val=green}"), + ( + "w:r/w:rPr/w:highlight{w:val=green}", + WD_COLOR.YELLOW, + "w:r/w:rPr/w:highlight{w:val=yellow}", + ), + ("w:r/w:rPr/w:highlight{w:val=yellow}", None, "w:r/w:rPr"), + ("w:r/w:rPr", None, "w:r/w:rPr"), + ("w:r", None, "w:r/w:rPr"), + ], + ) + def it_can_change_its_highlight_color( + self, r_cxml: str, value: WD_COLOR | None, expected_r_cxml: str + ): + r = cast(CT_R, element(r_cxml)) + font = Font(r) expected_xml = xml(expected_r_cxml) - return font, value, expected_xml - - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr/w:u', None), - ('w:r/w:rPr/w:u{w:val=single}', True), - ('w:r/w:rPr/w:u{w:val=none}', False), - ('w:r/w:rPr/w:u{w:val=double}', WD_UNDERLINE.DOUBLE), - ('w:r/w:rPr/w:u{w:val=wave}', WD_UNDERLINE.WAVY), - ]) - def underline_get_fixture(self, request): - r_cxml, expected_value = request.param - run = Font(element(r_cxml), None) - return run, expected_value - - @pytest.fixture(params=[ - ('w:r', True, 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r', False, 'w:r/w:rPr/w:u{w:val=none}'), - ('w:r', None, 'w:r/w:rPr'), - ('w:r', WD_UNDERLINE.SINGLE, 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r', WD_UNDERLINE.THICK, 'w:r/w:rPr/w:u{w:val=thick}'), - ('w:r/w:rPr/w:u{w:val=single}', True, - 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r/w:rPr/w:u{w:val=single}', False, - 'w:r/w:rPr/w:u{w:val=none}'), - ('w:r/w:rPr/w:u{w:val=single}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:u{w:val=single}', WD_UNDERLINE.SINGLE, - 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r/w:rPr/w:u{w:val=single}', WD_UNDERLINE.DOTTED, - 'w:r/w:rPr/w:u{w:val=dotted}'), - ]) - def underline_set_fixture(self, request): - initial_r_cxml, value, expected_cxml = request.param - run = Font(element(initial_r_cxml), None) - expected_xml = xml(expected_cxml) - return run, value, expected_xml - # fixture components --------------------------------------------- + font.highlight_color = value + + assert font._element.xml == expected_xml + + # -- fixtures ---------------------------------------------------- @pytest.fixture - def color_(self, request): + def color_(self, request: FixtureRequest): return instance_mock(request, ColorFormat) @pytest.fixture - def ColorFormat_(self, request, color_): - return class_mock( - request, 'docx.text.font.ColorFormat', return_value=color_ - ) + def ColorFormat_(self, request: FixtureRequest, color_: Mock): + return class_mock(request, "docx.text.font.ColorFormat", return_value=color_) diff --git a/tests/text/test_hyperlink.py b/tests/text/test_hyperlink.py new file mode 100644 index 000000000..484196902 --- /dev/null +++ b/tests/text/test_hyperlink.py @@ -0,0 +1,105 @@ +"""Test suite for the docx.text.hyperlink module.""" + +from typing import cast + +import pytest + +from docx import types as t +from docx.opc.rel import _Relationship # pyright: ignore[reportPrivateUsage] +from docx.oxml.text.hyperlink import CT_Hyperlink +from docx.parts.story import StoryPart +from docx.text.hyperlink import Hyperlink + +from ..unitutil.cxml import element +from ..unitutil.mock import FixtureRequest, Mock, instance_mock + + +class DescribeHyperlink: + """Unit-test suite for the docx.text.hyperlink.Hyperlink object.""" + + def it_knows_the_hyperlink_URL(self, fake_parent: t.StoryChild): + cxml = 'w:hyperlink{r:id=rId6}/w:r/w:t"post"' + hlink = cast(CT_Hyperlink, element(cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + assert hyperlink.address == "https://google.com/" + + @pytest.mark.parametrize( + ("hlink_cxml", "expected_value"), + [ + ("w:hyperlink", False), + ("w:hyperlink/w:r", False), + ('w:hyperlink/w:r/(w:t"abc",w:lastRenderedPageBreak,w:t"def")', True), + ('w:hyperlink/w:r/(w:lastRenderedPageBreak,w:t"abc",w:t"def")', True), + ('w:hyperlink/w:r/(w:t"abc",w:t"def",w:lastRenderedPageBreak)', True), + ], + ) + def it_knows_whether_it_contains_a_page_break( + self, hlink_cxml: str, expected_value: bool, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + assert hyperlink.contains_page_break is expected_value + + @pytest.mark.parametrize( + ("hlink_cxml", "count"), + [ + ("w:hyperlink", 0), + ("w:hyperlink/w:r", 1), + ("w:hyperlink/(w:r,w:r)", 2), + ("w:hyperlink/(w:r,w:lastRenderedPageBreak)", 1), + ("w:hyperlink/(w:lastRenderedPageBreak,w:r)", 1), + ("w:hyperlink/(w:r,w:lastRenderedPageBreak,w:r)", 2), + ], + ) + def it_provides_access_to_the_runs_it_contains( + self, hlink_cxml: str, count: int, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + runs = hyperlink.runs + + actual = [type(item).__name__ for item in runs] + expected = ["Run" for _ in range(count)] + assert actual == expected + + @pytest.mark.parametrize( + ("hlink_cxml", "expected_text"), + [ + ("w:hyperlink", ""), + ("w:hyperlink/w:r", ""), + ('w:hyperlink/w:r/w:t"foobar"', "foobar"), + ('w:hyperlink/w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")', "foobar"), + ('w:hyperlink/w:r/(w:t"abc",w:tab,w:t"def",w:noBreakHyphen)', "abc\tdef-"), + ], + ) + def it_knows_the_visible_text_of_the_link( + self, hlink_cxml: str, expected_text: str, fake_parent: t.StoryChild + ): + hlink = cast(CT_Hyperlink, element(hlink_cxml)) + hyperlink = Hyperlink(hlink, fake_parent) + + text = hyperlink.text + + assert text == expected_text + + # -- fixtures -------------------------------------------------------------------- + + @pytest.fixture + def fake_parent(self, story_part: Mock, rel: Mock) -> t.StoryChild: + class StoryChild: + @property + def part(self) -> StoryPart: + return story_part + + return StoryChild() + + @pytest.fixture + def rel(self, request: FixtureRequest): + return instance_mock(request, _Relationship, target_ref="https://google.com/") + + @pytest.fixture + def story_part(self, request: FixtureRequest, rel: Mock): + return instance_mock(request, StoryPart, rels={"rId6": rel}) diff --git a/tests/text/test_pagebreak.py b/tests/text/test_pagebreak.py new file mode 100644 index 000000000..6bb770619 --- /dev/null +++ b/tests/text/test_pagebreak.py @@ -0,0 +1,147 @@ +# pyright: reportPrivateUsage=false + +"""Unit-test suite for the docx.text.pagebreak module.""" + +from typing import cast + +import pytest + +from docx import types as t +from docx.oxml.text.paragraph import CT_P +from docx.text.pagebreak import RenderedPageBreak + +from ..unitutil.cxml import element, xml + + +class DescribeRenderedPageBreak: + """Unit-test suite for the docx.text.pagebreak.RenderedPageBreak object.""" + + def it_raises_on_preceding_fragment_when_page_break_is_not_first_in_paragrah( + self, fake_parent: t.StoryChild + ): + p_cxml = 'w:p/(w:r/(w:t"abc",w:lastRenderedPageBreak,w:lastRenderedPageBreak))' + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[-1] + page_break = RenderedPageBreak(lrpb, fake_parent) + + with pytest.raises(ValueError, match="only defined on first rendered page-br"): + page_break.preceding_paragraph_fragment + + def it_produces_None_for_preceding_fragment_when_page_break_is_leading( + self, fake_parent: t.StoryChild + ): + """A page-break with no preceding content is "leading".""" + p_cxml = 'w:p/(w:pPr/w:ind,w:r/(w:lastRenderedPageBreak,w:t"foo",w:t"bar"))' + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + preceding_fragment = page_break.preceding_paragraph_fragment + + assert preceding_fragment is None + + def it_can_split_off_the_preceding_paragraph_content_when_in_a_run( + self, fake_parent: t.StoryChild + ): + p_cxml = ( + "w:p/(" + " w:pPr/w:ind" + ' ,w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")' + ' ,w:r/w:t"barfoo"' + ")" + ) + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + preceding_fragment = page_break.preceding_paragraph_fragment + + expected_cxml = 'w:p/(w:pPr/w:ind,w:r/w:t"foo")' + assert preceding_fragment is not None + assert preceding_fragment._p.xml == xml(expected_cxml) + + def and_it_can_split_off_the_preceding_paragraph_content_when_in_a_hyperlink( + self, fake_parent: t.StoryChild + ): + p_cxml = ( + "w:p/(" + " w:pPr/w:ind" + ' ,w:hyperlink/w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")' + ' ,w:r/w:t"barfoo"' + ")" + ) + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + preceding_fragment = page_break.preceding_paragraph_fragment + + expected_cxml = 'w:p/(w:pPr/w:ind,w:hyperlink/w:r/(w:t"foo",w:t"bar"))' + assert preceding_fragment is not None + assert preceding_fragment._p.xml == xml(expected_cxml) + + def it_raises_on_following_fragment_when_page_break_is_not_first_in_paragrah( + self, fake_parent: t.StoryChild + ): + p_cxml = 'w:p/(w:r/(w:lastRenderedPageBreak,w:lastRenderedPageBreak,w:t"abc"))' + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[-1] + page_break = RenderedPageBreak(lrpb, fake_parent) + + with pytest.raises(ValueError, match="only defined on first rendered page-br"): + page_break.following_paragraph_fragment + + def it_produces_None_for_following_fragment_when_page_break_is_trailing( + self, fake_parent: t.StoryChild + ): + """A page-break with no following content is "trailing".""" + p_cxml = 'w:p/(w:pPr/w:ind,w:r/(w:t"foo",w:t"bar",w:lastRenderedPageBreak))' + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + following_fragment = page_break.following_paragraph_fragment + + assert following_fragment is None + + def it_can_split_off_the_following_paragraph_content_when_in_a_run( + self, fake_parent: t.StoryChild + ): + p_cxml = ( + "w:p/(" + " w:pPr/w:ind" + ' ,w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")' + ' ,w:r/w:t"foo"' + ")" + ) + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + following_fragment = page_break.following_paragraph_fragment + + expected_cxml = 'w:p/(w:pPr/w:ind,w:r/w:t"bar",w:r/w:t"foo")' + assert following_fragment is not None + assert following_fragment._p.xml == xml(expected_cxml) + + def and_it_can_split_off_the_following_paragraph_content_when_in_a_hyperlink( + self, fake_parent: t.StoryChild + ): + p_cxml = ( + "w:p/(" + " w:pPr/w:ind" + ' ,w:hyperlink/w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")' + ' ,w:r/w:t"baz"' + ' ,w:r/w:t"qux"' + ")" + ) + p = cast(CT_P, element(p_cxml)) + lrpb = p.lastRenderedPageBreaks[0] + page_break = RenderedPageBreak(lrpb, fake_parent) + + following_fragment = page_break.following_paragraph_fragment + + expected_cxml = 'w:p/(w:pPr/w:ind,w:r/w:t"baz",w:r/w:t"qux")' + + assert following_fragment is not None + assert following_fragment._p.xml == xml(expected_cxml) diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index b8313e61e..a5db30da8 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -1,9 +1,10 @@ -# encoding: utf-8 +"""Unit test suite for the docx.text.paragraph module.""" -"""Unit test suite for the docx.text.paragraph module""" +from typing import List, cast -from __future__ import absolute_import, division, print_function, unicode_literals +import pytest +from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.text.paragraph import CT_P @@ -13,15 +14,73 @@ from docx.text.parfmt import ParagraphFormat from docx.text.run import Run -import pytest - from ..unitutil.cxml import element, xml -from ..unitutil.mock import ( - call, class_mock, instance_mock, method_mock, property_mock -) - - -class DescribeParagraph(object): +from ..unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock + + +class DescribeParagraph: + """Unit-test suite for `docx.text.run.Paragraph`.""" + + @pytest.mark.parametrize( + ("p_cxml", "expected_value"), + [ + ("w:p/w:r", False), + ('w:p/w:r/w:t"foobar"', False), + ('w:p/w:hyperlink/w:r/(w:t"abc",w:lastRenderedPageBreak,w:t"def")', True), + ("w:p/w:r/(w:lastRenderedPageBreak, w:lastRenderedPageBreak)", True), + ], + ) + def it_knows_whether_it_contains_a_page_break( + self, p_cxml: str, expected_value: bool, fake_parent: t.StoryChild + ): + p = cast(CT_P, element(p_cxml)) + paragraph = Paragraph(p, fake_parent) + + assert paragraph.contains_page_break == expected_value + + @pytest.mark.parametrize( + ("p_cxml", "count"), + [ + ("w:p", 0), + ("w:p/w:r", 0), + ("w:p/w:hyperlink", 1), + ("w:p/(w:r,w:hyperlink,w:r)", 1), + ("w:p/(w:r,w:hyperlink,w:r,w:hyperlink)", 2), + ("w:p/(w:hyperlink,w:r,w:hyperlink,w:r)", 2), + ], + ) + def it_provides_access_to_the_hyperlinks_it_contains( + self, p_cxml: str, count: int, fake_parent: t.StoryChild + ): + p = cast(CT_P, element(p_cxml)) + paragraph = Paragraph(p, fake_parent) + + hyperlinks = paragraph.hyperlinks + + actual = [type(item).__name__ for item in hyperlinks] + expected = ["Hyperlink" for _ in range(count)] + assert actual == expected, f"expected: {expected}, got: {actual}" + + @pytest.mark.parametrize( + ("p_cxml", "expected"), + [ + ("w:p", []), + ("w:p/w:r", ["Run"]), + ("w:p/w:hyperlink", ["Hyperlink"]), + ("w:p/(w:r,w:hyperlink,w:r)", ["Run", "Hyperlink", "Run"]), + ("w:p/(w:hyperlink,w:r,w:hyperlink)", ["Hyperlink", "Run", "Hyperlink"]), + ], + ) + def it_can_iterate_its_inner_content_items( + self, p_cxml: str, expected: List[str], fake_parent: t.StoryChild + ): + p = cast(CT_P, element(p_cxml)) + paragraph = Paragraph(p, fake_parent) + + inner_content = paragraph.iter_inner_content() + + actual = [type(item).__name__ for item in inner_content] + assert actual == expected, f"expected: {expected}, got: {actual}" def it_knows_its_paragraph_style(self, style_get_fixture): paragraph, style_id_, style_ = style_get_fixture @@ -41,9 +100,60 @@ def it_can_change_its_paragraph_style(self, style_set_fixture): ) assert paragraph._p.xml == expected_xml - def it_knows_the_text_it_contains(self, text_get_fixture): - paragraph, expected_text = text_get_fixture - assert paragraph.text == expected_text + @pytest.mark.parametrize( + ("p_cxml", "count"), + [ + ("w:p", 0), + ("w:p/w:r", 0), + ("w:p/w:r/w:lastRenderedPageBreak", 1), + ("w:p/w:hyperlink/w:r/w:lastRenderedPageBreak", 1), + ( + "w:p/(w:r/w:lastRenderedPageBreak," + "w:hyperlink/w:r/w:lastRenderedPageBreak)", + 2, + ), + ( + "w:p/(w:hyperlink/w:r/w:lastRenderedPageBreak,w:r," + "w:r/w:lastRenderedPageBreak,w:r,w:hyperlink)", + 2, + ), + ], + ) + def it_provides_access_to_the_rendered_page_breaks_it_contains( + self, p_cxml: str, count: int, fake_parent: t.StoryChild + ): + p = cast(CT_P, element(p_cxml)) + paragraph = Paragraph(p, fake_parent) + + rendered_page_breaks = paragraph.rendered_page_breaks + + actual = [type(item).__name__ for item in rendered_page_breaks] + expected = ["RenderedPageBreak" for _ in range(count)] + assert actual == expected, f"expected: {expected}, got: {actual}" + + @pytest.mark.parametrize( + ("p_cxml", "expected_value"), + [ + ("w:p", ""), + ("w:p/w:r", ""), + ("w:p/w:r/w:t", ""), + ('w:p/w:r/w:t"foo"', "foo"), + ('w:p/w:r/(w:t"foo", w:t"bar")', "foobar"), + ('w:p/w:r/(w:t"fo ", w:t"bar")', "fo bar"), + ('w:p/w:r/(w:t"foo", w:tab, w:t"bar")', "foo\tbar"), + ('w:p/w:r/(w:t"foo", w:br, w:t"bar")', "foo\nbar"), + ('w:p/w:r/(w:t"foo", w:cr, w:t"bar")', "foo\nbar"), + ( + 'w:p/(w:r/w:t"click ",w:hyperlink{r:id=rId6}/w:r/w:t"here",' + 'w:r/w:t" for more")', + "click here for more", + ), + ], + ) + def it_knows_the_text_it_contains(self, p_cxml: str, expected_value: str): + """Including the text of embedded hyperlinks.""" + paragraph = Paragraph(element(p_cxml), None) + assert paragraph.text == expected_value def it_can_replace_the_text_it_contains(self, text_set_fixture): paragraph, text, expected_text = text_set_fixture @@ -68,9 +178,7 @@ def it_provides_access_to_its_paragraph_format(self, parfmt_fixture): def it_provides_access_to_the_runs_it_contains(self, runs_fixture): paragraph, Run_, r_, r_2_, run_, run_2_ = runs_fixture runs = paragraph.runs - assert Run_.mock_calls == [ - call(r_, paragraph), call(r_2_, paragraph) - ] + assert Run_.mock_calls == [call(r_, paragraph), call(r_2_, paragraph)] assert runs == [run_, run_2_] def it_can_add_a_run_to_itself(self, add_run_fixture): @@ -93,8 +201,7 @@ def it_can_insert_a_paragraph_before_itself(self, insert_before_fixture): assert new_paragraph.style == style assert new_paragraph is paragraph_ - def it_can_remove_its_content_while_preserving_formatting( - self, clear_fixture): + def it_can_remove_its_content_while_preserving_formatting(self, clear_fixture): paragraph, expected_xml = clear_fixture _paragraph = paragraph.clear() assert paragraph._p.xml == expected_xml @@ -108,60 +215,71 @@ def it_inserts_a_paragraph_before_to_help(self, _insert_before_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:p', None, None, 'w:p/w:r'), - ('w:p', 'foobar', None, 'w:p/w:r/w:t"foobar"'), - ('w:p', None, 'Strong', 'w:p/w:r'), - ('w:p', 'foobar', 'Strong', 'w:p/w:r/w:t"foobar"'), - ]) + @pytest.fixture( + params=[ + ("w:p", None, None, "w:p/w:r"), + ("w:p", "foobar", None, 'w:p/w:r/w:t"foobar"'), + ("w:p", None, "Strong", "w:p/w:r"), + ("w:p", "foobar", "Strong", 'w:p/w:r/w:t"foobar"'), + ] + ) def add_run_fixture(self, request, run_style_prop_): before_cxml, text, style, after_cxml = request.param paragraph = Paragraph(element(before_cxml), None) expected_xml = xml(after_cxml) return paragraph, text, style, run_style_prop_, expected_xml - @pytest.fixture(params=[ - ('w:p/w:pPr/w:jc{w:val=center}', WD_ALIGN_PARAGRAPH.CENTER), - ('w:p', None), - ]) + @pytest.fixture( + params=[ + ("w:p/w:pPr/w:jc{w:val=center}", WD_ALIGN_PARAGRAPH.CENTER), + ("w:p", None), + ] + ) def alignment_get_fixture(self, request): cxml, expected_alignment_value = request.param paragraph = Paragraph(element(cxml), None) return paragraph, expected_alignment_value - @pytest.fixture(params=[ - ('w:p', WD_ALIGN_PARAGRAPH.LEFT, - 'w:p/w:pPr/w:jc{w:val=left}'), - ('w:p/w:pPr/w:jc{w:val=left}', WD_ALIGN_PARAGRAPH.CENTER, - 'w:p/w:pPr/w:jc{w:val=center}'), - ('w:p/w:pPr/w:jc{w:val=left}', None, - 'w:p/w:pPr'), - ('w:p', None, 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", WD_ALIGN_PARAGRAPH.LEFT, "w:p/w:pPr/w:jc{w:val=left}"), + ( + "w:p/w:pPr/w:jc{w:val=left}", + WD_ALIGN_PARAGRAPH.CENTER, + "w:p/w:pPr/w:jc{w:val=center}", + ), + ("w:p/w:pPr/w:jc{w:val=left}", None, "w:p/w:pPr"), + ("w:p", None, "w:p/w:pPr"), + ] + ) def alignment_set_fixture(self, request): initial_cxml, new_alignment_value, expected_cxml = request.param paragraph = Paragraph(element(initial_cxml), None) expected_xml = xml(expected_cxml) return paragraph, new_alignment_value, expected_xml - @pytest.fixture(params=[ - ('w:p', 'w:p'), - ('w:p/w:pPr', 'w:p/w:pPr'), - ('w:p/w:r/w:t"foobar"', 'w:p'), - ('w:p/(w:pPr, w:r/w:t"foobar")', 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", "w:p"), + ("w:p/w:pPr", "w:p/w:pPr"), + ('w:p/w:r/w:t"foobar"', "w:p"), + ('w:p/(w:pPr, w:r/w:t"foobar")', "w:p/w:pPr"), + ] + ) def clear_fixture(self, request): initial_cxml, expected_cxml = request.param paragraph = Paragraph(element(initial_cxml), None) expected_xml = xml(expected_cxml) return paragraph, expected_xml - @pytest.fixture(params=[ - (None, None), - ('Foo', None), - (None, 'Bar'), - ('Foo', 'Bar'), - ]) + @pytest.fixture( + params=[ + (None, None), + ("Foo", None), + (None, "Bar"), + ("Foo", "Bar"), + ] + ) def insert_before_fixture(self, request, _insert_paragraph_before_, add_run_): text, style = request.param paragraph_ = _insert_paragraph_before_.return_value @@ -169,9 +287,7 @@ def insert_before_fixture(self, request, _insert_paragraph_before_, add_run_): paragraph_.style = None return text, style, paragraph_, add_run_calls - @pytest.fixture(params=[ - ('w:body/w:p{id=42}', 'w:body/(w:p,w:p{id=42})') - ]) + @pytest.fixture(params=[("w:body/w:p{id=42}", "w:body/(w:p,w:p{id=42})")]) def _insert_before_fixture(self, request): body_cxml, expected_cxml = request.param body = element(body_cxml) @@ -181,7 +297,7 @@ def _insert_before_fixture(self, request): @pytest.fixture def parfmt_fixture(self, ParagraphFormat_, paragraph_format_): - paragraph = Paragraph(element('w:p'), None) + paragraph = Paragraph(element("w:p"), None) return paragraph, ParagraphFormat_, paragraph_format_ @pytest.fixture @@ -192,24 +308,31 @@ def runs_fixture(self, p_, Run_, r_, r_2_, runs_): @pytest.fixture def style_get_fixture(self, part_prop_): - style_id = 'Foobar' - p_cxml = 'w:p/w:pPr/w:pStyle{w:val=%s}' % style_id + style_id = "Foobar" + p_cxml = "w:p/w:pPr/w:pStyle{w:val=%s}" % style_id paragraph = Paragraph(element(p_cxml), None) style_ = part_prop_.return_value.get_style.return_value return paragraph, style_id, style_ - @pytest.fixture(params=[ - ('w:p', 'Heading 1', 'Heading1', - 'w:p/w:pPr/w:pStyle{w:val=Heading1}'), - ('w:p/w:pPr', 'Heading 1', 'Heading1', - 'w:p/w:pPr/w:pStyle{w:val=Heading1}'), - ('w:p/w:pPr/w:pStyle{w:val=Heading1}', 'Heading 2', 'Heading2', - 'w:p/w:pPr/w:pStyle{w:val=Heading2}'), - ('w:p/w:pPr/w:pStyle{w:val=Heading1}', 'Normal', None, - 'w:p/w:pPr'), - ('w:p', None, None, - 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", "Heading 1", "Heading1", "w:p/w:pPr/w:pStyle{w:val=Heading1}"), + ( + "w:p/w:pPr", + "Heading 1", + "Heading1", + "w:p/w:pPr/w:pStyle{w:val=Heading1}", + ), + ( + "w:p/w:pPr/w:pStyle{w:val=Heading1}", + "Heading 2", + "Heading2", + "w:p/w:pPr/w:pStyle{w:val=Heading2}", + ), + ("w:p/w:pPr/w:pStyle{w:val=Heading1}", "Normal", None, "w:p/w:pPr"), + ("w:p", None, None, "w:p/w:pPr"), + ] + ) def style_set_fixture(self, request, part_prop_): p_cxml, value, style_id, expected_cxml = request.param paragraph = Paragraph(element(p_cxml), None) @@ -217,35 +340,19 @@ def style_set_fixture(self, request, part_prop_): expected_xml = xml(expected_cxml) return paragraph, value, expected_xml - @pytest.fixture(params=[ - ('w:p', ''), - ('w:p/w:r', ''), - ('w:p/w:r/w:t', ''), - ('w:p/w:r/w:t"foo"', 'foo'), - ('w:p/w:r/(w:t"foo", w:t"bar")', 'foobar'), - ('w:p/w:r/(w:t"fo ", w:t"bar")', 'fo bar'), - ('w:p/w:r/(w:t"foo", w:tab, w:t"bar")', 'foo\tbar'), - ('w:p/w:r/(w:t"foo", w:br, w:t"bar")', 'foo\nbar'), - ('w:p/w:r/(w:t"foo", w:cr, w:t"bar")', 'foo\nbar'), - ]) - def text_get_fixture(self, request): - p_cxml, expected_text_value = request.param - paragraph = Paragraph(element(p_cxml), None) - return paragraph, expected_text_value - @pytest.fixture def text_set_fixture(self): - paragraph = Paragraph(element('w:p'), None) - paragraph.add_run('must not appear in result') - new_text_value = 'foo\tbar\rbaz\n' - expected_text_value = 'foo\tbar\nbaz\n' + paragraph = Paragraph(element("w:p"), None) + paragraph.add_run("must not appear in result") + new_text_value = "foo\tbar\rbaz\n" + expected_text_value = "foo\tbar\nbaz\n" return paragraph, new_text_value, expected_text_value # fixture components --------------------------------------------- @pytest.fixture def add_run_(self, request): - return method_mock(request, Paragraph, 'add_run') + return method_mock(request, Paragraph, "add_run") @pytest.fixture def document_part_(self, request): @@ -253,7 +360,7 @@ def document_part_(self, request): @pytest.fixture def _insert_paragraph_before_(self, request): - return method_mock(request, Paragraph, '_insert_paragraph_before') + return method_mock(request, Paragraph, "_insert_paragraph_before") @pytest.fixture def p_(self, request, r_, r_2_): @@ -262,8 +369,9 @@ def p_(self, request, r_, r_2_): @pytest.fixture def ParagraphFormat_(self, request, paragraph_format_): return class_mock( - request, 'docx.text.paragraph.ParagraphFormat', - return_value=paragraph_format_ + request, + "docx.text.paragraph.ParagraphFormat", + return_value=paragraph_format_, ) @pytest.fixture @@ -272,15 +380,13 @@ def paragraph_format_(self, request): @pytest.fixture def part_prop_(self, request, document_part_): - return property_mock( - request, Paragraph, 'part', return_value=document_part_ - ) + return property_mock(request, Paragraph, "part", return_value=document_part_) @pytest.fixture def Run_(self, request, runs_): run_, run_2_ = runs_ return class_mock( - request, 'docx.text.paragraph.Run', side_effect=[run_, run_2_] + request, "docx.text.paragraph.Run", side_effect=[run_, run_2_] ) @pytest.fixture @@ -293,10 +399,10 @@ def r_2_(self, request): @pytest.fixture def run_style_prop_(self, request): - return property_mock(request, Run, 'style') + return property_mock(request, Run, "style") @pytest.fixture def runs_(self, request): - run_ = instance_mock(request, Run, name='run_') - run_2_ = instance_mock(request, Run, name='run_2_') + run_ = instance_mock(request, Run, name="run_") + run_2_ = instance_mock(request, Run, name="run_2_") return run_, run_2_ diff --git a/tests/text/test_parfmt.py b/tests/text/test_parfmt.py index 9e7bb4d52..be31329e9 100644 --- a/tests/text/test_parfmt.py +++ b/tests/text/test_parfmt.py @@ -1,27 +1,17 @@ -# encoding: utf-8 +"""Test suite for docx.text.parfmt module, containing the ParagraphFormat object.""" -""" -Test suite for the docx.text.parfmt module, containing the ParagraphFormat -object. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +import pytest from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.shared import Pt from docx.text.parfmt import ParagraphFormat from docx.text.tabstops import TabStops -import pytest - from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock -class DescribeParagraphFormat(object): - +class DescribeParagraphFormat: def it_knows_its_alignment_value(self, alignment_get_fixture): paragraph_format, expected_value = alignment_get_fixture assert paragraph_format.alignment == expected_value @@ -62,8 +52,7 @@ def it_knows_its_line_spacing_rule(self, line_spacing_rule_get_fixture): paragraph_format, expected_value = line_spacing_rule_get_fixture assert paragraph_format.line_spacing_rule == expected_value - def it_can_change_its_line_spacing_rule(self, - line_spacing_rule_set_fixture): + def it_can_change_its_line_spacing_rule(self, line_spacing_rule_set_fixture): paragraph_format, value, expected_xml = line_spacing_rule_set_fixture paragraph_format.line_spacing_rule = value assert paragraph_format._element.xml == expected_xml @@ -112,290 +101,367 @@ def it_provides_access_to_its_tab_stops(self, tab_stops_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:jc{w:val=center}', WD_ALIGN_PARAGRAPH.CENTER), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:jc{w:val=center}", WD_ALIGN_PARAGRAPH.CENTER), + ] + ) def alignment_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', WD_ALIGN_PARAGRAPH.LEFT, - 'w:p/w:pPr/w:jc{w:val=left}'), - ('w:p/w:pPr', WD_ALIGN_PARAGRAPH.CENTER, - 'w:p/w:pPr/w:jc{w:val=center}'), - ('w:p/w:pPr/w:jc{w:val=center}', WD_ALIGN_PARAGRAPH.RIGHT, - 'w:p/w:pPr/w:jc{w:val=right}'), - ('w:p/w:pPr/w:jc{w:val=right}', None, - 'w:p/w:pPr'), - ('w:p', None, - 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", WD_ALIGN_PARAGRAPH.LEFT, "w:p/w:pPr/w:jc{w:val=left}"), + ("w:p/w:pPr", WD_ALIGN_PARAGRAPH.CENTER, "w:p/w:pPr/w:jc{w:val=center}"), + ( + "w:p/w:pPr/w:jc{w:val=center}", + WD_ALIGN_PARAGRAPH.RIGHT, + "w:p/w:pPr/w:jc{w:val=right}", + ), + ("w:p/w:pPr/w:jc{w:val=right}", None, "w:p/w:pPr"), + ("w:p", None, "w:p/w:pPr"), + ] + ) def alignment_set_fixture(self, request): p_cxml, value, expected_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:ind', None), - ('w:p/w:pPr/w:ind{w:firstLine=240}', Pt(12)), - ('w:p/w:pPr/w:ind{w:hanging=240}', Pt(-12)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:ind", None), + ("w:p/w:pPr/w:ind{w:firstLine=240}", Pt(12)), + ("w:p/w:pPr/w:ind{w:hanging=240}", Pt(-12)), + ] + ) def first_indent_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', Pt(36), 'w:p/w:pPr/w:ind{w:firstLine=720}'), - ('w:p', Pt(-36), 'w:p/w:pPr/w:ind{w:hanging=720}'), - ('w:p', 0, 'w:p/w:pPr/w:ind{w:firstLine=0}'), - ('w:p', None, 'w:p/w:pPr'), - ('w:p/w:pPr/w:ind{w:firstLine=240}', None, - 'w:p/w:pPr/w:ind'), - ('w:p/w:pPr/w:ind{w:firstLine=240}', Pt(-18), - 'w:p/w:pPr/w:ind{w:hanging=360}'), - ('w:p/w:pPr/w:ind{w:hanging=240}', Pt(18), - 'w:p/w:pPr/w:ind{w:firstLine=360}'), - ]) + @pytest.fixture( + params=[ + ("w:p", Pt(36), "w:p/w:pPr/w:ind{w:firstLine=720}"), + ("w:p", Pt(-36), "w:p/w:pPr/w:ind{w:hanging=720}"), + ("w:p", 0, "w:p/w:pPr/w:ind{w:firstLine=0}"), + ("w:p", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:ind{w:firstLine=240}", None, "w:p/w:pPr/w:ind"), + ( + "w:p/w:pPr/w:ind{w:firstLine=240}", + Pt(-18), + "w:p/w:pPr/w:ind{w:hanging=360}", + ), + ( + "w:p/w:pPr/w:ind{w:hanging=240}", + Pt(18), + "w:p/w:pPr/w:ind{w:firstLine=360}", + ), + ] + ) def first_indent_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:ind', None), - ('w:p/w:pPr/w:ind{w:left=120}', Pt(6)), - ('w:p/w:pPr/w:ind{w:left=-06.3pt}', Pt(-6.3)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:ind", None), + ("w:p/w:pPr/w:ind{w:left=120}", Pt(6)), + ("w:p/w:pPr/w:ind{w:left=-06.3pt}", Pt(-6.3)), + ] + ) def left_indent_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', Pt(36), 'w:p/w:pPr/w:ind{w:left=720}'), - ('w:p', Pt(-3), 'w:p/w:pPr/w:ind{w:left=-60}'), - ('w:p', 0, 'w:p/w:pPr/w:ind{w:left=0}'), - ('w:p', None, 'w:p/w:pPr'), - ('w:p/w:pPr/w:ind{w:left=240}', None, 'w:p/w:pPr/w:ind'), - ]) + @pytest.fixture( + params=[ + ("w:p", Pt(36), "w:p/w:pPr/w:ind{w:left=720}"), + ("w:p", Pt(-3), "w:p/w:pPr/w:ind{w:left=-60}"), + ("w:p", 0, "w:p/w:pPr/w:ind{w:left=0}"), + ("w:p", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:ind{w:left=240}", None, "w:p/w:pPr/w:ind"), + ] + ) def left_indent_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:spacing', None), - ('w:p/w:pPr/w:spacing{w:line=420}', 1.75), - ('w:p/w:pPr/w:spacing{w:line=840,w:lineRule=exact}', Pt(42)), - ('w:p/w:pPr/w:spacing{w:line=840,w:lineRule=atLeast}', Pt(42)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:spacing", None), + ("w:p/w:pPr/w:spacing{w:line=420}", 1.75), + ("w:p/w:pPr/w:spacing{w:line=840,w:lineRule=exact}", Pt(42)), + ("w:p/w:pPr/w:spacing{w:line=840,w:lineRule=atLeast}", Pt(42)), + ] + ) def line_spacing_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', 1, 'w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}'), - ('w:p', 2.0, 'w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}'), - ('w:p', Pt(42), 'w:p/w:pPr/w:spacing{w:line=840,w:lineRule=exact}'), - ('w:p/w:pPr', 2, - 'w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}'), - ('w:p/w:pPr/w:spacing{w:line=360}', 1, - 'w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}'), - ('w:p/w:pPr/w:spacing{w:line=240,w:lineRule=exact}', 1.75, - 'w:p/w:pPr/w:spacing{w:line=420,w:lineRule=auto}'), - ('w:p/w:pPr/w:spacing{w:line=240,w:lineRule=atLeast}', Pt(42), - 'w:p/w:pPr/w:spacing{w:line=840,w:lineRule=atLeast}'), - ('w:p/w:pPr/w:spacing{w:line=240,w:lineRule=exact}', None, - 'w:p/w:pPr/w:spacing'), - ('w:p/w:pPr', None, - 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", 1, "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}"), + ("w:p", 2.0, "w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}"), + ("w:p", Pt(42), "w:p/w:pPr/w:spacing{w:line=840,w:lineRule=exact}"), + ("w:p/w:pPr", 2, "w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}"), + ( + "w:p/w:pPr/w:spacing{w:line=360}", + 1, + "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}", + ), + ( + "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=exact}", + 1.75, + "w:p/w:pPr/w:spacing{w:line=420,w:lineRule=auto}", + ), + ( + "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=atLeast}", + Pt(42), + "w:p/w:pPr/w:spacing{w:line=840,w:lineRule=atLeast}", + ), + ( + "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=exact}", + None, + "w:p/w:pPr/w:spacing", + ), + ("w:p/w:pPr", None, "w:p/w:pPr"), + ] + ) def line_spacing_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:spacing', None), - ('w:p/w:pPr/w:spacing{w:line=240}', WD_LINE_SPACING.SINGLE), - ('w:p/w:pPr/w:spacing{w:line=360}', WD_LINE_SPACING.ONE_POINT_FIVE), - ('w:p/w:pPr/w:spacing{w:line=480}', WD_LINE_SPACING.DOUBLE), - ('w:p/w:pPr/w:spacing{w:line=420}', WD_LINE_SPACING.MULTIPLE), - ('w:p/w:pPr/w:spacing{w:lineRule=auto}', - WD_LINE_SPACING.MULTIPLE), - ('w:p/w:pPr/w:spacing{w:lineRule=exact}', - WD_LINE_SPACING.EXACTLY), - ('w:p/w:pPr/w:spacing{w:lineRule=atLeast}', - WD_LINE_SPACING.AT_LEAST), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:spacing", None), + ("w:p/w:pPr/w:spacing{w:line=240}", WD_LINE_SPACING.SINGLE), + ("w:p/w:pPr/w:spacing{w:line=360}", WD_LINE_SPACING.ONE_POINT_FIVE), + ("w:p/w:pPr/w:spacing{w:line=480}", WD_LINE_SPACING.DOUBLE), + ("w:p/w:pPr/w:spacing{w:line=420}", WD_LINE_SPACING.MULTIPLE), + ("w:p/w:pPr/w:spacing{w:lineRule=auto}", WD_LINE_SPACING.MULTIPLE), + ("w:p/w:pPr/w:spacing{w:lineRule=exact}", WD_LINE_SPACING.EXACTLY), + ("w:p/w:pPr/w:spacing{w:lineRule=atLeast}", WD_LINE_SPACING.AT_LEAST), + ] + ) def line_spacing_rule_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', WD_LINE_SPACING.SINGLE, - 'w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}'), - ('w:p', WD_LINE_SPACING.ONE_POINT_FIVE, - 'w:p/w:pPr/w:spacing{w:line=360,w:lineRule=auto}'), - ('w:p', WD_LINE_SPACING.DOUBLE, - 'w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}'), - ('w:p', WD_LINE_SPACING.MULTIPLE, - 'w:p/w:pPr/w:spacing{w:lineRule=auto}'), - ('w:p', WD_LINE_SPACING.EXACTLY, - 'w:p/w:pPr/w:spacing{w:lineRule=exact}'), - ('w:p/w:pPr/w:spacing{w:line=280,w:lineRule=exact}', - WD_LINE_SPACING.AT_LEAST, - 'w:p/w:pPr/w:spacing{w:line=280,w:lineRule=atLeast}'), - ]) + @pytest.fixture( + params=[ + ( + "w:p", + WD_LINE_SPACING.SINGLE, + "w:p/w:pPr/w:spacing{w:line=240,w:lineRule=auto}", + ), + ( + "w:p", + WD_LINE_SPACING.ONE_POINT_FIVE, + "w:p/w:pPr/w:spacing{w:line=360,w:lineRule=auto}", + ), + ( + "w:p", + WD_LINE_SPACING.DOUBLE, + "w:p/w:pPr/w:spacing{w:line=480,w:lineRule=auto}", + ), + ("w:p", WD_LINE_SPACING.MULTIPLE, "w:p/w:pPr/w:spacing{w:lineRule=auto}"), + ("w:p", WD_LINE_SPACING.EXACTLY, "w:p/w:pPr/w:spacing{w:lineRule=exact}"), + ( + "w:p/w:pPr/w:spacing{w:line=280,w:lineRule=exact}", + WD_LINE_SPACING.AT_LEAST, + "w:p/w:pPr/w:spacing{w:line=280,w:lineRule=atLeast}", + ), + ] + ) def line_spacing_rule_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', 'keep_together', None), - ('w:p/w:pPr/w:keepLines{w:val=on}', 'keep_together', True), - ('w:p/w:pPr/w:keepLines{w:val=0}', 'keep_together', False), - ('w:p', 'keep_with_next', None), - ('w:p/w:pPr/w:keepNext{w:val=1}', 'keep_with_next', True), - ('w:p/w:pPr/w:keepNext{w:val=false}', 'keep_with_next', False), - ('w:p', 'page_break_before', None), - ('w:p/w:pPr/w:pageBreakBefore', 'page_break_before', True), - ('w:p/w:pPr/w:pageBreakBefore{w:val=0}', 'page_break_before', False), - ('w:p', 'widow_control', None), - ('w:p/w:pPr/w:widowControl{w:val=true}', 'widow_control', True), - ('w:p/w:pPr/w:widowControl{w:val=off}', 'widow_control', False), - ]) + @pytest.fixture( + params=[ + ("w:p", "keep_together", None), + ("w:p/w:pPr/w:keepLines{w:val=on}", "keep_together", True), + ("w:p/w:pPr/w:keepLines{w:val=0}", "keep_together", False), + ("w:p", "keep_with_next", None), + ("w:p/w:pPr/w:keepNext{w:val=1}", "keep_with_next", True), + ("w:p/w:pPr/w:keepNext{w:val=false}", "keep_with_next", False), + ("w:p", "page_break_before", None), + ("w:p/w:pPr/w:pageBreakBefore", "page_break_before", True), + ("w:p/w:pPr/w:pageBreakBefore{w:val=0}", "page_break_before", False), + ("w:p", "widow_control", None), + ("w:p/w:pPr/w:widowControl{w:val=true}", "widow_control", True), + ("w:p/w:pPr/w:widowControl{w:val=off}", "widow_control", False), + ] + ) def on_off_get_fixture(self, request): p_cxml, prop_name, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, prop_name, expected_value - @pytest.fixture(params=[ - ('w:p', 'keep_together', True, 'w:p/w:pPr/w:keepLines'), - ('w:p', 'keep_with_next', True, 'w:p/w:pPr/w:keepNext'), - ('w:p', 'page_break_before', True, 'w:p/w:pPr/w:pageBreakBefore'), - ('w:p', 'widow_control', True, 'w:p/w:pPr/w:widowControl'), - ('w:p/w:pPr/w:keepLines', 'keep_together', False, - 'w:p/w:pPr/w:keepLines{w:val=0}'), - ('w:p/w:pPr/w:keepNext', 'keep_with_next', False, - 'w:p/w:pPr/w:keepNext{w:val=0}'), - ('w:p/w:pPr/w:pageBreakBefore', 'page_break_before', False, - 'w:p/w:pPr/w:pageBreakBefore{w:val=0}'), - ('w:p/w:pPr/w:widowControl', 'widow_control', False, - 'w:p/w:pPr/w:widowControl{w:val=0}'), - ('w:p/w:pPr/w:keepLines{w:val=0}', 'keep_together', None, - 'w:p/w:pPr'), - ('w:p/w:pPr/w:keepNext{w:val=0}', 'keep_with_next', None, - 'w:p/w:pPr'), - ('w:p/w:pPr/w:pageBreakBefore{w:val=0}', 'page_break_before', None, - 'w:p/w:pPr'), - ('w:p/w:pPr/w:widowControl{w:val=0}', 'widow_control', None, - 'w:p/w:pPr'), - ]) + @pytest.fixture( + params=[ + ("w:p", "keep_together", True, "w:p/w:pPr/w:keepLines"), + ("w:p", "keep_with_next", True, "w:p/w:pPr/w:keepNext"), + ("w:p", "page_break_before", True, "w:p/w:pPr/w:pageBreakBefore"), + ("w:p", "widow_control", True, "w:p/w:pPr/w:widowControl"), + ( + "w:p/w:pPr/w:keepLines", + "keep_together", + False, + "w:p/w:pPr/w:keepLines{w:val=0}", + ), + ( + "w:p/w:pPr/w:keepNext", + "keep_with_next", + False, + "w:p/w:pPr/w:keepNext{w:val=0}", + ), + ( + "w:p/w:pPr/w:pageBreakBefore", + "page_break_before", + False, + "w:p/w:pPr/w:pageBreakBefore{w:val=0}", + ), + ( + "w:p/w:pPr/w:widowControl", + "widow_control", + False, + "w:p/w:pPr/w:widowControl{w:val=0}", + ), + ("w:p/w:pPr/w:keepLines{w:val=0}", "keep_together", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:keepNext{w:val=0}", "keep_with_next", None, "w:p/w:pPr"), + ( + "w:p/w:pPr/w:pageBreakBefore{w:val=0}", + "page_break_before", + None, + "w:p/w:pPr", + ), + ("w:p/w:pPr/w:widowControl{w:val=0}", "widow_control", None, "w:p/w:pPr"), + ] + ) def on_off_set_fixture(self, request): p_cxml, prop_name, value, expected_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_cxml) return paragraph_format, prop_name, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:ind', None), - ('w:p/w:pPr/w:ind{w:right=160}', Pt(8)), - ('w:p/w:pPr/w:ind{w:right=-4.2pt}', Pt(-4.2)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:ind", None), + ("w:p/w:pPr/w:ind{w:right=160}", Pt(8)), + ("w:p/w:pPr/w:ind{w:right=-4.2pt}", Pt(-4.2)), + ] + ) def right_indent_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', Pt(36), 'w:p/w:pPr/w:ind{w:right=720}'), - ('w:p', Pt(-3), 'w:p/w:pPr/w:ind{w:right=-60}'), - ('w:p', 0, 'w:p/w:pPr/w:ind{w:right=0}'), - ('w:p', None, 'w:p/w:pPr'), - ('w:p/w:pPr/w:ind{w:right=240}', None, 'w:p/w:pPr/w:ind'), - ]) + @pytest.fixture( + params=[ + ("w:p", Pt(36), "w:p/w:pPr/w:ind{w:right=720}"), + ("w:p", Pt(-3), "w:p/w:pPr/w:ind{w:right=-60}"), + ("w:p", 0, "w:p/w:pPr/w:ind{w:right=0}"), + ("w:p", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:ind{w:right=240}", None, "w:p/w:pPr/w:ind"), + ] + ) def right_indent_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:spacing', None), - ('w:p/w:pPr/w:spacing{w:after=240}', Pt(12)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:spacing", None), + ("w:p/w:pPr/w:spacing{w:after=240}", Pt(12)), + ] + ) def space_after_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', Pt(12), 'w:p/w:pPr/w:spacing{w:after=240}'), - ('w:p', None, 'w:p/w:pPr'), - ('w:p/w:pPr', Pt(12), 'w:p/w:pPr/w:spacing{w:after=240}'), - ('w:p/w:pPr', None, 'w:p/w:pPr'), - ('w:p/w:pPr/w:spacing', Pt(12), 'w:p/w:pPr/w:spacing{w:after=240}'), - ('w:p/w:pPr/w:spacing', None, 'w:p/w:pPr/w:spacing'), - ('w:p/w:pPr/w:spacing{w:after=240}', Pt(42), - 'w:p/w:pPr/w:spacing{w:after=840}'), - ('w:p/w:pPr/w:spacing{w:after=840}', None, - 'w:p/w:pPr/w:spacing'), - ]) + @pytest.fixture( + params=[ + ("w:p", Pt(12), "w:p/w:pPr/w:spacing{w:after=240}"), + ("w:p", None, "w:p/w:pPr"), + ("w:p/w:pPr", Pt(12), "w:p/w:pPr/w:spacing{w:after=240}"), + ("w:p/w:pPr", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:spacing", Pt(12), "w:p/w:pPr/w:spacing{w:after=240}"), + ("w:p/w:pPr/w:spacing", None, "w:p/w:pPr/w:spacing"), + ( + "w:p/w:pPr/w:spacing{w:after=240}", + Pt(42), + "w:p/w:pPr/w:spacing{w:after=840}", + ), + ("w:p/w:pPr/w:spacing{w:after=840}", None, "w:p/w:pPr/w:spacing"), + ] + ) def space_after_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) expected_xml = xml(expected_p_cxml) return paragraph_format, value, expected_xml - @pytest.fixture(params=[ - ('w:p', None), - ('w:p/w:pPr', None), - ('w:p/w:pPr/w:spacing', None), - ('w:p/w:pPr/w:spacing{w:before=420}', Pt(21)), - ]) + @pytest.fixture( + params=[ + ("w:p", None), + ("w:p/w:pPr", None), + ("w:p/w:pPr/w:spacing", None), + ("w:p/w:pPr/w:spacing{w:before=420}", Pt(21)), + ] + ) def space_before_get_fixture(self, request): p_cxml, expected_value = request.param paragraph_format = ParagraphFormat(element(p_cxml)) return paragraph_format, expected_value - @pytest.fixture(params=[ - ('w:p', Pt(12), 'w:p/w:pPr/w:spacing{w:before=240}'), - ('w:p', None, 'w:p/w:pPr'), - ('w:p/w:pPr', Pt(12), 'w:p/w:pPr/w:spacing{w:before=240}'), - ('w:p/w:pPr', None, 'w:p/w:pPr'), - ('w:p/w:pPr/w:spacing', Pt(12), 'w:p/w:pPr/w:spacing{w:before=240}'), - ('w:p/w:pPr/w:spacing', None, 'w:p/w:pPr/w:spacing'), - ('w:p/w:pPr/w:spacing{w:before=240}', Pt(42), - 'w:p/w:pPr/w:spacing{w:before=840}'), - ('w:p/w:pPr/w:spacing{w:before=840}', None, - 'w:p/w:pPr/w:spacing'), - ]) + @pytest.fixture( + params=[ + ("w:p", Pt(12), "w:p/w:pPr/w:spacing{w:before=240}"), + ("w:p", None, "w:p/w:pPr"), + ("w:p/w:pPr", Pt(12), "w:p/w:pPr/w:spacing{w:before=240}"), + ("w:p/w:pPr", None, "w:p/w:pPr"), + ("w:p/w:pPr/w:spacing", Pt(12), "w:p/w:pPr/w:spacing{w:before=240}"), + ("w:p/w:pPr/w:spacing", None, "w:p/w:pPr/w:spacing"), + ( + "w:p/w:pPr/w:spacing{w:before=240}", + Pt(42), + "w:p/w:pPr/w:spacing{w:before=840}", + ), + ("w:p/w:pPr/w:spacing{w:before=840}", None, "w:p/w:pPr/w:spacing"), + ] + ) def space_before_set_fixture(self, request): p_cxml, value, expected_p_cxml = request.param paragraph_format = ParagraphFormat(element(p_cxml)) @@ -404,7 +470,7 @@ def space_before_set_fixture(self, request): @pytest.fixture def tab_stops_fixture(self, TabStops_, tab_stops_): - p = element('w:p/w:pPr') + p = element("w:p/w:pPr") pPr = p.pPr paragraph_format = ParagraphFormat(p, None) return paragraph_format, TabStops_, pPr, tab_stops_ @@ -413,9 +479,7 @@ def tab_stops_fixture(self, TabStops_, tab_stops_): @pytest.fixture def TabStops_(self, request, tab_stops_): - return class_mock( - request, 'docx.text.parfmt.TabStops', return_value=tab_stops_ - ) + return class_mock(request, "docx.text.parfmt.TabStops", return_value=tab_stops_) @pytest.fixture def tab_stops_(self, request): diff --git a/tests/text/test_run.py b/tests/text/test_run.py index ae23c641c..3d5e82cd9 100644 --- a/tests/text/test_run.py +++ b/tests/text/test_run.py @@ -1,23 +1,28 @@ -# encoding: utf-8 +# pyright: reportPrivateUsage=false -"""Test suite for the docx.text.run module""" +"""Test suite for the docx.text.run module.""" -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import annotations +from typing import Any, List, cast + +import pytest + +from docx import types as t from docx.enum.style import WD_STYLE_TYPE from docx.enum.text import WD_BREAK, WD_UNDERLINE +from docx.oxml.text.run import CT_R from docx.parts.document import DocumentPart from docx.shape import InlineShape from docx.text.font import Font from docx.text.run import Run -import pytest - from ..unitutil.cxml import element, xml from ..unitutil.mock import class_mock, instance_mock, property_mock -class DescribeRun(object): +class DescribeRun: + """Unit-test suite for `docx.text.run.Run`.""" def it_knows_its_bool_prop_states(self, bool_prop_get_fixture): run, prop_name, expected_state = bool_prop_get_fixture @@ -28,20 +33,63 @@ def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture): setattr(run, prop_name, value) assert run._r.xml == expected_xml + @pytest.mark.parametrize( + ("r_cxml", "expected_value"), + [ + ("w:r", False), + ('w:r/w:t"foobar"', False), + ('w:r/(w:t"abc", w:lastRenderedPageBreak, w:t"def")', True), + ("w:r/(w:lastRenderedPageBreak, w:lastRenderedPageBreak)", True), + ], + ) + def it_knows_whether_it_contains_a_page_break( + self, r_cxml: str, expected_value: bool + ): + r = cast(CT_R, element(r_cxml)) + run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues] + + assert run.contains_page_break == expected_value + + @pytest.mark.parametrize( + ("r_cxml", "expected"), + [ + # -- no content produces an empty iterator -- + ("w:r", []), + # -- contiguous text content is condensed into a single str -- + ('w:r/(w:t"foo",w:cr,w:t"bar")', ["str"]), + # -- page-breaks are a form of inner-content -- + ( + 'w:r/(w:t"abc",w:br,w:lastRenderedPageBreak,w:noBreakHyphen,w:t"def")', + ["str", "RenderedPageBreak", "str"], + ), + # -- as are drawings -- + ( + 'w:r/(w:t"abc", w:lastRenderedPageBreak, w:drawing)', + ["str", "RenderedPageBreak", "Drawing"], + ), + ], + ) + def it_can_iterate_its_inner_content_items( + self, r_cxml: str, expected: List[str], fake_parent: t.StoryChild + ): + r = cast(CT_R, element(r_cxml)) + run = Run(r, fake_parent) + + inner_content = run.iter_inner_content() + + actual = [type(item).__name__ for item in inner_content] + assert actual == expected, f"expected: {expected}, got: {actual}" + def it_knows_its_character_style(self, style_get_fixture): run, style_id_, style_ = style_get_fixture style = run.style - run.part.get_style.assert_called_once_with( - style_id_, WD_STYLE_TYPE.CHARACTER - ) + run.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.CHARACTER) assert style is style_ def it_can_change_its_character_style(self, style_set_fixture): run, value, expected_xml = style_set_fixture run.style = value - run.part.get_style_id.assert_called_once_with( - value, WD_STYLE_TYPE.CHARACTER - ) + run.part.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.CHARACTER) assert run._r.xml == expected_xml def it_knows_its_underline_type(self, underline_get_fixture): @@ -53,11 +101,12 @@ def it_can_change_its_underline_type(self, underline_set_fixture): run.underline = underline assert run._r.xml == expected_xml - def it_raises_on_assign_invalid_underline_type( - self, underline_raise_fixture): - run, underline = underline_raise_fixture - with pytest.raises(ValueError): - run.underline = underline + @pytest.mark.parametrize("invalid_value", ["foobar", 42, "single"]) + def it_raises_on_assign_invalid_underline_value(self, invalid_value: Any): + r = cast(CT_R, element("w:r/w:rPr")) + run = Run(r, None) + with pytest.raises(ValueError, match=" is not a valid WD_UNDERLINE"): + run.underline = invalid_value def it_provides_access_to_its_font(self, font_fixture): run, Font_, font_ = font_fixture @@ -74,9 +123,24 @@ def it_can_add_text(self, add_text_fixture, Text_): assert run._r.xml == expected_xml assert _text is Text_.return_value - def it_can_add_a_break(self, add_break_fixture): - run, break_type, expected_xml = add_break_fixture + @pytest.mark.parametrize( + ("break_type", "expected_cxml"), + [ + (WD_BREAK.LINE, "w:r/w:br"), + (WD_BREAK.PAGE, "w:r/w:br{w:type=page}"), + (WD_BREAK.COLUMN, "w:r/w:br{w:type=column}"), + (WD_BREAK.LINE_CLEAR_LEFT, "w:r/w:br{w:clear=left}"), + (WD_BREAK.LINE_CLEAR_RIGHT, "w:r/w:br{w:clear=right}"), + (WD_BREAK.LINE_CLEAR_ALL, "w:r/w:br{w:clear=all}"), + ], + ) + def it_can_add_a_break(self, break_type: WD_BREAK, expected_cxml: str): + r = cast(CT_R, element("w:r")) + run = Run(r, None) # pyright:ignore[reportGeneralTypeIssues] + expected_xml = xml(expected_cxml) + run.add_break(break_type) + assert run._r.xml == expected_xml def it_can_add_a_tab(self, add_tab_fixture): @@ -95,14 +159,44 @@ def it_can_add_a_picture(self, add_picture_fixture): InlineShape_.assert_called_once_with(inline) assert picture is picture_ - def it_can_remove_its_content_but_keep_formatting(self, clear_fixture): - run, expected_xml = clear_fixture - _run = run.clear() - assert run._r.xml == expected_xml - assert _run is run + @pytest.mark.parametrize( + ("initial_r_cxml", "expected_cxml"), + [ + ("w:r", "w:r"), + ('w:r/w:t"foo"', "w:r"), + ("w:r/w:br", "w:r"), + ("w:r/w:rPr", "w:r/w:rPr"), + ('w:r/(w:rPr, w:t"foo")', "w:r/w:rPr"), + ( + 'w:r/(w:rPr/(w:b, w:i), w:t"foo", w:cr, w:t"bar")', + "w:r/w:rPr/(w:b, w:i)", + ), + ], + ) + def it_can_remove_its_content_but_keep_formatting( + self, initial_r_cxml: str, expected_cxml: str + ): + r = cast(CT_R, element(initial_r_cxml)) + run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues] + expected_xml = xml(expected_cxml) - def it_knows_the_text_it_contains(self, text_get_fixture): - run, expected_text = text_get_fixture + cleared_run = run.clear() + + assert run._r.xml == expected_xml + assert cleared_run is run + + @pytest.mark.parametrize( + ("r_cxml", "expected_text"), + [ + ("w:r", ""), + ('w:r/w:t"foobar"', "foobar"), + ('w:r/(w:t"abc", w:tab, w:t"def", w:cr)', "abc\tdef\n"), + ('w:r/(w:br{w:type=page}, w:t"abc", w:t"def", w:tab)', "abcdef\t"), + ], + ) + def it_knows_the_text_it_contains(self, r_cxml: str, expected_text: str): + r = cast(CT_R, element(r_cxml)) + run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues] assert run.text == expected_text def it_can_replace_the_text_it_contains(self, text_set_fixture): @@ -112,136 +206,109 @@ def it_can_replace_the_text_it_contains(self, text_set_fixture): # fixtures ------------------------------------------------------- - @pytest.fixture(params=[ - (WD_BREAK.LINE, 'w:r/w:br'), - (WD_BREAK.PAGE, 'w:r/w:br{w:type=page}'), - (WD_BREAK.COLUMN, 'w:r/w:br{w:type=column}'), - (WD_BREAK.LINE_CLEAR_LEFT, - 'w:r/w:br{w:type=textWrapping, w:clear=left}'), - (WD_BREAK.LINE_CLEAR_RIGHT, - 'w:r/w:br{w:type=textWrapping, w:clear=right}'), - (WD_BREAK.LINE_CLEAR_ALL, - 'w:r/w:br{w:type=textWrapping, w:clear=all}'), - ]) - def add_break_fixture(self, request): - break_type, expected_cxml = request.param - run = Run(element('w:r'), None) - expected_xml = xml(expected_cxml) - return run, break_type, expected_xml - @pytest.fixture - def add_picture_fixture(self, part_prop_, document_part_, InlineShape_, - picture_): - run = Run(element('w:r/wp:x'), None) - image = 'foobar.png' - width, height, inline = 1111, 2222, element('wp:inline{id=42}') - expected_xml = xml('w:r/(wp:x,w:drawing/wp:inline{id=42})') + def add_picture_fixture(self, part_prop_, document_part_, InlineShape_, picture_): + run = Run(element("w:r/wp:x"), None) + image = "foobar.png" + width, height, inline = 1111, 2222, element("wp:inline{id=42}") + expected_xml = xml("w:r/(wp:x,w:drawing/wp:inline{id=42})") document_part_.new_pic_inline.return_value = inline InlineShape_.return_value = picture_ - return ( - run, image, width, height, inline, expected_xml, InlineShape_, - picture_ - ) - - @pytest.fixture(params=[ - ('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)'), - ]) + return (run, image, width, height, inline, expected_xml, InlineShape_, picture_) + + @pytest.fixture( + params=[ + ('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)'), + ] + ) def add_tab_fixture(self, request): r_cxml, expected_cxml = request.param run = Run(element(r_cxml), None) expected_xml = xml(expected_cxml) return run, expected_xml - @pytest.fixture(params=[ - ('w:r', 'foo', 'w:r/w:t"foo"'), - ('w:r/w:t"foo"', 'bar', 'w:r/(w:t"foo", w:t"bar")'), - ('w:r', 'fo ', 'w:r/w:t{xml:space=preserve}"fo "'), - ('w:r', 'f o', 'w:r/w:t"f o"'), - ]) + @pytest.fixture( + params=[ + ("w:r", "foo", 'w:r/w:t"foo"'), + ('w:r/w:t"foo"', "bar", 'w:r/(w:t"foo", w:t"bar")'), + ("w:r", "fo ", 'w:r/w:t{xml:space=preserve}"fo "'), + ("w:r", "f o", 'w:r/w:t"f o"'), + ] + ) def add_text_fixture(self, request): r_cxml, text, expected_cxml = request.param r = element(r_cxml) expected_xml = xml(expected_cxml) return r, text, expected_xml - @pytest.fixture(params=[ - ('w:r/w:rPr', 'bold', None), - ('w:r/w:rPr/w:b', 'bold', True), - ('w:r/w:rPr/w:b{w:val=on}', 'bold', True), - ('w:r/w:rPr/w:b{w:val=off}', 'bold', False), - ('w:r/w:rPr/w:b{w:val=1}', 'bold', True), - ('w:r/w:rPr/w:i{w:val=0}', 'italic', False), - ]) + @pytest.fixture( + params=[ + ("w:r/w:rPr", "bold", None), + ("w:r/w:rPr/w:b", "bold", True), + ("w:r/w:rPr/w:b{w:val=on}", "bold", True), + ("w:r/w:rPr/w:b{w:val=off}", "bold", False), + ("w:r/w:rPr/w:b{w:val=1}", "bold", True), + ("w:r/w:rPr/w:i{w:val=0}", "italic", False), + ] + ) def bool_prop_get_fixture(self, request): r_cxml, bool_prop_name, expected_value = request.param run = Run(element(r_cxml), None) return run, bool_prop_name, expected_value - @pytest.fixture(params=[ - # nothing to True, False, and None --------------------------- - ('w:r', 'bold', True, 'w:r/w:rPr/w:b'), - ('w:r', 'bold', False, 'w:r/w:rPr/w:b{w:val=0}'), - ('w:r', 'italic', None, 'w:r/w:rPr'), - # default to True, False, and None --------------------------- - ('w:r/w:rPr/w:b', 'bold', True, 'w:r/w:rPr/w:b'), - ('w:r/w:rPr/w:b', 'bold', False, 'w:r/w:rPr/w:b{w:val=0}'), - ('w:r/w:rPr/w:i', 'italic', None, 'w:r/w:rPr'), - # True to True, False, and None ------------------------------ - ('w:r/w:rPr/w:b{w:val=on}', 'bold', True, 'w:r/w:rPr/w:b'), - ('w:r/w:rPr/w:b{w:val=1}', 'bold', False, 'w:r/w:rPr/w:b{w:val=0}'), - ('w:r/w:rPr/w:b{w:val=1}', 'bold', None, 'w:r/w:rPr'), - # False to True, False, and None ----------------------------- - ('w:r/w:rPr/w:i{w:val=false}', 'italic', True, 'w:r/w:rPr/w:i'), - ('w:r/w:rPr/w:i{w:val=0}', 'italic', False, - 'w:r/w:rPr/w:i{w:val=0}'), - ('w:r/w:rPr/w:i{w:val=off}', 'italic', None, 'w:r/w:rPr'), - ]) + @pytest.fixture( + params=[ + # nothing to True, False, and None --------------------------- + ("w:r", "bold", True, "w:r/w:rPr/w:b"), + ("w:r", "bold", False, "w:r/w:rPr/w:b{w:val=0}"), + ("w:r", "italic", None, "w:r/w:rPr"), + # default to True, False, and None --------------------------- + ("w:r/w:rPr/w:b", "bold", True, "w:r/w:rPr/w:b"), + ("w:r/w:rPr/w:b", "bold", False, "w:r/w:rPr/w:b{w:val=0}"), + ("w:r/w:rPr/w:i", "italic", None, "w:r/w:rPr"), + # True to True, False, and None ------------------------------ + ("w:r/w:rPr/w:b{w:val=on}", "bold", True, "w:r/w:rPr/w:b"), + ("w:r/w:rPr/w:b{w:val=1}", "bold", False, "w:r/w:rPr/w:b{w:val=0}"), + ("w:r/w:rPr/w:b{w:val=1}", "bold", None, "w:r/w:rPr"), + # False to True, False, and None ----------------------------- + ("w:r/w:rPr/w:i{w:val=false}", "italic", True, "w:r/w:rPr/w:i"), + ("w:r/w:rPr/w:i{w:val=0}", "italic", False, "w:r/w:rPr/w:i{w:val=0}"), + ("w:r/w:rPr/w:i{w:val=off}", "italic", None, "w:r/w:rPr"), + ] + ) def bool_prop_set_fixture(self, request): initial_r_cxml, bool_prop_name, value, expected_cxml = request.param run = Run(element(initial_r_cxml), None) expected_xml = xml(expected_cxml) return run, bool_prop_name, value, expected_xml - @pytest.fixture(params=[ - ('w:r', 'w:r'), - ('w:r/w:t"foo"', 'w:r'), - ('w:r/w:br', 'w:r'), - ('w:r/w:rPr', 'w:r/w:rPr'), - ('w:r/(w:rPr, w:t"foo")', 'w:r/w:rPr'), - ('w:r/(w:rPr/(w:b, w:i), w:t"foo", w:cr, w:t"bar")', - 'w:r/w:rPr/(w:b, w:i)'), - ]) - def clear_fixture(self, request): - initial_r_cxml, expected_cxml = request.param - run = Run(element(initial_r_cxml), None) - expected_xml = xml(expected_cxml) - return run, expected_xml - @pytest.fixture def font_fixture(self, Font_, font_): - run = Run(element('w:r'), None) + run = Run(element("w:r"), None) return run, Font_, font_ @pytest.fixture def style_get_fixture(self, part_prop_): - style_id = 'Barfoo' - r_cxml = 'w:r/w:rPr/w:rStyle{w:val=%s}' % style_id + style_id = "Barfoo" + r_cxml = "w:r/w:rPr/w:rStyle{w:val=%s}" % style_id run = Run(element(r_cxml), None) style_ = part_prop_.return_value.get_style.return_value return run, style_id, style_ - @pytest.fixture(params=[ - ('w:r', 'Foo Font', 'FooFont', - 'w:r/w:rPr/w:rStyle{w:val=FooFont}'), - ('w:r/w:rPr', 'Foo Font', 'FooFont', - 'w:r/w:rPr/w:rStyle{w:val=FooFont}'), - ('w:r/w:rPr/w:rStyle{w:val=FooFont}', 'Bar Font', 'BarFont', - 'w:r/w:rPr/w:rStyle{w:val=BarFont}'), - ('w:r/w:rPr/w:rStyle{w:val=FooFont}', None, None, - 'w:r/w:rPr'), - ('w:r', None, None, - 'w:r/w:rPr'), - ]) + @pytest.fixture( + params=[ + ("w:r", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"), + ("w:r/w:rPr", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"), + ( + "w:r/w:rPr/w:rStyle{w:val=FooFont}", + "Bar Font", + "BarFont", + "w:r/w:rPr/w:rStyle{w:val=BarFont}", + ), + ("w:r/w:rPr/w:rStyle{w:val=FooFont}", None, None, "w:r/w:rPr"), + ("w:r", None, None, "w:r/w:rPr"), + ] + ) def style_set_fixture(self, request, part_prop_): r_cxml, value, style_id, expected_cxml = request.param run = Run(element(r_cxml), None) @@ -249,23 +316,14 @@ def style_set_fixture(self, request, part_prop_): expected_xml = xml(expected_cxml) return run, value, expected_xml - @pytest.fixture(params=[ - ('w:r', ''), - ('w:r/w:t"foobar"', 'foobar'), - ('w:r/(w:t"abc", w:tab, w:t"def", w:cr)', 'abc\tdef\n'), - ('w:r/(w:br{w:type=page}, w:t"abc", w:t"def", w:tab)', '\nabcdef\t'), - ]) - def text_get_fixture(self, request): - r_cxml, expected_text = request.param - run = Run(element(r_cxml), None) - return run, expected_text - - @pytest.fixture(params=[ - ('abc def', 'w:r/w:t"abc def"'), - ('abc\tdef', 'w:r/(w:t"abc", w:tab, w:t"def")'), - ('abc\ndef', 'w:r/(w:t"abc", w:br, w:t"def")'), - ('abc\rdef', 'w:r/(w:t"abc", w:br, w:t"def")'), - ]) + @pytest.fixture( + params=[ + ("abc def", 'w:r/w:t"abc def"'), + ("abc\tdef", 'w:r/(w:t"abc", w:tab, w:t"def")'), + ("abc\ndef", 'w:r/(w:t"abc", w:br, w:t"def")'), + ("abc\rdef", 'w:r/(w:t"abc", w:br, w:t"def")'), + ] + ) def text_set_fixture(self, request): new_text, expected_cxml = request.param initial_r_cxml = 'w:r/w:t"should get deleted"' @@ -273,48 +331,49 @@ def text_set_fixture(self, request): expected_xml = xml(expected_cxml) return run, new_text, expected_xml - @pytest.fixture(params=[ - ('w:r', None), - ('w:r/w:rPr/w:u', None), - ('w:r/w:rPr/w:u{w:val=single}', True), - ('w:r/w:rPr/w:u{w:val=none}', False), - ('w:r/w:rPr/w:u{w:val=double}', WD_UNDERLINE.DOUBLE), - ('w:r/w:rPr/w:u{w:val=wave}', WD_UNDERLINE.WAVY), - ]) + @pytest.fixture( + params=[ + ("w:r", None), + ("w:r/w:rPr/w:u", None), + ("w:r/w:rPr/w:u{w:val=single}", True), + ("w:r/w:rPr/w:u{w:val=none}", False), + ("w:r/w:rPr/w:u{w:val=double}", WD_UNDERLINE.DOUBLE), + ("w:r/w:rPr/w:u{w:val=wave}", WD_UNDERLINE.WAVY), + ] + ) def underline_get_fixture(self, request): r_cxml, expected_underline = request.param run = Run(element(r_cxml), None) return run, expected_underline - @pytest.fixture(params=[ - ('w:r', True, 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r', False, 'w:r/w:rPr/w:u{w:val=none}'), - ('w:r', None, 'w:r/w:rPr'), - ('w:r', WD_UNDERLINE.SINGLE, 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r', WD_UNDERLINE.THICK, 'w:r/w:rPr/w:u{w:val=thick}'), - ('w:r/w:rPr/w:u{w:val=single}', True, - 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r/w:rPr/w:u{w:val=single}', False, - 'w:r/w:rPr/w:u{w:val=none}'), - ('w:r/w:rPr/w:u{w:val=single}', None, - 'w:r/w:rPr'), - ('w:r/w:rPr/w:u{w:val=single}', WD_UNDERLINE.SINGLE, - 'w:r/w:rPr/w:u{w:val=single}'), - ('w:r/w:rPr/w:u{w:val=single}', WD_UNDERLINE.DOTTED, - 'w:r/w:rPr/w:u{w:val=dotted}'), - ]) + @pytest.fixture( + params=[ + ("w:r", True, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r", False, "w:r/w:rPr/w:u{w:val=none}"), + ("w:r", None, "w:r/w:rPr"), + ("w:r", WD_UNDERLINE.SINGLE, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r", WD_UNDERLINE.THICK, "w:r/w:rPr/w:u{w:val=thick}"), + ("w:r/w:rPr/w:u{w:val=single}", True, "w:r/w:rPr/w:u{w:val=single}"), + ("w:r/w:rPr/w:u{w:val=single}", False, "w:r/w:rPr/w:u{w:val=none}"), + ("w:r/w:rPr/w:u{w:val=single}", None, "w:r/w:rPr"), + ( + "w:r/w:rPr/w:u{w:val=single}", + WD_UNDERLINE.SINGLE, + "w:r/w:rPr/w:u{w:val=single}", + ), + ( + "w:r/w:rPr/w:u{w:val=single}", + WD_UNDERLINE.DOTTED, + "w:r/w:rPr/w:u{w:val=dotted}", + ), + ] + ) def underline_set_fixture(self, request): initial_r_cxml, new_underline, expected_cxml = request.param run = Run(element(initial_r_cxml), None) expected_xml = xml(expected_cxml) return run, new_underline, expected_xml - @pytest.fixture(params=['foobar', 42, 'single']) - def underline_raise_fixture(self, request): - invalid_underline_setting = request.param - run = Run(element('w:r/w:rPr'), None) - return run, invalid_underline_setting - # fixture components --------------------------------------------- @pytest.fixture @@ -323,7 +382,7 @@ def document_part_(self, request): @pytest.fixture def Font_(self, request, font_): - return class_mock(request, 'docx.text.run.Font', return_value=font_) + return class_mock(request, "docx.text.run.Font", return_value=font_) @pytest.fixture def font_(self, request): @@ -331,13 +390,11 @@ def font_(self, request): @pytest.fixture def InlineShape_(self, request): - return class_mock(request, 'docx.text.run.InlineShape') + return class_mock(request, "docx.text.run.InlineShape") @pytest.fixture def part_prop_(self, request, document_part_): - return property_mock( - request, Run, 'part', return_value=document_part_ - ) + return property_mock(request, Run, "part", return_value=document_part_) @pytest.fixture def picture_(self, request): @@ -345,4 +402,4 @@ def picture_(self, request): @pytest.fixture def Text_(self, request): - return class_mock(request, 'docx.text.run._Text') + return class_mock(request, "docx.text.run._Text") diff --git a/tests/text/test_tabstops.py b/tests/text/test_tabstops.py index 0ca10e62d..79d920c24 100644 --- a/tests/text/test_tabstops.py +++ b/tests/text/test_tabstops.py @@ -1,26 +1,16 @@ -# encoding: utf-8 +"""Test suite for the docx.text.tabstops module.""" -""" -Test suite for the docx.text.tabstops module, containing the TabStops and -TabStop objects. -""" - -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +import pytest from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER from docx.shared import Twips from docx.text.tabstops import TabStop, TabStops -import pytest - from ..unitutil.cxml import element, xml from ..unitutil.mock import call, class_mock, instance_mock -class DescribeTabStop(object): - +class DescribeTabStop: def it_knows_its_position(self, position_get_fixture): tab_stop, expected_value = position_get_fixture assert tab_stop.position == expected_value @@ -51,20 +41,24 @@ def it_can_change_its_leader(self, leader_set_fixture): # fixture -------------------------------------------------------- - @pytest.fixture(params=[ - ('w:tab{w:val=left}', 'LEFT'), - ('w:tab{w:val=right}', 'RIGHT'), - ]) + @pytest.fixture( + params=[ + ("w:tab{w:val=left}", "LEFT"), + ("w:tab{w:val=right}", "RIGHT"), + ] + ) def alignment_get_fixture(self, request): tab_stop_cxml, member = request.param tab_stop = TabStop(element(tab_stop_cxml)) expected_value = getattr(WD_TAB_ALIGNMENT, member) return tab_stop, expected_value - @pytest.fixture(params=[ - ('w:tab{w:val=left}', 'RIGHT', 'w:tab{w:val=right}'), - ('w:tab{w:val=right}', 'LEFT', 'w:tab{w:val=left}'), - ]) + @pytest.fixture( + params=[ + ("w:tab{w:val=left}", "RIGHT", "w:tab{w:val=right}"), + ("w:tab{w:val=right}", "LEFT", "w:tab{w:val=left}"), + ] + ) def alignment_set_fixture(self, request): tab_stop_cxml, member, expected_cxml = request.param tab_stop = TabStop(element(tab_stop_cxml)) @@ -72,56 +66,75 @@ def alignment_set_fixture(self, request): value = getattr(WD_TAB_ALIGNMENT, member) return tab_stop, value, expected_xml - @pytest.fixture(params=[ - ('w:tab', 'SPACES'), - ('w:tab{w:leader=none}', 'SPACES'), - ('w:tab{w:leader=dot}', 'DOTS'), - ]) + @pytest.fixture( + params=[ + ("w:tab", "SPACES"), + ("w:tab{w:leader=none}", "SPACES"), + ("w:tab{w:leader=dot}", "DOTS"), + ] + ) def leader_get_fixture(self, request): tab_stop_cxml, member = request.param tab_stop = TabStop(element(tab_stop_cxml)) expected_value = getattr(WD_TAB_LEADER, member) return tab_stop, expected_value - @pytest.fixture(params=[ - ('w:tab', 'DOTS', 'w:tab{w:leader=dot}'), - ('w:tab{w:leader=dot}', 'DASHES', 'w:tab{w:leader=hyphen}'), - ('w:tab{w:leader=hyphen}', 'SPACES', 'w:tab'), - ('w:tab{w:leader=hyphen}', None, 'w:tab'), - ('w:tab', 'SPACES', 'w:tab'), - ('w:tab', None, 'w:tab'), - ]) + @pytest.fixture( + params=[ + ("w:tab", "DOTS", "w:tab{w:leader=dot}"), + ("w:tab{w:leader=dot}", "DASHES", "w:tab{w:leader=hyphen}"), + ("w:tab{w:leader=hyphen}", "SPACES", "w:tab"), + ("w:tab{w:leader=hyphen}", None, "w:tab"), + ("w:tab", "SPACES", "w:tab"), + ("w:tab", None, "w:tab"), + ] + ) def leader_set_fixture(self, request): tab_stop_cxml, new_value, expected_cxml = request.param tab_stop = TabStop(element(tab_stop_cxml)) - value = ( - None if new_value is None else getattr(WD_TAB_LEADER, new_value) - ) + value = None if new_value is None else getattr(WD_TAB_LEADER, new_value) expected_xml = xml(expected_cxml) return tab_stop, value, expected_xml @pytest.fixture def position_get_fixture(self, request): - tab_stop = TabStop(element('w:tab{w:pos=720}')) + tab_stop = TabStop(element("w:tab{w:pos=720}")) return tab_stop, Twips(720) - @pytest.fixture(params=[ - ('w:tabs/w:tab{w:pos=360,w:val=left}', - Twips(720), 0, - 'w:tabs/w:tab{w:pos=720,w:val=left}'), - ('w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,w:val=left})', - Twips(180), 0, - 'w:tabs/(w:tab{w:pos=180,w:val=left},w:tab{w:pos=720,w:val=left})'), - ('w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,w:val=left})', - Twips(960), 1, - 'w:tabs/(w:tab{w:pos=720,w:val=left},w:tab{w:pos=960,w:val=left})'), - ('w:tabs/(w:tab{w:pos=-72,w:val=left},w:tab{w:pos=-36,w:val=left})', - Twips(-48), 0, - 'w:tabs/(w:tab{w:pos=-48,w:val=left},w:tab{w:pos=-36,w:val=left})'), - ('w:tabs/(w:tab{w:pos=-72,w:val=left},w:tab{w:pos=-36,w:val=left})', - Twips(-16), 1, - 'w:tabs/(w:tab{w:pos=-36,w:val=left},w:tab{w:pos=-16,w:val=left})'), - ]) + @pytest.fixture( + params=[ + ( + "w:tabs/w:tab{w:pos=360,w:val=left}", + Twips(720), + 0, + "w:tabs/w:tab{w:pos=720,w:val=left}", + ), + ( + "w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,w:val=left})", + Twips(180), + 0, + "w:tabs/(w:tab{w:pos=180,w:val=left},w:tab{w:pos=720,w:val=left})", + ), + ( + "w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,w:val=left})", + Twips(960), + 1, + "w:tabs/(w:tab{w:pos=720,w:val=left},w:tab{w:pos=960,w:val=left})", + ), + ( + "w:tabs/(w:tab{w:pos=-72,w:val=left},w:tab{w:pos=-36,w:val=left})", + Twips(-48), + 0, + "w:tabs/(w:tab{w:pos=-48,w:val=left},w:tab{w:pos=-36,w:val=left})", + ), + ( + "w:tabs/(w:tab{w:pos=-72,w:val=left},w:tab{w:pos=-36,w:val=left})", + Twips(-16), + 1, + "w:tabs/(w:tab{w:pos=-36,w:val=left},w:tab{w:pos=-16,w:val=left})", + ), + ] + ) def position_set_fixture(self, request): tabs_cxml, value, new_idx, expected_cxml = request.param tabs = element(tabs_cxml) @@ -131,16 +144,13 @@ def position_set_fixture(self, request): return tab_stop, value, tabs, new_idx, expected_xml -class DescribeTabStops(object): - +class DescribeTabStops: def it_knows_its_length(self, len_fixture): tab_stops, expected_value = len_fixture assert len(tab_stops) == expected_value def it_can_iterate_over_its_tab_stops(self, iter_fixture): - tab_stops, expected_count, tab_stop_, TabStop_, expected_calls = ( - iter_fixture - ) + tab_stops, expected_count, tab_stop_, TabStop_, expected_calls = iter_fixture count = 0 for tab_stop in tab_stops: assert tab_stop is tab_stop_ @@ -155,7 +165,7 @@ def it_can_get_a_tab_stop_by_index(self, index_fixture): assert tab_stop is tab_stop_ def it_raises_on_indexed_access_when_empty(self): - tab_stops = TabStops(element('w:pPr')) + tab_stops = TabStops(element("w:pPr")) with pytest.raises(IndexError): tab_stops[0] @@ -173,7 +183,7 @@ def it_raises_on_del_idx_invalid(self, del_raises_fixture): tab_stops, idx = del_raises_fixture with pytest.raises(IndexError) as exc: del tab_stops[idx] - assert exc.value.args[0] == 'tab index out of range' + assert exc.value.args[0] == "tab index out of range" def it_can_clear_all_its_tab_stops(self, clear_all_fixture): tab_stops, expected_xml = clear_all_fixture @@ -182,91 +192,127 @@ def it_can_clear_all_its_tab_stops(self, clear_all_fixture): # fixture -------------------------------------------------------- - @pytest.fixture(params=[ - 'w:pPr', - 'w:pPr/w:tabs/w:tab{w:pos=42}', - 'w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', - ]) + @pytest.fixture( + params=[ + "w:pPr", + "w:pPr/w:tabs/w:tab{w:pos=42}", + "w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})", + ] + ) def clear_all_fixture(self, request): pPr_cxml = request.param tab_stops = TabStops(element(pPr_cxml)) - expected_xml = xml('w:pPr') + expected_xml = xml("w:pPr") return tab_stops, expected_xml - @pytest.fixture(params=[ - ('w:pPr/w:tabs/w:tab{w:pos=42}', 0, - 'w:pPr'), - ('w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', 0, - 'w:pPr/w:tabs/w:tab{w:pos=42}'), - ('w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', 1, - 'w:pPr/w:tabs/w:tab{w:pos=24}'), - ]) + @pytest.fixture( + params=[ + ("w:pPr/w:tabs/w:tab{w:pos=42}", 0, "w:pPr"), + ( + "w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})", + 0, + "w:pPr/w:tabs/w:tab{w:pos=42}", + ), + ( + "w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})", + 1, + "w:pPr/w:tabs/w:tab{w:pos=24}", + ), + ] + ) def del_fixture(self, request): pPr_cxml, idx, expected_cxml = request.param tab_stops = TabStops(element(pPr_cxml)) expected_xml = xml(expected_cxml) return tab_stops, idx, expected_xml - @pytest.fixture(params=[ - ('w:pPr', 0), - ('w:pPr/w:tabs/w:tab{w:pos=42}', 1), - ]) + @pytest.fixture( + params=[ + ("w:pPr", 0), + ("w:pPr/w:tabs/w:tab{w:pos=42}", 1), + ] + ) def del_raises_fixture(self, request): tab_stops_cxml, idx = request.param tab_stops = TabStops(element(tab_stops_cxml)) return tab_stops, idx - @pytest.fixture(params=[ - ('w:pPr', Twips(42), {}, - 'w:pPr/w:tabs/w:tab{w:pos=42,w:val=left}'), - ('w:pPr', Twips(72), {'alignment': WD_TAB_ALIGNMENT.RIGHT}, - 'w:pPr/w:tabs/w:tab{w:pos=72,w:val=right}'), - ('w:pPr', Twips(24), - {'alignment': WD_TAB_ALIGNMENT.CENTER, - 'leader': WD_TAB_LEADER.DOTS}, - 'w:pPr/w:tabs/w:tab{w:pos=24,w:val=center,w:leader=dot}'), - ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(72), {}, - 'w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=72,w:val=left})'), - ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(24), {}, - 'w:pPr/w:tabs/(w:tab{w:pos=24,w:val=left},w:tab{w:pos=42})'), - ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(42), {}, - 'w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=42,w:val=left})'), - ]) + @pytest.fixture( + params=[ + ("w:pPr", Twips(42), {}, "w:pPr/w:tabs/w:tab{w:pos=42,w:val=left}"), + ( + "w:pPr", + Twips(72), + {"alignment": WD_TAB_ALIGNMENT.RIGHT}, + "w:pPr/w:tabs/w:tab{w:pos=72,w:val=right}", + ), + ( + "w:pPr", + Twips(24), + {"alignment": WD_TAB_ALIGNMENT.CENTER, "leader": WD_TAB_LEADER.DOTS}, + "w:pPr/w:tabs/w:tab{w:pos=24,w:val=center,w:leader=dot}", + ), + ( + "w:pPr/w:tabs/w:tab{w:pos=42}", + Twips(72), + {}, + "w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=72,w:val=left})", + ), + ( + "w:pPr/w:tabs/w:tab{w:pos=42}", + Twips(24), + {}, + "w:pPr/w:tabs/(w:tab{w:pos=24,w:val=left},w:tab{w:pos=42})", + ), + ( + "w:pPr/w:tabs/w:tab{w:pos=42}", + Twips(42), + {}, + "w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=42,w:val=left})", + ), + ] + ) def add_tab_fixture(self, request): pPr_cxml, position, kwargs, expected_cxml = request.param tab_stops = TabStops(element(pPr_cxml)) expected_xml = xml(expected_cxml) return tab_stops, position, kwargs, expected_xml - @pytest.fixture(params=[ - ('w:pPr/w:tabs/w:tab{w:pos=0}', 0), - ('w:pPr/w:tabs/(w:tab{w:pos=1},w:tab{w:pos=2},w:tab{w:pos=3})', 1), - ('w:pPr/w:tabs/(w:tab{w:pos=4},w:tab{w:pos=5},w:tab{w:pos=6})', 2), - ]) + @pytest.fixture( + params=[ + ("w:pPr/w:tabs/w:tab{w:pos=0}", 0), + ("w:pPr/w:tabs/(w:tab{w:pos=1},w:tab{w:pos=2},w:tab{w:pos=3})", 1), + ("w:pPr/w:tabs/(w:tab{w:pos=4},w:tab{w:pos=5},w:tab{w:pos=6})", 2), + ] + ) def index_fixture(self, request, TabStop_, tab_stop_): pPr_cxml, idx = request.param pPr = element(pPr_cxml) - tab = pPr.xpath('./w:tabs/w:tab')[idx] + tab = pPr.xpath("./w:tabs/w:tab")[idx] tab_stops = TabStops(pPr) return tab_stops, idx, TabStop_, tab, tab_stop_ - @pytest.fixture(params=[ - ('w:pPr', 0), - ('w:pPr/w:tabs/w:tab{w:pos=2880}', 1), - ('w:pPr/w:tabs/(w:tab{w:pos=2880},w:tab{w:pos=5760})', 2), - ]) + @pytest.fixture( + params=[ + ("w:pPr", 0), + ("w:pPr/w:tabs/w:tab{w:pos=2880}", 1), + ("w:pPr/w:tabs/(w:tab{w:pos=2880},w:tab{w:pos=5760})", 2), + ] + ) def iter_fixture(self, request, TabStop_, tab_stop_): pPr_cxml, expected_count = request.param pPr = element(pPr_cxml) - tab_elms = pPr.xpath('//w:tab') + tab_elms = pPr.xpath("//w:tab") tab_stops = TabStops(pPr) expected_calls = [call(tab) for tab in tab_elms] return tab_stops, expected_count, tab_stop_, TabStop_, expected_calls - @pytest.fixture(params=[ - ('w:pPr', 0), - ('w:pPr/w:tabs/w:tab{w:pos=2880}', 1), - ]) + @pytest.fixture( + params=[ + ("w:pPr", 0), + ("w:pPr/w:tabs/w:tab{w:pos=2880}", 1), + ] + ) def len_fixture(self, request): tab_stops_cxml, expected_value = request.param tab_stops = TabStops(element(tab_stops_cxml)) @@ -276,9 +322,7 @@ def len_fixture(self, request): @pytest.fixture def TabStop_(self, request, tab_stop_): - return class_mock( - request, 'docx.text.tabstops.TabStop', return_value=tab_stop_ - ) + return class_mock(request, "docx.text.tabstops.TabStop", return_value=tab_stop_) @pytest.fixture def tab_stop_(self, request): diff --git a/tests/unitdata.py b/tests/unitdata.py index 208be48de..31f0044d8 100644 --- a/tests/unitdata.py +++ b/tests/unitdata.py @@ -1,30 +1,23 @@ -# encoding: utf-8 +"""Shared code for unit test data builders.""" -""" -Shared code for unit test data builders -""" - -from __future__ import absolute_import, print_function, unicode_literals - -from docx.oxml import parse_xml from docx.oxml.ns import nsdecls +from docx.oxml.parser import parse_xml -class BaseBuilder(object): +class BaseBuilder: """ Provides common behavior for all data builders. """ + def __init__(self): self._empty = False - self._nsdecls = '' - self._text = '' + self._nsdecls = "" + self._text = "" self._xmlattrs = [] self._xmlattr_method_map = {} for attr_name in self.__attrs__: - base_name = ( - attr_name.split(':')[1] if ':' in attr_name else attr_name - ) - method_name = 'with_%s' % base_name + base_name = attr_name.split(":")[1] if ":" in attr_name else attr_name + method_name = "with_%s" % base_name self._xmlattr_method_map[method_name] = attr_name self._child_bldrs = [] @@ -34,10 +27,12 @@ def __getattr__(self, name): methods. """ if name in self._xmlattr_method_map: + def with_xmlattr(value): xmlattr_name = self._xmlattr_method_map[name] self._set_xmlattr(xmlattr_name, value) return self + return with_xmlattr else: tmpl = "'%s' object has no attribute '%s'" @@ -61,7 +56,7 @@ def element(self): def with_child(self, child_bldr): """ - Cause new child element specified by *child_bldr* to be appended to + Cause new child element specified by `child_bldr` to be appended to the children of this element. """ self._child_bldrs.append(child_bldr) @@ -69,7 +64,7 @@ def with_child(self, child_bldr): def with_text(self, text): """ - Cause *text* to be placed between the start and end tags of this + Cause `text` to be placed between the start and end tags of this element. Not robust enough for mixed elements, intended only for elements having no child elements. """ @@ -85,45 +80,44 @@ def with_nsdecls(self, *nspfxs): """ if not nspfxs: nspfxs = self.__nspfxs__ - self._nsdecls = ' %s' % nsdecls(*nspfxs) + self._nsdecls = " %s" % nsdecls(*nspfxs) return self def xml(self, indent=0): """ Return element XML based on attribute settings """ - indent_str = ' ' * indent + indent_str = " " * indent if self._is_empty: - xml = '%s%s\n' % (indent_str, self._empty_element_tag) + xml = "%s%s\n" % (indent_str, self._empty_element_tag) else: - xml = '%s\n' % self._non_empty_element_xml(indent) + xml = "%s\n" % self._non_empty_element_xml(indent) return xml def xml_bytes(self, indent=0): - return self.xml(indent=indent).encode('utf-8') + return self.xml(indent=indent).encode("utf-8") @property def _empty_element_tag(self): - return '<%s%s%s/>' % (self.__tag__, self._nsdecls, self._xmlattrs_str) + return "<%s%s%s/>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @property def _end_tag(self): - return '' % self.__tag__ + return "" % self.__tag__ @property def _is_empty(self): return len(self._child_bldrs) == 0 and len(self._text) == 0 def _non_empty_element_xml(self, indent): - indent_str = ' ' * indent + indent_str = " " * indent if self._text: - xml = ('%s%s%s%s' % - (indent_str, self._start_tag, self._text, self._end_tag)) + xml = "%s%s%s%s" % (indent_str, self._start_tag, self._text, self._end_tag) else: - xml = '%s%s\n' % (indent_str, self._start_tag) + xml = "%s%s\n" % (indent_str, self._start_tag) for child_bldr in self._child_bldrs: - xml += child_bldr.xml(indent+2) - xml += '%s%s' % (indent_str, self._end_tag) + xml += child_bldr.xml(indent + 2) + xml += "%s%s" % (indent_str, self._end_tag) return xml def _set_xmlattr(self, xmlattr_name, value): @@ -132,11 +126,11 @@ def _set_xmlattr(self, xmlattr_name, value): @property def _start_tag(self): - return '<%s%s%s>' % (self.__tag__, self._nsdecls, self._xmlattrs_str) + return "<%s%s%s>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @property def _xmlattrs_str(self): """ Return all element attributes as a string, like ' foo="bar" x="1"'. """ - return ''.join(self._xmlattrs) + return "".join(self._xmlattrs) diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index f583a3c99..c7b7d172c 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -1,49 +1,41 @@ -# encoding: utf-8 - """Parser for Compact XML Expression Language (CXEL) ('see-ex-ell'). CXEL is a compact XML specification language I made up that's useful for producing XML element trees suitable for unit testing. """ -from __future__ import absolute_import, division, print_function, unicode_literals - from pyparsing import ( - alphas, - alphanums, Combine, - dblQuotedString, - delimitedList, Forward, Group, Literal, Optional, - removeQuotes, - stringEnd, Suppress, Word, + alphanums, + alphas, + dblQuotedString, + delimitedList, + removeQuotes, + stringEnd, ) -from docx.oxml import parse_xml from docx.oxml.ns import nsmap - +from docx.oxml.parser import parse_xml # ==================================================================== # api functions # ==================================================================== -def element(cxel_str): - """ - Return an oxml element parsed from the XML generated from *cxel_str*. - """ + +def element(cxel_str: str): + """Return an oxml element parsed from the XML generated from `cxel_str`.""" _xml = xml(cxel_str) return parse_xml(_xml) -def xml(cxel_str): - """ - Return the XML generated from *cxel_str*. - """ +def xml(cxel_str: str) -> str: + """Return the XML generated from `cxel_str`.""" root_token = root_node.parseString(cxel_str) xml = root_token.element.xml return xml @@ -55,21 +47,19 @@ def xml(cxel_str): def nsdecls(*nspfxs): - """ - Return a string containing a namespace declaration for each of *nspfxs*, - in the order they are specified. - """ - nsdecls = '' + """Namespace-declaration including each of `nspfxs`, in the order specified.""" + nsdecls = "" for nspfx in nspfxs: nsdecls += ' xmlns:%s="%s"' % (nspfx, nsmap[nspfx]) return nsdecls -class Element(object): +class Element: """ Represents an XML element, having a namespace, tagname, attributes, and may contain either text or children (but not both) or may be empty. """ + def __init__(self, tagname, attrs, text): self._tagname = tagname self._attrs = attrs @@ -86,7 +76,7 @@ def __repr__(self): def connect_children(self, child_node_list): """ - Make each of the elements appearing in *child_node_list* a child of + Make each of the elements appearing in `child_node_list` a child of this element. """ for node in child_node_list: @@ -122,10 +112,11 @@ def local_nspfxs(self): all of its attributes. An empty string (``''``) is used to represent the default namespace for an element tag having no prefix. """ + def nspfx(name, is_element=False): - idx = name.find(':') + idx = name.find(":") if idx == -1: - return '' if is_element else None + return "" if is_element else None return name[:idx] nspfxs = [nspfx(self._tagname, True)] @@ -143,6 +134,7 @@ def nspfxs(self): this tree. Each prefix appears once and only once, and in document order. """ + def merge(seq, seq_2): for item in seq_2: if item in seq: @@ -166,12 +158,12 @@ def xml(self): def _xml(self, indent): """ Return a string containing the XML of this element and all its - children with a starting indent of *indent* spaces. + children with a starting indent of `indent` spaces. """ - self._indent_str = ' ' * indent + self._indent_str = " " * indent xml = self._start_tag for child in self._children: - xml += child._xml(indent+2) + xml += child._xml(indent + 2) xml += self._end_tag return xml @@ -187,17 +179,17 @@ def _start_tag(self): a newline. The tag is indented by this element's indent value in all cases. """ - _nsdecls = nsdecls(*self.nspfxs) if self.is_root else '' - tag = '%s<%s%s' % (self._indent_str, self._tagname, _nsdecls) + _nsdecls = nsdecls(*self.nspfxs) if self.is_root else "" + tag = "%s<%s%s" % (self._indent_str, self._tagname, _nsdecls) for attr in self._attrs: name, value = attr tag += ' %s="%s"' % (name, value) if self._text: - tag += '>%s' % self._text + tag += ">%s" % self._text elif self._children: - tag += '>\n' + tag += ">\n" else: - tag += '/>\n' + tag += "/>\n" return tag @property @@ -207,11 +199,11 @@ def _end_tag(self): element contains text, no leading indentation is included. """ if self._text: - tag = '\n' % self._tagname + tag = "\n" % self._tagname elif self._children: - tag = '%s\n' % (self._indent_str, self._tagname) + tag = "%s\n" % (self._indent_str, self._tagname) else: - tag = '' + tag = "" return tag @@ -221,6 +213,7 @@ def _end_tag(self): # parse actions ---------------------------------- + def connect_node_children(s, loc, tokens): node = tokens[0] node.element.connect_children(node.child_node_list) @@ -233,13 +226,13 @@ def connect_root_node_children(root_node): def grammar(): # terminals ---------------------------------- - colon = Literal(':') - equal = Suppress('=') - slash = Suppress('/') - open_paren = Suppress('(') - close_paren = Suppress(')') - open_brace = Suppress('{') - close_brace = Suppress('}') + colon = Literal(":") + equal = Suppress("=") + slash = Suppress("/") + open_paren = Suppress("(") + close_paren = Suppress(")") + open_brace = Suppress("{") + close_brace = Suppress("}") # np:tagName --------------------------------- nspfx = Word(alphas) @@ -247,8 +240,8 @@ def grammar(): tagname = Combine(nspfx + colon + local_name) # np:attr_name=attr_val ---------------------- - attr_name = Word(alphas + ':') - attr_val = Word(alphanums + ' %-./:_') + attr_name = Word(alphas + ":") + attr_val = Word(alphanums + " %-./:_") attr_def = Group(attr_name + equal + attr_val) attr_list = open_brace + delimitedList(attr_def) + close_brace @@ -256,26 +249,22 @@ def grammar(): # w:jc{val=right} ---------------------------- element = ( - tagname('tagname') - + Group(Optional(attr_list))('attr_list') - + Optional(text, default='')('text') + tagname("tagname") + + Group(Optional(attr_list))("attr_list") + + Optional(text, default="")("text") ).setParseAction(Element.from_token) child_node_list = Forward() node = Group( - element('element') - + Group(Optional(slash + child_node_list))('child_node_list') + element("element") + Group(Optional(slash + child_node_list))("child_node_list") ).setParseAction(connect_node_children) - child_node_list << ( - open_paren + delimitedList(node) + close_paren - | node - ) + child_node_list << (open_paren + delimitedList(node) + close_paren | node) root_node = ( - element('element') - + Group(Optional(slash + child_node_list))('child_node_list') + element("element") + + Group(Optional(slash + child_node_list))("child_node_list") + stringEnd ).setParseAction(connect_root_node_children) diff --git a/tests/unitutil/file.py b/tests/unitutil/file.py index 8462e6c42..795052c8e 100644 --- a/tests/unitutil/file.py +++ b/tests/unitutil/file.py @@ -1,61 +1,58 @@ -# encoding: utf-8 +"""Utility functions for loading files for unit testing.""" -""" -Utility functions for loading files for unit testing -""" +from __future__ import annotations import os - _thisdir = os.path.split(__file__)[0] -test_file_dir = os.path.abspath(os.path.join(_thisdir, '..', 'test_files')) +test_file_dir = os.path.abspath(os.path.join(_thisdir, "..", "test_files")) -def abspath(relpath): +def abspath(relpath: str) -> str: thisdir = os.path.split(__file__)[0] return os.path.abspath(os.path.join(thisdir, relpath)) -def absjoin(*paths): +def absjoin(*paths: str) -> str: return os.path.abspath(os.path.join(*paths)) -def docx_path(name): +def docx_path(name: str): """ - Return the absolute path to test .docx file with root name *name*. + Return the absolute path to test .docx file with root name `name`. """ - return absjoin(test_file_dir, '%s.docx' % name) + return absjoin(test_file_dir, "%s.docx" % name) -def snippet_seq(name, offset=0, count=1024): +def snippet_seq(name: str, offset: int = 0, count: int = 1024): """ Return a tuple containing the unicode text snippets read from the snippet - file having *name*. Snippets are delimited by a blank line. If specified, - *count* snippets starting at *offset* are returned. - """ - path = os.path.join(test_file_dir, 'snippets', '%s.txt' % name) - with open(path, 'rb') as f: - text = f.read().decode('utf-8') - snippets = text.split('\n\n') - start, end = offset, offset+count + file having `name`. Snippets are delimited by a blank line. If specified, + `count` snippets starting at `offset` are returned. + """ + path = os.path.join(test_file_dir, "snippets", "%s.txt" % name) + with open(path, "rb") as f: + text = f.read().decode("utf-8") + snippets = text.split("\n\n") + start, end = offset, offset + count return tuple(snippets[start:end]) -def snippet_text(snippet_file_name): +def snippet_text(snippet_file_name: str): """ Return the unicode text read from the test snippet file having - *snippet_file_name*. + `snippet_file_name`. """ snippet_file_path = os.path.join( - test_file_dir, 'snippets', '%s.txt' % snippet_file_name + test_file_dir, "snippets", "%s.txt" % snippet_file_name ) - with open(snippet_file_path, 'rb') as f: + with open(snippet_file_path, "rb") as f: snippet_bytes = f.read() - return snippet_bytes.decode('utf-8') + return snippet_bytes.decode("utf-8") -def test_file(name): +def test_file(name: str): """ - Return the absolute path to test file having *name*. + Return the absolute path to test file having `name`. """ return absjoin(test_file_dir, name) diff --git a/tests/unitutil/mock.py b/tests/unitutil/mock.py index 828382e7e..d0e41ce93 100644 --- a/tests/unitutil/mock.py +++ b/tests/unitutil/mock.py @@ -1,26 +1,44 @@ -# encoding: utf-8 - -"""Utility functions wrapping the excellent *mock* library""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys - -if sys.version_info >= (3, 3): - from unittest import mock # noqa - from unittest.mock import ANY, call, MagicMock # noqa - from unittest.mock import create_autospec, Mock, mock_open, patch, PropertyMock -else: - import mock # noqa - from mock import ANY, call, MagicMock # noqa - from mock import create_autospec, Mock, patch, PropertyMock - - -def class_mock(request, q_class_name, autospec=True, **kwargs): - """Return mock patching class with qualified name *q_class_name*. +"""Utility functions wrapping the excellent `mock` library.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import ( + ANY, + MagicMock, + Mock, + PropertyMock, + call, + create_autospec, + mock_open, + patch, +) + +from pytest import FixtureRequest, LogCaptureFixture # noqa: PT013 + +__all__ = ( + "ANY", + "FixtureRequest", + "LogCaptureFixture", + "MagicMock", + "Mock", + "call", + "class_mock", + "function_mock", + "initializer_mock", + "instance_mock", + "method_mock", + "property_mock", +) + + +def class_mock( + request: FixtureRequest, q_class_name: str, autospec: bool = True, **kwargs: Any +) -> Mock: + """Return mock patching class with qualified name `q_class_name`. The mock is autospec'ed based on the patched class unless the optional - argument *autospec* is set to False. Any other keyword arguments are + argument `autospec` is set to False. Any other keyword arguments are passed through to Mock(). Patch is reversed after calling test returns. """ _patch = patch(q_class_name, autospec=autospec, **kwargs) @@ -28,10 +46,16 @@ def class_mock(request, q_class_name, autospec=True, **kwargs): return _patch.start() -def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): - """ - Return a mock for attribute *attr_name* on *cls* where the patch is - reversed after pytest uses it. +def cls_attr_mock( + request: FixtureRequest, + cls: type, + attr_name: str, + name: str | None = None, + **kwargs: Any, +): + """Return a mock for attribute `attr_name` on `cls`. + + Patch is reversed after pytest uses it. """ name = request.fixturename if name is None else name _patch = patch.object(cls, attr_name, name=name, **kwargs) @@ -39,8 +63,10 @@ def cls_attr_mock(request, cls, attr_name, name=None, **kwargs): return _patch.start() -def function_mock(request, q_function_name, autospec=True, **kwargs): - """Return mock patching function with qualified name *q_function_name*. +def function_mock( + request: FixtureRequest, q_function_name: str, autospec: bool = True, **kwargs: Any +): + """Return mock patching function with qualified name `q_function_name`. Patch is reversed after calling test returns. """ @@ -49,8 +75,10 @@ def function_mock(request, q_function_name, autospec=True, **kwargs): return _patch.start() -def initializer_mock(request, cls, autospec=True, **kwargs): - """Return mock for __init__() method on *cls*. +def initializer_mock( + request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any +): + """Return mock for __init__() method on `cls`. The patch is reversed after pytest uses it. """ @@ -61,21 +89,25 @@ def initializer_mock(request, cls, autospec=True, **kwargs): return _patch.start() -def instance_mock(request, cls, name=None, spec_set=True, **kwargs): +def instance_mock( + request: FixtureRequest, + cls: type, + name: str | None = None, + spec_set: bool = True, + **kwargs: Any, +): """ - Return a mock for an instance of *cls* that draws its spec from the class - and does not allow new attributes to be set on the instance. If *name* is + Return a mock for an instance of `cls` that draws its spec from the class + and does not allow new attributes to be set on the instance. If `name` is missing or |None|, the name of the returned |Mock| instance is set to *request.fixturename*. Additional keyword arguments are passed through to the Mock() call that creates the mock. """ name = name if name is not None else request.fixturename - return create_autospec( - cls, _name=name, spec_set=spec_set, instance=True, **kwargs - ) + return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) -def loose_mock(request, name=None, **kwargs): +def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any): """ Return a "loose" mock, meaning it has no spec to constrain calls on it. Additional keyword arguments are passed through to Mock(). If called @@ -86,8 +118,14 @@ def loose_mock(request, name=None, **kwargs): return Mock(name=name, **kwargs) -def method_mock(request, cls, method_name, autospec=True, **kwargs): - """Return mock for method *method_name* on *cls*. +def method_mock( + request: FixtureRequest, + cls: type, + method_name: str, + autospec: bool = True, + **kwargs: Any, +): + """Return mock for method `method_name` on `cls`. The patch is reversed after pytest uses it. """ @@ -96,19 +134,19 @@ def method_mock(request, cls, method_name, autospec=True, **kwargs): return _patch.start() -def open_mock(request, module_name, **kwargs): +def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any): """ - Return a mock for the builtin `open()` method in *module_name*. + Return a mock for the builtin `open()` method in `module_name`. """ - target = '%s.open' % module_name + target = "%s.open" % module_name _patch = patch(target, mock_open(), create=True, **kwargs) request.addfinalizer(_patch.stop) return _patch.start() -def property_mock(request, cls, prop_name, **kwargs): +def property_mock(request: FixtureRequest, cls: type, prop_name: str, **kwargs: Any): """ - Return a mock for property *prop_name* on class *cls* where the patch is + Return a mock for property `prop_name` on class `cls` where the patch is reversed after pytest uses it. """ _patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs) @@ -116,9 +154,9 @@ def property_mock(request, cls, prop_name, **kwargs): return _patch.start() -def var_mock(request, q_var_name, **kwargs): +def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any): """ - Return a mock patching the variable with qualified name *q_var_name*. + Return a mock patching the variable with qualified name `q_var_name`. Patch is reversed after calling test returns. """ _patch = patch(q_var_name, **kwargs) diff --git a/tox.ini b/tox.ini index 6ced79b71..1c4e3aea7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,43 +1,9 @@ -# -# Configuration for tox and pytest - -[flake8] -exclude = dist,docs,*.egg-info,.git,ref,_scratch,.tox -max-line-length = 88 - -[pytest] -norecursedirs = doc docx *.egg-info features .git ref _scratch .tox -python_files = test_*.py -python_classes = Test Describe -python_functions = it_ they_ and_it_ but_it_ - [tox] -envlist = py26, py27, py34, py35, py36, py38 +envlist = py37, py38, py39, py310, py311 [testenv] -deps = - behave - lxml - pyparsing - pytest +deps = -rrequirements-test.txt commands = py.test -qx behave --format progress --stop --tags=-wip - -[testenv:py26] -deps = - importlib>=1.0.3 - behave - lxml - mock - pyparsing - pytest - -[testenv:py27] -deps = - behave - lxml - mock - pyparsing - pytest diff --git a/typings/behave/__init__.pyi b/typings/behave/__init__.pyi new file mode 100644 index 000000000..f8ffc2058 --- /dev/null +++ b/typings/behave/__init__.pyi @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Callable + +from typing_extensions import TypeAlias + +from .runner import Context + +_ThreeArgStep: TypeAlias = Callable[[Context, str, str, str], None] +_TwoArgStep: TypeAlias = Callable[[Context, str, str], None] +_OneArgStep: TypeAlias = Callable[[Context, str], None] +_NoArgStep: TypeAlias = Callable[[Context], None] +_Step: TypeAlias = _NoArgStep | _OneArgStep | _TwoArgStep | _ThreeArgStep + +def given(phrase: str) -> Callable[[_Step], _Step]: ... +def when(phrase: str) -> Callable[[_Step], _Step]: ... +def then(phrase: str) -> Callable[[_Step], _Step]: ... diff --git a/typings/behave/runner.pyi b/typings/behave/runner.pyi new file mode 100644 index 000000000..aaea74dad --- /dev/null +++ b/typings/behave/runner.pyi @@ -0,0 +1,3 @@ +from types import SimpleNamespace + +class Context(SimpleNamespace): ...